Removing wrapped event handlers - javascript

I have a function which wraps a function around another one, then attaches it to a element.
function addCustomEvent(element, eventName, handler, useCapture) {
var wrappedHandler = function () {
// Do something here.
handler.call();
};
element.addEventListener(eventName, wrappedHandler, useCapture);
}
This works great and I also want to implement this function:
removeCustomEvent(element, eventName, handler, useCapture)
So I want to do something like this.
var clickHandler= function () { /* ... */ };
addCustomEvent(someElement, "click", clickHandler, false);
removeCustomEvent(someElement, "click", clickHandler, false);
There is a problem with this because I don't have a reference to the wrappedHandler in removeCustomEvent.
The only way I can think of now is to keep track of handlers and their corresponding wrappedHandlers in a dictionary so that I can find wrappedHandler from handler within the function, and remove it.
But I'm not fond of this approach because browser must have information about what handlers are attached, so creating a new dictionary seems redundant and waste of memory.
Is there a better, and much cleaner way?

Personally, I'd simply wrap the addCustomEvent and removeCustomEvent to a single module, and keep an object that tracks the bound handlers. You consider this "a waste of resources", but really, the impact of this approach would be negligible.
The upsides are: you have the beginning of a module that can easily be expanded on, to handle more complex event handlers (like simulating a tab event for mobile devices using the touchstart and touchend events).
An alternative approach would be to unbind the event handler internally, depending on the event object itself.
Then, you'll have to re-write your removeCustomEvent function to trigger a special event, that lets the bound handler know that you want to remove the event listener.
//in the wrappedHandler:
var wrappedHandler = function(e)
{
e = e || window.event;
if (e.synthetic === true)
{
e.preventDefault();
e.stopPropagation();
element.removeEventListener(eventName, wrappedHandler, useCapture);//<-- use closure vars
return e;//or return false.
}
//do normal event
handler.apply(this, [e]);//pass event object, and call handler in the same context!
};
var removeCustomEvent = function(event, node, capture)
{
var e, eClass,
doc = node.ownerDocument || (node.nodeType === (document.DOCUMENT_NODE || 9) ? node : document);
if (node.dispatchEvent)
{
if (event === 'click' || event.indexOf('mouse') >= 0)
eClass = 'MouseEvents';
else
eClass = 'HTMLEvents';
e = doc.createEvent(eClass);
e.initEvent(event, !(event === 'change'), true);
//THIS IS THE TRICK:
e.synthetic = true;
node.dispatchEvent(e, true);
return true;
}
if (node.fireEvent)
{
e = doc.createEventObject();
e.synthetic = true;
node.fireEvent('on' + event, e);
return true;
}
event = 'on' + event;
return node[event]();
};
here's a version of this code that is actually documented
I've set a synthetic property on the event object that will be passed to the event handler. the handler checks for this property, and if it's set to true, it will unbind the listener and return. This doesn't require you to keep DOM references and handlers in an object, but this is, I think you'll agree, quite a lot of work, too.
It also feels quite hacky, if you don't mind my saying so...
Compared to:
var binderModule = (function()
{
var module = {},
eventMap = {},
addEvent = function (elem, eventName, handler, capture)
{
var i, wrappedHandler;
if (!eventMap.hasOwnProperty(eventName))
eventMap[eventName] = [];
for (i=0;i<eventMap[eventName].length;++i)
{//look for elem reference
if (eventMap[eventName][i].node === elem)
break;
}
if (i>= eventMap[eventName].length)
{
i = eventMap[eventName].length;//set i to key
eventMap[eventName].push({
node: elem,
handlers: []//keep handlers here, in array for multiple handlers
});
}
wrappedHandler = function(e)
{
//stuff
return handler.apply(this, [e || window.event]);//pass arguments!
};
eventMap[eventNAme][i].handlers.push(wrappedHandler);
return elem.addEventListener(eventName, wrappedHandler, capture);
},
removeEvent(elem, eventName, capture)
{
var i, temp;
if (!eventMap.hasOwnProperty(eventName))
return;//no handlers bound, end here
for (i=0;i<eventMap[eventName].length;++i)
if (eventMap[eventName][i].node === elem)
break;
if (i < eventMap[eventName].length)
{//found element, remove listeners!
//get handlers
temp = eventMap[eventName][i].handlers;
//remove element + handlers from eventMap:
eventMap[evetnName][i] = undefined;
for (i=0;i<temp.length;++i)
elem.removeEventListener(eventName, temp[i], capture);
}
};
module.addCustomEvent = addEvent;
module.removeCustomEvent = removeEvent;
//or, perhaps better:
Object.defineProperty(module, 'addCustomEvent', {value: addEvent});//read-only
Object.defineProperty(module, 'removeCustomEvent', {value: removeEvent});
return module;
}());
Note that this is the basic setup to keep track of event handlers that are bound to particular DOM nodes, and how to mangage them. This code is not finished and is not tested. It probably contains typo's, syntax errors and some consistency issues. But this should be more than enough to get you started.

