Selfmade jQuery cannot handle event properly - javascript

Update: It might be the jQuery's trigger() do some extra works in testings, I opened a issue on github.
=====
I'm following learnQuery to build my simple jQuery. Now working on DOM event , implement on() and off() function. They provided some testings, I can't pass some of them.
Here is my code: (And you can clone this branch ,run 06.event_listeners/runner.html to run the testing)
"use strict";
function isEmpty(str) {
return (!str || 0 === str.length);
}
// listener use to bind to DOM element, call corresponding functions when event firing.
function geneEventListener(event) {
console.log('gene');
let type = Object.keys(this.handlers).find(type=>type===event.type);
if (!type) return;
let functions = this.handlers[type];
functions.forEach(f=>f.apply(this,event));
}
// cache elements which bound event listener
let Cache = function () {
this.elements = [];
this.uid = 1;
};
Cache.prototype = {
constructor:Cache,
init:function (element) {
if(!element.uid) element.uid = this.uid++;
if(!element.handlers) element.handlers = {};
if(!element.lqListener) element.lqListener = geneEventListener.bind(element);
this.elements.push(element);
},
removeElement:function (uid) {
this.elements.splice(this.elements.findIndex(e=>e.uid===uid),1);
},
removeType:function (uid,type) {
if(this.get(uid)) delete this.get(uid).handlers[type];
},
removeCallback:function (uid, type, callback) {
if(this.get(uid) && this.get(uid).handlers[type]) {
let functions = this.get(uid).handlers[type];
functions.splice(functions.findIndex(callback),1)
}
},
// return element or undefined
get:function (uid) {
return this.elements.find(e=>e.uid===uid);
},
};
/*
* One type could have many event listeners, One element could have many event types of listeners
* So use element.handlers = {'click':[listener1, listener2, ...], 'hover':[...], ...}
* */
let eventListener = (function() {
let cache = new Cache();
function add (element, type, callback){
cache.init(element);
element.addEventListener(type,element.lqListener);
if(!element.handlers[type]){
element.handlers[type] = [];
}
(element.handlers[type]).push(callback);
}
// remove a type of event listeners, should remove the callback array and remove DOM's event listener
function removeType (element, type) {
element.removeEventListener(type,element.lqListener);
cache.removeType(element.uid,type);
}
// remove a event listener, just remove it from the callback array
function removeCallback(element, type, callback) {
cache.removeCallback(element.uid,type,callback);
}
// bind a callback.
function on(element,type,callback) {
if(!(element||type||callback)) throw new Error('Invalid arguments');
add(element,type,callback);
}
function off(element,type,callback) {
if(!(element instanceof HTMLElement)) throw new Error('Invaild element, need a instance of HMTLElement');
let handlers = cache.get(element.uid).handlers;
if(isEmpty(type)&&!callback){
for(let type in handlers){
removeType(element,type);
}
}
console.log('off')
if(!isEmpty(type)&&!callback) removeType(element,type);
if(!isEmpty(type) && (typeof callback === 'function')) removeCallback(element,type,callback);
}
return {
on,
off
}
})();
I use chrome debugger to follow element.handlers's value, it seems fine, working great when add and remove callback.
And the testing have some console.log() in event's callback function, oddly enough, these console.log() do not log in console, and I try to set a breakpoint in callback, it also do not work.
I have little javascript experience, If anyone can tell me how to debug and where is the bug, thank you very much! And why console.log() cannot work in callback. It should work, since they wrote it in testing, I think.
Here is the testing code:
/*global affix*/
/*global eventListener*/
describe('EventListeners', function() {
'use strict';
var $selectedElement, selectedElement, methods;
beforeEach(function() {
affix('.learn-query-testing #toddler .hidden.toy+h1[class="title"]+span[class="subtitle"]+span[class="subtitle"]+input[name="toyName"][value="cuddle bunny"]+input[class="creature"][value="unicorn"]+.hidden+.infinum[value="awesome cool"]');
methods = {
showLove: function(e) {
console.log('<3 JavaScript <3');
},
giveLove: function(e) {
console.log('==> JavaScript ==>');
return '==> JavaScript ==>';
}
};
spyOn(methods, 'showLove');
spyOn(methods, 'giveLove');
$selectedElement = $('#toddler');
selectedElement = $selectedElement[0];
});
it('should be able to add a click event to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove).toHaveBeenCalled();
});
it('should be able to add the same event+callback two times to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should be able to add the same callback for two different events to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
console.log('3')
$selectedElement.trigger('click');
$selectedElement.trigger('hover');
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should be able to add two different callbacks for same event to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
$selectedElement.trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
expect(methods.giveLove.calls.count()).toEqual(1);
});
it('should be able to remove one event handler of an HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.off(selectedElement, 'click', methods.showLove);
console.log('5')
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(0);
expect(methods.giveLove.calls.count()).toEqual(1);
});
it('should be able to remove all click events of a HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
eventListener.off(selectedElement, 'click');
console.log('6')
$selectedElement.trigger('hover');
$selectedElement.trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
expect(methods.giveLove).not.toHaveBeenCalled();
});
it('should be able to remove all events of a HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
eventListener.off(selectedElement);
var eventHover = new Event('hover');
var eventClick = new Event('click');
selectedElement.dispatchEvent(eventClick);
selectedElement.dispatchEvent(eventHover);
expect(methods.showLove).not.toHaveBeenCalled();
expect(methods.giveLove).not.toHaveBeenCalled();
});
it('should trigger a click event on a HTML element', function() {
$selectedElement.off();
$selectedElement.on('click', methods.showLove);
eventListener.trigger(selectedElement, 'click');
expect(methods.showLove.calls.count()).toBe(1);
});
it('should delegate an event to elements with a given css class name', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
$('.title').trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should not delegate an event to elements without a given css class name', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
$('.subtitle').trigger('click');
$('.title').trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should delegate an event to elements that are added to the DOM to after delegate call', function() {
eventListener.delegate(selectedElement, 'new-element-class', 'click', methods.showLove);
var newElement = document.createElement('div');
newElement.className = 'new-element-class';
$selectedElement.append(newElement);
$(newElement).trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should trigger delegated event handler when clicked on an element inside a targeted element', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
var newElement = document.createElement('div');
newElement.className = 'new-element-class';
$selectedElement.append(newElement);
$('.title').append(newElement);
$(newElement).trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should not trigger delegated event handler if clicked on container of delegator', function() {
var $targetElement = $('<p class="target"></p>');
$selectedElement.append($targetElement);
eventListener.delegate(selectedElement, 'target', 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(0);
});
it('should trigger delegated event handler multiple times if event happens on multiple elements', function() {
eventListener.delegate(selectedElement, 'subtitle', 'click', methods.showLove);
$('.subtitle').trigger('click');
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should not trigger method registered on element A when event id triggered on element B', function() {
var elementA = document.createElement('div');
var elementB = document.createElement('div');
$selectedElement.append(elementA);
$selectedElement.append(elementB);
eventListener.on(elementA, 'click', methods.showLove);
eventListener.on(elementB, 'click', methods.giveLove);
$(elementA).trigger('click');
expect(methods.showLove).toHaveBeenCalled();
expect(methods.giveLove).not.toHaveBeenCalled();
});
});

