@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/events.js ADDED
@@ -0,0 +1,269 @@
1
+ import { hasCallback, getCallback } from './callbacks.js';
2
+
3
+ const EVENT_PREFIX = 'data-aegis-event-on-';
4
+ const EVENT_PREFIX_LENGTH = EVENT_PREFIX.length;
5
+ const DATA_PREFIX = 'aegisEventOn';
6
+ const DATA_PREFIX_LENGTH = DATA_PREFIX.length;
7
+
8
+ const once = 'data-aegis-event-once',
9
+ passive = 'data-aegis-event-passive',
10
+ capture = 'data-aegis-event-capture';
11
+
12
+ const eventAttrs = [
13
+ EVENT_PREFIX + 'abort',
14
+ EVENT_PREFIX + 'blur',
15
+ EVENT_PREFIX + 'focus',
16
+ EVENT_PREFIX + 'cancel',
17
+ EVENT_PREFIX + 'auxclick',
18
+ EVENT_PREFIX + 'beforeinput',
19
+ EVENT_PREFIX + 'beforetoggle',
20
+ EVENT_PREFIX + 'canplay',
21
+ EVENT_PREFIX + 'canplaythrough',
22
+ EVENT_PREFIX + 'change',
23
+ EVENT_PREFIX + 'click',
24
+ EVENT_PREFIX + 'close',
25
+ EVENT_PREFIX + 'contextmenu',
26
+ EVENT_PREFIX + 'copy',
27
+ EVENT_PREFIX + 'cuechange',
28
+ EVENT_PREFIX + 'cut',
29
+ EVENT_PREFIX + 'dblclick',
30
+ EVENT_PREFIX + 'drag',
31
+ EVENT_PREFIX + 'dragend',
32
+ EVENT_PREFIX + 'dragenter',
33
+ EVENT_PREFIX + 'dragexit',
34
+ EVENT_PREFIX + 'dragleave',
35
+ EVENT_PREFIX + 'dragover',
36
+ EVENT_PREFIX + 'dragstart',
37
+ EVENT_PREFIX + 'drop',
38
+ EVENT_PREFIX + 'durationchange',
39
+ EVENT_PREFIX + 'emptied',
40
+ EVENT_PREFIX + 'ended',
41
+ EVENT_PREFIX + 'formdata',
42
+ EVENT_PREFIX + 'input',
43
+ EVENT_PREFIX + 'invalid',
44
+ EVENT_PREFIX + 'keydown',
45
+ EVENT_PREFIX + 'keypress',
46
+ EVENT_PREFIX + 'keyup',
47
+ EVENT_PREFIX + 'load',
48
+ EVENT_PREFIX + 'loadeddata',
49
+ EVENT_PREFIX + 'loadedmetadata',
50
+ EVENT_PREFIX + 'loadstart',
51
+ EVENT_PREFIX + 'mousedown',
52
+ EVENT_PREFIX + 'mouseenter',
53
+ EVENT_PREFIX + 'mouseleave',
54
+ EVENT_PREFIX + 'mousemove',
55
+ EVENT_PREFIX + 'mouseout',
56
+ EVENT_PREFIX + 'mouseover',
57
+ EVENT_PREFIX + 'mouseup',
58
+ EVENT_PREFIX + 'wheel',
59
+ EVENT_PREFIX + 'paste',
60
+ EVENT_PREFIX + 'pause',
61
+ EVENT_PREFIX + 'play',
62
+ EVENT_PREFIX + 'playing',
63
+ EVENT_PREFIX + 'progress',
64
+ EVENT_PREFIX + 'ratechange',
65
+ EVENT_PREFIX + 'reset',
66
+ EVENT_PREFIX + 'resize',
67
+ EVENT_PREFIX + 'scroll',
68
+ EVENT_PREFIX + 'scrollend',
69
+ EVENT_PREFIX + 'securitypolicyviolation',
70
+ EVENT_PREFIX + 'seeked',
71
+ EVENT_PREFIX + 'seeking',
72
+ EVENT_PREFIX + 'select',
73
+ EVENT_PREFIX + 'slotchange',
74
+ EVENT_PREFIX + 'stalled',
75
+ EVENT_PREFIX + 'submit',
76
+ EVENT_PREFIX + 'suspend',
77
+ EVENT_PREFIX + 'timeupdate',
78
+ EVENT_PREFIX + 'volumechange',
79
+ EVENT_PREFIX + 'waiting',
80
+ EVENT_PREFIX + 'selectstart',
81
+ EVENT_PREFIX + 'selectionchange',
82
+ EVENT_PREFIX + 'toggle',
83
+ EVENT_PREFIX + 'pointercancel',
84
+ EVENT_PREFIX + 'pointerdown',
85
+ EVENT_PREFIX + 'pointerup',
86
+ EVENT_PREFIX + 'pointermove',
87
+ EVENT_PREFIX + 'pointerout',
88
+ EVENT_PREFIX + 'pointerover',
89
+ EVENT_PREFIX + 'pointerenter',
90
+ EVENT_PREFIX + 'pointerleave',
91
+ EVENT_PREFIX + 'gotpointercapture',
92
+ EVENT_PREFIX + 'lostpointercapture',
93
+ EVENT_PREFIX + 'mozfullscreenchange',
94
+ EVENT_PREFIX + 'mozfullscreenerror',
95
+ EVENT_PREFIX + 'animationcancel',
96
+ EVENT_PREFIX + 'animationend',
97
+ EVENT_PREFIX + 'animationiteration',
98
+ EVENT_PREFIX + 'animationstart',
99
+ EVENT_PREFIX + 'transitioncancel',
100
+ EVENT_PREFIX + 'transitionend',
101
+ EVENT_PREFIX + 'transitionrun',
102
+ EVENT_PREFIX + 'transitionstart',
103
+ EVENT_PREFIX + 'webkitanimationend',
104
+ EVENT_PREFIX + 'webkitanimationiteration',
105
+ EVENT_PREFIX + 'webkitanimationstart',
106
+ EVENT_PREFIX + 'webkittransitionend',
107
+ EVENT_PREFIX + 'error',
108
+ ];
109
+
110
+ let selector = eventAttrs.map(attr => `[${CSS.escape(attr)}]`).join(', ');
111
+
112
+ const attrToProp = attr => `on${attr[EVENT_PREFIX_LENGTH].toUpperCase()}${attr.substring(EVENT_PREFIX_LENGTH + 1)}`;
113
+
114
+ const attrEntriesMap = attr => [attrToProp(attr), attr];
115
+
116
+ const isEventDataAttr = ([name]) => name.startsWith(DATA_PREFIX);
117
+
118
+ const DATA_EVENTS = Object.fromEntries([...eventAttrs].map(attrEntriesMap));
119
+
120
+ function _addListeners(el, { signal, attrFilter = EVENTS } = {}) {
121
+ const dataset = el.dataset;
122
+
123
+ for (const [attr, val] of Object.entries(dataset).filter(isEventDataAttr)) {
124
+ try {
125
+ const event = 'on' + attr.substring(DATA_PREFIX_LENGTH);
126
+
127
+ if (attrFilter.hasOwnProperty(event) && hasCallback(val)) {
128
+ el.addEventListener(event.substring(2).toLowerCase(), getCallback(val), {
129
+ passive: dataset.hasOwnProperty('aegisEventPassive'),
130
+ capture: dataset.hasOwnProperty('aegisEventCapture'),
131
+ once: dataset.hasOwnProperty('aegisEventOnce'),
132
+ signal,
133
+ });
134
+ }
135
+ } catch(err) {
136
+ console.error(err);
137
+ }
138
+ }
139
+ }
140
+
141
+ const observer = new MutationObserver(records => {
142
+ records.forEach(record => {
143
+ switch(record.type) {
144
+ case 'childList':
145
+ [...record.addedNodes]
146
+ .filter(node => node.nodeType === Node.ELEMENT_NODE)
147
+ .forEach(node => attachListeners(node));
148
+ break;
149
+
150
+ case 'attributes':
151
+ if (typeof record.oldValue === 'string' && hasCallback(record.oldValue)) {
152
+ record.target.removeEventListener(
153
+ record.attributeName.substring(EVENT_PREFIX_LENGTH),
154
+ getCallback(record.oldValue), {
155
+ once: record.target.hasAttribute(once),
156
+ capture: record.target.hasAttribute(capture),
157
+ passive: record.target.hasAttribute(passive),
158
+ }
159
+ );
160
+ }
161
+
162
+ if (
163
+ record.target.hasAttribute(record.attributeName)
164
+ && hasCallback(record.target.getAttribute(record.attributeName))
165
+ ) {
166
+ record.target.addEventListener(
167
+ record.attributeName.substring(EVENT_PREFIX_LENGTH),
168
+ getCallback(record.target.getAttribute(record.attributeName)), {
169
+ once: record.target.hasAttribute(once),
170
+ capture: record.target.hasAttribute(capture),
171
+ passive: record.target.hasAttribute(passive),
172
+ }
173
+ );
174
+ }
175
+ break;
176
+ }
177
+ });
178
+ });
179
+
180
+ export const EVENTS = { ...DATA_EVENTS, once, passive, capture };
181
+
182
+ /**
183
+ * Register an attribute to observe for adding/removing event listeners
184
+ *
185
+ * @param {string} attr Name of the attribute to observe
186
+ * @param {object} options
187
+ * @param {boolean} [options.addListeners=false] Whether or not to automatically add listeners
188
+ * @param {Document|Element} [options.base=document.body] Root node to observe
189
+ * @param {AbortSignal} [options.signal] An abort signal to remove any listeners when aborted
190
+ * @returns {string} The resulting `data-*` attribute name
191
+ */
192
+ export function registerEventAttribute(attr, {
193
+ addListeners = false,
194
+ base = document.body,
195
+ signal,
196
+ } = {}) {
197
+ const fullAttr = EVENT_PREFIX + attr.toLowerCase();
198
+
199
+ if (! eventAttrs.includes(fullAttr)) {
200
+ const sel = `[${CSS.escape(fullAttr)}]`;
201
+ const prop = attrToProp(fullAttr);
202
+ eventAttrs.push(fullAttr);
203
+ EVENTS[prop] = fullAttr;
204
+ selector += `, ${sel}`;
205
+
206
+ if (addListeners) {
207
+ requestAnimationFrame(() => {
208
+ const config = { attrFilter: { [prop]: sel }, signal };
209
+ [base, ...base.querySelectorAll(sel)].forEach(el => _addListeners(el, config));
210
+ });
211
+ }
212
+ }
213
+
214
+ return fullAttr;
215
+ }
216
+
217
+ /**
218
+ * Add listeners to an element and its children, matching a generated query based on registered attributes
219
+ *
220
+ * @param {Element|Document} target Root node to add listeners from
221
+ * @param {object} options
222
+ * @param {AbortSignal} [options.signal] Optional signal to remove event listeners
223
+ * @returns {Element|Document} Returns the passed target node
224
+ */
225
+ export function attachListeners(target, { signal } = {}) {
226
+ const nodes = target instanceof Element && target.matches(selector)
227
+ ? [target, ...target.querySelectorAll(selector)]
228
+ : target.querySelectorAll(selector);
229
+
230
+ nodes.forEach(el => _addListeners(el, { signal }));
231
+
232
+ return target;
233
+ }
234
+ /**
235
+ * Add a node to the `MutationObserver` to observe attributes and add/remove event listeners
236
+ *
237
+ * @param {Document|Element} root Element to observe attributes on
238
+ */
239
+ export function observeEvents(root = document) {
240
+ attachListeners(root);
241
+ observer.observe(root, {
242
+ subtree: true,
243
+ childList:true,
244
+ attributes: true,
245
+ attributeOldValue: true,
246
+ attributeFilter: eventAttrs,
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Disconnects the `MutationObserver`, disabling observing of all attribute changes
252
+ *
253
+ * @returns {void}
254
+ */
255
+ export const disconnectEventsObserver = () => observer.disconnect();
256
+
257
+ /**
258
+ * Register a global error handler callback
259
+ *
260
+ * @param {Function} callback Callback to register as a global error handler
261
+ * @param {EventInit} config Typical event listener config object
262
+ */
263
+ export function setGlobalErrorHandler(callback, { capture, once, passive, signal } = {}) {
264
+ if (callback instanceof Function) {
265
+ globalThis.addEventListener('error', callback, { capture, once, passive, signal });
266
+ } else {
267
+ throw new TypeError('Callback is not a function.');
268
+ }
269
+ }
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@aegisjsproject/callback-registry",
3
+ "version": "1.0.0",
4
+ "description": " A callback registry for AegisJSProject",
5
+ "keywords": [
6
+ "aegis",
7
+ "callback-registry",
8
+ "event-binding",
9
+ "declarative-dom"
10
+ ],
11
+ "type": "module",
12
+ "main": "./callbacks.cjs",
13
+ "module": "./callbackRegistry.js",
14
+ "unpkg": "./callbackRegistry.mjs",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./callbackRegistry.js",
18
+ "require": "./callbacks.cjs"
19
+ },
20
+ "./*.js": {
21
+ "import": "./*.js",
22
+ "require": "./*.cjs"
23
+ },
24
+ "./*.mjs": {
25
+ "import": "./*.js",
26
+ "require": "./*.cjs"
27
+ },
28
+ "./*.cjs": {
29
+ "import": "./*.js",
30
+ "require": "./*.cjs"
31
+ },
32
+ "./*": {
33
+ "import": "./*.js",
34
+ "require": "./*.cjs"
35
+ }
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "private": false,
41
+ "scripts": {
42
+ "test": "npm run lint:js && npm run run:tests",
43
+ "preversion": "npm test && npm run build",
44
+ "prepare": "npm test && npm run build",
45
+ "lint:js": "eslint .",
46
+ "fix:js": "eslint . --fix",
47
+ "build": "npm run build:js",
48
+ "run:tests": "node --test",
49
+ "clean": "rm -f ./*.cjs",
50
+ "build:js": "npm run clean && rollup -c rollup.config.js",
51
+ "create:lock": "npm i --package-lock-only --ignore-scripts --no-audit --no-fund",
52
+ "version:bump": "npm run version:bump:patch",
53
+ "version:bump:patch": "npm version --no-git-tag-version patch && npm run create:lock",
54
+ "version:bump:minor": "npm version --no-git-tag-version minor && npm run create:lock",
55
+ "version:bump:major": "npm version --no-git-tag-version major && npm run create:lock"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/AegisJSProject/callback-registry.git"
60
+ },
61
+ "author": "Chris Zuber <admin@kernvalley.us>",
62
+ "license": "MIT",
63
+ "funding": [
64
+ {
65
+ "type": "librepay",
66
+ "url": "https://liberapay.com/shgysk8zer0"
67
+ },
68
+ {
69
+ "type": "github",
70
+ "url": "https://github.com/sponsors/shgysk8zer0"
71
+ }
72
+ ],
73
+ "bugs": {
74
+ "url": "https://github.com/AegisJSProject/callback-registry/issues"
75
+ },
76
+ "homepage": "https://github.com/AegisJSProject/callback-registry#readme",
77
+ "devDependencies": {
78
+ "@rollup/plugin-node-resolve": "^15.3.0",
79
+ "@shgysk8zer0/eslint-config": "^1.0.1",
80
+ "@shgysk8zer0/js-utils": "^1.0.2",
81
+ "@shgysk8zer0/npm-utils": "^1.1.3"
82
+ }
83
+ }