mount-observer 0.1.2 → 0.1.3
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/DefineCustomElementHandler.js +64 -0
- package/DefineCustomElementHandler.ts +77 -0
- package/Events.js +11 -9
- package/Events.ts +9 -4
- package/EvtRt.js +34 -0
- package/EvtRt.ts +42 -0
- package/MountObserver.js +211 -41
- package/MountObserver.ts +252 -42
- package/README.md +519 -185
- package/SharedMutationObserver.js +9 -6
- package/SharedMutationObserver.ts +11 -8
- package/arr.js +13 -0
- package/arr.ts +13 -0
- package/emitEvents.js +1 -1
- package/emitEvents.ts +1 -1
- package/index.js +13 -1
- package/index.ts +10 -1
- package/loadImports.js +2 -1
- package/loadImports.ts +2 -1
- package/mediaQuery.js +2 -7
- package/mediaQuery.ts +2 -7
- package/package.json +15 -3
- package/types.d.ts +21 -15
package/MountObserver.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { arr } from './arr.js';
|
|
1
2
|
import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent, AttrChangeEvent, } from './Events.js';
|
|
2
3
|
import { registerSharedObserver, unregisterSharedObserver } from './SharedMutationObserver.js';
|
|
3
4
|
import { whereOutside } from './whereOutside.js';
|
|
4
5
|
export class MountObserver extends EventTarget {
|
|
6
|
+
// Static registry for registered handlers
|
|
7
|
+
static #handlerRegistry = new Map();
|
|
8
|
+
static define(name, handler) {
|
|
9
|
+
if (this.#handlerRegistry.has(name)) {
|
|
10
|
+
throw new Error(`${name} already in use`);
|
|
11
|
+
}
|
|
12
|
+
this.#handlerRegistry.set(name, handler);
|
|
13
|
+
}
|
|
5
14
|
#init;
|
|
6
15
|
#options;
|
|
7
16
|
#abortController;
|
|
@@ -22,30 +31,81 @@ export class MountObserver extends EventTarget {
|
|
|
22
31
|
#checkAttrChangesFn = null;
|
|
23
32
|
#mediaQueryCleanup;
|
|
24
33
|
#mediaMatches = true;
|
|
25
|
-
#
|
|
34
|
+
#asgMtSource;
|
|
35
|
+
#asgDisMtSource;
|
|
36
|
+
#elementNotifiers = new WeakMap();
|
|
37
|
+
#notifierMountedElements = new WeakSet();
|
|
26
38
|
constructor(init, options = {}) {
|
|
27
39
|
super();
|
|
28
40
|
this.#init = init;
|
|
29
41
|
this.#options = options;
|
|
30
42
|
this.#abortController = new AbortController();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
const { assignOnMount, assignOnDismount, do: doValue, reference, whereAttr, loadingEagerness, import: imp } = init;
|
|
44
|
+
// Make a copy of assignOnMount config using structuredClone
|
|
45
|
+
if (assignOnMount !== undefined) {
|
|
46
|
+
this.#asgMtSource = structuredClone(assignOnMount);
|
|
47
|
+
}
|
|
48
|
+
if (assignOnDismount !== undefined) {
|
|
49
|
+
this.#asgDisMtSource = structuredClone(assignOnDismount);
|
|
34
50
|
}
|
|
35
51
|
if (options.disconnectedSignal) {
|
|
36
52
|
options.disconnectedSignal.addEventListener('abort', () => {
|
|
37
53
|
this.disconnect();
|
|
38
54
|
});
|
|
39
55
|
}
|
|
56
|
+
// Validate do property if it contains string references
|
|
57
|
+
if (doValue !== undefined) {
|
|
58
|
+
this.#validateDoHandlers();
|
|
59
|
+
}
|
|
60
|
+
// Validate reference property if present
|
|
61
|
+
if (reference !== undefined) {
|
|
62
|
+
this.#validateReference();
|
|
63
|
+
}
|
|
40
64
|
// Preload whereAttr utilities if needed
|
|
41
|
-
if (
|
|
65
|
+
if (whereAttr) {
|
|
42
66
|
this.#preloadWhereAttrUtilities();
|
|
43
67
|
}
|
|
44
68
|
// Start loading imports if eager
|
|
45
|
-
if (
|
|
69
|
+
if (loadingEagerness === 'eager' && imp) {
|
|
46
70
|
this.#loadImports();
|
|
47
71
|
}
|
|
48
72
|
}
|
|
73
|
+
#validateDoHandlers() {
|
|
74
|
+
const doValue = this.#init.do;
|
|
75
|
+
if (doValue === undefined)
|
|
76
|
+
return;
|
|
77
|
+
const handlers = Array.isArray(doValue) ? doValue : [doValue];
|
|
78
|
+
for (const handler of handlers) {
|
|
79
|
+
if (typeof handler === 'string') {
|
|
80
|
+
if (!MountObserver.#handlerRegistry.has(handler)) {
|
|
81
|
+
throw new Error(`No handler defined for ${handler}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
#validateReference() {
|
|
87
|
+
if (!this.#init.import) {
|
|
88
|
+
throw new Error('reference property requires import to be defined');
|
|
89
|
+
}
|
|
90
|
+
// Normalize import to array
|
|
91
|
+
const imports = Array.isArray(this.#init.import)
|
|
92
|
+
? this.#init.import
|
|
93
|
+
: [this.#init.import];
|
|
94
|
+
// Normalize reference to array
|
|
95
|
+
const references = arr(this.#init.reference);
|
|
96
|
+
// Validate each reference index
|
|
97
|
+
for (const index of references) {
|
|
98
|
+
// Check if index is within bounds
|
|
99
|
+
if (index < 0 || index >= imports.length) {
|
|
100
|
+
throw new Error(`reference index ${index} is out of bounds (import array length: ${imports.length})`);
|
|
101
|
+
}
|
|
102
|
+
const importItem = imports[index];
|
|
103
|
+
// Check if it's a JS module (not a 2D array with type option)
|
|
104
|
+
if (Array.isArray(importItem)) {
|
|
105
|
+
throw new Error(`reference index ${index} points to a non-JS module import (array with type option)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
49
109
|
async #preloadWhereAttrUtilities() {
|
|
50
110
|
if (!this.#matchesWhereAttrFn) {
|
|
51
111
|
const { matchesWhereAttr } = await import('./whereAttr.js');
|
|
@@ -75,10 +135,24 @@ export class MountObserver extends EventTarget {
|
|
|
75
135
|
get disconnectedSignal() {
|
|
76
136
|
return this.#abortController.signal;
|
|
77
137
|
}
|
|
138
|
+
getNotifier(element) {
|
|
139
|
+
// Return cached notifier if it exists
|
|
140
|
+
let notifier = this.#elementNotifiers.get(element);
|
|
141
|
+
if (notifier) {
|
|
142
|
+
return notifier;
|
|
143
|
+
}
|
|
144
|
+
// Create new EventTarget for this element
|
|
145
|
+
notifier = new EventTarget();
|
|
146
|
+
this.#elementNotifiers.set(element, notifier);
|
|
147
|
+
return notifier;
|
|
148
|
+
}
|
|
78
149
|
async observe(rootNode) {
|
|
79
150
|
if (this.#rootNode) {
|
|
80
151
|
throw new Error('Already observing');
|
|
81
152
|
}
|
|
153
|
+
if (this.#asgMtSource || this.#asgDisMtSource) {
|
|
154
|
+
await import('assign-gingerly/object-extension.js');
|
|
155
|
+
}
|
|
82
156
|
this.#rootNode = new WeakRef(rootNode);
|
|
83
157
|
// Set up media query if specified (needs rootNode to be set first)
|
|
84
158
|
if (this.#init.whereMediaMatches) {
|
|
@@ -88,6 +162,10 @@ export class MountObserver extends EventTarget {
|
|
|
88
162
|
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
89
163
|
await this.#preloadWhereAttrUtilities();
|
|
90
164
|
}
|
|
165
|
+
// Wait for eager imports to complete if they were started in constructor
|
|
166
|
+
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
167
|
+
await this.#loadImports();
|
|
168
|
+
}
|
|
91
169
|
// Process existing elements only if media matches
|
|
92
170
|
if (this.#mediaMatches) {
|
|
93
171
|
this.#processNode(rootNode);
|
|
@@ -124,6 +202,20 @@ export class MountObserver extends EventTarget {
|
|
|
124
202
|
// Batch and dispatch attribute changes
|
|
125
203
|
if (attrChanges.length > 0) {
|
|
126
204
|
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
205
|
+
// Dispatch filtered attrchange events to element-specific notifiers
|
|
206
|
+
const changesByElement = new Map();
|
|
207
|
+
for (const change of attrChanges) {
|
|
208
|
+
if (!changesByElement.has(change.element)) {
|
|
209
|
+
changesByElement.set(change.element, []);
|
|
210
|
+
}
|
|
211
|
+
changesByElement.get(change.element).push(change);
|
|
212
|
+
}
|
|
213
|
+
for (const [element, changes] of changesByElement) {
|
|
214
|
+
const notifier = this.#elementNotifiers.get(element);
|
|
215
|
+
if (notifier) {
|
|
216
|
+
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
127
219
|
}
|
|
128
220
|
};
|
|
129
221
|
const observerConfig = {
|
|
@@ -161,6 +253,23 @@ export class MountObserver extends EventTarget {
|
|
|
161
253
|
const { loadImports } = await import('./loadImports.js');
|
|
162
254
|
this.#modules = await loadImports(this.#init.import);
|
|
163
255
|
this.#importsLoaded = true;
|
|
256
|
+
// Validate referenced whereInstanceOf if reference is specified
|
|
257
|
+
if (this.#init.reference !== undefined) {
|
|
258
|
+
const references = arr(this.#init.reference);
|
|
259
|
+
for (const index of references) {
|
|
260
|
+
const module = this.#modules[index];
|
|
261
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
262
|
+
// Validate that it's a Constructor or array of Constructors
|
|
263
|
+
const whereInstanceOf = module.whereInstanceOf;
|
|
264
|
+
const constructors = arr(whereInstanceOf);
|
|
265
|
+
for (const constructor of constructors) {
|
|
266
|
+
if (typeof constructor !== 'function') {
|
|
267
|
+
throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
164
273
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
165
274
|
}
|
|
166
275
|
#processNode(node) {
|
|
@@ -209,15 +318,28 @@ export class MountObserver extends EventTarget {
|
|
|
209
318
|
}
|
|
210
319
|
// Check whereInstanceOf condition if specified
|
|
211
320
|
if (this.#init.whereInstanceOf) {
|
|
212
|
-
const constructors =
|
|
213
|
-
? this.#init.whereInstanceOf
|
|
214
|
-
: [this.#init.whereInstanceOf];
|
|
321
|
+
const constructors = arr(this.#init.whereInstanceOf);
|
|
215
322
|
// Element must be an instance of at least one constructor (OR logic for array)
|
|
216
323
|
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
217
324
|
if (!matchesInstanceOf) {
|
|
218
325
|
return false;
|
|
219
326
|
}
|
|
220
327
|
}
|
|
328
|
+
// Check referenced whereInstanceOf if imports are loaded and reference is specified
|
|
329
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
330
|
+
const references = arr(this.#init.reference);
|
|
331
|
+
for (const index of references) {
|
|
332
|
+
const module = this.#modules[index];
|
|
333
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
334
|
+
const constructors = arr(module.whereInstanceOf);
|
|
335
|
+
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
336
|
+
const matchesInstanceOf = constructors.some((constructor) => element instanceof constructor);
|
|
337
|
+
if (!matchesInstanceOf) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
221
343
|
// All conditions passed
|
|
222
344
|
return true;
|
|
223
345
|
}
|
|
@@ -243,26 +365,55 @@ export class MountObserver extends EventTarget {
|
|
|
243
365
|
const context = {
|
|
244
366
|
modules: this.#modules,
|
|
245
367
|
observer: this,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
368
|
+
rootNode,
|
|
369
|
+
mountInit: this.#init,
|
|
249
370
|
};
|
|
250
371
|
// Apply assignGingerly if specified
|
|
251
|
-
if (this.#
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
372
|
+
if (this.#asgMtSource) {
|
|
373
|
+
element.assignGingerly(this.#asgMtSource);
|
|
374
|
+
}
|
|
375
|
+
// Check if notifier exists BEFORE calling do callback
|
|
376
|
+
const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
|
|
377
|
+
// Call do callback(s) - can be string, function, or array
|
|
378
|
+
if (this.#init.do !== undefined) {
|
|
379
|
+
const doHandlers = Array.isArray(this.#init.do) ? this.#init.do : [this.#init.do];
|
|
380
|
+
for (const handler of doHandlers) {
|
|
381
|
+
if (typeof handler === 'string') {
|
|
382
|
+
// Registered handler - instantiate it
|
|
383
|
+
const HandlerClass = MountObserver.#handlerRegistry.get(handler);
|
|
384
|
+
if (HandlerClass) {
|
|
385
|
+
new HandlerClass(element, context);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (typeof handler === 'function') {
|
|
389
|
+
// Inline function
|
|
390
|
+
handler(element, context);
|
|
391
|
+
}
|
|
259
392
|
}
|
|
260
|
-
|
|
261
|
-
|
|
393
|
+
}
|
|
394
|
+
// Call referenced do functions from imported modules
|
|
395
|
+
if (this.#init.reference !== undefined) {
|
|
396
|
+
const references = arr(this.#init.reference);
|
|
397
|
+
for (const index of references) {
|
|
398
|
+
const module = this.#modules[index];
|
|
399
|
+
if (module && typeof module.do === 'function') {
|
|
400
|
+
module.do(element, context);
|
|
401
|
+
}
|
|
262
402
|
}
|
|
263
403
|
}
|
|
264
404
|
// Dispatch mount event
|
|
265
|
-
|
|
405
|
+
const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
|
|
406
|
+
this.dispatchEvent(mountEvent);
|
|
407
|
+
// Dispatch to element-specific notifier only if:
|
|
408
|
+
// 1. Notifier existed before do callback (wasn't just created), AND
|
|
409
|
+
// 2. Element hasn't already received a mount event on its notifier
|
|
410
|
+
if (notifierExistedBeforeDo && !this.#notifierMountedElements.has(element)) {
|
|
411
|
+
const notifier = this.#elementNotifiers.get(element);
|
|
412
|
+
if (notifier) {
|
|
413
|
+
this.#notifierMountedElements.add(element);
|
|
414
|
+
notifier.dispatchEvent(mountEvent);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
266
417
|
// Emit events from mounted element if configured
|
|
267
418
|
if (this.#init.mountedElemEmits) {
|
|
268
419
|
const { emitMountedElementEvents } = await import('./emitEvents.js');
|
|
@@ -273,37 +424,48 @@ export class MountObserver extends EventTarget {
|
|
|
273
424
|
const changes = this.#checkAttrChangesFn(element);
|
|
274
425
|
if (changes.length > 0) {
|
|
275
426
|
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
427
|
+
// Also dispatch to element-specific notifier
|
|
428
|
+
const notifier = this.#elementNotifiers.get(element);
|
|
429
|
+
if (notifier) {
|
|
430
|
+
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
431
|
+
}
|
|
276
432
|
}
|
|
277
433
|
}
|
|
278
434
|
}
|
|
279
435
|
async assignGingerly(config) {
|
|
280
436
|
// Handle undefined case
|
|
281
437
|
if (config === undefined) {
|
|
282
|
-
this.#
|
|
438
|
+
this.#asgMtSource = undefined;
|
|
283
439
|
return;
|
|
284
440
|
}
|
|
285
|
-
|
|
441
|
+
await import('assign-gingerly/object-extension.js');
|
|
286
442
|
// Update the source config for future mounted elements
|
|
287
|
-
if (this.#
|
|
443
|
+
if (this.#asgMtSource === undefined) {
|
|
288
444
|
// No existing config, just clone the passed in object
|
|
289
|
-
this.#
|
|
445
|
+
this.#asgMtSource = structuredClone(config);
|
|
290
446
|
}
|
|
291
447
|
else {
|
|
292
448
|
// Merge into existing config using assignGingerly
|
|
293
|
-
assignGingerly(
|
|
449
|
+
this.#asgMtSource.assignGingerly(config);
|
|
450
|
+
//assignGingerly(this.#asgMtSource, config);
|
|
294
451
|
}
|
|
295
452
|
// Apply to already mounted elements using setWeak for iteration
|
|
296
453
|
for (const ref of this.#mountedElements.setWeak) {
|
|
297
454
|
const element = ref.deref();
|
|
298
455
|
if (element) {
|
|
299
|
-
assignGingerly(
|
|
456
|
+
element.assignGingerly(config);
|
|
457
|
+
//assignGingerly(element, config);
|
|
300
458
|
}
|
|
301
459
|
}
|
|
302
460
|
}
|
|
303
|
-
#handleRemoval(element) {
|
|
461
|
+
async #handleRemoval(element) {
|
|
304
462
|
if (!this.#mountedElements.weakSet.has(element)) {
|
|
305
463
|
return;
|
|
306
464
|
}
|
|
465
|
+
// Apply assignGingerly if specified for dismount
|
|
466
|
+
if (this.#asgDisMtSource) {
|
|
467
|
+
element.assignGingerly(this.#asgDisMtSource);
|
|
468
|
+
}
|
|
307
469
|
// Remove from both structures
|
|
308
470
|
this.#mountedElements.weakSet.delete(element);
|
|
309
471
|
for (const ref of this.#mountedElements.setWeak) {
|
|
@@ -312,6 +474,10 @@ export class MountObserver extends EventTarget {
|
|
|
312
474
|
break;
|
|
313
475
|
}
|
|
314
476
|
}
|
|
477
|
+
// Remove from processed set so element can be re-mounted
|
|
478
|
+
this.#processedDoForElement.delete(element);
|
|
479
|
+
// Remove from notifier mounted tracking so mount event can fire again
|
|
480
|
+
this.#notifierMountedElements.delete(element);
|
|
315
481
|
const rootNode = this.#rootNode?.deref();
|
|
316
482
|
if (!rootNode) {
|
|
317
483
|
// Root node was garbage collected
|
|
@@ -320,24 +486,28 @@ export class MountObserver extends EventTarget {
|
|
|
320
486
|
const context = {
|
|
321
487
|
modules: this.#modules,
|
|
322
488
|
observer: this,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
489
|
+
rootNode,
|
|
490
|
+
mountInit: this.#init,
|
|
326
491
|
};
|
|
327
|
-
// Call dismount callback
|
|
328
|
-
if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.dismount) {
|
|
329
|
-
this.#init.do.dismount(element, context);
|
|
330
|
-
}
|
|
331
492
|
// Dispatch dismount event
|
|
332
|
-
|
|
493
|
+
const dismountEvent = new DismountEvent(element, 'where-element-matches-failed', this.#init);
|
|
494
|
+
this.dispatchEvent(dismountEvent);
|
|
495
|
+
// Dispatch to element-specific notifier
|
|
496
|
+
const notifier = this.#elementNotifiers.get(element);
|
|
497
|
+
if (notifier) {
|
|
498
|
+
notifier.dispatchEvent(dismountEvent);
|
|
499
|
+
}
|
|
333
500
|
// Check if element is being moved within the same root
|
|
334
501
|
// If it's truly disconnected, dispatch disconnect event
|
|
335
502
|
setTimeout(() => {
|
|
336
503
|
if (!rootNode.contains(element)) {
|
|
337
|
-
|
|
338
|
-
|
|
504
|
+
const disconnectEvent = new DisconnectEvent(element, this.#init);
|
|
505
|
+
this.dispatchEvent(disconnectEvent);
|
|
506
|
+
// Dispatch to element-specific notifier
|
|
507
|
+
const notifier = this.#elementNotifiers.get(element);
|
|
508
|
+
if (notifier) {
|
|
509
|
+
notifier.dispatchEvent(disconnectEvent);
|
|
339
510
|
}
|
|
340
|
-
this.dispatchEvent(new DisconnectEvent(element, this.#init));
|
|
341
511
|
}
|
|
342
512
|
}, 0);
|
|
343
513
|
}
|