Mixpanel track_links does not work with dynamically added elements - javascript

I'm having trouble using mixpanel.track_links with links added dynamically (after page load).
For a general example, given this page:
<div id="link-div"></div>
<input type="button" id="add-link" />
<script type="text/javascript">
mixpanel.track_links(".mixpanel-event", "event name", function(ele) { return { "type": $(ele).attr("type")}});
</script>
At some user action, links are added to the page using jquery. For example:
$('#add-link).click(function() {
$('#link-div').html('<a class="mixpanel-event" type="event-type" href="#>Link to track</a>');
})
The problem is that track_links isn't triggered on click of the newly created link. I'm hoping someone can share their experience in enabling the track_link function to work for dynamically added links.

I was curious so I checked out their code and went ahead and did as they suggested. I tested it, and it worked fine. This requires jQuery though.
Example usage: mixpanel.delegate_links(document.body, 'a', 'clicked link');
// with jQuery and mixpanel
mixpanel.delegate_links = function (parent, selector, event_name, properties) {
properties = properties || {};
parent = parent || document.body;
parent = $(parent);
parent.on('click', selector, function (event) {
var new_tab = event.which === 2 || event.metaKey || event.target.target === '_blank';
properties.url = event.target.href;
function callback() {
if (new_tab) {
return;
}
window.location = properties.url;
}
if (!new_tab) {
event.preventDefault();
setTimeout(callback, 300);
}
mixpanel.track(event_name, properties, callback);
});
};

I had a somewhat hard time trying to get tracking links working as expected on react. The main caveat I noticed was that duplicated events may be sent to mixpanel in bursts.
I used a slightly modified version of #Kyle to solve my problem. Additionally, this accounts for properties being possibly a function as supported by the mixpanel API.
// mixpanelSetup.js
import md5 from "md5";
const setup = () => {
mixpanel.init(TOKEN);
// Sets ensure unique items
mixpanel.delegated_links = new Set();
mixpanel.delegate_links = (parent, selector, eventName, eventProperties, {ignoreUrl=false}) => {
// Hash by whatever thing(s) the use case considers 'unique' (e.g md5(`${selector}__${eventName}`))
const linkHash = md5(selector);
parent = parent || document.body;
parent = $(parent);
// Do not add additional trackers for an already tracked event.
if (mixpanel.delegated_links.has(linkHash)) {
return;
}
mixpanel.delegated_links.add(linkHash);
parent.on("click", selector, (event) => {
const newTab = event.which === 2 || event.metaKey || event.target.target === "_blank";
if (typeof eventProperties === "function") {
eventProperties = eventProperties(event.target) || {};
}
eventProperties.url = event.target.href;
// In case we don't want the url on the properties.
if (ignoreUrl) {
delete eventProperties.url;
}
const callback = () => {
if (newTab) {
return;
}
window.location = event.target.href;
};
if (!newTab) {
event.preventDefault();
setTimeout(callback, 300);
}
console.debug("Tracking link click!");
mixpanel.track(eventName, eventProperties, callback);
});
};
}
And can be used as:
// MyComponent.jsx
import React, { useEffect } from "react";
import { Link, useLocation } from "#reach/router";
const MyComponent = ({ moduleName, key, ...props }) => {
const id = `#${id}__${moduleName}`;
useEffect(() => {
mixpanel.delegate_links(document.parent, id, event => {
return {
module: event.id.split("__").pop(),
...props.otherPropsToTrack
};
})
}, [])
return <>
<Link {...props} to="/some/path" id={id}>My Page</Link>
</>
}

Related

check if class exists on that page javascript