The problem lies in that there is no event called hover.
Just a combination of mouseenter and mouseleave.
You can see all event types listed here.
When calling element.addEventListener(type, element.lqListener)
with type value hover, it just does not work.
You can see more info from this question Is it possible to use jQuery .on and hover?.

You are very close to meeting your own requirement. The only issue that could locate at code, here, is that Function.prototype.apply() expects an Array at second parameter
Syntax
func.apply(thisArg, [argsArray])
Substitute
// pass `event` as element to array literal as second parameter to `.apply()`
functions.forEach(f => f.apply(this, [event]));
for
functions.forEach(f => f.apply(this, event));
Also, substituted function name _Cache for Cache, as Cache is a globally defined function
The Cache interface provides a storage mechanism for Request /
Response object pairs that are cached, for example as part of the
ServiceWorker life cycle.
"use strict";
function isEmpty(str) {
return (!str || 0 === str.length);
}
// listener use to bind to DOM element, call corresponding functions when event firing.
function geneEventListener(event) {
console.log('gene');
let type = Object.keys(this.handlers).find(type => type === event.type);
if (!type) return;
let functions = this.handlers[type];
functions.forEach(f => f.apply(this, [event]));
}
// cache elements which bound event listener
let _Cache = function() {
this.elements = [];
this.uid = 1;
};
_Cache.prototype = {
constructor: _Cache,
init: function(element) {
if (!element.uid) element.uid = this.uid++;
if (!element.handlers) element.handlers = {};
if (!element.lqListener) element.lqListener = geneEventListener.bind(element);
this.elements.push(element);
},
removeElement: function(uid) {
this.elements.splice(this.elements.findIndex(e => e.uid === uid), 1);
},
removeType: function(uid, type) {
if (this.get(uid)) delete this.get(uid).handlers[type];
},
removeCallback: function(uid, type, callback) {
if (this.get(uid) && this.get(uid).handlers[type]) {
let functions = this.get(uid).handlers[type];
functions.splice(functions.findIndex(callback), 1)
}
},
// return element or undefined
get: function(uid) {
return this.elements.find(e => e.uid === uid);
},
};
/*
* One type could have many event listeners, One element could have many event types of listeners
* So use element.handlers = {'click':[listener1, listener2, ...], 'hover':[...], ...}
* */
let eventListener = (function() {
let cache = new _Cache();
function add(element, type, callback) {
cache.init(element);
element.addEventListener(type, element.lqListener);
if (!element.handlers[type]) {
element.handlers[type] = [];
}
(element.handlers[type]).push(callback);
}
// remove a type of event listeners, should remove the callback array and remove DOM's event listener
function removeType(element, type) {
element.removeEventListener(type, element.lqListener);
cache.removeType(element.uid, type);
}
// remove a event listener, just remove it from the callback array
function removeCallback(element, type, callback) {
cache.removeCallback(element.uid, type, callback);
}
// bind a callback.
function on(element, type, callback) {
if (!(element || type || callback)) throw new Error('Invalid arguments');
add(element, type, callback);
}
function off(element, type, callback) {
if (!(element instanceof HTMLElement)) throw new Error('Invaild element, need a instance of HMTLElement');
let handlers = cache.get(element.uid).handlers;
if (isEmpty(type) && !callback) {
for (let type in handlers) {
removeType(element, type);
}
}
console.log('off')
if (!isEmpty(type) && !callback) removeType(element, type);
if (!isEmpty(type) && (typeof callback === 'function')) removeCallback(element, type, callback);
}
return {
on,
off
}
})();
onload = () => {
eventListener.on(document.querySelector("div"), "click", function(event) {
console.log(event.type);
eventListener.off(event.target, "click");
});
}
<div>click</div>

