Yasir 768
Yasir 768

Reputation: 41

How to Get Event Listener of an Element

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.

Upvotes: 4

Views: 9657

Answers (3)

Stefan Steiger
Stefan Steiger

Reputation: 82336

Here a more-refined way than the top answer:

If you intercept/override the addEventListener, you should also intercept the removeEventListener prototype functions with your own interceptor, so the list is not wrong.

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

iateadonut
iateadonut

Reputation: 2239

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.

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 371138

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.

Upvotes: 7

Related Questions