I have an index page and a dashboard, on the index I'm using typewriter and particlesjs on the index only, and on the dashboard I have a sidebar.
If I have all the code as-is, I get errors as the page is still looking for typewriter and particlesjs on all pages.
So I have attempted to wrap each section around an if so the plan is if that class or id exists on that page it will only render that JS. So I've created the following code.
edited code below based on groovy_guy's answer
document.querySelector('nav .toggle').addEventListener('click', e => {
document.querySelector('nav .hidden').classList.toggle('show')
});
let checkTypewriter = document.getElementById('typewriter');
if (checkTypewriter.length > 0) {
new Typewriter('#typewriter', {
strings: ['Website Developer', 'Freelancer' , 'System Admin'],
autoStart: true,
loop: true
});
}
let checkParticlesjs = document.getElementsByClassName('particlesjs');
if (checkParticlesjs.length > 0) {
let particles = Particles.init({
selector: '.particlesjs',
color: ['#48F2E3', '#48F2E3', '#48F2E3'],
connectParticles: true,
maxParticles: 200
});
}
let checkSidebar = document.getElementsByClassName('sidebar');
if (checkSidebar.length > 0) {
user_wants_collapse = false;
// Fetch all the details element.
const details = document.querySelectorAll("details");
// Add the onclick listeners.
details.forEach((targetDetail) => {
targetDetail.addEventListener("click", () => {
// Close all the details that are not targetDetail.
details.forEach((detail) => {
if (detail !== targetDetail) {
detail.removeAttribute("open");
};
});
});
});
document.querySelector('section').addEventListener('click', (ev) => {
// close any open details elements that this click is outside of
let target = ev.target;
let detailsClickedWithin = null;
while (target && target.tagName != 'DETAILS') {
target = target.parentNode;
};
if (target && target.tagName == 'DETAILS') {
detailsClickedWithin = target;
};
Array.from(document.getElementsByTagName('details')).filter(
(details) => details.open && details != detailsClickedWithin
).forEach(details => details.open = false);
// if the sidebar collapse is true and is re-expanded by clicking a menu item then clicking on the body should re-close it
if (user_wants_collapse == true && (document.querySelectorAll('.sidebar details'))) {
document.querySelector('body').classList.add('is--expand');
};
});
// when the sidebar menu is clicked this sets the user_wants_collapse var to true or false and toggles is--expand class on body
document.querySelector('.sidebar .menu-link').addEventListener('click', () => {
document.querySelector('body').classList.toggle('is--expand');
user_wants_collapse = !user_wants_collapse
document.querySelector('.sidebar').classList.toggle('is--expand');
// show all text
document.querySelectorAll('.sidebar .title').forEach((el) => {
el.classList.toggle('hidden');
});
// changing sidebar menu items and menu collapse icons
const icon = document.querySelector('.menu-link-arrows span');
if (icon.classList.contains('fa-angle-double-left')) {
icon.classList.remove('fa-angle-double-left');
icon.classList.add('fa-angle-double-right');
} else {
icon.classList.remove('fa-angle-double-right');
icon.classList.add('fa-angle-double-left');
}
});
// making sure the sidebar menu items re-expands the sidebar on click
let x = document.querySelectorAll('.sidebar details');
let i;
for (i = 1; i < x.length; i++) {
x[i].addEventListener('click', () => {
// changing sidebar menu items and menu collapse icons
// change menu items and menu collapse icons
const icon = document.querySelector('.sidebar-drop-parent-arrow span');
if (icon.classList.contains('fa-chevron-down')) {
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
} else {
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
}
if (document.querySelector('body').classList.contains('is--expand')) {
document.querySelector('body').classList.remove('is--expand');
};
});
};
};
when loading the JS I'm not getting any console errors but I'm not seeing any result of the JS.
Why don't you use querySelector()? I think that's more uniform across your codebase. Besides, I see that you only care about one element and not a list of elements, so this method is ideal since it gets the first element that encounters.
const checkTypewriter = document.querySelector('#typewriter')
if (checkTypewriter) {
// Element with an ID 'typewriter' exist in the DOM.
}
const checkParticlesjs = document.querySelector('.particlesjs')
if (checkParticlesjs) {
// Element with a class named "particlesjs" exist in the DOM.
}
Also, make sure to check if an element exist before attaching an event listener:
const toggleNav = document.querySelector('nav .toggle')
if (toggleNav) {
toggleNav.addEventListener('click', (e) => {
document.querySelector('nav .hidden').classList.toggle('show')
});
}
For Javascript:
var checkTypewriter = document.getElementsByClassName('typewriter');
if (checkTypewriter.length > 0) {
// Here write your code
}
var checkParticlesjs = document.getElementsByClassName('particlesjs');
if (checkParticlesjs.length > 0) {
// Here write your specific code
}
For JQuery:
if ($("#typewriter")[0]){
// Here write your code
}
if ($(".particlesjs")[0]){
// Here write your specific code
}
This is how you can check if your classes exist,

mutationobserver firing when no change and method not found issue