Related

Can't remove all event listeners

I am trying to remove event listeners from some website but I don't succeed to do it.
This is the event listeners from the website:
There is a javascript script that creates these events with:
document.addEventListener('contextmenu', function (ev) {
ev.preventDefault();
}
)
I tried to remove it with:
document.removeEventListener('contextmenu', function (ev) {
ev.preventDefault();
})
I also tried with:
(getEventListeners(document)).contextmenu = null
But it didn't work, I think because I am using a new function which is not the same one.
Is there a way just to clear all the events ?
Referenced:
Can't remove event listener
You need to specify the function that was bound to removeEventListener
You can do this by creating a function and passing a reference to both addEventListener and removeEventListener.
// create a function
function onRightClick(ev) {
ev.preventDefault();
console.log('onRightClick')
}
// pass the function to both add and remove
document.addEventListener('contextmenu', onRightClick)
document.removeEventListener('contextmenu', onRightClick)
Here is a full example that will cache all the events to the element allowing you to remove specific events, all events for a type of all events.
It's also got a MutationObserver watching the DOM for changes, if an element get's removed so will the events attached to it.
const Events = (() => {
const cache = new Map
const observer = new MutationObserver(function(mutations) {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
if (mutation.removedNodes.length) {
console.log('element removed from the dom, removing all events for the element')
mutation.removedNodes.forEach(x => Events.remove(x))
}
}
}
})
// watch the dom for the element being deleted
observer.observe(document.body, { childList: true })
return {
add(el, type, fn, capture = false) {
let cached = cache.get(el)
if (!cached) {
cached = {}
cache.set(el, cached)
}
if (!cached[type]) {
cached[type] = new Set
}
cached[type].add(fn)
el.addEventListener(type, fn, capture)
},
remove(el, type, fn) {
const cached = cache.get(el)
if (!cached) {
return false
}
// remove all events for an event type
if (type && !fn) {
cached[type].forEach(fn => {
el.removeEventListener(type, fn)
})
cached[type] = new Set
}
// remove a specific event
else if (type && fn) {
el.removeEventListener(type, fn)
// remove the event from the cache
cached[type].delete(fn)
}
// remove all events for the element
else {
for (key in cached) {
cached[key].forEach(fn => {
el.removeEventListener(key, fn)
})
}
cache.delete(el)
}
},
show(el, type) {
const cached = cache.get(el)
if (!cached) {
return false
}
if (type) {
return cached[type]
}
return cached
}
}
})()
function onRightClick() {}
Events.add(document, 'contextmenu', onRightClick)
Events.remove(document, 'contextmenu', onRightClick) // remove a specific event callback
Events.remove(document, 'contextmenu') // remove specific event types from an element
Events.remove(document) // remove all events from an element
const testElement = document.querySelector('#test_element')
Events.add(testElement, 'click', function deleteSelf(e) {
this.parentNode.removeChild(this)
})
<div id="test_element">
when you <strong>click me</strong> I will be deleted from the DOM which will fire the MutationObserver to remove all my events
</div>
You can try cloning the element to which you've added all the listeners and add it back to its parent. With cloning, you lose all the listeners attached to the element. Try this,
var element = document.getElementById('myElement'),
clone = el.cloneNode(true);
element.parentNode.replaceChild(clone, element);
However, this won't work on global event listeners, or simply, those set directly on document instead of an element as the document is the root of hierarchy (can't have parentNode)
To get rid of unknown event listeners you can clone the element and then move the original's content into the cloned one and then replace the original one with the clone.
If you don't care about the contained elements' event listeners, you can also deep clone the original with .clone(true) (or false, can't remember). Then you don't have to move the contents over.

Remove All Event Listeners of Specific Type

I want to remove all event listeners of a specific type that were added using addEventListener(). All the resources I'm seeing are saying you need to do this:
elem.addEventListener('mousedown',specific_function);
elem.removeEventListener('mousedown',specific_function);
But I want to be able to clear it without knowing what it is currently, like this:
elem.addEventListener('mousedown',specific_function);
elem.removeEventListener('mousedown');
That is not possible without intercepting addEventListener calls and keep track of the listeners or use a library that allows such features unfortunately. It would have been if the listeners collection was accessible but the feature wasn't implemented.
The closest thing you can do is to remove all listeners by cloning the element, which will not clone the listeners collection.
Note: This will also remove listeners on element's children.
var el = document.getElementById('el-id'),
elClone = el.cloneNode(true);
el.parentNode.replaceChild(elClone, el);
If your only goal by removing the listeners is to stop them from running, you can add an event listener to the window capturing and canceling all events of the given type:
window.addEventListener(type, function(event) {
event.stopImmediatePropagation();
}, true);
Passing in true for the third parameter causes the event to be captured on the way down. Stopping propagation means that the event never reaches the listeners that are listening for it.
Keep in mind though that this has very limited use as you can't add new listeners for the given type (they will all be blocked). There are ways to get around this somewhat, e.g., by firing a new kind of event that only your listeners would know to listen for. Here is how you can do that:
window.addEventListener('click', function (event) {
// (note: not cross-browser)
var event2 = new CustomEvent('click2', {detail: {original: event}});
event.target.dispatchEvent(event2);
event.stopPropagation();
}, true);
element.addEventListener('click2', function(event) {
if (event.detail && event.detail.original) {
event = event.detail.original
}
// Do something with event
});
However, note that this may not work as well for fast events like mousemove, given that the re-dispatching of the event introduces a delay.
Better would be to just keep track of the listeners added in the first place, as outlined in Martin Wantke's answer, if you need to do this.
You must override EventTarget.prototype.addEventListener to build an trap function for logging all 'add listener' calls. Something like this:
var _listeners = [];
EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener)
{
_listeners.push({target: this, type: type, listener: listener});
this.addEventListenerBase(type, listener);
};
Then you can build an EventTarget.prototype.removeEventListeners:
EventTarget.prototype.removeEventListeners = function(targetType)
{
for(var index = 0; index != _listeners.length; index++)
{
var item = _listeners[index];
var target = item.target;
var type = item.type;
var listener = item.listener;
if(target == this && type == targetType)
{
this.removeEventListener(type, listener);
}
}
}
In ES6 you can use a Symbol, to hide the original function and the list of all added listener directly in the instantiated object self.
(function()
{
let target = EventTarget.prototype;
let functionName = 'addEventListener';
let func = target[functionName];
let symbolHidden = Symbol('hidden');
function hidden(instance)
{
if(instance[symbolHidden] === undefined)
{
let area = {};
instance[symbolHidden] = area;
return area;
}
return instance[symbolHidden];
}
function listenersFrom(instance)
{
let area = hidden(instance);
if(!area.listeners) { area.listeners = []; }
return area.listeners;
}
target[functionName] = function(type, listener)
{
let listeners = listenersFrom(this);
listeners.push({ type, listener });
func.apply(this, [type, listener]);
};
target['removeEventListeners'] = function(targetType)
{
let self = this;
let listeners = listenersFrom(this);
let removed = [];
listeners.forEach(item =>
{
let type = item.type;
let listener = item.listener;
if(type == targetType)
{
self.removeEventListener(type, listener);
}
});
};
})();
You can test this code with this little snipper:
document.addEventListener("DOMContentLoaded", event => { console.log('event 1'); });
document.addEventListener("DOMContentLoaded", event => { console.log('event 2'); });
document.addEventListener("click", event => { console.log('click event'); });
document.dispatchEvent(new Event('DOMContentLoaded'));
document.removeEventListeners('DOMContentLoaded');
document.dispatchEvent(new Event('DOMContentLoaded'));
// click event still works, just do a click in the browser
Remove all listeners on a global event
element.onmousedown = null;
now you can go back to adding event listeners via
element.addEventListener('mousedown', handler, ...);
This solution only works on "Global" events. Custom events won't work. Here's a list of all global events: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers
I know this is old, but I had a similar issue with no real answers, where I wanted to remove all keydown event listeners from the document. Instead of removing them, I override the addEventListener to ignore them before they were even added, similar to Toms answer above, by adding this before any other scripts are loaded:
<script type="text/javascript">
var current = document.addEventListener;
document.addEventListener = function (type, listener) {
if(type =="keydown")
{
//do nothing
}
else
{
var args = [];
args[0] = type;
args[1] = listener;
current.apply(this, args);
}
};
</script>
A modern way to remove event listeners without referencing the original function is to use AbortController. A caveat being that you can only abort the listeners that you added yourself.
const buttonOne = document.querySelector('#button-one');
const buttonTwo = document.querySelector('#button-two');
const abortController = new AbortController();
// Add multiple click event listeners to button one
buttonOne.addEventListener(
'click',
() => alert('First'),
{ signal: abortController.signal }
);
buttonOne.addEventListener(
'click',
() => alert('Second'),
{ signal: abortController.signal }
);
// Add listener to remove first button's listeners
buttonTwo.addEventListener(
'click',
() => abortController.abort()
);
<p>The first button will fire two alert dialogs when clicked. Click the second button to remove those listeners from the first button.</p>
<button type="button" id="button-one">Click for alerts</button>
<button type="button" id="button-two">Remove listeners</button>
Remove all listeners in element by one js line:
element.parentNode.innerHTML += '';
You cant remove a single event, but all? at once? just do
document.body.innerHTML = document.body.innerHTML
In the extreme case of not knowing which callback is attached to a window listener, an handler can be wrapper around window addEventListener and a variable can store ever listeners to properly remove each one of those through a removeAllEventListener('scroll') for example.
var listeners = {};
var originalEventListener = window.addEventListener;
window.addEventListener = function(type, fn, options) {
if (!listeners[type])
listeners[type] = [];
listeners[type].push(fn);
return originalEventListener(type, fn, options);
}
var removeAllEventListener = function(type) {
if (!listeners[type] || !listeners[type].length)
return;
for (let i = 0; i < listeners[type].length; i++)
window.removeEventListener(type, listeners[type][i]);
}
So this function gets rid of most of a specified listener type on an element:
function removeListenersFromElement(element, listenerType){
const listeners = getEventListeners(element)[listenerType];
let l = listeners.length;
for(let i = l-1; i >=0; i--){
removeEventListener(listenerType, listeners[i].listener);
}
}
There have been a few rare exceptions where one can't be removed for some reason.
You could alternatively overwrite the 'yourElement.addEventListener()' method and use the '.apply()' method to execute the listener like normal, but intercepting the function in the process. Like:
<script type="text/javascript">
var args = [];
var orginalAddEvent = yourElement.addEventListener;
yourElement.addEventListener = function() {
//console.log(arguments);
args[args.length] = arguments[0];
args[args.length] = arguments[1];
orginalAddEvent.apply(this, arguments);
};
function removeListeners() {
for(var n=0;n<args.length;n+=2) {
yourElement.removeEventListener(args[n], args[n+1]);
}
}
removeListeners();
</script>
This script must be run on page load or it might not intercept all event listeners.
Make sure to remove the 'removeListeners()' call before using.
var events = [event_1, event_2,event_3] // your events
//make a for loop of your events and remove them all in a single instance
for (let i in events){
canvas_1.removeEventListener("mousedown", events[i], false)
}

