Some code I am working with replaces some HTML elements that have Dojo event listeners with new HTML coming from an AJAX call (using .innerHTML=). I have read that event listeners should be disconnected using the dojo.disconnect(handle) method before they are replaced to prevent memory leaks.
Is it possible to derive all handles connected to a particular element, so that I can pass each one to .disconnect(handle), or is it up to me to maintain this list in my code?
Actually if you are using widgets they normally should disconnect stuff in tehir destroy() method. If you are handling the nodes yourself, I see two ways you can go.
1) Manage all connects manually, means storing them somewhere.
2) Probably the safer one: store all the connect handlers in the node they connect to, like so:
node._connectHandlers = [];
node._connectHandlers.push(dojo.connect(node, "onclick", ...));
And later you can simply disconnect them all using
dojo.query("*", nodeContainingConnects).forEach(function(node){
if (typeof node._connectHandlers!="undefined"){
dojo.forEach(node._connectHandlers, "dojo.disconnect(item)");
}
});
Actually, this may work well, but there might be a more efficient way to get all connects by nodes. I just didnt find it. hth
Following the answer of Wolfram Kriesing this can be "improved":
dojo._connect_tmp = dojo.connect;
dojo.connect = function (obj, event, context, method, dontFix) {
if(obj._connectHandlers == undefined){ obj._connectHandlers = [];}
var handler = dojo._connect_tmp (obj, event, context, method, dontFix);
obj._connectHandlers.push(handler);
return handler;
};
dojo.iwanttobefree = function (obj) {
if(obj._connectHandlers == undefined) {
} else {
dojo.forEach(obj._connectHandlers, "dojo.disconnect(item)");
}
};
Then you can do this:
dojo.connect(myObj, 'onfocus', function(){alert('weee')});
dojo.iwanttobefree(myObj);
Replacing dojo code can be very very very ugly for multiple reasons, so maybe you want to create your own namespace.
Related
Is there any way to get the list of all event listeners of an element on the HTML page using JavaScript on that page.
Note: I know we can see them with Chrome dev tools event listeners but I want to log/access see list using the JavaScript of the page.
Also, I know we can get them through jQuery but for that, we also have to apply the events using jQuery, but I want something that would be generic so I could also access the event listeners applied to other elements such as web components or react components.
If you really had to, a general way to do this would be to patch EventTarget.prototype.addEventListener:
const listeners = [];
const orig = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(...args) {
if (this instanceof HTMLElement) {
listeners.push({
type: args[0],
fn: args[1],
target: this,
});
}
return orig.apply(this, args);
};
document.body.addEventListener('click', () => console.log('body clicked'));
console.log(listeners[0].fn);
click this body
To find listeners attached to an element, iterate through the listeners array and look for targets which match the element you're looking for.
To be complete, also patch removeEventListener so that items can be removed from the array when removed.
If you need to watch for listeners attached via on, then you'll have to do something similar to the above to patch the HTMLElement.prototype.onclick getter/setter, and for each listener you want to be able to detect.
That said, although you said you want a generic solution, rather than patching built-in prototypes, it'd be better to add the listeners through jQuery or through your own function.
What I did when I had a similar problem is add a data attribute when the listener was set, so I could identify it later.
At the end of the function that adds the listener:
elm.setAttribute('data-has_mask', true);
At the beginning of that same function:
if("true" == elm.getAttribute('data-has_mask')) {
return;
}
Maybe not exactly what the OP is looking for, but I was having a lot of trouble with this, and this is an obvious solution for a particular use case, and I guess it might help someone out.
I have a context menuitem which is activated if an image is right-clicked, the exact same way that 'context-copyimage' is activated.
Is it possible to tie/pair that menuitem to the 'context-copyimage' therefore eliminating the need to add extra (duplicate) event-listeners and show/hide handlers??!!
(Adding an observer to 'context-copyimage' defeats the purpose)
If not, is it possible to use the event-listener that 'context-copyimage' uses?
Update:
I am trying to reduce listeners. At the moment, script has a popupshowing listeners. On popupshowing, it checks for gContextMenu.onImag and if true, it shows the menuitem. Firefox's context-copyimage does the exact same thing. I was wondering if it was possible to tie these 2 in order to remove/reduce the in-script event listeners.
I was also chatting with Dagger and he said that:
... the state of built-in items isn't set from an event handler, it's
set from the constructor for nsContextMenu, and there are no
mechanisms to hook into it
So it seems, that is not possible
No, there is no sane way of avoiding the event listener that would perform better than another event listener and is compatible with unloading the add-on in session.
Hooking nsContextMenu
As you have been already told, the state is initialized via gContextMenu = new nsContextMenu(...). So you'd need to hook the stuff, which is actually quite easy.
var newProto = Object.create(nsContextMenu.prototype);
newProto.initMenuOriginal = nsContextMenu.prototype.initMenu;
newProto.initMenu = function() {
let rv = this.initMenuOriginal.apply(this, arguments);
console.log("ctx", this.onImage, this); // Or whatever code you'd like to run.
return rv;
};
nsContextMenu.prototype = newProto;
Now, the first question is: Does it actually perform better? After all this just introduced another link in the prototype-chain. Of course, one could avoid Object.create and just override nsContextMenu.prototype.initMenu directly.
But the real question is: How would one remove the hook again? Answer: you really cannot, as other add-ons might have hooked the same thing after you and unhooking would also unhook the other add-ons. But you need to get rid of the reference, or else the add-on will leak memory when disabled/uninstalled. Well, you could fight with Components.utils.makeObjectPropsNormal, but that doesn't really help with closed-over variables. So lets avoid closures... Hmm... You'd need some kind of messaging, e.g. event listeners or observers... and we're back to square one.
Also I wouldn't call this sane compared to
document.getElementById("contentAreaContextMenu").addEventListener(...)
I'd call it "overkill for no measurable benefit".
Overriding onpopupshowing=
One could override the <menupopup onpopupshowing=. Yeah, that might fly... Except that other add-ons might have the same idea, so welcome to compatibility hell. Also this again involves pushing stuff into the window, which causes cross-compartment wrappers, which makes things error-prone again.
Is this a solution? Maybe, but not a sane one.
What else?
Not much, really.
Yes this is absolutely possible.
Morat from mozillazine gave a great solution here: http://forums.mozillazine.org/viewtopic.php?p=13307339&sid=0700480c573017c00f6e99b74854b0b2#p13307339
function handleClick(event) {
window.removeEventListener("click", handleClick, true);
event.preventDefault();
event.stopPropagation();
var node = document.popupNode;
document.popupNode = event.originalTarget;
var menuPopup = document.getElementById("contentAreaContextMenu");
var shiftKey = false;
gContextMenu = new nsContextMenu(menuPopup, shiftKey);
if (gContextMenu.onImage) {
var imgurl = gContextMenu.mediaURL || gContextMenu.imageURL;
}
else if (gContextMenu.hasBGImage && !gContextMenu.isTextSelected) {
var imgurl = gContextMenu.bgImageURL;
}
console.log('imgurl = ', imgurl)
document.popupNode = node;
gContextMenu = null;
}
window.addEventListener("click", handleClick, true);
this gives you access to gContextMenu which has all kinds of properties like if you are over a link, or if you right click on an image, and if you did than gContextMenu.imageURL holds its value. cool stuff
This code here console logs imgurl, if you are not over an image it will log undefined
I am making a custom events object so that I can inject custom events into another object, so far I have (simplified),
function Game() {
new Events(["pause", "resume"], this);
};
function Events(events, obj) {
// Event object for object
obj.events = {};
// For each event
events.forEach(function(event) {
// Attach event array to store callbacks
this.events[event] = [];
}, obj);
// Fire event
obj.fire = function(event) {
////////////
};
// Add event
obj.on = function(event, callback) {
////////////
};
};
My question is, is this the right way to do this? Is it considered ok to call Event from Game and add to Game from Events? It for some reason seems wrong to me, and I do not know why?
Is there any way that I should be structuring this code that I am not aware of?
(I do not want to add Events to Game's prototype for the sole reason that Game has events and is not an extension of Events)
Thank you for taking the time to read my question.
My question is, is this the right way to do this? Is it considered ok to call Event from Game and add to Game from Events?
Yes, it's perfectly fine. This is called the decorator pattern. I don't see a reason for your feeling that it was wrong.
Is there any way that I should be structuring this code that I am not aware of?
Don't use new. Events is not a constructor. A better and more descriptive signature might be
function makeEventEmitter(obj, events) {
I have Javascript that people are including in their page. In my Javascript I have a version of jQuery (1.8 for sake of easy reference) that is sectioned off into its own namespace, and referenced via a global variable (but not one of the two default vars of "$" or "jQuery"). This allows users to have jQuery in their page and have it not interfere with the stuff I'm doing internally in my functions.
So we have one page that has jQuery already (1.4), and everything works fine, except that the user and my code are both listening to "click" events on elements, and theirs is going first, so on the few events they do that return false, jQuery stops propagation and my event never gets triggered. I need my event to go first. The user is expecting my onClick functionality to still work.
Now I know that jQuery keeps its own order of events internally through the _data() object, and through this it is possible to unbind existing events, bind my event, then rebind the existing events, but that only applies to objects bound through that instance of jQuery. I'd rather not just blindly look for the jQuery object in hopes that the conflict was introduced by a user's own version of jQuery. After all what happens when a user binds the event not through jQuery? Trying to manipulate the existing jQuery object in the page isn't a good solution.
I know that, depending on browser, they are using addEventListener/removeEventListener or attachEvent/detachEvent. If only I could get a listing of the already added events, I could rebind them in the order I wanted, but I can't find out how. Looking through the DOM via Chrome inspect I don't see onclick bound anywhere (not on the object, not on window or document either).
I'm having the darndest time trying to figure out just exactly where jQuery binds its listening. To be able to control the order of its own events, jQuery must blanketly listen somewhere and then fire off its own functions right? If I could figure out where that's done I might get some insight into how to ensure my event is always first. Or maybe there's some Javascript API I haven't been able to find on Google.
Any suggestions?
We solved this by just adding a little jQuery extension that inserts events at the head of the event chain:
$.fn.bindFirst = function(name, fn) {
var elem, handlers, i, _len;
this.bind(name, fn);
for (i = 0, _len = this.length; i < _len; i++) {
elem = this[i];
handlers = jQuery._data(elem).events[name.split('.')[0]];
handlers.unshift(handlers.pop());
}
};
Then, to bind your event:
$(".foo").bindFirst("click", function() { /* Your handler */ });
Easy peasy!
As Bergi and Chris Heald said in the comments, it turns out there's no way to get at the existing events from the DOM, and no method to insert events "first". They are fired in the order they were inserted by design, and hidden by design. As a few posters mentioned you have access to the ones added through the same instance of jQuery that you're using via jQuery's data, but that's it.
There is one other case where you can run before an event that was bound before your code ran, and that's if they used the "onclick" HTML attribute. In that case you can write a wrapper function, as nothingisnecessary pointed out in a rather over-the-top toned comment below. While this wouldn't help in the instance of the original question I asked, and it's now very rare for events to be bound this way (most people and frameworks use addEvent or attachEventListener underneath now), it is one scenario in which you can solve the issue of "running first", and since a lot of people visit this question looking for answers now, I thought I'd make sure the answer is complete.
I encounter an opposite situation where I was asked to include a library, which uses event.stopImmediatePropagation() on an element, to our website. So some of my event handlers are skipped. Here is what I do (as answered here):
<span onclick="yourEventHandler(event)">Button</span>
Warning: this is not the recommended way to bind events, other developers may murder you for this.
Its not a proper solution, but ... You can add event handler to parent node in capture phase. Not on target element itself!
<div>
<div id="target"></div>
</div>
target.parentNode.addEventListener('click',()=>{console.log('parent capture phase handler')},true)
Third argument in addEventListener means:
true - capture phase
false - bubble phase
Helpful links:
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
https://javascript.info/bubbling-and-capturing
Found it easiest to add addListener and removeListener methods to document (as that's only where I need them - I suppose you can use Element.prototype and this instead). Only one "real" listener is added per type, and it's just a func to call the actual listeners in order. The eventListeners dictionary is added to document (so can mess with the handler or order).
[edit]
I think the correct answer for most cases is to use the 3rd argument of addEventListener: https://stackoverflow.com/a/29923421. The answer below ignores the argument (on purpose).
[edit] Updated code to only add one extra property: document.eventHandlers + modified naming.
// Storage.
document.eventListeners = {}; // { type: [ handlerFunc, listenerFuncs ] }
// Add event listener - returns index.
document.addListener = (type, listener, atIndex) => {
// Get info.
const listening = document.eventListeners[type];
// Add to existing.
if (listening) {
// Clean up.
atIndex = atIndex || 0;
const listeners = listening[1]; // Array of funcs.
// Already has.
const iExists = listeners.indexOf(listener);
if (iExists !== -1) {
// Nothing to do.
if (iExists === atIndex)
return atIndex;
// Remove from old position.
listeners.splice(atIndex, 1);
}
// Add (supporting one cycle of negatives).
const nListeners = listeners.length;
if (atIndex > nListeners)
atIndex = nListeners;
else if (atIndex < 0)
atIndex = Math.max(0, atIndex + nListeners + 1);
listeners.splice(atIndex, 0, listener);
}
// New one.
else {
// Handler func.
const handler = (...args) => {
const listening = document.eventListeners[type];
if (listening) {
const listeners = listening[1]; // Array of funcs.
for (const listener of listeners)
listener(...args);
}
};
// Update dictionary.
document.eventListeners[type] = [ handler, [ listener ] ];
// Add listener.
document.addEventListener(type, handler);
// First one.
atIndex = 0;
}
// Return index.
return atIndex;
};
// Remove event listener - returns index (-1 if not found).
document.removeListener = (type, listener) => {
// Get info.
const listening = document.eventListeners[type];
if (!listening)
return -1;
// Check if exists.
const listeners = listening[1];
const iExists = listeners.indexOf(listener);
if (iExists !== -1) {
// Remove listener.
listeners.splice(iExists, 1);
// If last one.
if (!listeners.length) {
// Remove listener.
const handlerFunc = listening[0];
document.removeEventListener(type, handlerFunc);
// Update dictionary.
delete document.eventListeners[type];
}
}
// Return index.
return iExists;
}
Aliaksei Pavlenkos suggestion about useCapture can be used. His allegation that it must be attached to the parent node is wrong: MDN
Event listeners in the “capturing” phase are called before event listeners in any non-capturing phases
target.addEventListener(type, listener, useCapture);
Just so it's said, I think this might be possible if you override the native implementations of these functions. This is BAD practice - very bad practice when developing a library to alter native implementations, because it can easily conflict with other libraries.
However, for completeness, here's one possibility (completely untested, just demonstrating the general concept):
// override createElement()
var temp = document.createElement;
document.createElement = function() {
// create element
var el = document.createElement.original.apply(document, arguments);
// override addEventListener()
el.addEventListenerOriginal = el.addEventListener;
el._my_stored_events = [];
// add custom functions
el.addEventListener = addEventListenerCustom;
el.addEventListenerFirst = addEventListenerFirst;
// ...
};
document.createElement.original = temp;
// define main event listeners
function myMainEventListeners(type) {
if (myMainEventListeners.all[type] === undefined) {
myMainEventListeners.all[type] = function() {
for (var i = 0; i < this._my_stored_events.length; i++) {
var event = this._my_stored_events[i];
if (event.type == type) {
event.listener.apply(this, arguments);
}
}
}
}
return myMainEventListeners.all[type];
}
myMainEventListeners.all = {};
// define functions to mess with the event list
function addEventListenerCustom(type, listener, useCapture, wantsUntrusted) {
// register handler in personal storage list
this._my_stored_events.push({
'type' : type,
'listener' : listener
});
// register custom event handler
if (this.type === undefined) {
this.type = myMainEventListeners(type);
}
}
function addEventListenerFirst(type, listener) {
// register handler in personal storage list
this._my_stored_events.push({
'type' : type,
'listener' : listener
});
// register custom event handler
if (this.type === undefined) {
this.type = myMainEventListeners(type);
}
}
// ...
A lot more work would need to be done in this regard to truly lock this down, and again, it's best not to modify native libraries. But it's a useful mental exercise that helps to demonstrate the flexibility JavaScript provides in solving problems like this.
I want to add an listener exactly once for beforeunload. This is my pseudocode:
if(window.hasEventListener('beforeunload') === false) {
window.addEventListener('beforeunload', function() { ... }, false);
}
But hasEventListener does not exist obviously. How can I achieve this? Thanks.
In fact there is no need to check if an listener was added to a target:
If multiple identical EventListeners are registered on the same EventTarget with the same parameters, the duplicate instances are discarded. They do not cause the EventListener to be called twice, and since the duplicates are discarded, they do not need to be removed manually with the removeEventListener method.
Source:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener#Multiple_identical_event_listeners
Using jquery you can do use data("events") on any object (here the window) :
var hasbeforeunload = $(window).data("events") && $(window).data("events").['beforeunload'];
But this works only for jquery added events.
In a more general case, you should simply store the information that you add a listener somewhere :
var addedListeners = {};
function addWindowListenerIfNone(eventType, fun) {
if (addedListeners[eventType]) return;
addedListeners[eventType] = fun;
window.addEventListener(eventType, fun);
}
I think there is no standard way in javascript to get the existing event handlers. At best you could surcharge the addEventListener function of Node to intercept and store the listeners but I don't recommend it...
EDIT :
From jQuery 1.8, event data are available in $._data(element, "events"). The change log has a warning that should be taken into account :
Note that this is not a supported public interface; the actual data
structures may change incompatibly from version to version.
In Chrome Dev tool, you can check all events attached to an element (For debugging)-
// print all events attached to document
var eventObjectAttachedToDocument = getEventListeners(document);
for (var event in eventObjectAttachedToDocument) {
console.log(event);
}