mount-observer 0.1.11 → 0.1.12
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 +1 -0
- package/DefineCustomElementHandler.ts +1 -0
- package/EnhanceMountedElementHandler.js +3 -2
- package/EnhanceMountedElementHandler.ts +3 -2
- package/EvtRt.js +8 -3
- package/EvtRt.ts +12 -3
- package/MountObserver.js +119 -19
- package/MountObserver.ts +200 -56
- package/README.md +153 -74
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- package/observedRootHas.js +87 -0
- package/package.json +1 -1
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
package/MountObserver.ts
CHANGED
|
@@ -48,7 +48,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
48
48
|
#rootNode: WeakRef<Node> | undefined;
|
|
49
49
|
#importsLoaded = false;
|
|
50
50
|
#mediaQueryCleanup?: () => void;
|
|
51
|
+
#rootSizeCleanup?: () => void;
|
|
52
|
+
#intersectionCleanup?: () => void;
|
|
53
|
+
#connectionCleanup?: () => void;
|
|
54
|
+
#intersectionObserver?: IntersectionObserver;
|
|
51
55
|
#mediaMatches: boolean = true;
|
|
56
|
+
#rootSizeMatches: boolean = true;
|
|
57
|
+
#connectionMatches: boolean = true;
|
|
52
58
|
#asgMtSource: Record<string, any> | undefined;
|
|
53
59
|
#asgDisMtSource: Record<string, any> | undefined;
|
|
54
60
|
#stageMtSource: Record<string, any> | undefined;
|
|
@@ -57,17 +63,51 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
57
63
|
#elementNotifiers = new WeakMap<Element, EventTarget>();
|
|
58
64
|
#notifierMountedElements = new WeakSet<Element>();
|
|
59
65
|
|
|
66
|
+
#mergeHandlerDefaults(config: MountConfig): MountConfig {
|
|
67
|
+
const doValue = config.do;
|
|
68
|
+
|
|
69
|
+
// Only process if do is a string (single handler reference)
|
|
70
|
+
if (typeof doValue !== 'string') {
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Look up the handler class
|
|
75
|
+
const HandlerClass = MountObserver.#handlerRegistry.get(doValue);
|
|
76
|
+
if (!HandlerClass) {
|
|
77
|
+
// Validation will catch this later
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract static properties from the handler class
|
|
82
|
+
const handlerDefaults: Partial<MountConfig> = {};
|
|
83
|
+
const proto = HandlerClass as any;
|
|
84
|
+
|
|
85
|
+
// Get all static properties
|
|
86
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
87
|
+
if (key !== 'prototype' && key !== 'length' && key !== 'name') {
|
|
88
|
+
handlerDefaults[key as keyof MountConfig] = proto[key];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Merge: handler defaults first, then inline config (inline trumps)
|
|
93
|
+
// Using object spread - inline config overwrites handler defaults
|
|
94
|
+
return { ...handlerDefaults, ...config };
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
constructor(config: MountConfig, options: MountObserverOptions = {}) {
|
|
61
98
|
super();
|
|
62
99
|
|
|
63
|
-
|
|
100
|
+
// Merge handler defaults if do is a string reference
|
|
101
|
+
const mergedConfig = this.#mergeHandlerDefaults(config);
|
|
102
|
+
|
|
103
|
+
this.#init = mergedConfig;
|
|
64
104
|
this.#options = options;
|
|
65
105
|
this.#abortController = new AbortController();
|
|
66
106
|
|
|
67
107
|
const {
|
|
68
108
|
assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness,
|
|
69
109
|
import: imp
|
|
70
|
-
} =
|
|
110
|
+
} = mergedConfig;
|
|
71
111
|
// Make a copy of assignOnMount config using structuredClone
|
|
72
112
|
if (assignOnMount !== undefined) {
|
|
73
113
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -165,6 +205,64 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
165
205
|
this.#mediaQueryCleanup = result.cleanup;
|
|
166
206
|
}
|
|
167
207
|
|
|
208
|
+
async #setupRootSizeObserver(): Promise<void> {
|
|
209
|
+
if (!this.#rootNode) {
|
|
210
|
+
throw new Error('Cannot setup root size observer before observe() is called');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { setupRootSizeObserver } = await import('./rootSizeObserver.js');
|
|
214
|
+
const result = setupRootSizeObserver(
|
|
215
|
+
this.#init,
|
|
216
|
+
this.#rootNode,
|
|
217
|
+
this.#mountedElements,
|
|
218
|
+
this.#modules,
|
|
219
|
+
this,
|
|
220
|
+
(node) => this.#processNode(node)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
this.#rootSizeMatches = result.conditionMatches;
|
|
224
|
+
this.#rootSizeCleanup = result.cleanup;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async #setupElementIntersection(): Promise<void> {
|
|
228
|
+
if (!this.#rootNode) {
|
|
229
|
+
throw new Error('Cannot setup element intersection before observe() is called');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { setupElementIntersection } = await import('./elementIntersection.js');
|
|
233
|
+
const result = setupElementIntersection(
|
|
234
|
+
this.#init,
|
|
235
|
+
this.#rootNode,
|
|
236
|
+
this.#mountedElements,
|
|
237
|
+
this.#modules,
|
|
238
|
+
this,
|
|
239
|
+
(element) => this.#matchesSelector(element),
|
|
240
|
+
(element) => this.#handleMatch(element)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
this.#intersectionObserver = result.intersectionObserver;
|
|
244
|
+
this.#intersectionCleanup = result.cleanup;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async #setupConnectionMonitor(): Promise<void> {
|
|
248
|
+
if (!this.#rootNode) {
|
|
249
|
+
throw new Error('Cannot setup connection monitor before observe() is called');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const { setupConnectionMonitor } = await import('./connectionMonitor.js');
|
|
253
|
+
const result = setupConnectionMonitor(
|
|
254
|
+
this.#init,
|
|
255
|
+
this.#rootNode,
|
|
256
|
+
this.#mountedElements,
|
|
257
|
+
this.#modules,
|
|
258
|
+
this,
|
|
259
|
+
(node) => this.#processNode(node)
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.#connectionMatches = result.conditionMatches;
|
|
263
|
+
this.#connectionCleanup = result.cleanup;
|
|
264
|
+
}
|
|
265
|
+
|
|
168
266
|
get disconnectedSignal(): AbortSignal {
|
|
169
267
|
return this.#abortController.signal;
|
|
170
268
|
}
|
|
@@ -201,20 +299,35 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
201
299
|
await this.#setupMediaQuery();
|
|
202
300
|
}
|
|
203
301
|
|
|
302
|
+
// Set up root size observer if specified (needs rootNode to be set first)
|
|
303
|
+
if (this.#init.whereObservedRootSizeMatches) {
|
|
304
|
+
await this.#setupRootSizeObserver();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Set up element intersection observer if specified (needs rootNode to be set first)
|
|
308
|
+
if (this.#init.whereElementIntersectsWith) {
|
|
309
|
+
await this.#setupElementIntersection();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Set up connection monitor if specified (needs rootNode to be set first)
|
|
313
|
+
if (this.#init.whereConnectionHas) {
|
|
314
|
+
await this.#setupConnectionMonitor();
|
|
315
|
+
}
|
|
316
|
+
|
|
204
317
|
// Wait for eager imports to complete if they were started in constructor
|
|
205
318
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
206
319
|
await this.#loadImports();
|
|
207
320
|
}
|
|
208
321
|
|
|
209
|
-
// Process existing elements only if
|
|
210
|
-
if (this.#mediaMatches) {
|
|
322
|
+
// Process existing elements only if all conditions match
|
|
323
|
+
if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
|
|
211
324
|
this.#processNode(rootNode);
|
|
212
325
|
}
|
|
213
326
|
|
|
214
327
|
// Create mutation callback
|
|
215
328
|
this.#mutationCallback = (mutations) => {
|
|
216
|
-
// Skip processing if
|
|
217
|
-
if (!this.#mediaMatches) {
|
|
329
|
+
// Skip processing if any condition doesn't match
|
|
330
|
+
if (!this.#mediaMatches || !this.#rootSizeMatches || !this.#connectionMatches) {
|
|
218
331
|
return;
|
|
219
332
|
}
|
|
220
333
|
|
|
@@ -258,6 +371,24 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
258
371
|
this.#mediaQueryCleanup = undefined;
|
|
259
372
|
}
|
|
260
373
|
|
|
374
|
+
// Remove root size observer
|
|
375
|
+
if (this.#rootSizeCleanup) {
|
|
376
|
+
this.#rootSizeCleanup();
|
|
377
|
+
this.#rootSizeCleanup = undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Remove intersection observer
|
|
381
|
+
if (this.#intersectionCleanup) {
|
|
382
|
+
this.#intersectionCleanup();
|
|
383
|
+
this.#intersectionCleanup = undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Remove connection monitor
|
|
387
|
+
if (this.#connectionCleanup) {
|
|
388
|
+
this.#connectionCleanup();
|
|
389
|
+
this.#connectionCleanup = undefined;
|
|
390
|
+
}
|
|
391
|
+
|
|
261
392
|
this.#abortController.abort();
|
|
262
393
|
this.#rootNode = undefined;
|
|
263
394
|
}
|
|
@@ -272,20 +403,20 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
272
403
|
this.#modules = await loadImports(this.#init.import);
|
|
273
404
|
this.#importsLoaded = true;
|
|
274
405
|
|
|
275
|
-
// Validate referenced
|
|
406
|
+
// Validate referenced whereInstanceOf if reference is specified
|
|
276
407
|
if (this.#init.reference !== undefined) {
|
|
277
408
|
const references = arr(this.#init.reference);
|
|
278
409
|
|
|
279
410
|
for (const index of references) {
|
|
280
411
|
const module = this.#modules[index];
|
|
281
|
-
if (module && module.
|
|
412
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
282
413
|
// Validate that it's a Constructor or array of Constructors
|
|
283
|
-
const
|
|
284
|
-
const constructors = arr(
|
|
414
|
+
const whereInstanceOf = module.whereInstanceOf;
|
|
415
|
+
const constructors = arr(whereInstanceOf);
|
|
285
416
|
|
|
286
417
|
for (const constructor of constructors) {
|
|
287
418
|
if (typeof constructor !== 'function') {
|
|
288
|
-
throw new Error(`Referenced module at index ${index} exports invalid
|
|
419
|
+
throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
|
|
289
420
|
}
|
|
290
421
|
}
|
|
291
422
|
}
|
|
@@ -300,7 +431,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
300
431
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
301
432
|
const element = node as Element;
|
|
302
433
|
|
|
303
|
-
|
|
434
|
+
// If intersection observer is active, start observing the element
|
|
435
|
+
// The intersection callback will handle mounting when it intersects
|
|
436
|
+
if (this.#intersectionObserver) {
|
|
437
|
+
this.#intersectionObserver.observe(element);
|
|
438
|
+
} else if (this.#matchesSelector(element)) {
|
|
304
439
|
this.#handleMatch(element);
|
|
305
440
|
}
|
|
306
441
|
}
|
|
@@ -311,7 +446,10 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
311
446
|
|
|
312
447
|
// Get all elements matching the CSS selector first
|
|
313
448
|
root.querySelectorAll(this.#init.matching).forEach(child => {
|
|
314
|
-
|
|
449
|
+
// If intersection observer is active, start observing the element
|
|
450
|
+
if (this.#intersectionObserver) {
|
|
451
|
+
this.#intersectionObserver.observe(child);
|
|
452
|
+
} else if (this.#matchesSelector(child)) {
|
|
315
453
|
this.#handleMatch(child);
|
|
316
454
|
}
|
|
317
455
|
});
|
|
@@ -319,59 +457,65 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
319
457
|
}
|
|
320
458
|
|
|
321
459
|
#matchesSelector(element: Element): boolean {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const matchesElement = element.matches(this.#init.matching);
|
|
329
|
-
if (!matchesElement) {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
334
|
-
if (this.#init.withScopePerimeter) {
|
|
335
|
-
const rootNode = this.#rootNode?.deref();
|
|
336
|
-
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
460
|
+
//TODO: reduce redundncy with this.#init?
|
|
461
|
+
// Check matching condition
|
|
462
|
+
if (!this.#init.matching) {
|
|
337
463
|
return false;
|
|
338
464
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (this.#init.withInstance) {
|
|
343
|
-
const constructors = arr(this.#init.withInstance);
|
|
344
|
-
|
|
345
|
-
// Element must be an instance of at least one constructor (OR logic for array)
|
|
346
|
-
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
347
|
-
|
|
348
|
-
if (!matchesInstanceOf) {
|
|
465
|
+
|
|
466
|
+
const matchesElement = element.matches(this.#init.matching);
|
|
467
|
+
if (!matchesElement) {
|
|
349
468
|
return false;
|
|
350
469
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Check referenced withInstance if imports are loaded and reference is specified
|
|
354
|
-
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
355
|
-
const references = arr(this.#init.reference);
|
|
356
470
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
471
|
+
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
472
|
+
if (this.#init.withScopePerimeter) {
|
|
473
|
+
const rootNode = this.#rootNode?.deref();
|
|
474
|
+
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check whereObservedRootSizeMatches condition if specified
|
|
480
|
+
if (this.#init.whereObservedRootSizeMatches && !this.#rootSizeMatches) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Check whereInstanceOf condition if specified
|
|
485
|
+
if (this.#init.whereInstanceOf) {
|
|
486
|
+
const constructors = arr(this.#init.whereInstanceOf);
|
|
487
|
+
|
|
488
|
+
// Element must be an instance of at least one constructor (OR logic for array)
|
|
489
|
+
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
490
|
+
|
|
491
|
+
if (!matchesInstanceOf) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check referenced whereInstanceOf if imports are loaded and reference is specified
|
|
497
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
498
|
+
const references = arr(this.#init.reference);
|
|
499
|
+
|
|
500
|
+
for (const index of references) {
|
|
501
|
+
const module = this.#modules[index];
|
|
502
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
503
|
+
const constructors = arr(module.whereInstanceOf);
|
|
504
|
+
|
|
505
|
+
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
506
|
+
const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
|
|
507
|
+
|
|
508
|
+
if (!matchesInstanceOf) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
367
511
|
}
|
|
368
512
|
}
|
|
369
513
|
}
|
|
514
|
+
|
|
515
|
+
// All conditions passed
|
|
516
|
+
return true;
|
|
370
517
|
}
|
|
371
518
|
|
|
372
|
-
// All conditions passed
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
519
|
|
|
376
520
|
async #handleMatch(element: Element): Promise<void> {
|
|
377
521
|
if (this.#processedDoForElement.has(element)) {
|