diff --git a/index.ts b/index.ts index 05e9cd6..2c4a553 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import type {ParseSelector} from 'typed-query-selector/parser'; +export type DelegateOptions = boolean | Omit; export type EventType = keyof GlobalEventHandlersEventMap; type GlobalEvent = Event; @@ -10,12 +11,12 @@ namespace delegate { export type EventHandler< TEvent extends GlobalEvent = GlobalEvent, - TElement extends Element = Element + TElement extends Element = Element, > = (event: Event) => void; export type Event< TEvent extends GlobalEvent = GlobalEvent, - TElement extends Element = Element + TElement extends Element = Element, > = TEvent & { delegateTarget: TElement; }; @@ -31,15 +32,15 @@ function editLedger( wanted: boolean, baseElement: EventTarget | Document, callback: delegate.EventHandler, - setup: string + setup: string, ): boolean { if (!wanted && !ledger.has(baseElement)) { return false; } - const elementMap = - ledger.get(baseElement) ?? - new WeakMap>(); + const elementMap + = ledger.get(baseElement) + ?? new WeakMap>(); ledger.set(baseElement, elementMap); if (!wanted && !ledger.has(baseElement)) { @@ -60,7 +61,7 @@ function editLedger( } function isEventTarget( - elements: EventTarget | Document | ArrayLike | string + elements: EventTarget | Document | ArrayLike | string, ): elements is EventTarget { return typeof (elements as EventTarget).addEventListener === 'function'; } @@ -82,40 +83,41 @@ function safeClosest(event: Event, selector: string): Element | void { /** * Delegates event to a selector. + * @param options A boolean value setting options.capture or an options object of type AddEventListenerOptions without the `once` option */ function delegate< Selector extends string, TElement extends Element = ParseSelector, - TEventType extends EventType = EventType + TEventType extends EventType = EventType, >( base: EventTarget | Document | ArrayLike | string, selector: Selector, type: TEventType, callback: delegate.EventHandler, - options?: boolean | AddEventListenerOptions + options?: DelegateOptions ): delegate.Subscription; function delegate< TElement extends Element = HTMLElement, - TEventType extends EventType = EventType + TEventType extends EventType = EventType, >( base: EventTarget | Document | ArrayLike | string, selector: string, type: TEventType, callback: delegate.EventHandler, - options?: boolean | AddEventListenerOptions + options?: DelegateOptions ): delegate.Subscription; // This type isn't exported as a declaration, so it needs to be duplicated above function delegate< TElement extends Element, - TEventType extends EventType = EventType + TEventType extends EventType = EventType, >( base: EventTarget | Document | ArrayLike | string, selector: string, type: TEventType, callback: delegate.EventHandler, - options?: boolean | AddEventListenerOptions + options?: DelegateOptions, ): delegate.Subscription { // Handle Selector-based usage if (typeof base === 'string') { @@ -126,15 +128,13 @@ function delegate< if (!isEventTarget(base)) { const subscriptions = Array.prototype.map.call( base, - (element: EventTarget) => { - return delegate( - element, - selector, - type, - callback, - options - ); - } + (element: EventTarget) => delegate( + element, + selector, + type, + callback, + options, + ), ) as delegate.Subscription[]; return { @@ -142,7 +142,7 @@ function delegate< for (const subscription of subscriptions) { subscription.destroy(); } - } + }, }; } @@ -159,13 +159,18 @@ function delegate< } }; + // Drop unsupported `once` option https://github.com/fregante/delegate-it/pull/28#discussion_r863467939 + if (typeof options === 'object') { + delete (options as AddEventListenerOptions).once; + } + const setup = JSON.stringify({selector, type, capture}); const isAlreadyListening = editLedger(true, baseElement, callback, setup); const delegateSubscription = { destroy() { baseElement.removeEventListener(type, listenerFn, options); editLedger(false, baseElement, callback, setup); - } + }, }; if (!isAlreadyListening) { diff --git a/package.json b/package.json index f224ad6..714e5cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "delegate-it", - "version": "3.0.0", + "version": "3.0.1", "description": "Lightweight and modern event delegation in the browser", "keywords": [ "delegate", @@ -38,22 +38,21 @@ "browser" ], "rules": { - "import/extensions": "off", - "import/no-useless-path-segments": "off", "max-params": "off", - "unicorn/import-index": "off" + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/naming-convention": "off" } }, "dependencies": { - "typed-query-selector": "^2.4.1" + "typed-query-selector": "^2.6.1" }, "devDependencies": { - "@sindresorhus/tsconfig": "^1.0.1", - "ava": "^3.15.0", - "jsdom": "^16.5.2", + "@sindresorhus/tsconfig": "^2.0.0", + "ava": "^4.2.0", + "jsdom": "^19.0.0", "npm-run-all": "^4.1.5", - "sinon": "^10.0.0", - "typescript": "^4.2.4", - "xo": "^0.38.2" + "sinon": "^14.0.0", + "typescript": "^4.6.4", + "xo": "^0.48.0" } } diff --git a/readme.md b/readme.md index 7ea625a..4b292ea 100644 --- a/readme.md +++ b/readme.md @@ -52,6 +52,23 @@ delegate(document.querySelectorAll('.container'), '.btn', 'click', event => { }); ``` +#### With listener options + +```js +delegate(document.body, '.btn', 'click', event => { + console.log(event.delegateTarget); +}, true); + +// Or equivalent: +delegate(document.body, '.btn', 'click', event => { + console.log(event.delegateTarget); +}, { + capture: true +}); +``` + +**Note:** the `once` option is currently not supported. + ### Remove event delegation ```js diff --git a/test.js b/test.js index cee71e8..6431658 100644 --- a/test.js +++ b/test.js @@ -1,11 +1,8 @@ import test from 'ava'; import sinon from 'sinon'; -import {createRequire} from 'module'; +import {JSDOM} from 'jsdom'; import delegate from './index.js'; -const require = createRequire(import.meta.url); -export const {JSDOM} = require('jsdom'); - const {window} = new JSDOM(`
  • Item 1
  • @@ -75,9 +72,7 @@ test.serial('should add event listeners to all the elements in a base selector', test.serial('should remove the event listeners from all the elements in a base selector', t => { const items = document.querySelectorAll('li'); - const spies = Array.prototype.map.call(items, li => { - return sinon.spy(li, 'removeEventListener'); - }); + const spies = Array.prototype.map.call(items, li => sinon.spy(li, 'removeEventListener')); const delegation = delegate('li', 'a', 'click', () => {}); delegation.destroy(); @@ -102,9 +97,7 @@ test.serial('should add event listeners to all the elements in a base array', t test.serial('should remove the event listeners from all the elements in a base array', t => { const items = document.querySelectorAll('li'); - const spies = Array.prototype.map.call(items, li => { - return sinon.spy(li, 'removeEventListener'); - }); + const spies = Array.prototype.map.call(items, li => sinon.spy(li, 'removeEventListener')); const delegation = delegate(items, 'a', 'click', () => {}); delegation.destroy(); @@ -123,3 +116,12 @@ test.serial('should not fire when the selector matches an ancestor of the base e anchor.click(); t.true(spy.notCalled); }); + +test.serial('should not consider the `once` option', t => { + const spy = sinon.spy(); + delegate(container, 'a', 'click', spy, {once: true}); + + anchor.click(); + anchor.click(); + t.true(spy.calledTwice); +});