@jjwesterkamp/event-delegation
Version:
Event delegation for browser DOM events. Flexible, cross-browser compatible and Typescript-focused.
274 lines (252 loc) • 10 kB
JavaScript
/*!
* @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