I'm the question owner.
After I created a issue, they fixed the bug in testing. In testing, we can't use our selfmade on() and off() to add event listeners, then use jQuery's trigger() to test it, since jQuery will do some extra works behind. So they replaced it by dispatchEvent().
Also, in my code there are some bugs. As #guest271314 mentioned, I misused apply(), should use call(), and should use _Cache to replace Cache. Besides,
in function removeCallback, I misused functions.findIndex(callback), it should be functions.findIndex(f=>f===callback)
The correct code is on this branch, passed all on and off testing.
Thanks all of you!

Related

Jasmine test event listener function for nodeList from given selector

I have following code which listens for keydown event in given array of nodeList.
var obj = {
method: function(nodeSelector) {
var nodeContainers = document.querySelectorAll(nodeSelector);
var keyListenerFunc = this.keyListener.bind(this);
this.bindListener(nodeContainers, keyListenerFunc);
},
isListNode: function (evt){
return evt.target.nodeName.toLowerCase() === 'li';
},
isContainer: function(evt){
return evt.target.parentNode.classList.contains(this.indicatorClass);
},
keyListener: function(evt) {
if (evt.keyCode === 32 && (this.isContainer(evt) && this.isListNode(evt))) {
evt.preventDefault();
evt.target.click();
}
},
bindListener: function(targets, callbackFunc) {
[].forEach.call(targets, function(item) {
item.addEventListener('keydown', callbackFunc);
});
},
indicatorClass: 'indicator'
};
I'm using it like: obj.method('.someClassNames');
But now I want to test it completely including the triggering of keydown event. How can I attach event listener and then trigger keydown event on given dom nodes so that my Jasmine tests would work ? How can I create some dummy html code here and then trigger event on it ? I am expecting to write tests of this type =>
it('It should put event listeners on each carousel passed to the service', function(){});
it('It should call event.preventDefault', function(){});
it('It should call event.target.click', function(){});
My markup is follwing
var html = '<div class="someClassNames">'+
'<div class="indicator">'+
'<li>text</li>'+
'</div>'
'</div>';
I am assuming that I am going to need to trigger following keydown event but I am not sure as to how to trigger is on the given markup and check in the test description.
var e = new window.KeyboardEvent('keydown', {
bubbles: true
});
Object.defineProperty(e, 'keyCode', {'value': 32});
I am very much new to testing with Jasmine and I couldn't find any examples that would help me test this scenario. I hope my example makes it clear.
few observations:
Note that the callbackFunc is actually assigned to the onkeydown
attribute of the element. Hence you may want to spy on the
element.onkeydown rather than obj.keyListener
Sometimes the render of the UI element may take place after spec has
been run.
So to ensure that you have the element is present, I've used the
setTimeout with a jasmine clock
If you really want to test your obk.keyListener, try using an
anonymous function like here
here is how I've it running. I've used mouseover as I'm lazy :)
var obj = {
testVar : "Object",
method: function(nodeSelector) {
var nodeContainers = document.querySelectorAll(nodeSelector);
var keyListenerFunc = this.keyListener.bind(this);
this.bindListener(nodeContainers, keyListenerFunc);
},
isListNode: function(evt) {
return evt.target.nodeName.toLowerCase() === 'li';
},
isContainer: function(evt) {
return evt.target.parentNode.classList.contains(this.indicatorClass);
},
keyListener: function(evt) {
console.log('Yo! You hovered!');
},
bindListener: function(targets, callbackFunc) {
targets.forEach(function(item) {
item.addEventListener('mouseover', callbackFunc, false);
});
},
indicatorClass: 'indicator'
};
describe('Sample tests', function() {
//this ensures you have the element set up
beforeEach(function() {
jasmine.clock().install();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200;
setTimeout(function() {
obj.method('div.indicator');
}, 0);
});
it('It should put event listeners', function() {
jasmine.clock().tick(10);
var ele= document.getElementsByClassName("indicator")[0];
spyOn(ele, 'onmouseover').and.callThrough();
$('.indicator').trigger('mouseover');
expect(ele.onmouseover).toHaveBeenCalled();
expect(typeof ele.onmouseover).toBe('function');
});
});
HTML CONTENT:
<div class="someClassNames">
<div class="indicator">
<li>text</li>
<br/> </div>
</div>

Override (wrap) an existing jQuery click event with another in javascript

