src/event/index.js
/**
* @module Events
*/
import { each } from '../util';
import { closest } from '../selector/closest';
/**
* Shorthand for `addEventListener`. Supports event delegation if a filter (`selector`) is provided.
*
* @param {String} eventNames List of space-separated event types to be added to the element(s)
* @param {String} [selector] Selector to filter descendants that delegate the event to this element.
* @param {Function} handler Event handler
* @param {Boolean} useCapture=false
* @param {Boolean} once=false
* @return {Object} The wrapped collection
* @chainable
* @example
* $('.item').on('click', callback);
* $('.container').on('click focus', '.item', handler);
*/
export const on = function(eventNames, selector, handler, useCapture, once) {
if(typeof selector === 'function') {
handler = selector;
selector = null;
}
let parts,
namespace,
eventListener;
eventNames.split(' ').forEach(eventName => {
parts = eventName.split('.');
eventName = parts[0] || null;
namespace = parts[1] || null;
eventListener = proxyHandler(handler);
each(this, element => {
if(selector) {
eventListener = delegateHandler.bind(element, selector, eventListener);
}
if(once) {
const listener = eventListener;
eventListener = event => {
off.call(element, eventNames, selector, handler, useCapture);
listener.call(element, event);
};
}
element.addEventListener(eventName, eventListener, useCapture || false);
getHandlers(element).push({
eventName,
handler,
eventListener,
selector,
namespace
});
});
}, this);
return this;
};
/**
* Shorthand for `removeEventListener`.
*
* @param {String} eventNames List of space-separated event types to be removed from the element(s)
* @param {String} [selector] Selector to filter descendants that undelegate the event to this element.
* @param {Function} handler Event handler
* @param {Boolean} useCapture=false
* @return {Object} The wrapped collection
* @chainable
* @example
* $('.item').off('click', callback);
* $('#my-element').off('myEvent myOtherEvent');
* $('.item').off();
*/
export const off = function(eventNames = '', selector, handler, useCapture) {
if(typeof selector === 'function') {
handler = selector;
selector = null;
}
let parts,
namespace,
handlers;
eventNames.split(' ').forEach(eventName => {
parts = eventName.split('.');
eventName = parts[0] || null;
namespace = parts[1] || null;
return each(this, element => {
handlers = getHandlers(element);
each(handlers.filter(item => {
return (
(!eventName || item.eventName === eventName) &&
(!namespace || item.namespace === namespace) &&
(!handler || item.handler === handler) &&
(!selector || item.selector === selector)
);
}), item => {
element.removeEventListener(item.eventName, item.eventListener, useCapture || false);
handlers.splice(handlers.indexOf(item), 1);
});
if(!eventName && !namespace && !selector && !handler) {
clearHandlers(element);
} else if(handlers.length === 0) {
clearHandlers(element);
}
});
}, this);
return this;
};
/**
* Add event listener and execute the handler at most once per element.
*
* @param eventNames
* @param selector
* @param handler
* @param useCapture
* @return {Object} The wrapped collection
* @chainable
* @example
* $('.item').one('click', callback);
*/
export const one = function(eventNames, selector, handler, useCapture) {
return on.call(this, eventNames, selector, handler, useCapture, 1);
};
/**
* Get event handlers from an element
*
* @private
* @param {Node} element
* @return {Array}
*/
const eventKeyProp = '__domtastic_event__';
let id = 1;
let handlers = {};
let unusedKeys = [];
export const getHandlers = element => {
if(!element[eventKeyProp]) {
element[eventKeyProp] = unusedKeys.length === 0 ? ++id : unusedKeys.pop();
}
const key = element[eventKeyProp];
return handlers[key] || (handlers[key] = []);
};
/**
* Clear event handlers for an element
*
* @private
* @param {Node} element
*/
export const clearHandlers = element => {
const key = element[eventKeyProp];
if(handlers[key]) {
handlers[key] = null;
element[eventKeyProp] = null;
unusedKeys.push(key);
}
};
/**
* Function to create a handler that augments the event object with some extra methods,
* and executes the callback with the event and the event data (i.e. `event.detail`).
*
* @private
* @param handler Callback to execute as `handler(event, data)`
* @return {Function}
*/
export const proxyHandler = handler => function(event) {
return handler.call(this, augmentEvent(event));
};
const eventMethods = {
preventDefault: 'isDefaultPrevented',
stopImmediatePropagation: 'isImmediatePropagationStopped',
stopPropagation: 'isPropagationStopped'
};
const returnTrue = () => true;
const returnFalse = () => false;
/**
* Attempt to augment events and implement something closer to DOM Level 3 Events.
*
* @private
* @param {Object} event
* @return {Function}
*/
const augmentEvent = event => {
if(!event.isDefaultPrevented || event.stopImmediatePropagation || event.stopPropagation) {
for(const methodName in eventMethods) {
(function(methodName, testMethodName, originalMethod) {
event[methodName] = function() {
this[testMethodName] = returnTrue;
return originalMethod && originalMethod.apply(this, arguments);
};
event[testMethodName] = returnFalse;
}(methodName, eventMethods[methodName], event[methodName]));
}
if(event._preventDefault) {
event.preventDefault();
}
}
return event;
};
/**
* Function to test whether delegated events match the provided `selector` (filter),
* if the event propagation was stopped, and then actually call the provided event handler.
* Use `this` instead of `event.currentTarget` on the event object.
*
* @private
* @param {String} selector Selector to filter descendants that undelegate the event to this element.
* @param {Function} handler Event handler
* @param {Event} event
*/
export const delegateHandler = function(selector, handler, event) {
const eventTarget = event._target || event.target;
const currentTarget = closest.call([eventTarget], selector, this)[0];
if(currentTarget && currentTarget !== this) {
if(currentTarget === eventTarget || !(event.isPropagationStopped && event.isPropagationStopped())) {
handler.call(currentTarget, event);
}
}
};
export const bind = on;
export const unbind = off;