Reputation: 70
I'm looking for a way to list all events added to a certain element by Javascript. I know getEventListeners
is available in dev tools, but it is not available in script.
I know with jQuery, I can check events with the below code. However, as this is a private method, it can only list events added by jQuery.
$._data(element, 'events')
I also checked this page, but this method needs to be executed before events are added. https://www.sqlpac.com/en/documents/javascript-listing-active-event-listeners.html
After all, is there no general way to check if an element has events after those events are added?
Upvotes: 3
Views: 401
Reputation: 44088
Stefan Steiger's answer is correct in that a prototype
of a built-in object (EventTarget
being the best choice) must override the addEventListener()
(and removeEventListener()
) method in order to find them. My answer includes this and the ability to list all on-attribute handlers as well. The example below is a modernized version of the code here.
Details are commented in the example. View in Full Page mode, the list is long.
/**
* Override EventTarget.prototype addEventListener() method so
* when it's used an object (listenerList) is bound to the
* DOM node that is registered to the given event.type (etype).
* For every event.type the node is registered to it will have
* an array of objects (listenerList[eType]). Each object
* within those arrays will have function (callback/handler)
* and capture (options/config) data.
* @param {string} etype - event.type
* @param {function} callback - The event handler function.
* @param {object|boolean} config - Options as an object or
* capture as a boolean.
* If undefined it defaults
* to false.
*/
const addEL = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(etype, handler, config) {
if (!config) config = false;
addEL.call(this, etype, handler, config);
if (!this.listenerList) this.listenerList = {};
if (!this.listenerList[etype]) this.listenerList[etype] = [];
this.listenerList[etype].push({
callback: handler,
options: config
});
};
/**
* Override EventTarget.prototype removeEventListener() method
* so when it's used the corresponding object nested within an
* array of objects (listenerList[evType]) that's nested within
* a node's bound object (listenerList).
* (listenerList) is removed.
* @param {string} etype - event.type
* @param {function} callback - The event handler function.
* @param {object|boolean} config - Options as an object or
* capture as a boolean.
* If undefined it defaults
* to false.
*/
const remEL = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function(etype, handler, config) {
if (!config) config = false;
remEL.call(this, etype, handler, config);
if (!this.listenerList) this.listenerList = {};
if (!this.listenerList[etype]) this.listenerList[etype] = [];
this.listenerList[etype].forEach((listener, index) => {
if (listener.callback == handler, listener.options == config) {
this.listenerList[etype].splice(index, 1);
}
});
if (this.listenerList[etype].length === 0) {
delete this.listenerList[etype];
}
};
/**
* Add a method to EventTarget.prototype getListeners() which
* returns the object (listenerList) in it's entirity unless
* the @param etype is specified to which it will only return
* the array of objects (listenerList[etype]) containing data
* for etype only.
* @param {string} etype - event.type
* @return {object|array} - If event.type is specified (etype)
* then it's an array
* (listenerList[etype]).
* If undefined then the main object
* is returned (listenerList).
*/
EventTarget.prototype.getListeners = function(etype) {
if (!this.listenerList) this.listenerList = {};
if (!etype) {
return this.listenerList;
}
return this.listenerList[etype];
};
/**
* This will collect data on any:
* - attribute event handler
* <div onclick="callback()">
* - property event handler
* div.onclick = callback
* - eventListener()
* div.addEventListener("click", callback)
* on every DOM node on the page (including window, document,
* etc).
* @return {array} - An array of objects, each object is like
* the following:
* {
* node: htmlString of element,
* type: event.type,
* func: stringified callback function
* }
* <<<-IMPORTANT->>>
* In order to gather data on any listeners created by
* addEventListener(), EventTarget prototype addEventListener()
* must be overridden with the previous code. Moreover that
* code must be placed before the other scripts as much as
* possible.
*/
function listListeners() {
// Collect all DOM nodes into an array
const domNodes = [window, document, ...document.querySelectorAll('*')];
// Create an array of all on-attributes from the window object
const types = Object.keys(window).filter(key => key.startsWith("on"));
/**
* Result is an array of objects, see comment above.
* flatMap() is used to return a single array
* by removing a level of arrays.
* There are three arrays to be processed:
* - Outer iteration of domNodes array each node will need
* it's on-attribute events (types) and eventListeners
* (events) listed
* - First inner iteration of types array for each node
* - Last inner iteration of events array for each node
*/
const listeners = domNodes.flatMap(node => {
/**
* Here flatMap() iterates through types array to find any
* on-attributes that match with the current node. An
* object is returned if there's a match, if not, an empty
* array is returned which becomes nothing (which is
* cleaner than an undefined).
*/
const tags = types.flatMap(etype => {
return typeof node[etype] === 'function' ? {
node: node,
type: etype,
func: node[etype].toString()
} : [];
});
/**
* The new method getListeners() (see previous) returns
* the node's object (listenerList).
*/
const events = node.getListeners();
// Convert object (listenerList) into an array of properties (keys)
const evKeys = Object.keys(events);
// If there's any event.types in evKeys array...
if (evKeys.length > 0) {
// for each event.types (evt) in evKeys array...
evKeys.forEach(evt => {
// find it (evt) in events array and extract it's data
events[evt].forEach((_, idx) => {
tags.push({
node: node,
type: evt,
func: events[evt][idx].callback.toString()
});
});
});
}
return tags;
});
return listeners.sort();
}
// For testing purposes.
const fc = document.forms[0].elements;
fc[0].oninput = test;
fc[1].addEventListener("change", test);
fc[3].onmouseenter = test;
Array.from(fc.rad).forEach(r => r.onchange = test);
fc[8].addEventListener("change", e => console.log(e.target.value));
fc[8].addEventListener("change", test);
console.log(listListeners());
function test(e) {
console.log((e.target.type || e.target.tagName) +
" triggered the " + e.type + " event");
}
.as-console-wrapper {
left: auto !important;
top: 0;
width: 60%;
min-height: 350px !important;
}
.as-console-row::after {
display: none !important;
}
<form>
<textarea>TEST</textarea><br><br>
<label><input type="checkbox"> TEST</label><br><br>
<button onclick="console.log('button triggered the click event')" type="button">TEST</button><br><br>
<fieldset>
<label><input name="rad" type="radio"> TEST</label>
<label><input name="rad" type="radio"> TEST</label>
<label><input name="rad" type="radio"> TEST</label>
<label><input name="rad" type="radio"> TEST</label>
</fieldset><br>
<select>
<option>TEST A</option>
<option>TEST B</option>
<option>TEST C</option>
<option>TEST D</option>
</select>
</form>
Upvotes: 2
Reputation: 82306
It is possible, but only if you intercept/override the addEventListener and removeEventListener prototype functions with your own interceptor, so you can intercept them.
This will work for anything that has been added with addEventListener (and it accounts for removeEventListener).
But if you added them without EventListener, e.g. with element.onclick (or in the onclick/onAnything-attribute in the markup), this won't list them, you'll have to manually check for them.
Be sure that the bellow JavaScript is the first script that is executed on your page, otherwise it might not work properly.
Here's how (TypeScript):
type EventHandlerMapType = {
// [key: EventTarget]: { [type: string]: EventListenerOrEventListenerObject[] };
[key: string]: { [type: string]: EventListenerOrEventListenerObject[] };
};
type EventHandlerMapValue = { [type: string]: EventListenerOrEventListenerObject[] };
interface EventTarget
{
getEventHandlers: (type?: string) => EventHandlerMapValue | EventListenerOrEventListenerObject[];
}
// function addEventListener<K extends keyof ElementEventMap>(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options ?: boolean | AddEventListenerOptions): void;
// addEventListener(type: string, listener: EventListenerOrEventListenerObject, options ?: boolean | AddEventListenerOptions): void;
(function ()
{
// Store the handlers by element reference
// WeakMap can take an object, such as an Element, as a key, object cannot.
// This is useful because WeakMap allows for garbage collection of the keys(the elements),
// meaning when an Element is removed from the DOM and no longer referenced, it gets garbage - collected,
// and its entry in the WeakMap is automatically removed.
// This prevents memory leaks.
const eventHandlerMap = new WeakMap<EventTarget>(); // Dictionary<Element, { type:[]}> // where type is string and array is an array of handlers/listeners
// Override the native addEventListener
const originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions)
{
// Call the original addEventListener to ensure normal behavior
originalAddEventListener.call(this, type, listener, options);
// Initialize tracking for the current element if it doesn't exist
if (!eventHandlerMap.has(this))
{
eventHandlerMap.set(this, {});
}
// Get the event type handlers for this element
const handlersForElement = eventHandlerMap.get(this);
if (!handlersForElement[type])
{
handlersForElement[type] = [];
}
// Add the handler to the list for this event type
handlersForElement[type].push(listener);
};
// Override the native removeEventListener
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions)
{
// Call the original removeEventListener to ensure normal behavior
originalRemoveEventListener.call(this, type, listener, options);
// Remove the handler from the tracking list
if (eventHandlerMap.has(this))
{
const handlersForElement = eventHandlerMap.get(this);
if (handlersForElement[type])
{
// Filter out the handler that matches the one being removed
handlersForElement[type] = handlersForElement[type].filter((h: EventListenerOrEventListenerObject) => h !== listener);
// Clean up if no handlers left for this event type
if (handlersForElement[type].length === 0)
{
delete handlersForElement[type];
}
}
// Clean up the element if no handlers left for any event type
if (Object.keys(handlersForElement).length === 0)
{
eventHandlerMap.delete(this);
}
}
};
// Function to retrieve all event handlers for an element
EventTarget.prototype.getEventHandlers = function (type?: string): EventHandlerMapValue | EventListenerOrEventListenerObject[]
{
// Get the tracking list for the current element
const handlersForElement = eventHandlerMap.get(this) || {};
if (type)
{
// If a specific event type is requested, return its handlers
return handlersForElement[type] || [];
}
// If no type is specified, return all handlers grouped by type
return handlersForElement;
};
})();
Now on EventTarget (Element, Node, etc):
getEventHandlers(type?: string)
or in plain-JS
(function () {
var eventHandlerMap = new WeakMap();
var originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options) {
originalAddEventListener.call(this, type, listener, options);
if (!eventHandlerMap.has(this)) {
eventHandlerMap.set(this, {});
}
var handlersForElement = eventHandlerMap.get(this);
if (!handlersForElement[type]) {
handlersForElement[type] = [];
}
handlersForElement[type].push(listener);
};
var originalRemoveEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function (type, listener, options) {
originalRemoveEventListener.call(this, type, listener, options);
if (eventHandlerMap.has(this)) {
var handlersForElement = eventHandlerMap.get(this);
if (handlersForElement[type]) {
handlersForElement[type] = handlersForElement[type].filter(function (h) { return h !== listener; });
if (handlersForElement[type].length === 0) {
delete handlersForElement[type];
}
}
if (Object.keys(handlersForElement).length === 0) {
eventHandlerMap.delete(this);
}
}
};
EventTarget.prototype.getEventHandlers = function (type) {
var handlersForElement = eventHandlerMap.get(this) || {};
if (type) {
return handlersForElement[type] || [];
}
return handlersForElement;
};
})();
Tests:
var btnCreated = document.createElement("button");
btnCreated.textContent = "Hello Kitty";
btnCreated.value = "Hello Kitty";
document.body.appendChild(btnCreated);
var btn = document.querySelector('button');
function handleClick() {
console.log('Button clicked');
}
btn.addEventListener('click', handleClick);
btn.addEventListener('clock', handleClick);
console.log(btn.getEventHandlers('click'));
console.log("before click");
btn.click();
console.log("after click");
btn.removeEventListener('click', handleClick);
console.log("before click after click removed");
btn.click();
console.log("after click after click removed");
console.log("click handlers", btn.getEventHandlers('click'));
console.log("all handlers", btn.getEventHandlers());
Upvotes: 0