Say I have an existing button and attach a click to it via jQuery:
var $button = $('#test').click(function () { console.log('original function') });
Now, say I want to override that click so that I can add some logic to the function before and after it. I have tried binding and wrapping using the functions below.
Function.prototype.bind = function () {
var fn = this;
var args = Array.prototype.slice.call(arguments);
var object = args.shift();
return function () {
return fn.apply(object, args.concat(Array.prototype.slice.call(arguments)));
}
}
function wrap(object, method, wrapper) {
var fn = object[method];
return object[method] = function() {
return wrapper.apply(this, [fn.bind(this)].concat(
Array.prototype.slice.call(arguments)));
}
}
so I call wrap with the object that the method is a property of, the method and an anonymous function that I want to execute instead. I thought:
wrap($button 'click', function (click) {
console.log('do stuff before original function');
click();
console.log('do stuff after original function');
});
This only calls the original function. I have used this approach on a method of an object before with success. Something like: See this Plunker
Can anyone help me do this with my specific example please?
Thanks
You could create a jQuery function that gets the original event handler function from data, removes the click event, then adds a new event handler. This function would have two parameters (each functions) of before and after handlers.
$(function() {
jQuery.fn.wrapClick = function(before, after) {
// Get and store the original click handler.
// TODO: add a conditional to check if click event exists.
var _orgClick = $._data(this[0], 'events').click[0].handler,
_self = this;
// Remove click event from object.
_self.off('click');
// Add new click event with before and after functions.
return _self.click(function() {
before.call(_self);
_orgClick.call(_self);
after.call(_self);
});
};
var $btn = $('.btn').click(function() {
console.log('original click');
});
$btn.wrapClick(function() {
console.log('before click');
}, function() {
console.log('after click');
});
});
Here is a Codepen
After a long search I reached the same answer as #Corey, here is a similar way of doing it considering multiple events:
function wrap(object, method, wrapper) {
var arr = []
var events = $._data(object[0], 'events')
if(events[method] && events[method].length > 0){ // add all functions to array
events[method].forEach(function(obj){
arr.push(obj.handler)
})
}
if(arr.length){
function processAll(){ // process all original functions in the right order
arr.forEach(function(func){
func.call(object)
})
}
object.off(method).on(method, function(e){wrapper.call(object,processAll)}) //unregister previous events and call new method passing old methods
}
}
$(function(){
$('#test').click(function () { console.log('original function 1') });
var $button = $('#test').click(function () { console.log('original function 2') });
wrap($button, 'click', function (click,e) {
console.log('do stuff before original functions');
click()
console.log('do stuff after original functions');
});
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<div id='test'>click me</div>

How to assing new mouse event on node without overriding those set earlier?

Example:
domNode.onmouseover = function() {
this.innerHTML = "The mighty mouse is over me!"
}
domNode.onmouseover = function() {
this.style.backgroundColor = "yellow";
}
In this example the text won't change, but the thing is also that I don't always know what was assigned before, so is there a way to tell to js: Run everything that was eventually assigned without knowing what was that and then run my function?
It's possible to do this by passing the current event handler to the new handler:
domNode.onmouseover = function()
{
console.log('first handler');
}
domNode.onmouseover = (function (current)
{
return function()
{
current();//call handler that was set when this handler was created
console.log('new handler');
};
})(domNode.onmouseover);//pass reference to current handler
See this fiddle, to see it in actionYou can keep on doing this as much as you want/need:
domNode.onmouseover = function()
{
console.log('first handler');
}
domNode.onmouseover = (function (current)
{
return function()
{
current();
console.log('second handler');
};
})(domNode.onmouseover);
domNode.onmouseover = (function (current)
{
return function()
{
current();
console.log('third handler');
};
})(domNode.onmouseover);
This will log:
first handler
second handler
third handler
That's all there is to it!
First of all, place it in a document.ready. (not sure if you done that)
If you want 2 actions for one action place it in once function.
You can also create 2 functions and call them in your mouseover.
$(document).ready(function(){
domNode.onmouseover = function() {
this.innerHTML = "The mighty mouse is over me!"
this.style.backgroundColor = "yellow";
}
});

adding multiple event listeners to one element

So my dilemma is that I don't want to write the same code twice. Once for the click event and another for the touchstart event.
Here is the original code:
document.getElementById('first').addEventListener('touchstart', function(event) {
do_something();
});
document.getElementById('first').addEventListener('click', function(event) {
do_something();
});
How can I compact this? There HAS to be a simpler way!
I thought some might find this approach useful; it could be applied to any similarly repetitive code:
ES6
['click','ontouchstart'].forEach( evt =>
element.addEventListener(evt, dosomething, false)
);
ES5
['click','ontouchstart'].forEach( function(evt) {
element.addEventListener(evt, dosomething, false);
});
You can just define a function and pass it. Anonymous functions are not special in any way, all functions can be passed around as values.
var elem = document.getElementById('first');
elem.addEventListener('touchstart', handler, false);
elem.addEventListener('click', handler, false);
function handler(event) {
do_something();
}
Maybe you can use a helper function like this:
// events and args should be of type Array
function addMultipleListeners(element,events,handler,useCapture,args){
if (!(events instanceof Array)){
throw 'addMultipleListeners: '+
'please supply an array of eventstrings '+
'(like ["click","mouseover"])';
}
//create a wrapper to be able to use additional arguments
var handlerFn = function(e){
handler.apply(this, args && args instanceof Array ? args : []);
}
for (var i=0;i<events.length;i+=1){
element.addEventListener(events[i],handlerFn,useCapture);
}
}
function handler(e) {
// do things
};
// usage
addMultipleListeners(
document.getElementById('first'),
['touchstart','click'],
handler,
false);
[Edit nov. 2020] This answer is pretty old. The way I solve this nowadays is by using an actions object where handlers are specified per event type, a data-attribute for an element to indicate which action should be executed on it and one generic document wide handler method (so event delegation).
const firstElemHandler = (elem, evt) =>
elem.textContent = `You ${evt.type === "click" ? "clicked" : "touched"}!`;
const actions = {
click: {
firstElemHandler,
},
touchstart: {
firstElemHandler,
},
mouseover: {
firstElemHandler: elem => elem.textContent = "Now ... click me!",
outerHandling: elem => {
console.clear();
console.log(`Hi from outerHandling, handle time ${
new Date().toLocaleTimeString()}`);
},
}
};
Object.keys(actions).forEach(key => document.addEventListener(key, handle));
function handle(evt) {
const origin = evt.target.closest("[data-action]");
return origin &&
actions[evt.type] &&
actions[evt.type][origin.dataset.action] &&
actions[evt.type][origin.dataset.action](origin, evt) ||
true;
}
[data-action]:hover {
cursor: pointer;
}
<div data-action="outerHandling">
<div id="first" data-action="firstElemHandler">
<b>Hover, click or tap</b>
</div>
this is handled too (on mouse over)
</div>
For large numbers of events this might help:
var element = document.getElementById("myId");
var myEvents = "click touchstart touchend".split(" ");
var handler = function (e) {
do something
};
for (var i=0, len = myEvents.length; i < len; i++) {
element.addEventListener(myEvents[i], handler, false);
}
Update 06/2017:
Now that new language features are more widely available you could simplify adding a limited list of events that share one listener.
const element = document.querySelector("#myId");
function handleEvent(e) {
// do something
}
// I prefer string.split because it makes editing the event list slightly easier
"click touchstart touchend touchmove".split(" ")
.map(name => element.addEventListener(name, handleEvent, false));
If you want to handle lots of events and have different requirements per listener you can also pass an object which most people tend to forget.
const el = document.querySelector("#myId");
const eventHandler = {
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handleClick(e);
break;
case "touchend":
this.handleTouchend(e);
break;
default:
this.handleDefault(e);
}
},
handleClick(e) {
// do something
},
handleTouchend(e) {
// do something different
},
handleDefault(e) {
console.log("unhandled event: %s", e.type);
}
}
el.addEventListener(eventHandler);
Update 05/2019:
const el = document.querySelector("#myId");
const eventHandler = {
handlers: {
click(e) {
// do something
},
touchend(e) {
// do something different
},
default(e) {
console.log("unhandled event: %s", e.type);
}
},
// called for each event on this element
handleEvent(evt) {
switch (evt.type) {
case "click":
case "touchstart":
// click and touchstart share click handler
this.handlers.click(e);
break;
case "touchend":
this.handlers.touchend(e);
break;
default:
this.handlers.default(e);
}
}
}
Object.keys(eventHandler.handlers)
.map(eventName => el.addEventListener(eventName, eventHandler))
Unless your do_something function actually does something with any given arguments, you can just pass it as the event handler.
var first = document.getElementById('first');
first.addEventListener('touchstart', do_something, false);
first.addEventListener('click', do_something, false);
Simplest solution for me was passing the code into a separate function and then calling that function in an event listener, works like a charm.
function somefunction() { ..code goes here ..}
variable.addEventListener('keyup', function() {
somefunction(); // calling function on keyup event
})
variable.addEventListener('keydown', function() {
somefunction(); //calling function on keydown event
})
I have a small solution that attaches to the prototype
EventTarget.prototype.addEventListeners = function(type, listener, options,extra) {
let arr = type;
if(typeof type == 'string'){
let sp = type.split(/[\s,;]+/);
arr = sp;
}
for(let a of arr){
this.addEventListener(a,listener,options,extra);
}
};
Allows you to give it a string or Array. The string can be separated with a space(' '), a comma(',') OR a Semicolon(';')
I just made this function (intentionally minified):
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, events, handler)
Usage:
((i,e,f)=>e.forEach(o=>i.addEventListener(o,f)))(element, ['click', 'touchstart'], (event) => {
// function body
});
The difference compared to other approaches is that the handling function is defined only once and then passed to every addEventListener.
EDIT:
Adding a non-minified version to make it more comprehensible. The minified version was meant just to be copy-pasted and used.
((element, event_names, handler) => {
event_names.forEach( (event_name) => {
element.addEventListener(event_name, handler)
})
})(element, ['click', 'touchstart'], (event) => {
// function body
});
I'm new at JavaScript coding, so forgive me if I'm wrong.
I think you can create an object and the event handlers like this:
const myEvents = {
click: clickOnce,
dblclick: clickTwice,
};
function clickOnce() {
console.log("Once");
}
function clickTwice() {
console.log("Twice");
}
Object.keys(myEvents).forEach((key) => {
const myButton = document.querySelector(".myButton")
myButton.addEventListener(key, myEvents[key]);
});
<h1 class="myButton">Button</h1>
And then click on the element.
document.getElementById('first').addEventListener('touchstart',myFunction);
document.getElementById('first').addEventListener('click',myFunction);
function myFunction(e){
e.preventDefault();e.stopPropagation()
do_something();
}
You should be using e.stopPropagation() because if not, your function will fired twice on mobile
This is my solution in which I deal with multiple events in my workflow.
let h2 = document.querySelector("h2");
function addMultipleEvents(eventsArray, targetElem, handler) {
eventsArray.map(function(event) {
targetElem.addEventListener(event, handler, false);
}
);
}
let counter = 0;
function countP() {
counter++;
h2.innerHTML = counter;
}
// magic starts over here...
addMultipleEvents(['click', 'mouseleave', 'mouseenter'], h2, countP);
<h1>MULTI EVENTS DEMO - If you click, move away or enter the mouse on the number, it counts...</h1>
<h2 style="text-align:center; font: bold 3em comic; cursor: pointer">0</h2>
What about something like this:
['focusout','keydown'].forEach( function(evt) {
self.slave.addEventListener(evt, function(event) {
// Here `this` is for the slave, i.e. `self.slave`
if ((event.type === 'keydown' && event.which === 27) || event.type === 'focusout') {
this.style.display = 'none';
this.parentNode.querySelector('.master').style.display = '';
this.parentNode.querySelector('.master').value = this.value;
console.log('out');
}
}, false);
});
// The above is replacement of:
/* self.slave.addEventListener("focusout", function(event) { })
self.slave.addEventListener("keydown", function(event) {
if (event.which === 27) { // Esc
}
})
*/
You can simply do it iterating an Object. This can work with a single or multiple elements. This is an example:
const ELEMENTS = {'click': element1, ...};
for (const [key, value] of Object.entries(ELEMENTS)) {
value.addEventListener(key, () => {
do_something();
});
}
When key is the type of event and value is the element when you are adding the event, so you can edit ELEMENTS adding your elements and the type of event.
Semi-related, but this is for initializing one unique event listener specific per element.
You can use the slider to show the values in realtime, or check the console.
On the <input> element I have a attr tag called data-whatever, so you can customize that data if you want to.
sliders = document.querySelectorAll("input");
sliders.forEach(item=> {
item.addEventListener('input', (e) => {
console.log(`${item.getAttribute("data-whatever")} is this value: ${e.target.value}`);
item.nextElementSibling.textContent = e.target.value;
});
})
.wrapper {
display: flex;
}
span {
padding-right: 30px;
margin-left: 5px;
}
* {
font-size: 12px
}
<div class="wrapper">
<input type="range" min="1" data-whatever="size" max="800" value="50" id="sliderSize">
<em>50</em>
<span>Size</span>
<br>
<input type="range" min="1" data-whatever="OriginY" max="800" value="50" id="sliderOriginY">
<em>50</em>
<span>OriginY</span>
<br>
<input type="range" min="1" data-whatever="OriginX" max="800" value="50" id="sliderOriginX">
<em>50</em>
<span>OriginX</span>
</div>
//catch volume update
var volEvents = "change,input";
var volEventsArr = volEvents.split(",");
for(var i = 0;i<volknob.length;i++) {
for(var k=0;k<volEventsArr.length;k++) {
volknob[i].addEventListener(volEventsArr[k], function() {
var cfa = document.getElementsByClassName('watch_televised');
for (var j = 0; j<cfa.length; j++) {
cfa[j].volume = this.value / 100;
}
});
}
}
'onclick' in the html works for both touch and click event. Here's the example.
This mini javascript libary (1.3 KB) can do all these things
https://github.com/Norair1997/norjs/
nor.event(["#first"], ["touchstart", "click"], [doSomething, doSomething]);

