Trigger all browser events using dispatchEvent - javascript

I'm thinking about building a tool that converts all browser events (either native dom events like .click() or jQuery events) to a standard form.
The standard form is: HTMLElement.dispatchEvent(new Event(eventType, eventInitDict))
For example, I want to change HTMLElement.click() to HTMLElement.dispatchEvent(new Event("click", {"bubbles": true, ...})) for all events.
My question:
Is there a complete mapping from events to this standard form and if
so, is it documented anywhere?
Are there any events that could be fired that couldn't be converted
to this standard form?
Does jQuery do anything fancy where I wouldn't be able to do this
conversion.
It is imperative that I completely convert all events into this format... None can be spared!
Thanks!
Why am I trying to do this?
I am trying to capture all events fired by a Chrome extension. To do this, I've decided to modify an extensions content script before it is injected into the page (I don't care about background page or popup pages) so all events triggered are "tagged" as originating from an extension (this will be added to the eventInitDict in the examples above. I'm modifying Chromium to do this.
PS. I couldn't think of a better question title but if you have one, please let me know / change it.

To capture all events you'll need to add event listeners to the DOM for each event.
There are a lot of events to capture and this is laborious. I know because I've had to do this in a project i'm currently working on.
Normally in a SO answer it's advisable to place all the relevant text into the answer, in this instance because of the number of events available, I'm not going to do that. You can find a detailed list of DOM Events here (MDN)
Now fortunately for you events can be captured regardless of what triggers them. Underneath, JQuery most likely triggers DOM events although as I haven't looked at the JQuery source, I couldn't say for sure.
When adding event listeners, you'll want to pass a boolean set to true for useCapture on the addEventListener function.
target.addEventListener(type, listener, useCapture);
Further documentation on addEventListener here
It's likely you'll want to declare a JSON array of events you want to capture, or a static array in your code so that you can maintain the list of events you wish to capture.
Furthermore for triggering events, you'll need to store a reference to the element that they targeted. I've had to do this as well. You'll want to find a way to get the selector for whatever element fired the event.
something like the following
var eventList = ['click', 'keydown'];
for(var i = 0;i < eventList.length; i++) {
var eventName = eventList[i];
document.addEventListener(eventName, function (evt) {
var selector = getElementSelector(evt.target); // where getElementSelector will be a function that returns an id, css path or xpath selector.
var captured = { eventName: eventName, selector: selector };
});
}
To trigger the captured event you might do the following, assuming you use the object structure above
var eventObject = { eventName: 'click', selector: '#targetId'};
var target = document.querySelector(eventObject.selector);
var event = new Event(eventName, {
view: window,
bubbles: true,
cancelable: true
});
target.dispatchEvent(event);
There is a caveat, for each event captured, you'll want to capture properties on the event that may be specific to that event type, and save that in your custom object that stored event properties like eventName, selector and the like.
The 'standard' form of new Event won't suffice. Some events have different initialisation dictionaries which will be ignored by a plain Event if the properties aren't standard for the Event and this is why you should instantiate the correct object type, e.g. new MouseEvent, new KeyboardEvent. etc.
You can use new CustomEvent for capturing synthetic events that add none standard properties to the detail property of the event, but this will not suffice for regular DOM events.
It's not a small task by any measure.

Related

Using document to handle all events in JavaScript

Is it a good idea to manage all click events under the document element? The DOM is being constantly manipulated, so instead of constantly registering new events for each newly created DOM element, can't I just assign one event handler on the document element? For example:
document.onclick = function(event) {
switch(event.target.id) {
case 'someid':
// SOME ACTION
break;
case 'someotherid':
// SOME OTHER ACTION
break;
default:
// A CLICK WITH NO ACTION
}
};
Yes. This pattern is called event delegation, you can find a great article on the blog of David Walsh
You should also take a look at the Element matches / matchesSelector API
-https://developer.mozilla.org/es/docs/Web/API/Element/matches
-https://davidwalsh.name/element-matches-selector
You can do this, but it's not as efficient as binding events to specific elements. It means your function will run if someone clicks in a place that isn't mentioned in any of your cases. And even if it is, it will have to search sequentially through your cases until it finds the right one.
A somewhat better way to do it is to use an object keyed off the IDs.
var handlers = {
"someid": function(event) { // some action
},
"someotherid": function(event) { // some other action
},
...
}
document.onclick = function(event) {
if (handlers[event.target.id]) {
handlers[event.target.id](event);
} else {
// default action
}
}
This addresses the sequential searching problem, but it still runs when someone clicks on an unbound element. This probably isn't much of an issue for clicks, but imagine doing the same thing for mouse movement events, which occur almost constantly.
Also, this doesn't generalize easily to binding handlers to classes or more complicated selectors.
What you're doing is similar to how jQuery implements .on() event binding, when you write:
$(document).on("click", "someSelector", handlerFunction);
This form is generally only used when specifically needed, which is when the elements that match the selector are created dynamically -- it allows you to define the handler once, not add and remove it as elements change. But for static elements, we generally use the simpler
$("selector").on("click", handlerFunction);
because then the browser takes care of running the handler only when one of the selected elements is clicked.

"Trickle-down" custom JavaScript events

I'm looking into custom events in JavaScript.
According to MDN, using the CustomEvent constructor, there is an option to make the event "bubble up" (false by default):
https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#CustomEventInit
Example:
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });
// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);
I tested it on jsfiddle:
http://jsfiddle.net/ppx4gcxe/
And the bubble up functionality seems to work. But I'd like my custom event to "trickle down", that is to trigger even listeners on child elements; the opposite of bubbling up.
I vaguely remember some default browser events "trickling down". This was supposedly one of these points of contention in the early browser days.
Anyway, is there any way to get this functionality on my custom events? Any relatively easy and straightforward way, of course. I don't really want to write a function to traverse all child elements and manually trigger any listeners on them. I hope there's another way.
The behavior you're looking for is called event capturing (the opposite of event bubbling). You can enable event capturing by passing in true as the third argument to addEventListener.
See: http://jsfiddle.net/zs1a6ywo/
NOTE: event capturing is not supported in IE 8 or below.
For more information, see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener

add event listener on elements created dynamically

Is possible to add event listener (Javascript) to all dynamically generated elements?
I'm not the owner of the page, so I cannot add a listener in a static way.
For all the elements created when the page loaded I use:
doc.body.addEventListener('click', function(e){
//my code
},true);
I need a method to call this code when new elements appear on the page, but I cannot use jQuery (delegate, on, etc cannot work in my project). How can I do this?
It sounds like you need to pursue a delegation strategy without falling back to a library. I've posted some sample code in a Fiddle here: http://jsfiddle.net/founddrama/ggMUn/
The gist of it is to use the target on the event object to look for the elements you're interested in, and respond accordingly. Something like:
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'li') {
// do your action on your 'li' or whatever it is you're listening for
}
});
CAVEATS! The example Fiddle only includes code for the standards-compliant browsers (i.e., IE9+, and pretty much every version of everyone else) If you need to support "old IE's" attachEvent, then you'll want to also provide your own custom wrapper around the proper native functions. (There are lots of good discussions out there about this; I like the solution Nicholas Zakas provides in his book Professional JavaScript for Web Developers.)
Depends on how you add new elements.
If you add using createElement, you can try this:
var btn = document.createElement("button");
btn.addEventListener('click', masterEventHandler, false);
document.body.appendChild(btn);
Then you can use masterEventHandler() to handle all clicks.
An obscure problem worth noting here may also be this fact I just discovered:
If an element has z-index set to -1 or smaller, you may think the
listener is not being bound, when in fact it is, but the browser
thinks you are clicking on a higher z-index element.
The problem, in this case, is not that the listener isn't bound, but instead it isn't able to get the focus, because something else (e.g., perhaps a hidden element) is on top of your element, and that what get's the focus instead (meaning: the event is not being triggered). Fortunately, you can detect this easily enough by right-clicking the element, selecting 'inspect' and checking to see if what you clicked on is what is being "inspected".
I am using Chrome, and I don't know if other browsers are so affected. But, it was hard to find because functionally, it resembles in most ways the problem with the listener not being bound. I fixed it by removing from CSS the line: z-index:-1;
When you have to support only "modern" web browsers (not Microsoft Internet Explorer), mutation observers are the right tool for this task:
new MutationObserver(function(mutationsList, observer) {
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
// put your own source code here
}
}
}).observe(document.body, {childList: true, subtree: true});
I have created a small function to add dynamic event listeners, similar to jQuery.on().
It uses the same idea as the accepted answer, only that it uses the Element.matches() method to check if the target matches the given selector string.
addDynamicEventListener(document.body, 'click', '.myClass, li', function (e) {
console.log('Clicked', e.target.innerText);
});
You can get if from github.
Delegating the anonymous task to dynamically created HTML elements with event.target.classList.contains('someClass')
returns true or false
eg.
let myEvnt = document.createElement('span');
myEvnt.setAttribute('class', 'someClass');
myEvnt.addEventListener('click', e => {
if(event.target.classList.contains('someClass')){
console.log(${event.currentTarget.classList})
}})
Reference: https://gomakethings.com/attaching-multiple-elements-to-a-single-event-listener-in-vanilla-js/
Good Read: https://eloquentjavascript.net/15_event.html#h_NEhx0cDpml
MDN : https://developer.mozilla.org/en-US/docs/Web/API/Event/Comparison_of_Event_Targets
Insertion Points:
You might wanna take a look at this library: https://github.com/Financial-Times/ftdomdelegate which is 1,8K gzipped
It is made for binding to events on all target elements matching the given selector, irrespective of whether anything exists in the DOM at registration time or not.
You need to import the script and then instantiate it like that:
var delegate = new Delegate(document.body);
delegate.on('click', 'button', handleButtonClicks);
// Listen to all touch move
// events that reach the body
delegate.on('touchmove', handleTouchMove);
});
Use classList property to bind more than one class at a time
var container = document.getElementById("table");
container.classList.add("row", "oddrow", "firstrow");