how to trigger "online" event manually [duplicate]

I was wondering if anyone can help me understand how exactly to create different Custom event listeners.
I don't have a specific case of an event but I want to learn just in general how it is done, so I can apply it where it is needed.
What I was looking to do, just incase some folks might need to know, was:
var position = 0;
for(var i = 0; i < 10; i++)
{
position++;
if((position + 1) % 4 == 0)
{
// do some functions
}
}
var evt = document.createEvent("Event");
evt.initEvent("myEvent",true,true);
// custom param
evt.foo = "bar";
//register
document.addEventListener("myEvent",myEventHandler,false);
//invoke
document.dispatchEvent(evt);
Here is the way to do it more locally, pinpointing listeners and publishers:
http://www.kaizou.org/2010/03/generating-custom-javascript-events/
Implementing custom events is not hard. You can implement it in many ways. Lately I'm doing it like this:
/***************************************************************
*
* Observable
*
***************************************************************/
var Observable;
(Observable = function() {
}).prototype = {
listen: function(type, method, scope, context) {
var listeners, handlers;
if (!(listeners = this.listeners)) {
listeners = this.listeners = {};
}
if (!(handlers = listeners[type])){
handlers = listeners[type] = [];
}
scope = (scope ? scope : window);
handlers.push({
method: method,
scope: scope,
context: (context ? context : scope)
});
},
fireEvent: function(type, data, context) {
var listeners, handlers, i, n, handler, scope;
if (!(listeners = this.listeners)) {
return;
}
if (!(handlers = listeners[type])){
return;
}
for (i = 0, n = handlers.length; i < n; i++){
handler = handlers[i];
if (typeof(context)!=="undefined" && context !== handler.context) continue;
if (handler.method.call(
handler.scope, this, type, data
)===false) {
return false;
}
}
return true;
}
};
The Observable object can be reused and applied by whatever constructor needs it simply by mixng the prototype of Observable with the protoype of that constructor.
To start listening, you have to register yourself to the observable object, like so:
var obs = new Observable();
obs.listen("myEvent", function(observable, eventType, data){
//handle myEvent
});
Or if your listener is a method of an object, like so:
obs.listen("myEvent", listener.handler, listener);
Where listener is an instance of an object, which implements the method "handler".
The Observable object can now call its fireEvent method whenever something happens that it wants to communicate to its listeners:
this.fireEvent("myEvent", data);
Where data is some data that the listeners my find interesting. Whatever you put in there is up to you - you know best what your custom event is made up of.
The fireEvent method simply goes through all the listeners that were registered for "myEvent", and calls the registered function. If the function returns false, then that is taken to mean that the event is canceled, and the observable will not call the other listeners. As a result the entire fireEvent method will return fasle too so the observable knows that whatever action it was notifying its listeners of should now be rolled back.
Perhaps this solution doesn't suit everybody, but I;ve had much benefit from this relatively simple piece of code.
From here:
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
// create the event
const event = new Event('build');
// elem is any element
elem.dispatchEvent(event);
// later on.. binding to that event
// we'll bind to the document for the event delegation style.
document.addEventListener('build', function(e){
// e.target matches the elem from above
}, false);
Here is a really simple (TypeScript/Babelish) implementation:
const simpleEvent = <T extends Function>(context = null) => {
let cbs: T[] = [];
return {
addListener: (cb: T) => { cbs.push(cb); },
removeListener: (cb: T) => { let i = cbs.indexOf(cb); cbs.splice(i, Math.max(i, 0)); },
trigger: (<T> (((...args) => cbs.forEach(cb => cb.apply(context, args))) as any))
};
};
You use it like this:
let onMyEvent = simpleEvent();
let listener = (test) => { console.log("triggered", test); };
onMyEvent.addListener(listener);
onMyEvent.trigger("hello");
onMyEvent.removeListener(listener);
Or in classes like this
class Example {
public onMyEvent = simpleEvent(this);
}
If you want plain JavaScript you can transpile it using TypeScript playground.

