UNPKG

@jjwesterkamp/event-delegation

Version:

Event delegation for browser DOM events. Flexible, cross-browser compatible and Typescript-focused.

274 lines (252 loc) 10 kB
/*! * @jjwesterkamp/event-delegation * https://jjwesterkamp.github.io/event-delegation * (c) 2018-2021 Jeffrey Westerkamp * This software may be freely distributed under the MIT license. */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["EventDelegation"] = factory(); else root["EventDelegation"] = factory(); })(self, function() { return /******/ (function() { // webpackBootstrap /******/ "use strict"; /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ !function() { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = function(exports, definition) { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ }(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ !function() { /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } /******/ }(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // EXPORTS __webpack_require__.d(__webpack_exports__, { "default": function() { return /* binding */ src; } }); ;// CONCATENATED MODULE: ./src/lib/assertions.ts function isNil(subject) { return subject === null || subject === undefined; } function isString(subject) { return typeof subject === 'string'; } function isFunction(subject) { return typeof subject === 'function'; } ;// CONCATENATED MODULE: ./src/lib/matches.ts /** * Tells whether given element matches a given CSS selector. * * @param element * @param selector */ function matches(element, selector) { if (!(element instanceof Element)) { throw new Error('cannot match a non-element against a selector'); } if (isFunction(element.matches)) return element.matches(selector); if (isFunction(element.matchesSelector)) return element.matchesSelector(selector); if (isFunction(element.mozMatchesSelector)) return element.mozMatchesSelector(selector); if (isFunction(element.msMatchesSelector)) return element.msMatchesSelector(selector); if (isFunction(element.oMatchesSelector)) return element.oMatchesSelector(selector); /* istanbul ignore else */ if (isFunction(element.webkitMatchesSelector)) return element.webkitMatchesSelector(selector); /* istanbul ignore next */ { var matches_1 = (element.document || element.ownerDocument).querySelectorAll(selector); var i = matches_1.length; while (--i >= 0 && matches_1.item(i) !== element) { } return i > -1; } } ;// CONCATENATED MODULE: ./src/lib/closestWithin.ts /** * Queries up the DOM for given `selector`, starting from given `leafElement`. * The first element found matching `selector` will be returned. * Querying will stop as soon as given `root` is encountered. * If no matching element was found, `null` is returned. * * Based on the {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill MDN `closest` polyfill} * * @param leaf The innermost element in the DOM tree to start searching from. * @param selector The selector (CSS style) to match ancestor elements with. * @param root The element that acts as a scope for the query. */ function closestWithin(leaf, selector, root) { if (isFunction(leaf.closest)) { var closest = leaf.closest(selector); if (closest === null || !root.contains(closest)) { return null; } return closest; } if (!root.contains(leaf)) { return null; } var current = leaf; do { if (matches(current, selector)) { return current; } current = current.parentElement; } while (current !== null && current !== root && current.nodeType === Node.ELEMENT_NODE); return null; } ;// CONCATENATED MODULE: ./src/EventHandler.ts var EventHandler = /** @class */ (function () { function EventHandler(config) { var _this = this; this.config = config; this._isAttached = false; this._isDestroyed = false; // If config has an object for listenerOptions with { once: true }, create a surrogate // listenerOptions object with { once: false } for the actual native listener. // We want the listener to detach after one callback execution rather than after the first // event. The first event(s) might not match the given selector, resulting in the listener // callback to never run: this.actualListenerOptions = this.isOnceListener() ? Object.assign({}, config.listenerOptions, { once: false }) : config.listenerOptions; this.handler = function (event) { var delegator = closestWithin(event.target, config.selector, config.root); if (isNil(delegator)) { return; } config.listener.call(delegator, Object.assign(event, { delegator: delegator })); // If this is a { once: true } listener we'll manually remove the listener after the first matching event: if (_this.isOnceListener()) { _this.remove(); } }; config.root.addEventListener(config.eventType, this.handler, this.actualListenerOptions); this._isAttached = true; } EventHandler.prototype.isAttached = function () { return this._isAttached; }; EventHandler.prototype.isDestroyed = function () { return this._isDestroyed; }; EventHandler.prototype.root = function () { return this.config.root; }; EventHandler.prototype.selector = function () { return this.config.selector; }; EventHandler.prototype.eventType = function () { return this.config.eventType; }; EventHandler.prototype.remove = function () { if (this._isDestroyed) { return; } this.config.root.removeEventListener(this.config.eventType, this.handler, this.actualListenerOptions); this._isDestroyed = true; this._isAttached = false; }; EventHandler.prototype.isOnceListener = function () { return typeof this.config.listenerOptions === 'object' && this.config.listenerOptions.once === true; }; return EventHandler; }()); ;// CONCATENATED MODULE: ./src/lib/createBuilder.ts var createBuilder = function (root) { return ({ events: function (eventType) { return ({ select: function (selector) { return ({ listen: function (listener, listenerOptions) { return new EventHandler({ root: root, eventType: eventType, selector: selector, listener: listener, listenerOptions: listenerOptions, }); } }); } }); } }); }; ;// CONCATENATED MODULE: ./src/lib/createCompositeBuilder.ts var createCompositeBuilder = function (builders) { return ({ events: function (eventType) { return ({ select: function (selector) { return ({ listen: function (listener, listenerOptions) { return builders.map(function (builder) { return builder .events(eventType) .select(selector) .listen(listener, listenerOptions); }); } }); } }); } }); }; ;// CONCATENATED MODULE: ./src/lib/normalizeRoot.ts function normalizeRoot(rootOrSelector) { if (isString(rootOrSelector)) { var rootOrNull = document.querySelector(rootOrSelector); if (isNil(rootOrNull)) { throw new Error("Couldn't find any root element matching selector '" + rootOrSelector + "'."); } return rootOrNull; } return rootOrSelector; } ;// CONCATENATED MODULE: ./src/index.ts var EventDelegation = { // ---------------------------------------------------------------------------------------- // Create a global delegated event listener. // ---------------------------------------------------------------------------------------- global: function () { return createBuilder(document.body); }, // ---------------------------------------------------------------------------------------- // Create one delegated event listener for a specified root element. // ---------------------------------------------------------------------------------------- within: function (rootOrSelector) { return createBuilder(normalizeRoot(rootOrSelector)); }, // ---------------------------------------------------------------------------------------- // Create many delegated event listeners for multiple specified root elements. // ---------------------------------------------------------------------------------------- withinMany: function (rootsOrSelector) { var roots = isString(rootsOrSelector) ? Array.from(document.querySelectorAll(rootsOrSelector)) : rootsOrSelector; return createCompositeBuilder(roots.map(createBuilder)); }, }; /* harmony default export */ var src = (EventDelegation); __webpack_exports__ = __webpack_exports__.default; /******/ return __webpack_exports__; /******/ })() ; }); //# sourceMappingURL=event-delegation.js.map