I've placed an observer on an element using MutationObserver. In a one use case it works exactly as expected and fires after a change but in another user action where the element has not changed it appears to be firing - but in doing so it comes up with a method not found error, which doesn't appear in the first use case using the same observer.
The observer watches for an update within an element which gets updated with an image as a user selects an image.
In the working case a user selects an image from a list of images, it then updates and the observer fires - all great.
In the non-working case a user uploads an image - at this point though no update has happened to the target element (which is in view but below a colorbox.(not sure if that's relevant).
The firing itself would not normally be a problem but within the observer callback it calls a method which in the second case it says is not defined.
So in the first instance there are no errors but in the second instance:
I get an error _this.buttons is not a function at MutationObserver.callback
The code is being compiled with webpack
1. Why is the observer firing when the doesn't appear to the type of change being observed?
Why does this error occur in this scenario - when the method appears to exist and works as expected when there is a change?
Any help appreciated
here's the code - this class manages the actions for a page - I've removed some code to try and keep it brief (but still a bit lengthy - refactoring to be done):
First, here's the code of the observer:
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
And here's the class in which the observer method is contained.
export let fileImageWidgetControls = class {
constructor({
previewBtn = '.preview-image',
addBtn = '#add-image',
replaceBtn = '#replace-image',
removeBtn = '#remove-image'
} = {}) {
this.options = {
previewBtn: previewBtn,
addBtn: addBtn,
replaceBtn: replaceBtn,
removeBtn: removeBtn
}
this.filemanager = new filemanagerHandler; //file selector class
this.imageWidget = new updateWidget; //handles updating the image
this.initialiseControls(); //sets up the page controls
this.observer(); //sets up the observer
}
openFileManager = () =>{
//open colbox (which opens filemanager page
//When filemanager loaded then initialise filemanager
$(document).bind('cbox_complete', ()=>{
console.log('Colbox complete');
this.filemanager.init();
});
//handle colbox closing and update image in widget (if needed)
$(document).bind('cbox_closed', ()=>{
let selectedAsset = this.filemanager.getSelectedAsset();
if(selectedAsset) {
this.imageWidget.update(selectedAsset.filename);
}
});
colBox.init({
href: this.options.serverURL
});
colBox.colorbox()
}
remove = ()=> {
//clear file and update visible buttons
this.buttons();
}
/**
* preview the image in a colorbox
* #param filename
*/
preview = function () {
//open image in preview
}
/**
* select image via filemanager
*/
select = () =>{
console.log('select');
this.openFileManager();
}
replace = () => {
// image already exists in widget but needs replacing
console.log('replace');
this.openFileManager();
}
initialiseControls = () => {
console.log('init controls');
//preview button
$(this.options.previewBtn).on('click', (e) => {
e.preventDefault();
this.preview();
}).attr('disabled', false);
$('#img-preview-link').on('click', (e)=> {
e.preventDefault();
this.preview();
});
// add button
$(this.options.addBtn).on('click', (e) => {
e.preventDefault();
this.select();
}).attr('disabled', false);
//replace button
$(this.options.replaceBtn).on('click', (e) => {
e.preventDefault();
this.replace();
}).attr('disabled', false);
//remove button
$(this.options.removeBtn).on('click', (e) => {
e.preventDefault();
this.remove();
}).attr('disabled', false);
this.buttons();
}
//set an observer to watch preview image for changes
observer= ()=> {
const module = this;
const targetNode = document.getElementById('image-preview-panel');
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
buttons = function() {
let imagePreview = $('#image-preview');
if(imagePreview.data('updated')=== true && imagePreview.data('updated') !== "false") {
console.log('image present');
$(this.options.addBtn).fadeOut().attr('disabled', true);
$(this.options.removeBtn).fadeIn().attr('disabled', false);
$(this.options.replaceBtn).fadeIn().attr('disabled', false);
$(this.options.previewBtn).fadeIn().attr('disabled', false);
} else {
console.log('image not present', imagePreview.data());
console.log('image element:', imagePreview);
$(this.options.addBtn).fadeIn().attr('disabled', false);
$(this.options.removeBtn).fadeOut().attr('disabled', true);
$(this.options.replaceBtn).fadeOut().attr('disabled', true);
$(this.options.previewBtn).fadeOut().attr('disabled', true);
}
}
}
I copied code from a tutorial hence some of the comments until I refactor
Added const module = this; within the method and referenced that within the nested function and now pointing to the correctthis`

How do you handle click-outside of element properly in vuejs?

Is there any proper solution for handling click-outside of elements?
there are general solutions out there, like Handling clicks outside an element without jquery :
window.onload = function() {
// For clicks inside the element
document.getElementById('myElement').onclick = function(e) {
// Make sure the event doesn't bubble from your element
if (e) { e.stopPropagation(); }
else { window.event.cancelBubble = true; }
// Place the code for this element here
alert('this was a click inside');
};
// For clicks elsewhere on the page
document.onclick = function() {
alert('this was a click outside');
};
};
But the problem is almost all projects have multiple and different popups in different components which i should handle their click-outsides.
how should i handle click-outisde without using a global window.on?(I think it is not possible to put all components outside-case handler in window.on )
After struggling with this and searching about this, i found how to solve this problem using vuejs directive without bleeding:
1. using libraries:
v-click-outside is a good one,
https://www.npmjs.com/package/v-click-outside
2. without a library:
```
//main.js
import '#/directives';
......
// directives.js
import Vue from "vue";
Vue.directive('click-outside', {
bind: function (element, binding, vnode) {
element.clickOutsideEvent = function (event) { // check that click was outside the el and his children
if (!(element === event.target || element.contains(event.target))) { // and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
// binding.value(); run the arg
}
};
document.body.addEventListener('click', element.clickOutsideEvent)
},
unbind: function (element) {
document.body.removeEventListener('click', element.clickOutsideEvent)
}
});
```
use it every-where you want with v-click-outside directive like below:
//header.vue
<div class="profileQuickAction col-lg-4 col-md-12" v-click-outside="hidePopUps">
...
</>
you can check this on
You can also directly use VueUse vOnClickOUtside directive.
<script setup lang="ts">
import { vOnClickOutside } from '#vueuse/components'
const modal = ref(true)
function closeModal() {
modal.value = false
}
</script>
<template>
<div v-if="modal" v-on-click-outside="closeModal">
Hello World
</div>
</template>
It might be little late but i ve found a clear solution
<button class="m-search-btn" #click="checkSearch" v-bind:class="{'m-nav-active':search === true}">
methods:{
checkSearch(e){
var clicked = e.target;
this.search = !this.search;
var that = this;
window.addEventListener('click',function check(e){
if (clicked === e.target || e.target.closest('.search-input')){ //I ve used target closest to protect the input that has search bar in it
that.search = true;
}else {
that.search = false;
removeEventListener('click',check)
}
})
}}

Mixin functions only work in render()

For some reason, it appears that mixin functions in my code only work properly in render() function. It could be that I'm not calling them in the right manner outside of the render(), but shouldn't it be exactly the same way?
This way everything works fine (but I can't stick with this since I have to add some extra stuff to click handling, at the same time not altering the mixin):
var Row = React.createClass({
mixins: [someMixin]
},
render: function () {
var clickHandler = null;
var btn = null;
if (firstCase) {
clickHandler = this.order(this.props.name, this.props.else);
btn = (<a href="" onClick={clickHandler}>Order</a>);
} else if (secondCase) {
clickHandler = this.noOrder(this.props.name, this.props.else);
btn = (<a href="" onClick={clickHandler}>No order</a>);
}
return (
<div>
{btn}
</div>
);
}
});
But when I do the obvious and include the mixin functions in another function to handle the click - like this - everything fails and even 'test' is not printed in the console:
var Row = React.createClass({
mixins: [someMixin]
},
handleOrderClick(type) {
console.log('test');
if (type == 'order') {
this.order(this.props.name, this.props.else);
} else if (type == 'no-order') {
this.noOrder(this.props.name, this.props.else);
}
},
render: function () {
var clickHandler = null;
var btn = null;
if (firstCase) {
clickHandler = this.handleOrderClick('order');
btn = (<a href="" onClick={clickHandler}>Order</a>);
} else if (secondCase) {
clickHandler = this.handleOrderClick('no-order');
btn = (<a href="" onClick={clickHandler}>No order</a>);
}
return (
<div>
{btn}
</div>
);
}
});
EDIT:
order and noOrder functions look like this:
order: function (name, else) {
return function (event) {
event.preventDefault();
var term = name + '&&¤%' + else;
Order.order(name, else, period, function (data) {
if (term === (global || window).MAIN_NAME + '.' + (global || window).MAIN) {
$(window).trigger('Name:update');
}
}.bind(this));
}.bind(this);
},
noOrder: function (name, else) {
return function (event) {
event.preventDefault();
if (!this.state.transferModalOpen) {
this.toggleTransferModal();
}
}.bind(this);
}
In order to use this.setState in handleOrderClick you'll have to use the bind method in your render method. Therefore handleOrderClick will become:
handleOrderClick(type, event) {
this.setState({foo: 'bar'});
if (type == 'order') {
this.order(this.props.name, this.props.else)(event);
} else if (type == 'no-order') {
this.noOrder(this.props.name, this.props.else)(event);
}
},
and your render method becomes:
render: function () {
var clickHandler = null;
var btn = null;
if (firstCase) {
clickHandler = this.handleOrderClick.bind(this, 'order');
btn = (<a href="" onClick={clickHandler}>Order</a>);
} else if (secondCase) {
clickHandler = this.handleOrderClick(this, 'no-order');
btn = (<a href="" onClick={clickHandler}>No order</a>);
}
return (
<div>
{btn}
</div>
);
}
You'll notice that the functions that are returned by this.order and this.noOrder are no longer returned by handleOrderClick, but are instead executed immediately. This should provide the effect you desire.
I've put the code in your example into a jsfiddle and it now seems to be working correctly. I've had to change the prop 'else' to 'alt' because 'else' is a reserved word. I've also just applied the mixin to the class directly for simplicity. I have simpilfied the order and noOrder functions as I don't have access to the Order object and we are only interested in them firing at the correct time. I've also added a second button that you can click to flip the cases so the other button is rendered, causing the component to render again. I've added a label that will display which function had been called when the button was last pressed.
for reference, you can find more information about the bind method here.
Hope this helps ^_^

Event trigger on a class change

I'd like my event to be triggered when a div tag containing a trigger class is changed.
I have no idea how to make it listen to the class' adding event.
<div id="test">test</div>
<script type="text/javascript">
document.getElementById.setAttribute("class", "trigger");
function workOnClassAdd() {
alert("I'm triggered");
}
</script>
The future is here, and you can use the MutationObserver interface to watch for a specific class change.
let targetNode = document.getElementById('test')
function workOnClassAdd() {
alert("I'm triggered when the class is added")
}
function workOnClassRemoval() {
alert("I'm triggered when the class is removed")
}
// watch for a specific class change
let classWatcher = new ClassWatcher(targetNode, 'trigger', workOnClassAdd, workOnClassRemoval)
// tests:
targetNode.classList.add('trigger') // triggers workOnClassAdd callback
targetNode.classList.add('trigger') // won't trigger (class is already exist)
targetNode.classList.add('another-class') // won't trigger (class is not watched)
targetNode.classList.remove('trigger') // triggers workOnClassRemoval callback
targetNode.classList.remove('trigger') // won't trigger (class was already removed)
targetNode.setAttribute('disabled', true) // won't trigger (the class is unchanged)
I wrapped MutationObserver with a simple class:
class ClassWatcher {
constructor(targetNode, classToWatch, classAddedCallback, classRemovedCallback) {
this.targetNode = targetNode
this.classToWatch = classToWatch
this.classAddedCallback = classAddedCallback
this.classRemovedCallback = classRemovedCallback
this.observer = null
this.lastClassState = targetNode.classList.contains(this.classToWatch)
this.init()
}
init() {
this.observer = new MutationObserver(this.mutationCallback)
this.observe()
}
observe() {
this.observer.observe(this.targetNode, { attributes: true })
}
disconnect() {
this.observer.disconnect()
}
mutationCallback = mutationsList => {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
let currentClassState = mutation.target.classList.contains(this.classToWatch)
if(this.lastClassState !== currentClassState) {
this.lastClassState = currentClassState
if(currentClassState) {
this.classAddedCallback()
}
else {
this.classRemovedCallback()
}
}
}
}
}
}
Well there were mutation events, but they were deprecated and the future there will be Mutation Observers, but they will not be fully supported for a long time. So what can you do in the mean time?
You can use a timer to check the element.
function addClassNameListener(elemId, callback) {
var elem = document.getElementById(elemId);
var lastClassName = elem.className;
window.setInterval( function() {
var className = elem.className;
if (className !== lastClassName) {
callback();
lastClassName = className;
}
},10);
}
Running example: jsFiddle
Here's a simple, basic example on how to trigger a callback on Class attribute change
MutationObserver API
const attrObserver = new MutationObserver((mutations) => {
mutations.forEach(mu => {
if (mu.type !== "attributes" && mu.attributeName !== "class") return;
console.log("class was modified!");
});
});
const ELS_test = document.querySelectorAll(".test");
ELS_test.forEach(el => attrObserver.observe(el, {attributes: true}));
// Example of Buttons toggling several .test classNames
document.querySelectorAll(".btn").forEach(btn => {
btn.addEventListener("click", () => ELS_test.forEach(el => el.classList.toggle(btn.dataset.class)));
});
.blue {background: blue;}
.gold {color: gold;}
<div class="test">TEST DIV</div>
<button class="btn" data-class="blue">BACKGROUND</button>
<button class="btn" data-class="gold">COLOR</button>
Can use this onClassChange function to watch whenever classList of an element changes
function onClassChange(element, callback) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class'
) {
callback(mutation.target);
}
});
});
observer.observe(element, { attributes: true });
return observer.disconnect;
}
var itemToWatch = document.querySelector('#item-to-watch');
onClassChange(itemToWatch, (node) => {
node.classList.contains('active')
? alert('class added')
: alert('class removed');
node.textContent = 'Item to watch. classList: ' + node.className;
});
function addClass() {
itemToWatch.classList.add('active');
}
function removeClass() {
itemToWatch.classList.remove('active');
}
<div id="item-to-watch">Item to watch</div>
<button onclick="addClass();">Add Class</button>
<button onclick="removeClass();">Remove Class</button>
I needed a class update listener for a project, so I whipped this up. I didn’t end up using it, so it’s not fully tested, but should be fine on browsers supporting Element.classList DOMTokenList.
Bonus: allows “chaining” of the 4 supported methods, for example el.classList.remove(“inactive”).remove(“disabled”).add(“active”)
function ClassListListener( el ) {
const ecl = el.classList;
['add','remove','toggle','replace'].forEach(prop=>{
el.classList['_'+prop] = ecl[prop]
el.classList[prop] = function() {
const args = Array.from(arguments)
this['_'+prop].apply(this, args)
el.dispatchEvent(new CustomEvent(
'classlistupdate',
{ detail: { method: prop, args } }
))
return this
}
})
return el
}
Useage:
const el = document.body
ClassListListener(el).addEventListener('classlistupdate', e => {
const args = e.detail.args.join(', ')
console.log('el.classList.'+e.detail.method+'('+args+')')
}, false)
el.classList
.add('test')
.replace('test', 'tested')
The idea is to substitute class manipulation functions, such as 'add', 'remove'... with wrappers, that send class change messages before or after class list changed. It's very simple to use:
choose element(s) or query that selects elements, and pass it to the function.
add 'class-change' and/or 'class-add', 'class-remove'... handlers to either elements or their container ('document', for example).
after that, any class list change by either add, remove, replace or toggle methods will fire corresponding events.
Event sequence is:
A) 'class-change' request event is fired, that can be rejected by handler by preventDefault() if needed. If rejected, then class change will be cancelled.
B) class change function will be executed
B) 'class-add' or 'class-remove'... information event is fired.
function addClassChangeEventDispatcher( el )
{
// select or use multiple elements
if(typeof el === 'string') el = [...document.querySelectorAll( el )];
// process multiple elements
if(Array.isArray( el )) return el.map( addClassChangeEventDispatcher );
// process single element
// methods that are called by user to manipulate
let clMethods = ['add','remove','toggle','replace'];
// substitute each method of target element with wrapper that fires event after class change
clMethods.forEach( method =>
{
let f = el.classList[method];
el.classList[method] = function()
{
// prepare message info
let detail = method == 'toggle' ? { method, className: arguments[0] } :
method == 'replace' ? { method, className: arguments[0], newClassName: arguments[1] } :
{ method, className: arguments[0], classNames: [...arguments] };
// fire class change request, and if rejected, cancel class operation
if(!el.dispatchEvent( new CustomEvent( 'class-change', {bubbles: true, cancelable: true, detail} ))) return;
// call original method and then fire changed event
f.call( this, ...arguments );
el.dispatchEvent( new CustomEvent( 'class-' + method, {bubbles: true, detail} ));
};
});
return el;
}

Categories

Resources