@aegisjsproject/callback-registry 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ <!-- markdownlint-disable -->
2
+ # Changelog
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [v1.0.0] - 2024-11-16
11
+
12
+ Initial Release
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Chris Zuber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # `@aegisjsproject/callback-registry`
2
+
3
+ A template repo for npm packages
4
+
5
+ [![CodeQL](https://github.com/AegisJSProject/callback-registry/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/AegisJSProject/callback-registry/actions/workflows/codeql-analysis.yml)
6
+ ![Node CI](https://github.com/AegisJSProject/callback-registry/workflows/Node%20CI/badge.svg)
7
+ ![Lint Code Base](https://github.com/AegisJSProject/callback-registry/workflows/Lint%20Code%20Base/badge.svg)
8
+
9
+ [![GitHub license](https://img.shields.io/github/license/AegisJSProject/callback-registry.svg)](https://github.com/AegisJSProject/callback-registry/blob/master/LICENSE)
10
+ [![GitHub last commit](https://img.shields.io/github/last-commit/AegisJSProject/callback-registry.svg)](https://github.com/AegisJSProject/callback-registry/commits/master)
11
+ [![GitHub release](https://img.shields.io/github/release/AegisJSProject/callback-registry?logo=github)](https://github.com/AegisJSProject/callback-registry/releases)
12
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/shgysk8zer0?logo=github)](https://github.com/sponsors/shgysk8zer0)
13
+
14
+ [![npm](https://img.shields.io/npm/v/@aegisjsproject/callback-registry)](https://www.npmjs.com/package/@aegisjsproject/callback-registry)
15
+ ![node-current](https://img.shields.io/node/v/@aegisjsproject/callback-registry)
16
+ ![npm bundle size gzipped](https://img.shields.io/bundlephobia/minzip/@aegisjsproject/callback-registry)
17
+ [![npm](https://img.shields.io/npm/dw/@aegisjsproject/callback-registry?logo=npm)](https://www.npmjs.com/package/@aegisjsproject/callback-registry)
18
+
19
+ [![GitHub followers](https://img.shields.io/github/followers/AegisJSProject.svg?style=social)](https://github.com/shgysk8zer0)
20
+ ![GitHub forks](https://img.shields.io/github/forks/AegisJSProject/callback-registry.svg?style=social)
21
+ ![GitHub stars](https://img.shields.io/github/stars/AegisJSProject/callback-registry.svg?style=social)
22
+ [![Twitter Follow](https://img.shields.io/twitter/follow/shgysk8zer0.svg?style=social)](https://twitter.com/shgysk8zer0)
23
+
24
+ [![Donate using Liberapay](https://img.shields.io/liberapay/receives/shgysk8zer0.svg?logo=liberapay)](https://liberapay.com/shgysk8zer0/donate "Donate using Liberapay")
25
+ - - -
26
+
27
+ - [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
28
+ - [Contributing](./.github/CONTRIBUTING.md)
29
+ <!-- - [Security Policy](./.github/SECURITY.md) -->
30
+
31
+ A lightweight, modular JavaScript library for managing DOM events and callbacks. The library is designed for flexibility and efficiency, with a focus on leveraging modern JavaScript standards.
32
+
33
+ ## Features
34
+
35
+ - **Callback Management**: Centralized registration and retrieval of reusable callback functions.
36
+ - **Event Handling**: Simplified DOM event binding with support for custom attributes (e.g., `data-aegis-event-on-*`).
37
+ - **Custom Events**: Easily define and trigger application-specific events.
38
+ - **Declarative Attributes**: Leverages custom data attributes for declarative event configuration.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install @aegisjsproject/callback-registry
44
+ ```
45
+
46
+ ## Example usage
47
+ ```js
48
+ import { createCallback, observeEvents, EVENTS } from '@aegisjsproject/callback-registry';
49
+
50
+ observeEvents();
51
+
52
+ const el = document.querySelector('.container');
53
+
54
+ el.setHTMLUnsafe(`<button ${EVENTS.onClick}="${createCallback(({ target }) => alert(target.innerHTML))}">Hello, World!</button>`);
55
+ ```
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ let _isRegistrationOpen = true;
4
+
5
+ const $$ = (selector, base = document) => base.querySelectorAll(selector);
6
+
7
+ const $ = (selector, base = document) => base.querySelector(selector);
8
+
9
+ const FUNCS = {
10
+ debug: {
11
+ log: 'aegis:debug:log',
12
+ info: 'aegis:debug:info',
13
+ warn: 'aegis:debug:warn',
14
+ error: 'aegis:debug:error',
15
+ },
16
+ navigate: {
17
+ back: 'aegis:navigate:back',
18
+ forward: 'aegis:navigate:forward',
19
+ reload: 'aegis:navigate:reload',
20
+ link: 'aegis:navigate:go',
21
+ popup: 'aegis:navigate:popup',
22
+ },
23
+ ui: {
24
+ print: 'aegis:ui:print',
25
+ remove: 'aegis:ui:remove',
26
+ hide: 'aegis:ui:hide',
27
+ unhide: 'aegis:ui:unhide',
28
+ showModal: 'aegis:ui:showModal',
29
+ closeModal: 'aegis:ui:closeModal',
30
+ showPopover: 'aegis:ui:showPopover',
31
+ hidePopover: 'aegis:ui:hidePopover',
32
+ togglePopover: 'aegis:ui:togglePopover',
33
+ enable: 'aegis:ui:enable',
34
+ disable: 'aegis:ui:disable',
35
+ scrollTo: 'aegis:ui:scrollTo',
36
+ prevent: 'aegis:ui:prevent',
37
+ },
38
+ };
39
+
40
+ const registry = new Map([
41
+ [FUNCS.debug.log, console.log],
42
+ [FUNCS.debug.warn, console.warn],
43
+ [FUNCS.debug.error, console.error],
44
+ [FUNCS.debug.info, console.info],
45
+ [FUNCS.navigate.back, history.back],
46
+ [FUNCS.navigate.forward, history.forward],
47
+ [FUNCS.navigate.reload, () => history.go(0)],
48
+ [FUNCS.navigate.link, event => {
49
+ if (event.isTrusted) {
50
+ event.preventDefault();
51
+ location.href = event.currentTarget.dataset.url;
52
+ }
53
+ }],
54
+ [FUNCS.navigate.popup, event => {
55
+ if (event.isTrusted) {
56
+ event.preventDefault();
57
+ globalThis.open(event.currentTarget.dataset.url);
58
+ }
59
+ }],
60
+ [FUNCS.ui.hide, ({ currentTarget }) => {
61
+ $$(currentTarget.dataset.hideSelector).forEach(el => el.hidden = true);
62
+ }],
63
+ [FUNCS.ui.unhide, ({ currentTarget }) => {
64
+ $$(currentTarget.dataset.unhideSelector).forEach(el => el.hidden = false);
65
+ }],
66
+ [FUNCS.ui.disable, ({ currentTarget }) => {
67
+ $$(currentTarget.dataset.disableSelector).forEach(el => el.disabled = true);
68
+ }],
69
+ [FUNCS.ui.enable, ({ currentTarget }) => {
70
+ $$(currentTarget.dataset.enableSelector).forEach(el => el.disabled = false);
71
+ }],
72
+ [FUNCS.ui.remove, ({ currentTarget }) => {
73
+ $$(currentTarget.dataset.removeSelector).forEach(el => el.remove());
74
+ }],
75
+ [FUNCS.ui.scrollTo, ({ currentTarget }) => {
76
+ const target = $(currentTarget.dataset.scrollToSelector);
77
+
78
+ if (target instanceof Element) {
79
+ target.scrollIntoView({
80
+ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches
81
+ ? 'instant'
82
+ : 'smooth',
83
+ });
84
+ }
85
+ }],
86
+ [FUNCS.ui.showModal, ({ currentTarget }) => {
87
+ const target = $(currentTarget.dataset.showModalSelector);
88
+
89
+ if (target instanceof HTMLDialogElement) {
90
+ target.showModal();
91
+ }
92
+ }],
93
+ [FUNCS.ui.closeModal, ({ currentTarget }) => {
94
+ const target = $(currentTarget.dataset.closeModalSelector);
95
+
96
+ if (target instanceof HTMLDialogElement) {
97
+ target.close();
98
+ }
99
+ }],
100
+ [FUNCS.ui.showPopover, ({ currentTarget }) => {
101
+ const target = $(currentTarget.dataset.showPopoverSelector);
102
+
103
+ if (target instanceof HTMLElement) {
104
+ target.showPopover();
105
+ }
106
+ }],
107
+ [FUNCS.ui.hidePopover, ({ currentTarget }) => {
108
+ const target = $(currentTarget.dataset.hidePopoverSelector);
109
+
110
+ if (target instanceof HTMLElement) {
111
+ target.hidePopover();
112
+ }
113
+ }],
114
+ [FUNCS.ui.togglePopover, ({ currentTarget }) => {
115
+ const target = $(currentTarget.dataset.togglePopoverSelector);
116
+
117
+ if (target instanceof HTMLElement) {
118
+ target.togglePopover();
119
+ }
120
+ }],
121
+ [FUNCS.ui.print, () => globalThis.print()],
122
+ [FUNCS.ui.prevent, event => event.preventDefault()],
123
+ ]);
124
+
125
+ /**
126
+ * Check if callback registry is open
127
+ *
128
+ * @returns {boolean} Whether or not callback registry is open
129
+ */
130
+ const isRegistrationOpen = () => _isRegistrationOpen;
131
+
132
+ /**
133
+ * Close callback registry
134
+ *
135
+ * @returns {boolean} Whether or not the callback was succesfully removed
136
+ */
137
+ const closeRegistration = () => _isRegistrationOpen = false;
138
+
139
+ /**
140
+ * Get an array of registered callbacks
141
+ *
142
+ * @returns {Array} A frozen array listing keys to all registered callbacks
143
+ */
144
+ const listCallbacks = () => Object.freeze(Array.from(registry.keys()));
145
+
146
+ /**
147
+ * Check if a callback is registered
148
+ *
149
+ * @param {string} name The name/key to check for in callback registry
150
+ * @returns {boolean} Whether or not a callback is registered
151
+ */
152
+ const hasCallback = name => registry.has(name);
153
+
154
+ /**
155
+ * Get a callback from the registry by name/key
156
+ *
157
+ * @param {string} name The name/key of the callback to get
158
+ * @returns {Function|undefined} The corresponding function registered under that name/key
159
+ */
160
+ const getCallback = name => registry.get(name);
161
+
162
+ /**
163
+ * Remove a callback from the registry
164
+ *
165
+ * @param {string} name The name/key of the callback to get
166
+ * @returns {boolean} Whether or not the callback was successfully unregisterd
167
+ */
168
+ const unregisterCallback = name => _isRegistrationOpen && registry.delete(name);
169
+
170
+ /**
171
+ * Remove all callbacks from the registry
172
+ *
173
+ * @returns {undefined}
174
+ */
175
+ const clearRegistry = () => registry.clear();
176
+
177
+ /**
178
+ * Create a registered callback with a randomly generated name
179
+ *
180
+ * @param {Function} callback Callback function to register
181
+ * @returns {string} The automatically generated key/name of the registered callback
182
+ */
183
+ const createCallback = (callback) => registerCallback('aegis:callback:' + crypto.randomUUID(), callback);
184
+
185
+ /**
186
+ * Call a callback fromt the registry by name/key
187
+ *
188
+ * @param {string} name The name/key of the registered function
189
+ * @param {...any} args Any arguments to pass along to the function
190
+ * @returns {any} Whatever the return value of the function is
191
+ * @throws {Error} Throws if callback is not found or any error resulting from calling the function
192
+ */
193
+ function callCallback(name, ...args) {
194
+ if (registry.has(name)) {
195
+ return registry.get(name).apply(this || globalThis, args);
196
+ } else {
197
+ throw new Error(`No ${name} function registered.`);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Register a named callback in registry
203
+ *
204
+ * @param {string} name The name/key to register the callback under
205
+ * @param {Function} callback The callback value to register
206
+ * @returns {string} The registered name/key
207
+ */
208
+ function registerCallback(name, callback) {
209
+ if (typeof name !== 'string' || name.length === 0) {
210
+ throw new TypeError('Callback name must be a string.');
211
+ } if (! (callback instanceof Function)) {
212
+ throw new TypeError('Callback must be a function.');
213
+ } else if (! _isRegistrationOpen) {
214
+ throw new TypeError('Cannot register new callbacks because registry is closed.');
215
+ } else if (registry.has(name)) {
216
+ throw new Error(`Handler "${name}" is already registered.`);
217
+ } else {
218
+ registry.set(name, callback);
219
+ return name;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Get the host/root node of a given thing.
225
+ *
226
+ * @param {Event|Document|Element|ShadowRoot} target Source thing to search for host of
227
+ * @returns {Document|Element|null} The host/root node, or null
228
+ */
229
+ function getHost(target) {
230
+ if (target instanceof Event) {
231
+ return getHost(target.currentTarget);
232
+ } else if (target instanceof Document) {
233
+ return target;
234
+ } else if (target instanceof Element) {
235
+ return getHost(target.getRootNode());
236
+ } else if (target instanceof ShadowRoot) {
237
+ return target.host;
238
+ } else {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ const EVENT_PREFIX = 'data-aegis-event-on-';
244
+ const EVENT_PREFIX_LENGTH = EVENT_PREFIX.length;
245
+ const DATA_PREFIX = 'aegisEventOn';
246
+ const DATA_PREFIX_LENGTH = DATA_PREFIX.length;
247
+
248
+ const once = 'data-aegis-event-once',
249
+ passive = 'data-aegis-event-passive',
250
+ capture = 'data-aegis-event-capture';
251
+
252
+ const eventAttrs = [
253
+ EVENT_PREFIX + 'abort',
254
+ EVENT_PREFIX + 'blur',
255
+ EVENT_PREFIX + 'focus',
256
+ EVENT_PREFIX + 'cancel',
257
+ EVENT_PREFIX + 'auxclick',
258
+ EVENT_PREFIX + 'beforeinput',
259
+ EVENT_PREFIX + 'beforetoggle',
260
+ EVENT_PREFIX + 'canplay',
261
+ EVENT_PREFIX + 'canplaythrough',
262
+ EVENT_PREFIX + 'change',
263
+ EVENT_PREFIX + 'click',
264
+ EVENT_PREFIX + 'close',
265
+ EVENT_PREFIX + 'contextmenu',
266
+ EVENT_PREFIX + 'copy',
267
+ EVENT_PREFIX + 'cuechange',
268
+ EVENT_PREFIX + 'cut',
269
+ EVENT_PREFIX + 'dblclick',
270
+ EVENT_PREFIX + 'drag',
271
+ EVENT_PREFIX + 'dragend',
272
+ EVENT_PREFIX + 'dragenter',
273
+ EVENT_PREFIX + 'dragexit',
274
+ EVENT_PREFIX + 'dragleave',
275
+ EVENT_PREFIX + 'dragover',
276
+ EVENT_PREFIX + 'dragstart',
277
+ EVENT_PREFIX + 'drop',
278
+ EVENT_PREFIX + 'durationchange',
279
+ EVENT_PREFIX + 'emptied',
280
+ EVENT_PREFIX + 'ended',
281
+ EVENT_PREFIX + 'formdata',
282
+ EVENT_PREFIX + 'input',
283
+ EVENT_PREFIX + 'invalid',
284
+ EVENT_PREFIX + 'keydown',
285
+ EVENT_PREFIX + 'keypress',
286
+ EVENT_PREFIX + 'keyup',
287
+ EVENT_PREFIX + 'load',
288
+ EVENT_PREFIX + 'loadeddata',
289
+ EVENT_PREFIX + 'loadedmetadata',
290
+ EVENT_PREFIX + 'loadstart',
291
+ EVENT_PREFIX + 'mousedown',
292
+ EVENT_PREFIX + 'mouseenter',
293
+ EVENT_PREFIX + 'mouseleave',
294
+ EVENT_PREFIX + 'mousemove',
295
+ EVENT_PREFIX + 'mouseout',
296
+ EVENT_PREFIX + 'mouseover',
297
+ EVENT_PREFIX + 'mouseup',
298
+ EVENT_PREFIX + 'wheel',
299
+ EVENT_PREFIX + 'paste',
300
+ EVENT_PREFIX + 'pause',
301
+ EVENT_PREFIX + 'play',
302
+ EVENT_PREFIX + 'playing',
303
+ EVENT_PREFIX + 'progress',
304
+ EVENT_PREFIX + 'ratechange',
305
+ EVENT_PREFIX + 'reset',
306
+ EVENT_PREFIX + 'resize',
307
+ EVENT_PREFIX + 'scroll',
308
+ EVENT_PREFIX + 'scrollend',
309
+ EVENT_PREFIX + 'securitypolicyviolation',
310
+ EVENT_PREFIX + 'seeked',
311
+ EVENT_PREFIX + 'seeking',
312
+ EVENT_PREFIX + 'select',
313
+ EVENT_PREFIX + 'slotchange',
314
+ EVENT_PREFIX + 'stalled',
315
+ EVENT_PREFIX + 'submit',
316
+ EVENT_PREFIX + 'suspend',
317
+ EVENT_PREFIX + 'timeupdate',
318
+ EVENT_PREFIX + 'volumechange',
319
+ EVENT_PREFIX + 'waiting',
320
+ EVENT_PREFIX + 'selectstart',
321
+ EVENT_PREFIX + 'selectionchange',
322
+ EVENT_PREFIX + 'toggle',
323
+ EVENT_PREFIX + 'pointercancel',
324
+ EVENT_PREFIX + 'pointerdown',
325
+ EVENT_PREFIX + 'pointerup',
326
+ EVENT_PREFIX + 'pointermove',
327
+ EVENT_PREFIX + 'pointerout',
328
+ EVENT_PREFIX + 'pointerover',
329
+ EVENT_PREFIX + 'pointerenter',
330
+ EVENT_PREFIX + 'pointerleave',
331
+ EVENT_PREFIX + 'gotpointercapture',
332
+ EVENT_PREFIX + 'lostpointercapture',
333
+ EVENT_PREFIX + 'mozfullscreenchange',
334
+ EVENT_PREFIX + 'mozfullscreenerror',
335
+ EVENT_PREFIX + 'animationcancel',
336
+ EVENT_PREFIX + 'animationend',
337
+ EVENT_PREFIX + 'animationiteration',
338
+ EVENT_PREFIX + 'animationstart',
339
+ EVENT_PREFIX + 'transitioncancel',
340
+ EVENT_PREFIX + 'transitionend',
341
+ EVENT_PREFIX + 'transitionrun',
342
+ EVENT_PREFIX + 'transitionstart',
343
+ EVENT_PREFIX + 'webkitanimationend',
344
+ EVENT_PREFIX + 'webkitanimationiteration',
345
+ EVENT_PREFIX + 'webkitanimationstart',
346
+ EVENT_PREFIX + 'webkittransitionend',
347
+ EVENT_PREFIX + 'error',
348
+ ];
349
+
350
+ let selector = eventAttrs.map(attr => `[${CSS.escape(attr)}]`).join(', ');
351
+
352
+ const attrToProp = attr => `on${attr[EVENT_PREFIX_LENGTH].toUpperCase()}${attr.substring(EVENT_PREFIX_LENGTH + 1)}`;
353
+
354
+ const attrEntriesMap = attr => [attrToProp(attr), attr];
355
+
356
+ const isEventDataAttr = ([name]) => name.startsWith(DATA_PREFIX);
357
+
358
+ const DATA_EVENTS = Object.fromEntries([...eventAttrs].map(attrEntriesMap));
359
+
360
+ function _addListeners(el, { signal, attrFilter = EVENTS } = {}) {
361
+ const dataset = el.dataset;
362
+
363
+ for (const [attr, val] of Object.entries(dataset).filter(isEventDataAttr)) {
364
+ try {
365
+ const event = 'on' + attr.substring(DATA_PREFIX_LENGTH);
366
+
367
+ if (attrFilter.hasOwnProperty(event) && hasCallback(val)) {
368
+ el.addEventListener(event.substring(2).toLowerCase(), getCallback(val), {
369
+ passive: dataset.hasOwnProperty('aegisEventPassive'),
370
+ capture: dataset.hasOwnProperty('aegisEventCapture'),
371
+ once: dataset.hasOwnProperty('aegisEventOnce'),
372
+ signal,
373
+ });
374
+ }
375
+ } catch(err) {
376
+ console.error(err);
377
+ }
378
+ }
379
+ }
380
+
381
+ const observer = new MutationObserver(records => {
382
+ records.forEach(record => {
383
+ switch(record.type) {
384
+ case 'childList':
385
+ [...record.addedNodes]
386
+ .filter(node => node.nodeType === Node.ELEMENT_NODE)
387
+ .forEach(node => attachListeners(node));
388
+ break;
389
+
390
+ case 'attributes':
391
+ if (typeof record.oldValue === 'string' && hasCallback(record.oldValue)) {
392
+ record.target.removeEventListener(
393
+ record.attributeName.substring(EVENT_PREFIX_LENGTH),
394
+ getCallback(record.oldValue), {
395
+ once: record.target.hasAttribute(once),
396
+ capture: record.target.hasAttribute(capture),
397
+ passive: record.target.hasAttribute(passive),
398
+ }
399
+ );
400
+ }
401
+
402
+ if (
403
+ record.target.hasAttribute(record.attributeName)
404
+ && hasCallback(record.target.getAttribute(record.attributeName))
405
+ ) {
406
+ record.target.addEventListener(
407
+ record.attributeName.substring(EVENT_PREFIX_LENGTH),
408
+ getCallback(record.target.getAttribute(record.attributeName)), {
409
+ once: record.target.hasAttribute(once),
410
+ capture: record.target.hasAttribute(capture),
411
+ passive: record.target.hasAttribute(passive),
412
+ }
413
+ );
414
+ }
415
+ break;
416
+ }
417
+ });
418
+ });
419
+
420
+ const EVENTS = { ...DATA_EVENTS, once, passive, capture };
421
+
422
+ /**
423
+ * Register an attribute to observe for adding/removing event listeners
424
+ *
425
+ * @param {string} attr Name of the attribute to observe
426
+ * @param {object} options
427
+ * @param {boolean} [options.addListeners=false] Whether or not to automatically add listeners
428
+ * @param {Document|Element} [options.base=document.body] Root node to observe
429
+ * @param {AbortSignal} [options.signal] An abort signal to remove any listeners when aborted
430
+ * @returns {string} The resulting `data-*` attribute name
431
+ */
432
+ function registerEventAttribute(attr, {
433
+ addListeners = false,
434
+ base = document.body,
435
+ signal,
436
+ } = {}) {
437
+ const fullAttr = EVENT_PREFIX + attr.toLowerCase();
438
+
439
+ if (! eventAttrs.includes(fullAttr)) {
440
+ const sel = `[${CSS.escape(fullAttr)}]`;
441
+ const prop = attrToProp(fullAttr);
442
+ eventAttrs.push(fullAttr);
443
+ EVENTS[prop] = fullAttr;
444
+ selector += `, ${sel}`;
445
+
446
+ if (addListeners) {
447
+ requestAnimationFrame(() => {
448
+ const config = { attrFilter: { [prop]: sel }, signal };
449
+ [base, ...base.querySelectorAll(sel)].forEach(el => _addListeners(el, config));
450
+ });
451
+ }
452
+ }
453
+
454
+ return fullAttr;
455
+ }
456
+
457
+ /**
458
+ * Add listeners to an element and its children, matching a generated query based on registered attributes
459
+ *
460
+ * @param {Element|Document} target Root node to add listeners from
461
+ * @param {object} options
462
+ * @param {AbortSignal} [options.signal] Optional signal to remove event listeners
463
+ * @returns {Element|Document} Returns the passed target node
464
+ */
465
+ function attachListeners(target, { signal } = {}) {
466
+ const nodes = target instanceof Element && target.matches(selector)
467
+ ? [target, ...target.querySelectorAll(selector)]
468
+ : target.querySelectorAll(selector);
469
+
470
+ nodes.forEach(el => _addListeners(el, { signal }));
471
+
472
+ return target;
473
+ }
474
+ /**
475
+ * Add a node to the `MutationObserver` to observe attributes and add/remove event listeners
476
+ *
477
+ * @param {Document|Element} root Element to observe attributes on
478
+ */
479
+ function observeEvents(root = document) {
480
+ attachListeners(root);
481
+ observer.observe(root, {
482
+ subtree: true,
483
+ childList:true,
484
+ attributes: true,
485
+ attributeOldValue: true,
486
+ attributeFilter: eventAttrs,
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Disconnects the `MutationObserver`, disabling observing of all attribute changes
492
+ *
493
+ * @returns {void}
494
+ */
495
+ const disconnectEventsObserver = () => observer.disconnect();
496
+
497
+ /**
498
+ * Register a global error handler callback
499
+ *
500
+ * @param {Function} callback Callback to register as a global error handler
501
+ * @param {EventInit} config Typical event listener config object
502
+ */
503
+ function setGlobalErrorHandler(callback, { capture, once, passive, signal } = {}) {
504
+ if (callback instanceof Function) {
505
+ globalThis.addEventListener('error', callback, { capture, once, passive, signal });
506
+ } else {
507
+ throw new TypeError('Callback is not a function.');
508
+ }
509
+ }
510
+
511
+ exports.EVENTS = EVENTS;
512
+ exports.FUNCS = FUNCS;
513
+ exports.attachListeners = attachListeners;
514
+ exports.callCallback = callCallback;
515
+ exports.clearRegistry = clearRegistry;
516
+ exports.closeRegistration = closeRegistration;
517
+ exports.createCallback = createCallback;
518
+ exports.disconnectEventsObserver = disconnectEventsObserver;
519
+ exports.getCallback = getCallback;
520
+ exports.getHost = getHost;
521
+ exports.hasCallback = hasCallback;
522
+ exports.isRegistrationOpen = isRegistrationOpen;
523
+ exports.listCallbacks = listCallbacks;
524
+ exports.observeEvents = observeEvents;
525
+ exports.registerCallback = registerCallback;
526
+ exports.registerEventAttribute = registerEventAttribute;
527
+ exports.setGlobalErrorHandler = setGlobalErrorHandler;
528
+ exports.unregisterCallback = unregisterCallback;
@@ -0,0 +1,2 @@
1
+ export * from './callbacks.js';
2
+ export * from './events.js';