mount-observer 0.1.10 → 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/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
- this.#init = config;
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
- } = config;
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 media matches
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 media doesn't match
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 withInstance if reference is specified
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.withInstance !== undefined) {
412
+ if (module && module.whereInstanceOf !== undefined) {
282
413
  // Validate that it's a Constructor or array of Constructors
283
- const withInstance = module.withInstance;
284
- const constructors = arr(withInstance);
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 withInstance: must be a Constructor or array of Constructors`);
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
- if (this.#matchesSelector(element)) {
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
- if (this.#matchesSelector(child)) {
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
- //TODO: reduce redundncy with this.#init?
323
- // Check matching condition
324
- if (!this.#init.matching) {
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
- // Check withInstance condition if specified
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
- for (const index of references) {
358
- const module = this.#modules[index];
359
- if (module && module.withInstance !== undefined) {
360
- const constructors = arr(module.withInstance);
361
-
362
- // Element must be an instance of at least one constructor (OR logic within this module)
363
- const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
364
-
365
- if (!matchesInstanceOf) {
366
- return false;
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)) {