How do I access what events have been bound to a dom element using only JavaScript, no frameworks

How do I access what events have been bound to a DOM element using JavaScript and NOT using a library/framework or Firefox add-on? Just pure JavaScript. I incorrectly assumed there would be an events object stored as a property of the element which has the binding.
For example if I had bound say, click, dblclick and mouseover to an element how would I do the following NOT using jQuery. Just JavaScript.
function check(el){
var events = $(el).data('events');
for (i in $(el).data('events')) {
console.log(i) //logs click dblclick and mouseover
}
}
I know jQuery stores an events object as a data property i.e. $(el).data('events') and the eventbug add-on displays the event binding so there must be way.
I will also add that this question came about because I read about memory leaks in older IE browsers and how it's best to remove the bound events before removing a node from the DOM, which lead me to think, how can I test for what events are bound to an element?
You can't reliably know what listeners are on an element if you aren't in complete control of the environment. Some libraries manually control event listeners, e.g. jQuery stores them in an object and adds a custom attribute to the element so that when an even occurs, it uses the custom property to look up the listeners for that element and event and calls them.
Try:
<div id="d0">div 0</div>
<script type="text/javascript">
var el = document.getElementById('d0')
el.addEventListener('click',function(){alert('hey');},false);
// Make sure listener has had time to be set then look at property
window.setTimeout(function(){alert(el.onclick)}, 100); // null
</script>
So to know what listeners have been added, you need to be in complete control.
Edit:
To make it clear, there is no reliable way to inspect an element with javascript and discover what listeners have been added, or even if any have been added at all. You can create an event registration scheme to track listeners added using your event registration methods. But if listeners are added some other way (e.g. directly by addEventListener) then your registration scheme won't know about them.
As pointed out elsewhere, jQuery (and others) can't track listeners that are added directly to elements without using the jQuery event registration methods (click, bind, mouseover, etc.) because they use those methods to register (and call) the listeners.
// list of onevent attributes; you'll have to complete this list
var arr = ['onclick', 'onmouseover', 'onmouseup'];
for ( var i = 0; i < arr.length; i++ ) {
if ( el[arr[i]] != null ) {
// element has an arr[i] handler
}
}
where el is a reference to your DOM element
Complete lists are here

Inspect attached event handlers for any DOM element

