I'm going through the tutorial at the address: http://www.stanford.edu/~ouster/cgi-bin/cs142-spring12/lecture.php?topic=event.
And I don't understand about code at the lines that I have marked with asterisks.
function Dragger(id) {
this.isMouseDown = false;
this.element = document.getElementById(id);
var obj = this;
this.element.onmousedown = function(event) {
obj.mouseDown(event);
}
}
Dragger.prototype.mouseDown = function(event) {
var obj = this;
this.oldMoveHandler = document.body.onmousemove; /******/
document.body.onmousemove = function(event) { /******/
obj.mouseMove(event);
}
this.oldUpHandler = document.body.onmouseup; /******/
document.body.onmouseup = function(event) { /******/
obj.mouseUp(event);
}
this.oldX = event.clientX;
this.oldY = event.clientY;
this.isMouseDown = true;
}
Dragger.prototype.mouseMove = function(event) {
if (!this.isMouseDown) {
return;
}
this.element.style.left = (this.element.offsetLeft
+ (event.clientX - this.oldX)) + "px";
this.element.style.top = (this.element.offsetTop
+ (event.clientY - this.oldY)) + "px";
this.oldX = event.clientX;
this.oldY = event.clientY;
}
Dragger.prototype.mouseUp = function(event) {
this.isMouseDown = false;
document.body.onmousemove = this.oldMoveHandler; /******/
document.body.onmouseup = this.oldUpHandler; /******/
}
The purpose of the this.oldMoveHandler references are to store whatever event handlers a previous developer of the page may have added to "document.body.onmousemove", with the goal of not interrupting whatever behavior that developer no doubt spend painful hours to build. It goes like this:
Press down with the mouse, store the old handler, add our fancy dragging handler.
Move the mouse, lovely dragging behavior occurs.
Release the mouse, dragging behavior stops, restore old handler (even if it's null).
This is a way to stay out of the way of previous code, although it's a bad solution. The much preferred way is to use addEventListener/removeEventListener or attachEvent/detachEvent for barbaric IE browsers. These functions are designed so that multiple handlers can subscribe to the same event without stepping on each other. Here's some good reading:
http://www.quirksmode.org/js/introevents.html
Setting document.body.onmousemove is one (ugly) way to listen for mousemove events on the document.body element.
Therefore, this.oldMoveHandler = document.body.onmousemove; is simply storing a reference to the event handler function, if any.
Please note that using element.addEventListener is preferred for attaching event handlers.
As noted in the comments the use of intrusive event handling is very old fashioned and not recommended now.
However to answer your question the code is implementing drag and drop, when the mousedown event is triggered by a mouse press the current event handlers for mouseup and mouseover (first 4 lines of marked code) are "saved" and replaced by the event handlers that will perform the drag and drop.
When the dragged element is "dropped" i.e. the mouseup event fires, the mousemove and mouseup event handlers are replaced with the original event handlers that were saved (last 2 lines of marked code)
Related
I'd like to handle dragend events differently depending on whether an element has been just dragged inside the browser window (or site resp.) or or outside, e.g. to an external file manager.
After I didn't find any attribute of the DragEvent instance indicating whether it's inside or outside the sites context I started to arithmetically figure out if the corresponding mouse event still takes place inside the geometry of the site.
Eventually I might succeed with that approach (currently not working yet) but it has one major disadvantage (leaving alone its ugliness): the drop target window might be on top of the browser, so the geometry is no real indicator at all..
so.. how do I find out if a dragend (or any other event I could use to store some state) is pointing outside of the browser window (or source site)?
I couldn't find any super straightforward ways to do this, but I could do it fairly concisely with a handful of listeners. If you're ok with having a variable to assist with the state then you can do something like this.
First, listen for the mouse leaving on a drag event. I've found the most reliable way to do this is to use a dragleave listener and then examine the event data to make sure it's really leaving the window. This event runs a ton though, so we need to filter out the ones we need.
dragleave runs every time any element's drop zone is left. To make sure that the drag event is just leaving the page, we can check the target to make sure leaving the html or body tag and going to null. This explains how to see the correct targets for the event.
Right before the dragend event, dragleave is ran as though it left the window. This is problematic because it makes every drop seem as though it were out of the window. It seems that this behavior isn't well defined in the specs and there is some variation between how Firefox and Chrome handle this.
For Chrome, we can make the code in dragleave run a cycle after the code in dragend by wrapping it in a timeout with 0 seconds.
This doesn't work well in Firefox though because the dragend event doesn't come as fast. Firefox does, however, set buttons to 0 so we know it's the end of an event.
Here is what the dragleave event listener might look like
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
From here we just need to do something similar to see when the drag re-enters the viewport. This won't run before dragend like dragleave so it is simpler.
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
It's also a good idea to reset outside every time the drag event starts.
element.addEventListener('dragstart', (e) => {
outside = false;
});
Now it's possible in the dragend event listener to see where the drop ended.
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
Here is a snippet with everything put together (or fiddle)
Note #1: You'll need to drag the element out of the browser window, not just the demo window for it to appear as "outside".
Note #2: There has to be a better way for stopping the last dragleave event, but after a few hours of trying other things, this seemed the most consistent and reliable.
const element = document.querySelector('div');
var outside = false;
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
element.addEventListener('dragstart', (e) => {
outside = false;
});
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
div[draggable] {
width: fit-content;
margin-bottom: 32px;
padding: 16px 32px;
background-color: black;
color: white;
}
<div draggable="true">Drag Me</div>
This might help. You can click 'Run code snippet' to see how it works
Note: Increasing the offset would help detect the drag out sooner, but might affect precision (whether is has actually been dragged out or right on the edge)
/* events fired on the draggable target */
let offset = 2; // in case required
let width = window.innerWidth;
let height = window.innerHeight;
console.log('Starting screen width: ' + width);
console.log('Starting screen height: ' + height);
document.addEventListener("drag", function(event) {
let posX = event.pageX;
let posY = event.pageY;
console.log('X:' + posX + ' Y:' + posY)
let isExceedingWidth = posX >= (width - offset) || posX <= (0 + offset);
let isExceedingHeight = posY >= (height - offset) || posY <= (0 + offset);
if (isExceedingWidth || isExceedingHeight) {
console.log('dragged out');
} else {
console.log('in');
}
}, false);
#draggable {
width: fit-content;
padding: 1px;
height: fit-content;
text-align: center;
background: black;
color: white;
}
<div id="draggable" draggable="true">
Drag Me
</div>
Here is a possibly controversial, but more programmatic alternative that worked well for me.
Create a function wrapper that takes a callback that will fire every time something is dragged into or out of the window:
export interface OutsideWindow {(outside: boolean): void}
export interface OutsideCleanup {(): {(): void}}
export const whenOutsideWindow = (onChangeCB: OutsideWindow): OutsideCleanup => {
const windowDragLeaveHandler = () => onChangeCB(true);
window.addEventListener('dragleave', windowDragLeaveHandler);
const bodyDragLeaveHandler = (e: DragEvent) => e.stopPropagation();
document.body.addEventListener('dragleave', bodyDragLeaveHandler);
const windowDragEnterHandler = () => onChangeCB(false);
window.addEventListener('dragenter', windowDragEnterHandler);
return () => () => {
window.removeEventListener('dragleave', windowDragLeaveHandler);
document.body.removeEventListener('dragleave', bodyDragLeaveHandler);
window.removeEventListener('dragenter', windowDragEnterHandler);
}
}
A few things to note:
This function passes a boolean value into its callback. In this case, it will pass a value, true, when outside the browser window and false when inside it
It helps to know a little about bubbling and stopPrapogation(), but basically, we are making the window's child element, body, stop propagating the dragleave event, so it will never reach its parent, window
This only works in one direction, so there is no way to do a similar thing for the dragenter event, thus this will fire every time the draggable is dragged iinto a child element.
It is a good idea to cleanup any events you add to the DOM, so this function returns its cleanup function for you to call later.
The return value of this function is a function that returns a function. More on that below
That all looks a little messy, but it provides a relatively clean interface to use
let outside = false;
let cleanup = () => {};
// Probably inside onDragStart()
const foo = (out) => { outside = out; }
cleanup = whenOutsideWindow( outside => foo(outside) );
...
// Later in your code, when you want to cleanup...
// Probably inside onDragEnd()
cleanup()();
No, the multiple calls to cleanup is not a mistake. Remember that whenOutsideWindow returns a function that returns a function. This is because when a function is returned in javascript, it is run immediately. I have no idea why. Hopefully someone in the comments can shed some light on that.
But the important things is that if whenOutsideWindow returned a simple void function...
export interface OutsideCleanup {(): void}
...our dragstart and dragend events would be added and removed immediately. We get around this by calling the function that is returned from the function that is returned by whenOutsideWindow.
cleanup()();
I've developed a javascript drag and drop that mostly uses the standard 'allowdrop', 'drag' and 'drop' events.
I wanted to customise the 'ghosted' dragged object, so I've added a display:none div that get populated with the innerHTML of the draggable element and made visible (display:block;) when the user starts dragging.
The draggable div is absolutely positioned and matches the mouse movements. For this I needed to add 3 event listeners to document.body. They are as follows:
document.body.addEventListener('dragover', function (ev) {
console.log("dragover event triggered");
ev = ev || window.event;
ev.preventDefault();
dragX = ev.pageX;
dragY = ev.pageY;
document.getElementById("dragged-container").style.left = (dragX - dragOffsetX) + "px";
document.getElementById("dragged-container").style.top = (dragY - dragOffsetY - 10) + "px";
if (mostRecentHoveredDropTargetId!="") {
if (dragX<mostRecentHoveredDropTargetRect.left || dragX>mostRecentHoveredDropTargetRect.right || dragY<mostRecentHoveredDropTargetRect.top || dragY>mostRecentHoveredDropTargetRect.bottom) {
document.getElementById(mostRecentHoveredDropTargetId).classList.remove("drop-target-hover");
mostRecentHoveredDropTargetId = "";
}
}
});
document.body.addEventListener('drop', function (ev) {
console.log("drop event triggered");
ev.preventDefault();
var data = ev.dataTransfer.getData("text"); // data set to the id of the draggable element
if (document.getElementById(data)!=null) {
document.getElementById(data).classList.remove("dragged");
document.getElementById("dragged-container").innerHTML = "";
document.getElementById("dragged-container").style.display = "none";
var draggablesClasses = document.getElementById(data).className;
if ((draggablesClasses.indexOf('draggable')==-1 || draggablesClasses=="") && document.getElementById(data).getAttribute('draggable')=="true") {
if (draggablesClasses=="") {
document.getElementById(data).className += "draggable";
} else {
document.getElementById(data).className += " draggable";
}
}
}
});
// resets dragged-container and origin .draggable, when mouse released outside browser window
document.body.addEventListener('mouseleave', function (ev) {
if (jqueryReady==true) {
$(".dragged").addClass("draggable");
$(".dragged").removeClass("dragged");
}
document.getElementById("dragged-container").innerHTML = "";
document.getElementById("dragged-container").style.display = "none";
});
This is all working fine. The drag and drop performs exactly as I expect.
The problem is when I go to another page, obviously those body event listeners are still running.
I've seen a number of answers here and have tried everything I've seen. For starters this:
window.onunload = function() {
console.log("about to clear event listeners prior to leaving page");
document.body.removeEventListener('dragover', null);
document.body.removeEventListener('drop', null);
document.body.removeEventListener('mouseleave', null);
return;
}
...but the console.log output doesn't even appear (let alone the 'null's being wrong, I'm pretty sure). I've also tried this, in the jQuery ready function:
$(window).bind('beforeunload', function(){
console.log("about to clear event listeners prior to leaving page");
document.body.removeEventListener('dragover', null);
document.body.removeEventListener('drop', null);
document.body.removeEventListener('mouseleave', null);
});
..but, once again, the console isn't even receiving that output.
I have also tried both the above with 'onbeforeunload' AND 'onunload'.
What am I doing wrong? - specifically to do with removing these window.body event listeners, I mean (Anything else I can sort out later).
Thanks.
removeEventListener requires the handler
Don't use anonymous functions is the solution.
Like this:
var dragHandler = function (ev) {
console.log("dragover event triggered");
};
document.body.addEventListener('dragover', dragHandler);
and after:
window.onunload = function() {
console.log("about to clear event listeners prior to leaving page");
document.body.removeEventListener('dragover', dragHandler);
return;
}
As a javascript learning exercise, i have made a little app which (should) let me create a web page 'graphically'(dreamweaver style). I've added as separate scripts a basic custom menu and a message popup. The menu opens on left mouse clicks binded to document.body, so anywhere on the page including the popup (if present, of course). Is there a way to avoid such behavior?
Edit
Here is the (rough) code for the function handling the drag operations on the popup:
window.addEventListener('click', show_mnu, false); // Menu onclick event (simply shows the menu onscreen)
mb_hdr.addEventListener('mousedown', drag, false); // Popup event handlers
document.addEventListener('mouseup', drag, false); //
var dragOffsetX, dragOffsetY;
function drag(evt)
{
var evt = evt||window.event;
var target = document.getElementById('popup');
switch(evt.type)
{
case 'mousedown':
{
dragOffsetX = evt.clientX - target.offsetLeft;
dragOffsetY = evt.clientY - target.offsetTop;
document.addEventListener('mousemove', drag, false);
break;
}
case 'mouseup':
{
dragOffsetX = dragOffsetY = null;
document.removeEventListener('mousemove', drag, false);
break;
}
case 'mousemove' :
{
if(dragOffsetX && dragOffsetY)
{
target.style.left = (evt.clientX - dragOffsetX) + 'px';
target.style.top = (evt.clientY - dragOffsetY) + 'px';
}
break;
}
}
evt.stopPropagation(); //????
}
I suppose you already read about event bubbling and propagation in the links in the comments to your question: if you click on any element, the event propagates down until it reaches window, with its show_mnu handler.
To cancel the propagation, you need to add this handler
function stop(e) {
e.stopPropagation();
(e||event).cancelBubble = true; // if you need to support IE<9
}
to every mousedown, mouseup and click handler you do not wish to propagate it down.
See the fiddle: clicking the gray box stops the propagation to window.
Note if you want to implement drag&drop, you may find this tutorial useful, since it uses HTML5 features which are supported well on desktop systems.
I have an Raphael element with click event handler:
var paper = Raphael("container", 770, 160);
var c = paper.rect(10, 10, 50, 50);
c.click(function () {
alert("triggering");
})
How I can manually fire this event? (c.click() don't work)
Thanks!
Although this question is already answered, I will post my solution which I found out by random.
It's possible with the Raphael internals:
When you attach an event listener like element.click(func) then the element object holds an array with all events. In this array there's an object which has a method f (strange naming convention) which triggers the event.
So to sum it up you can call your event with knowing the order of your events, in your case there's just the click event which is on index 0, you can call it like: c.events[0].f();
A more generic solution would be a function like
function triggerClick(element) {
for(var i = 0, len = element.events.length; i < len; i++) {
if (element.events[i].name == 'click') {
element.events[i].f();
}
}
}
But beware that if you had multiple click events all were triggered.
Here's a fiddle to demonstrate.
Even though this has already been answered a while ago, I was looking for something a little "more native" in nature. Here's how I go about it without relying on Mootools or jQuery.
var evObj = document.createEvent('MouseEvents');
evObj.initEvent('click', true, false);
c.node.dispatchEvent(evObj);
This basically creates the event in the browser and then dispatches it to the node associated with the Raphaël object.
Here's also the MOZ link for reference:
https://developer.mozilla.org/en-US/docs/DOM/document.createEvent
I actually found a slightly more elegant way to use Kris' method.
Raphael supports native extension of the element object so I built a little patch to add the trigger method to raphael
here it is:
//raphael programatic event firing
Raphael.el.trigger = function (str, scope, params){ //takes the name of the event to fire and the scope to fire it with
scope = scope || this;
for(var i = 0; i < this.events.length; i++){
if(this.events[i].name === str){
this.events[i].f.call(scope, params);
}
}
};
I set up a Js fiddle to show how it works: here
EDIT :
The solution purposed by Dan Lee is working better.
I write a plug-in for this, use event to calculate the right position;
Raphael.el.trigger = function(evtName, event) {
var el = this[0]; //get pure elements;
if (event.initMouseEvent) { // all browsers except IE before version 9
var mousedownEvent = document.createEvent ("MouseEvent");
mousedownEvent.initMouseEvent (
"mousedown", true, true, window, 0,
event.screenX, event.screenY, event.clientX, event.clientY,
event.ctrlKey, event.altKey, event.shiftKey, event.metaKey,
0, null);
el.dispatchEvent (mousedownEvent);
} else {
if (document.createEventObject) { // IE before version 9
var mousedownEvent = document.createEventObject (window.event);
mousedownEvent.button = 1; // left button is down
el.fireEvent (evtName, mousedownEvent);
}
}
};
Usage:
circle2.mousedown(function(e) {
var circle = this.clone();
circle.drag(move, down, up);
circle.trigger("mousedown", e);
});
First of all, here is a list of event types that are defined by the W3C standards. (This list is based on the onevent attributes defined in the HTML5 standard. I assume that there are dozens of other event types, but this list is long enough as it is.)
abort
afterprint
beforeprint
beforeunload
blur
canplay
canplaythrough
change
click
contextmenu
copy
cuechange
cut
dblclick
DOMContentLoaded
drag
dragend
dragenter
dragleave
dragover
dragstart
drop
durationchange
emptied
ended
error
focus
focusin
focusout
formchange
forminput
hashchange
input
invalid
keydown
keypress
keyup
load
loadeddata
loadedmetadata
loadstart
message
mousedown
mouseenter
mouseleave
mousemove
mouseout
mouseover
mouseup
mousewheel
offline
online
pagehide
pageshow
paste
pause
play
playing
popstate
progress
ratechange
readystatechange
redo
reset
resize
scroll
seeked
seeking
select
show
stalled
storage
submit
suspend
timeupdate
undo
unload
volumechange
waiting
Now, is it possible to define a global event handler that is called when any event originally occurs on any element on the page? (In this case, I don't want to count those events that occurred on elements because they bubbled up from a descendant element - that's why I wrote "originally occurs".)
If that is not possible, is it at least possible to define an event handler that is called when any event bubbles up to the root of the DOM tree (which is either the document object or the window object - both should work)? (I know that it's possible to stop bubbling programmatically, but I would use this event handler on a page that has no other handlers defined on any other elements.) (Also, I believe some events don't bubble up, but let's ignore these cases for the sake of this argument.)
I know that I can do this (using jQuery):
$(document).bind('abort afterprint beforeprint beforeunload etc.', function() {
// handle event
});
but that would be a rather undesirable solution for me.
btw I don't need a cross-browser solution. If it works in just one browser, I'm fine.
Also, Firebug is able to log events, but I would like to be able to catch the event programmatically (via JavaScript) rather then having them simply logged in the console.
/*
function getAllEventTypes(){
if(location.href !='https://developer.mozilla.org/en-US/docs/Web/Events') return;
var types = {};
$('.standard-table:eq(0) tr').find('td:eq(1)').map(function(){
var type = $.trim(this.innerText) || 'OtherEvent';
types[type] = types[type] || [];
var event = $.trim(this.previousElementSibling.innerText);
if(event) types[type].push(event);
});
for(var t in types) types[t] = types[t].join(' ');
return "var DOMEvents = "+JSON.stringify(types, null, 4).replace(/"(\w+)\":/ig, '$1:');
}
*/
var DOMEvents = {
UIEvent: "abort DOMActivate error load resize scroll select unload",
ProgressEvent: "abort error load loadend loadstart progress progress timeout",
Event: "abort afterprint beforeprint cached canplay canplaythrough change chargingchange chargingtimechange checking close dischargingtimechange DOMContentLoaded downloading durationchange emptied ended ended error error error error fullscreenchange fullscreenerror input invalid languagechange levelchange loadeddata loadedmetadata noupdate obsolete offline online open open orientationchange pause pointerlockchange pointerlockerror play playing ratechange readystatechange reset seeked seeking stalled submit success suspend timeupdate updateready visibilitychange volumechange waiting",
AnimationEvent: "animationend animationiteration animationstart",
AudioProcessingEvent: "audioprocess",
BeforeUnloadEvent: "beforeunload",
TimeEvent: "beginEvent endEvent repeatEvent",
OtherEvent: "blocked complete upgradeneeded versionchange",
FocusEvent: "blur DOMFocusIn Unimplemented DOMFocusOut Unimplemented focus focusin focusout",
MouseEvent: "click contextmenu dblclick mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup show",
SensorEvent: "compassneedscalibration Unimplemented userproximity",
OfflineAudioCompletionEvent: "complete",
CompositionEvent: "compositionend compositionstart compositionupdate",
ClipboardEvent: "copy cut paste",
DeviceLightEvent: "devicelight",
DeviceMotionEvent: "devicemotion",
DeviceOrientationEvent: "deviceorientation",
DeviceProximityEvent: "deviceproximity",
MutationNameEvent: "DOMAttributeNameChanged DOMElementNameChanged",
MutationEvent: "DOMAttrModified DOMCharacterDataModified DOMNodeInserted DOMNodeInsertedIntoDocument DOMNodeRemoved DOMNodeRemovedFromDocument DOMSubtreeModified",
DragEvent: "drag dragend dragenter dragleave dragover dragstart drop",
GamepadEvent: "gamepadconnected gamepaddisconnected",
HashChangeEvent: "hashchange",
KeyboardEvent: "keydown keypress keyup",
MessageEvent: "message message message message",
PageTransitionEvent: "pagehide pageshow",
PopStateEvent: "popstate",
StorageEvent: "storage",
SVGEvent: "SVGAbort SVGError SVGLoad SVGResize SVGScroll SVGUnload",
SVGZoomEvent: "SVGZoom",
TouchEvent: "touchcancel touchend touchenter touchleave touchmove touchstart",
TransitionEvent: "transitionend",
WheelEvent: "wheel"
}
var RecentlyLoggedDOMEventTypes = {};
for (var DOMEvent in DOMEvents) {
var DOMEventTypes = DOMEvents[DOMEvent].split(' ');
DOMEventTypes.filter(function(DOMEventType) {
var DOMEventCategory = DOMEvent + ' ' + DOMEventType;
document.addEventListener(DOMEventType, function(e){
if(RecentlyLoggedDOMEventTypes[DOMEventCategory]) { return; }
RecentlyLoggedDOMEventTypes[DOMEventCategory] = true;
setTimeout(function(){ RecentlyLoggedDOMEventTypes[DOMEventCategory] = false }, 5000);
var isActive = e.target == document.activeElement;
if(isActive) {
console.info(DOMEventCategory,
' target=', e.target,
' active=', document.activeElement,
' isActive=', true );
} else {
console.log(DOMEventCategory,
' target=', e.target,
' active=', document.activeElement,
' isActive=', false );
}
}, true);
});
}
You can iterate through all properties of dom element and select ones that match /on(.*)/ pattern (for example onclick or onmousemove):
var events = [];
for (var property in element) {
var match = property.match(/^on(.*)/)
if (match) {
events.push(match[1]);
}
}
console.log(events.join(' '))
I highly doubt there's a way to do this in Firefox. Looking at Firebug's source code (particularly the attachAllListeners method), turns out that iterating through a list of event names is obviously the way to go, but this doesn't solve the bubbling issues.
There doesn't seem to be any 'easy-way' to do that.
My idea:
You know which are all the events, so you can handle all events for every DOM element:
var events =
[
"onabort",
"onafterprint",
"onbeforeprint",
"onbeforeunload",
...
];
var root = document.body;
var elms = root.childNodes;
for(var i = 0; i < elms.length; i++)
{
for(var j = 0; j < events.length; j++)
{
elms[i][events[j]] = globalHandler;
}
}
function globalHandler()
{
alert("Global handler called");
}
That's the 'intuitive idea' but doesn't seem to be very efficient. However, it should work.
Good luck.
How to listen for all events on a specific target Element 👾
For all native events, we can retrieve a list of supported events by iterating over the target.onevent properties and installing our listener for all of them.
for (const key in target) {
if(/^on/.test(key)) {
const eventType = key.substr(2);
target.addEventListener(eventType, listener);
}
}
The only other way that events are emitted which I know of is via EventTarget.dispatchEvent, which every Node and thefore every Element inherits.
To listen for all these manually triggered events, we can proxy the dispatchEvent method globally and install our listener just-in-time for the event whose name we just saw ✨ ^^
const dispatchEvent_original = EventTarget.prototype.dispatchEvent;
EventTarget.prototype.dispatchEvent = function (event) {
if (!alreadyListenedEventTypes.has(event.type)) {
target.addEventListener(event.type, listener, ...otherArguments);
alreadyListenedEventTypes.add(event.type);
}
dispatchEvent_original.apply(this, arguments);
};
🔥 function snippet 🔥
function addEventListenerAll(target, listener, ...otherArguments) {
// install listeners for all natively triggered events
for (const key in target) {
if (/^on/.test(key)) {
const eventType = key.substr(2);
target.addEventListener(eventType, listener, ...otherArguments);
}
}
// dynamically install listeners for all manually triggered events, just-in-time before they're dispatched ;D
const dispatchEvent_original = EventTarget.prototype.dispatchEvent;
function dispatchEvent(event) {
target.addEventListener(event.type, listener, ...otherArguments); // multiple identical listeners are automatically discarded
dispatchEvent_original.apply(this, arguments);
}
EventTarget.prototype.dispatchEvent = dispatchEvent;
if (EventTarget.prototype.dispatchEvent !== dispatchEvent) throw new Error(`Browser is smarter than you think!`);
}
// usage example
addEventListenerAll(window, (evt) => {
console.log(evt.type);
});
document.body.click();
document.body.dispatchEvent(new Event('omg!', { bubbles: true }));
// usage example with `useCapture`
// (also receives `bubbles: false` events, but in reverse order)
addEventListenerAll(
window,
(evt) => { console.log(evt.type); },
true
);
document.body.dispatchEvent(new Event('omfggg!', { bubbles: false }));
A bit late to the party but I did create something that might be useful for others here.
https://codepen.io/phreaknation/pen/QmJjEa
This is an ES6 Class that captures all events from an element that is known to that element. This demo allows you to change the element time in the page, as well as read out the events with clickable links to their MDN page as well as interact with the element and see how the events are triggered with time stamps.
I hope this helps
Class code
class EventSystem {
constructor(element) {
this._ = {
element: null
}
return this;
}
getAllEventTypes({blacklist = [], whitelist = []} = {}) {
const events = [];
for (let property in this._.element) {
const match = property.match(/^on(.*)/);
if (match) {
if ((whitelist.length > 0 ? whitelist.indexOf(match) !== -1 : true) &&
(blacklist.length > 0 ? blacklist.indexOf(match) === -1 : true)) {
events.push(match[1]);
}
}
}
return events;
}
getElementType() {
return this._.element.tagName.toLowerCase();
}
setElement(element) {
this._.element = element;
return this;
}
applyEvents(events, callback) {
events.forEach((event) => {
this._.element.addEventListener(event, (ev) => {
if (typeof callback === 'function') {
callback(event, ev);
}
})
})
}
}
My solution to this problem. I loop through all datatypes on the global context (window, in this case), check if the type extends EventTarget, and then extracts them via checking for the "on" prefix.
const getEventNames = (root) => {
let events = [ ];
const objectHasSubPrototype = (object, comp) => {
let proto = Object.getPrototypeOf(object);
while(proto !== null && proto !== EventTarget) {
proto = Object.getPrototypeOf(proto);
}
return (proto !== null);
};
const addEventNames = (propNames) => {
propNames.filter(x => x.match(/^on\w+$/)).forEach((propName) => {
propName = propName.substr(2);
if(events.indexOf(propName) === -1) {
events.push(propName);
}
});
};
Object.getOwnPropertyNames(root).forEach((name) => {
let value = root[name];
if(value) {
if(objectHasSubPrototype(value, EventTarget)) {
let propNames = Object.getOwnPropertyNames(Object.getPrototypeOf(value).prototype);
addEventNames(propNames);
propNames = Object.getOwnPropertyNames(window);
addEventNames(propNames);
}
}
});
return events;
};
// Attach all events to the window
getEventNames(window).forEach((eventName) => {
window.addEventListener(eventName, (event) => console.log(eventName, event));
});
For the last version of the MDN website:
(function getAllEventTypes(){
if(location.href !='https://developer.mozilla.org/en-US/docs/Web/Events') return;
var types = {};
$('.standard-table').map(function(){
if($(this).find('caption').length > 0){
var type = $(this).find('caption')[0].innerHTML || 'OtherEvent';
types[type] = types[type] || [];
$(this).find('tbody tr td code a').each(function(el){
if(this.innerText) types[type].push(this.innerText);
});
}
});
for(var t in types) types[t] = types[t].join(' ');
return "var DOMEvents = "+JSON.stringify(types, null, 4).replace(/"(\w+)\":/ig, '$1:');
})();