How do you log all events fired by an element in jQuery?

I'd like to see all the events fired by an input field as a user interacts with it. This includes stuff like:
Clicking on it.
Clicking off it.
Tabbing into it.
Tabbing away from it.
Ctrl+C and Ctrl+V on the keyboard.
Right click -> Paste.
Right click -> Cut.
Right click -> Copy.
Dragging and dropping text from another application.
Modifying it with Javascript.
Modifying it with a debug tool, like Firebug.
I'd like to display it using console.log. Is this possible in Javascript/jQuery, and if so, how do I do it?
I have no idea why no-one uses this... (maybe because it's only a webkit thing)
Open console:
monitorEvents(document.body); // logs all events on the body
monitorEvents(document.body, 'mouse'); // logs mouse events on the body
monitorEvents(document.body.querySelectorAll('input')); // logs all events on inputs
$(element).on("click mousedown mouseup focus blur keydown change",function(e){
console.log(e);
});
That will get you a lot (but not all) of the information on if an event is fired... other than manually coding it like this, I can't think of any other way to do that.
There is a nice generic way using the .data('events') collection:
function getEventsList($obj) {
var ev = new Array(),
events = $obj.data('events'),
i;
for(i in events) { ev.push(i); }
return ev.join(' ');
}
$obj.on(getEventsList($obj), function(e) {
console.log(e);
});
This logs every event that has been already bound to the element by jQuery the moment this specific event gets fired. This code was pretty damn helpful for me many times.
Btw: If you want to see every possible event being fired on an object use firebug: just right click on the DOM element in html tab and check "Log Events". Every event then gets logged to the console (this is sometimes a bit annoying because it logs every mouse movement...).
$('body').on("click mousedown mouseup focus blur keydown change mouseup click dblclick mousemove mouseover mouseout mousewheel keydown keyup keypress textInput touchstart touchmove touchend touchcancel resize scroll zoom focus blur select change submit reset",function(e){
console.log(e);
});
I know the answer has already been accepted to this, but I think there might be a slightly more reliable way where you don't necessarily have to know the name of the event beforehand. This only works for native events though as far as I know, not custom ones that have been created by plugins. I opted to omit the use of jQuery to simplify things a little.
let input = document.getElementById('inputId');
Object.getOwnPropertyNames(input)
.filter(key => key.slice(0, 2) === 'on')
.map(key => key.slice(2))
.forEach(eventName => {
input.addEventListener(eventName, event => {
console.log(event.type);
console.log(event);
});
});
I hope this helps anyone who reads this.
EDIT
So I saw another question here that was similar, so another suggestion would be to do the following:
monitorEvents(document.getElementById('inputId'));
Old thread, I know. I needed also something to monitor events and wrote this very handy (excellent) solution. You can monitor all events with this hook (in windows programming this is called a hook). This hook does not affects the operation of your software/program.
In the console log you can see something like this:
Explanation of what you see:
In the console log you will see all events you select (see below "how to use") and shows the object-type, classname(s), id, <:name of function>, <:eventname>.
The formatting of the objects is css-like.
When you click a button or whatever binded event, you will see it in the console log.
The code I wrote:
function setJQueryEventHandlersDebugHooks(bMonTrigger, bMonOn, bMonOff)
{
jQuery.fn.___getHookName___ = function()
{
// First, get object name
var sName = new String( this[0].constructor ),
i = sName.indexOf(' ');
sName = sName.substr( i, sName.indexOf('(')-i );
// Classname can be more than one, add class points to all
if( typeof this[0].className === 'string' )
{
var sClasses = this[0].className.split(' ');
sClasses[0]='.'+sClasses[0];
sClasses = sClasses.join('.');
sName+=sClasses;
}
// Get id if there is one
sName+=(this[0].id)?('#'+this[0].id):'';
return sName;
};
var bTrigger = (typeof bMonTrigger !== "undefined")?bMonTrigger:true,
bOn = (typeof bMonOn !== "undefined")?bMonOn:true,
bOff = (typeof bMonOff !== "undefined")?bMonOff:true,
fTriggerInherited = jQuery.fn.trigger,
fOnInherited = jQuery.fn.on,
fOffInherited = jQuery.fn.off;
if( bTrigger )
{
jQuery.fn.trigger = function()
{
console.log( this.___getHookName___()+':trigger('+arguments[0]+')' );
return fTriggerInherited.apply(this,arguments);
};
}
if( bOn )
{
jQuery.fn.on = function()
{
if( !this[0].__hooked__ )
{
this[0].__hooked__ = true; // avoids infinite loop!
console.log( this.___getHookName___()+':on('+arguments[0]+') - binded' );
$(this).on( arguments[0], function(e)
{
console.log( $(this).___getHookName___()+':'+e.type );
});
}
var uResult = fOnInherited.apply(this,arguments);
this[0].__hooked__ = false; // reset for another event
return uResult;
};
}
if( bOff )
{
jQuery.fn.off = function()
{
if( !this[0].__unhooked__ )
{
this[0].__unhooked__ = true; // avoids infinite loop!
console.log( this.___getHookName___()+':off('+arguments[0]+') - unbinded' );
$(this).off( arguments[0] );
}
var uResult = fOffInherited.apply(this,arguments);
this[0].__unhooked__ = false; // reset for another event
return uResult;
};
}
}
Examples how to use it:
Monitor all events:
setJQueryEventHandlersDebugHooks();
Monitor all triggers only:
setJQueryEventHandlersDebugHooks(true,false,false);
Monitor all ON events only:
setJQueryEventHandlersDebugHooks(false,true,false);
Monitor all OFF unbinds only:
setJQueryEventHandlersDebugHooks(false,false,true);
Remarks/Notice:
Use this for debugging only, turn it off when using in product final
version
If you want to see all events, you have to call this function
directly after jQuery is loaded
If you want to see only less events, you can call the function on the time you need it
If you want to auto execute it, place ( )(); around function
Hope it helps! ;-)
https://github.com/robertleeplummerjr/wiretap.js
new Wiretap({
add: function() {
//fire when an event is bound to element
},
before: function() {
//fire just before an event executes, arguments are automatic
},
after: function() {
//fire just after an event executes, arguments are automatic
}
});
Just add this to the page and no other worries, will handle rest for you:
$('input').live('click mousedown mouseup focus keydown change blur', function(e) {
console.log(e);
});
You can also use console.log('Input event:' + e.type) to make it easier.
STEP 1: Check the events for an HTML element on the developer console:
STEP 2: Listen to the events we want to capture:
$(document).on('ch-ui-container-closed ch-ui-container-opened', function(evt){
console.log(evt);
});
Good Luck...
I recently found and modified this snippet from an existing SO post that I have not been able to find again but I've found it very useful
// specify any elements you've attached listeners to here
const nodes = [document]
// https://developer.mozilla.org/en-US/docs/Web/Events
const logBrowserEvents = () => {
const AllEvents = {
AnimationEvent: ['animationend', 'animationiteration', 'animationstart'],
AudioProcessingEvent: ['audioprocess'],
BeforeUnloadEvent: ['beforeunload'],
CompositionEvent: [
'compositionend',
'compositionstart',
'compositionupdate',
],
ClipboardEvent: ['copy', 'cut', 'paste'],
DeviceLightEvent: ['devicelight'],
DeviceMotionEvent: ['devicemotion'],
DeviceOrientationEvent: ['deviceorientation'],
DeviceProximityEvent: ['deviceproximity'],
DragEvent: [
'drag',
'dragend',
'dragenter',
'dragleave',
'dragover',
'dragstart',
'drop',
],
Event: [
'DOMContentLoaded',
'abort',
'afterprint',
'beforeprint',
'cached',
'canplay',
'canplaythrough',
'change',
'chargingchange',
'chargingtimechange',
'checking',
'close',
'dischargingtimechange',
'downloading',
'durationchange',
'emptied',
'ended',
'error',
'fullscreenchange',
'fullscreenerror',
'input',
'invalid',
'languagechange',
'levelchange',
'loadeddata',
'loadedmetadata',
'noupdate',
'obsolete',
'offline',
'online',
'open',
'open',
'orientationchange',
'pause',
'play',
'playing',
'pointerlockchange',
'pointerlockerror',
'ratechange',
'readystatechange',
'reset',
'seeked',
'seeking',
'stalled',
'submit',
'success',
'suspend',
'timeupdate',
'updateready',
'visibilitychange',
'volumechange',
'waiting',
],
FocusEvent: [
'DOMFocusIn',
'DOMFocusOut',
'Unimplemented',
'blur',
'focus',
'focusin',
'focusout',
],
GamepadEvent: ['gamepadconnected', 'gamepaddisconnected'],
HashChangeEvent: ['hashchange'],
KeyboardEvent: ['keydown', 'keypress', 'keyup'],
MessageEvent: ['message'],
MouseEvent: [
'click',
'contextmenu',
'dblclick',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'show',
],
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events
MutationNameEvent: ['DOMAttributeNameChanged', 'DOMElementNameChanged'],
MutationEvent: [
'DOMAttrModified',
'DOMCharacterDataModified',
'DOMNodeInserted',
'DOMNodeInsertedIntoDocument',
'DOMNodeRemoved',
'DOMNodeRemovedFromDocument',
'DOMSubtreeModified',
],
OfflineAudioCompletionEvent: ['complete'],
OtherEvent: ['blocked', 'complete', 'upgradeneeded', 'versionchange'],
UIEvent: [
'DOMActivate',
'abort',
'error',
'load',
'resize',
'scroll',
'select',
'unload',
],
PageTransitionEvent: ['pagehide', 'pageshow'],
PopStateEvent: ['popstate'],
ProgressEvent: [
'abort',
'error',
'load',
'loadend',
'loadstart',
'progress',
],
SensorEvent: ['compassneedscalibration', 'Unimplemented', 'userproximity'],
StorageEvent: ['storage'],
SVGEvent: [
'SVGAbort',
'SVGError',
'SVGLoad',
'SVGResize',
'SVGScroll',
'SVGUnload',
],
SVGZoomEvent: ['SVGZoom'],
TimeEvent: ['beginEvent', 'endEvent', 'repeatEvent'],
TouchEvent: [
'touchcancel',
'touchend',
'touchenter',
'touchleave',
'touchmove',
'touchstart',
],
TransitionEvent: ['transitionend'],
WheelEvent: ['wheel'],
}
const RecentlyLoggedDOMEventTypes = {}
Object.keys(AllEvents).forEach((DOMEvent) => {
const DOMEventTypes = AllEvents[DOMEvent]
if (Object.prototype.hasOwnProperty.call(AllEvents, DOMEvent)) {
DOMEventTypes.forEach((DOMEventType) => {
const DOMEventCategory = `${DOMEvent} ${DOMEventType}`
nodes.forEach((node) => {
node.addEventListener(
DOMEventType,
(e) => {
if (RecentlyLoggedDOMEventTypes[DOMEventCategory]) return
RecentlyLoggedDOMEventTypes[DOMEventCategory] = true
// NOTE: throttle continuous events
setTimeout(() => {
RecentlyLoggedDOMEventTypes[DOMEventCategory] = false
}, 1000)
const isActive = e.target === document.activeElement
// https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/activeElement
const hasActiveElement = document.activeElement !== document.body
const msg = [
DOMEventCategory,
'target:',
e.target,
...(hasActiveElement
? ['active:', document.activeElement]
: []),
]
if (isActive) {
console.info(...msg)
}
},
true,
)
})
})
}
})
}
logBrowserEvents()
// export default logBrowserEvents
function bindAllEvents (el) {
for (const key in el) {
if (key.slice(0, 2) === 'on') {
el.addEventListener(key.slice(2), e => console.log(e.type));
}
}
}
bindAllEvents($('.yourElement'))
This uses a bit of ES6 for prettiness, but can easily be translated for legacy browsers as well. In the function attached to the event listeners, it's currently just logging out what kind of event occurred but this is where you could print out additional information, or using a switch case on the e.type, you could only print information on specific events
Here is a non-jquery way to monitor events in the console with your code and without the use of monitorEvents() because that only works in Chrome Developer Console. You can also choose to not monitor certain events by editing the no_watch array.
function getEvents(obj) {
window["events_list"] = [];
var no_watch = ['mouse', 'pointer']; // Array of event types not to watch
var no_watch_reg = new RegExp(no_watch.join("|"));
for (var prop in obj) {
if (prop.indexOf("on") === 0) {
prop = prop.substring(2); // remove "on" from beginning
if (!prop.match(no_watch_reg)) {
window["events_list"].push(prop);
window.addEventListener(prop, function() {
console.log(this.event); // Display fired event in console
} , false);
}
}
}
window["events_list"].sort(); // Alphabetical order
}
getEvents(document); // Put window, document or any html element here
console.log(events_list); // List every event on element
How to listen for all events on an Element (Vanilla JS)
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
const input = document.querySelector('input');
addEventListenerAll(input, (evt) => {
console.log(evt.type);
});
input.focus();
input.click();
input.dispatchEvent(new Event('omg!', { bubbles: true }));
// usage example with `useCapture`
// (also receives `bubbles: false` events, but in reverse order)
addEventListenerAll(
input,
(evt) => { console.log(evt.type); },
true
);
document.body.dispatchEvent(new Event('omfggg!', { bubbles: false }));

Categories

Resources