Is there any way to view what functions / code are attached to any event for a DOM element? Using Firebug or any other tool.
The Elements Panel in Google Chrome Developer tools has had this since Chrome releases in mid 2011 and Chrome developer channel releases since 2010.
Also, the event listeners shown for the selected node are in the order in which they are fired through the capturing and bubbling phases.
Hit command + option + i on Mac OSX and Ctrl + Shift + i on Windows to fire this up in Chrome
Event handlers attached using traditional element.onclick= handler or HTML <element onclick="handler"> can be retrieved trivially from the element.onclick property from script or in-debugger.
Event handlers attached using DOM Level 2 Events addEventListener methods and IE's attachEvent cannot currently be retrieved from script at all. DOM Level 3 once proposed element.eventListenerList to get all listeners, but it is unclear whether this will make it to the final specification. There is no implementation in any browser today.
A debugging tool as browser extension could get access to these kinds of listeners, but I'm not aware of any that actually do.
Some JS frameworks leave enough of a record of event binding to work out what they've been up to. Visual Event takes this approach to discover listeners registered through a few popular frameworks.
Chrome Dev Tools recently announced some new tools for Monitoring JavaScript Events.
TL;DR
Listen to events of a certain type using monitorEvents().
Use unmonitorEvents() to stop listening.
Get listeners of a DOM element using getEventListeners().
Use the Event Listeners Inspector panel to get information on event listeners.
Finding Custom Events
For my need, discovering custom JS events in 3rd party code, the following two versions of the getEventListeners() were amazingly helpful;
getEventListeners(window)
getEventListeners(document)
If you know what DOM Node the event listener was attached to you'd pass that instead of window or document.
Known Event
If you know what event you wish to monitor e.g. click on the document body you could use the following: monitorEvents(document.body, 'click');.
You should now start seeing all the click events on the document.body being logged in the console.
You can view directly attached events (element.onclick = handler) by looking at the DOM.
You can view jQuery-attached events in Firefox using FireBug with FireQuery. There doesn't appear to be any way to see addEventListener-added events using FireBug. However, you can see them in Chrome using the Chrome debugger.
You can use Visual Event by Allan Jardine to inspect all the currently attached event handlers from several major JavaScript libraries on your page. It works with jQuery, YUI and several others.
Visual Event is a JavaScript bookmarklet so is compatible with all major browsers.
You can extend your javascript environment to keep track of event listeners. Wrap (or 'overload') the native addEventListener() method with some code that cans keep a record of any event listener added from then onwards. You'd also have to extend HTMLElement.prototype.removeEventListener to keep records that accurately reflect what is happening in the DOM.
Just for the sake of illustration (untested code) - this is an example of how you would 'wrap' addEventListener to have records of the registered event listeners on object itself:
var nativeMethod = HTMLElement.prototype.addEventListener;
HTMLElement.prototype.addEventListener = function (type, listener) {
var el = e.currentTarget;
if(!(el.eventListeners instanceof Array)) { el.eventListeners = []}
el.eventListeners.push({'type':type, 'listener':listener});
nativeMethod.call(el, type, listener);
}
I was curious whether #Rolf's approach would actually work. Remember, it is a "crude" way of replacing the standard HTMLElement.prototype.addEventLister() with a wrapped version of the same. Obviously this can only be an "injection method for testing" and would definitely have to be removed for anything approaching the "production version".
When tesing it I found out that, apart from a minor glitch (his e was not defined anywhere but could easily be replaced by a this) the approach does work, as long as
you are consistently working with addEventListener() only on the actual elements themselves
and if you do not use delegated event attachment
or direct event assignments by setting attributes like onclick or oninput.
I went on to find out whether the "sniffing" could be made a little more universal and came up with the following modified version:
(nativeMethod=>{ // IIFE-closure to manipulate the standard addEventListener method:
HTMLElement.prototype.addEventListener = function (type,fun) {
(this.ELL=this.ELL||[]).push([type,fun]);
nativeMethod.call(this,type,fun);
}
})(HTMLElement.prototype.addEventListener);
// LIST direct and indirect event attachments for element `el`:
function listELfor(el){
const events="click,change,input,keyup,keydown,blur,focus,mouseover,mouseout"
.split(",").map(e=>"on"+e); // possible direct event assignments to check up on
const evlist = (el.ELL||[]).map(([t,f])=>[t,f.toString()]);
events.forEach(e=> el[e] && (evlist[e]=[e.substr(2),el[e].toString()]) )
let p=el.parentNode;
if (p.tagName!=="HTML"){ // if available: run function on parent level recursively:
evlist[p.tagName+(p.id?'#'+p.id:'')+(p.className?'.'+p.className:'')]=listELfor(el.parentNode);
}
return evlist;
};
// ============ TESTING ==========================================
// now, let's do some sample event attachments in different ways:
const sp=document.querySelector('h1 span'); // sp = the target SPAN within H1
sp.addEventListener('click',function(e){console.log('first:',e.target)});
sp.addEventListener('click',function(e){console.log('second:',e.target.tagName)});
sp.addEventListener('click',function(e){console.log('third:',e.target.dataset.val)});
// attach an event to the parent node (H1):
sp.parentNode.addEventListener('click',function(e){console.log('Click event attached to H1, click-target is',e.target.tagName);});
// and finally, let's also assign an onclick event directly by using the ONCLICK attribute:
sp.onclick=e=>console.log('direct onclick on span, text:',e.target.textContent);
// Get all event handler functions linked to `sp`?
const allHandlers=listELfor(sp);
for (id in allHandlers) console.log(id,allHandlers[id]);
h1 span {cursor:pointer}
.as-console-wrapper {max-height:85% !important}
<div id="main-frame-error" class="interstitial-wrapper">
<div id="main-content">
<div class=""></div>
<div id="main-message">
<h1>Hello, <span data-val="123">THESE WORDS ARE CLICKABLE</span></h1>
<p>Some more text here to pad it out. This text should be unresponsive.</p>
</div>
</div>
</div>
The IIFE structure captures .addEventListener() function handler attachments as an array store in the ELL attribute of the concerned DOM element. The function listELfor(el) then picks up these function handlers of the element itself and walk up the parent hierarchy to also get assignments to its parents. The function will also take care of direct event assignments using onclick and similar attributes.
listELfor() will return an array object with extra properties. These properties will not necessarily be visible in a plain console.log(). This is the reason why I used the for (id in allHandlers) loop.
Please note:
Chrome will list these "extra" Array attributes too - and even further properties, relating to the parent's and their parent's parent's event attachments, like shown below:

Categories

Resources