UNPKG

domtastic

Version:

Small, fast, and modular DOM and event library for modern browsers.

257 lines (213 loc) 6.81 kB
/** * @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;