Jquery not deep copying from event handler

I have an event handler set up using plain javascript like this:
myElement.addEventListener('drop', handleDrop, false);
Then, inside handle drop I try to do this:
var myContainer = $(this.parentNode);
myContainer.after(myContainer.clone(true, true));
However, it appears that the event is not being carried over to the cloned element. Is this happening because I am not binding the event with jQuery also?
I tried to test this by binding the event with jQuery instead, but that doesn't support the dataTransfer object so it broke other code.
One solution is to write your own wrapper for addEventListener that remembers the listeners which were added, so they can be "replayed":
// set an event handler after memoizing it
function myAddEventListener(element, type, listener, useCapture) {
// store listeners as an array under element.listeners
if (!element.listeners) { element.listeners=[]; }
// each element of the array is an array of arguments to addEventListener
element.listeners[element.listeners.length] =
Array.prototype.slice.call(arguments,1);
// apply listener to element itself
element.addEventListener (type, listener, useCapture);
}
// copy a list of event handlers from one element to another
function copyEventListeners (from_element, to_element) {
var i;
if (from_element.listeners) {
for (i=0; i<from_element.listeners.length; i++) {
Element.addEventListener.apply (to_element, from_element.listeners[i]);
}
}
}
Then:
function clone_with_listeners (element) {
var cloned_element = element.cloneNode();
copyEventListeners (element, cloned_element);
return cloned_element;
}
If you have no religious convictions preventing you from overwriting the original method on the Element object:
var orgAddEventListener = Element.addEventListener;
// our version of addEventListener
Element.addEventListener = function (type, listener, useCapture) {
// store listeners as an array under element.listeners
if (!this.listeners) { this.listeners=[]; }
// each element of the array is an array of arguments to addEventListener
this.listeners[element.listeners.length] =
Array.prototype.slice.call (arguments,0);
// apply listener to element itself
orgAddEventListener.call (element, type, listener, useCapture);
};
// copy a list of event handlers from this element to another
Element.copyEventListeners = function (to_element) {
var i;
if (from_element.listeners) {
for (i=0; i<this.listeners.length; i++) {
Element.addEventListener.apply (to_element, this.listeners[i]);
}
}
};
and then:
Element.cloneNode = function () {
var cloned_element = this.cloneNode();
this.copyEventListeners (cloned_element);
return cloned_element;
};

Use Chrome extension to unbind click events?

I'm trying to make an extension that unbinds a click event added by the website itself.
The website uses jQuery, which would make this insanely simple:
jQuery('a[rel]').unbind('click');
The problem is that my extension (using "content_scripts") can't access the website's jQuery object and therefore doesn't have the event functions to unbind. I can include a jQuery in my extension, but that wouldn't help, because jQuery stores 'data' in the jQuery object (not in the DOM elements). My jQuery will not have those events stored.
Is there another way? It doesn't have to be pretty. Maybe without "content_scripts"?
var unbind_event_listeners = function (node) {
var parent = node.parentNode;
if (parent) {
parent.replaceChild(node.cloneNode(true), node);
} else {
var ex = new Error("Cannot remove event listeners from detached or document nodes");
ex.code = DOMException[ex.name = "HIERARCHY_REQUEST_ERR"];
throw ex;
}
};
Just call unbind_event_listeners(a_node) to unbind any listeners from a node. This will work on every node in the document except document itself. As for window, you're out of luck. unbind_event_listeners(document.documentElement) should remove most event listeners attached to nodes in the document.
In the case of a[rel], you'd want to do this:
var nodes = document.querySelectorAll("a[rel]"), i = nodes.length;
while (i--) {
unbind_event_listeners(nodes.item(i));
}
If it doesn't need to be pretty and you're okay with doing things slightly hack-like, this should forcefully unbind every click listener bound to that element:
var el = document.querySelector('a[rel]');
el.onclick = function() {};
el.addEventListener = function() {};
or for every element:
Array.prototype.slice.call(document.querySelectorAll('a[rel]')).forEach(function(el) {
el.onclick = function() {};
el.addEventListener = function() {};
});
EDIT: You could maybe do something even uglier and have a content script run at "document_start" and do:
Element.prototype.addEventListener = (function() {
var real = Element.prototype.addEventListener;
return function(ev) {
if (ev === 'click' && this.tagName === 'A' && this.hasAttribute('rel')) {
console.log('try again, jquery!');
} else {
return real.apply(this, arguments);
}
};
})();

Categories