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.
@@ -1,6 +1,7 @@
1
1
  import { EvtRt } from './EvtRt.js';
2
2
  export class DefineCustomElementHandler extends EvtRt {
3
3
  mount(mountedElement, MountConfig, context) {
4
+ this.abort();
4
5
  // Check if modules are specified
5
6
  if (!context.modules || context.modules.length === 0) {
6
7
  throw new Error('Must specify an ES Module');
@@ -3,6 +3,7 @@ import { MountConfig, MountContext } from './types/mount-observer/types.js';
3
3
 
4
4
  export class DefineCustomElementHandler extends EvtRt {
5
5
  mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): void {
6
+ this.abort();
6
7
  // Check if modules are specified
7
8
  if (!context.modules || context.modules.length === 0) {
8
9
  throw new Error('Must specify an ES Module');
@@ -8,6 +8,7 @@ import 'assign-gingerly/object-extension.js';
8
8
  */
9
9
  export class EnhanceMountedElementHandler extends EvtRt {
10
10
  async mount(mountedElement, MountConfig, context) {
11
+ this.abort();
11
12
  // Check if modules are specified
12
13
  if (!context.modules || context.modules.length === 0) {
13
14
  throw new Error('Must specify an ES Module with import property');
@@ -23,13 +24,13 @@ export class EnhanceMountedElementHandler extends EvtRt {
23
24
  throw new Error('Registry item "spawn" property must be a constructor function');
24
25
  }
25
26
  // Spawn the enhancement
26
- this._spawnEnhancement(mountedElement, registryItem, context);
27
+ this.#spawnEnhancement(mountedElement, registryItem, context);
27
28
  }
28
29
  /**
29
30
  * Spawn the enhancement using element.enh.get().
30
31
  * Polyfills customElementRegistry if needed for browsers without scoped registry support.
31
32
  */
32
- _spawnEnhancement(element, registryItem, context) {
33
+ #spawnEnhancement(element, registryItem, context) {
33
34
  // Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
34
35
  if (!element.customElementRegistry) {
35
36
  Object.defineProperty(element, 'customElementRegistry', {
@@ -11,6 +11,7 @@ import 'assign-gingerly/object-extension.js';
11
11
  */
12
12
  export class EnhanceMountedElementHandler extends EvtRt {
13
13
  async mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext){
14
+ this.abort();
14
15
  // Check if modules are specified
15
16
  if (!context.modules || context.modules.length === 0) {
16
17
  throw new Error('Must specify an ES Module with import property');
@@ -31,14 +32,14 @@ export class EnhanceMountedElementHandler extends EvtRt {
31
32
  }
32
33
 
33
34
  // Spawn the enhancement
34
- this._spawnEnhancement(mountedElement, registryItem, context);
35
+ this.#spawnEnhancement(mountedElement, registryItem, context);
35
36
  }
36
37
 
37
38
  /**
38
39
  * Spawn the enhancement using element.enh.get().
39
40
  * Polyfills customElementRegistry if needed for browsers without scoped registry support.
40
41
  */
41
- protected _spawnEnhancement(element: Element, registryItem: any, context: MountContext): void {
42
+ #spawnEnhancement(element: Element, registryItem: any, context: MountContext): void {
42
43
  // Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
43
44
  if (!(element as any).customElementRegistry) {
44
45
  Object.defineProperty(element, 'customElementRegistry', {
package/EvtRt.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { DismountEvent, MountEvent, DisconnectEvent, dismountEventName, disconnectEventName, mountEventName } from './Events.js';
2
2
  export class EvtRt {
3
+ #ac;
3
4
  constructor(mountedElement, ctx) {
4
5
  const { observer, MountConfig } = ctx;
6
+ this.#ac = new AbortController();
5
7
  const et = observer.getNotifier(mountedElement);
6
- et.addEventListener(mountEventName, this);
7
- et.addEventListener(disconnectEventName, this);
8
- et.addEventListener(dismountEventName, this);
8
+ et.addEventListener(mountEventName, this, { signal: this.#ac.signal });
9
+ et.addEventListener(disconnectEventName, this, { signal: this.#ac.signal });
10
+ et.addEventListener(dismountEventName, this, { signal: this.#ac.signal });
9
11
  this.mount(mountedElement, MountConfig, ctx);
10
12
  }
13
+ abort() {
14
+ this.#ac.abort();
15
+ }
11
16
  mount(mountedElement, MountConfig, context) {
12
17
  console.log({ mountedElement, MountConfig, context });
13
18
  }
package/EvtRt.ts CHANGED
@@ -5,16 +5,25 @@ import {
5
5
  dismountEventName, disconnectEventName, mountEventName
6
6
  } from './Events.js';
7
7
  export class EvtRt implements EventListenerObject{
8
+
9
+
10
+ #ac: AbortController;
11
+
8
12
  constructor(mountedElement: Element, ctx: MountContext ){
9
13
  const {observer, MountConfig} = ctx;
14
+ this.#ac = new AbortController();
10
15
  const et = observer.getNotifier(mountedElement);
11
- et.addEventListener(mountEventName, this);
12
- et.addEventListener(disconnectEventName, this);
13
- et.addEventListener(dismountEventName, this);
16
+ et.addEventListener(mountEventName, this, {signal: this.#ac.signal});
17
+ et.addEventListener(disconnectEventName, this, {signal: this.#ac.signal});
18
+ et.addEventListener(dismountEventName, this, {signal: this.#ac.signal});
14
19
  this.mount(mountedElement, MountConfig, ctx);
15
20
 
16
21
  }
17
22
 
23
+ abort(){
24
+ this.#ac.abort();
25
+ }
26
+
18
27
  mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext){
19
28
  console.log({mountedElement, MountConfig, context});
20
29
  }
package/MountObserver.js CHANGED
@@ -25,7 +25,13 @@ export class MountObserver extends EventTarget {
25
25
  #rootNode;
26
26
  #importsLoaded = false;
27
27
  #mediaQueryCleanup;
28
+ #rootSizeCleanup;
29
+ #intersectionCleanup;
30
+ #connectionCleanup;
31
+ #intersectionObserver;
28
32
  #mediaMatches = true;
33
+ #rootSizeMatches = true;
34
+ #connectionMatches = true;
29
35
  #asgMtSource;
30
36
  #asgDisMtSource;
31
37
  #stageMtSource;
@@ -33,12 +39,39 @@ export class MountObserver extends EventTarget {
33
39
  #assignTentatively;
34
40
  #elementNotifiers = new WeakMap();
35
41
  #notifierMountedElements = new WeakSet();
42
+ #mergeHandlerDefaults(config) {
43
+ const doValue = config.do;
44
+ // Only process if do is a string (single handler reference)
45
+ if (typeof doValue !== 'string') {
46
+ return config;
47
+ }
48
+ // Look up the handler class
49
+ const HandlerClass = MountObserver.#handlerRegistry.get(doValue);
50
+ if (!HandlerClass) {
51
+ // Validation will catch this later
52
+ return config;
53
+ }
54
+ // Extract static properties from the handler class
55
+ const handlerDefaults = {};
56
+ const proto = HandlerClass;
57
+ // Get all static properties
58
+ for (const key of Object.getOwnPropertyNames(proto)) {
59
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
60
+ handlerDefaults[key] = proto[key];
61
+ }
62
+ }
63
+ // Merge: handler defaults first, then inline config (inline trumps)
64
+ // Using object spread - inline config overwrites handler defaults
65
+ return { ...handlerDefaults, ...config };
66
+ }
36
67
  constructor(config, options = {}) {
37
68
  super();
38
- this.#init = config;
69
+ // Merge handler defaults if do is a string reference
70
+ const mergedConfig = this.#mergeHandlerDefaults(config);
71
+ this.#init = mergedConfig;
39
72
  this.#options = options;
40
73
  this.#abortController = new AbortController();
41
- const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } = config;
74
+ const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } = mergedConfig;
42
75
  // Make a copy of assignOnMount config using structuredClone
43
76
  if (assignOnMount !== undefined) {
44
77
  this.#asgMtSource = structuredClone(assignOnMount);
@@ -112,6 +145,33 @@ export class MountObserver extends EventTarget {
112
145
  this.#mediaMatches = result.mediaMatches;
113
146
  this.#mediaQueryCleanup = result.cleanup;
114
147
  }
148
+ async #setupRootSizeObserver() {
149
+ if (!this.#rootNode) {
150
+ throw new Error('Cannot setup root size observer before observe() is called');
151
+ }
152
+ const { setupRootSizeObserver } = await import('./rootSizeObserver.js');
153
+ const result = setupRootSizeObserver(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (node) => this.#processNode(node));
154
+ this.#rootSizeMatches = result.conditionMatches;
155
+ this.#rootSizeCleanup = result.cleanup;
156
+ }
157
+ async #setupElementIntersection() {
158
+ if (!this.#rootNode) {
159
+ throw new Error('Cannot setup element intersection before observe() is called');
160
+ }
161
+ const { setupElementIntersection } = await import('./elementIntersection.js');
162
+ const result = setupElementIntersection(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (element) => this.#matchesSelector(element), (element) => this.#handleMatch(element));
163
+ this.#intersectionObserver = result.intersectionObserver;
164
+ this.#intersectionCleanup = result.cleanup;
165
+ }
166
+ async #setupConnectionMonitor() {
167
+ if (!this.#rootNode) {
168
+ throw new Error('Cannot setup connection monitor before observe() is called');
169
+ }
170
+ const { setupConnectionMonitor } = await import('./connectionMonitor.js');
171
+ const result = setupConnectionMonitor(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (node) => this.#processNode(node));
172
+ this.#connectionMatches = result.conditionMatches;
173
+ this.#connectionCleanup = result.cleanup;
174
+ }
115
175
  get disconnectedSignal() {
116
176
  return this.#abortController.signal;
117
177
  }
@@ -142,18 +202,30 @@ export class MountObserver extends EventTarget {
142
202
  if (this.#init.withMediaMatching) {
143
203
  await this.#setupMediaQuery();
144
204
  }
205
+ // Set up root size observer if specified (needs rootNode to be set first)
206
+ if (this.#init.whereObservedRootSizeMatches) {
207
+ await this.#setupRootSizeObserver();
208
+ }
209
+ // Set up element intersection observer if specified (needs rootNode to be set first)
210
+ if (this.#init.whereElementIntersectsWith) {
211
+ await this.#setupElementIntersection();
212
+ }
213
+ // Set up connection monitor if specified (needs rootNode to be set first)
214
+ if (this.#init.whereConnectionHas) {
215
+ await this.#setupConnectionMonitor();
216
+ }
145
217
  // Wait for eager imports to complete if they were started in constructor
146
218
  if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
147
219
  await this.#loadImports();
148
220
  }
149
- // Process existing elements only if media matches
150
- if (this.#mediaMatches) {
221
+ // Process existing elements only if all conditions match
222
+ if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
151
223
  this.#processNode(rootNode);
152
224
  }
153
225
  // Create mutation callback
154
226
  this.#mutationCallback = (mutations) => {
155
- // Skip processing if media doesn't match
156
- if (!this.#mediaMatches) {
227
+ // Skip processing if any condition doesn't match
228
+ if (!this.#mediaMatches || !this.#rootSizeMatches || !this.#connectionMatches) {
157
229
  return;
158
230
  }
159
231
  for (const mutation of mutations) {
@@ -190,6 +262,21 @@ export class MountObserver extends EventTarget {
190
262
  this.#mediaQueryCleanup();
191
263
  this.#mediaQueryCleanup = undefined;
192
264
  }
265
+ // Remove root size observer
266
+ if (this.#rootSizeCleanup) {
267
+ this.#rootSizeCleanup();
268
+ this.#rootSizeCleanup = undefined;
269
+ }
270
+ // Remove intersection observer
271
+ if (this.#intersectionCleanup) {
272
+ this.#intersectionCleanup();
273
+ this.#intersectionCleanup = undefined;
274
+ }
275
+ // Remove connection monitor
276
+ if (this.#connectionCleanup) {
277
+ this.#connectionCleanup();
278
+ this.#connectionCleanup = undefined;
279
+ }
193
280
  this.#abortController.abort();
194
281
  this.#rootNode = undefined;
195
282
  }
@@ -201,18 +288,18 @@ export class MountObserver extends EventTarget {
201
288
  const { loadImports } = await import('./loadImports.js');
202
289
  this.#modules = await loadImports(this.#init.import);
203
290
  this.#importsLoaded = true;
204
- // Validate referenced withInstance if reference is specified
291
+ // Validate referenced whereInstanceOf if reference is specified
205
292
  if (this.#init.reference !== undefined) {
206
293
  const references = arr(this.#init.reference);
207
294
  for (const index of references) {
208
295
  const module = this.#modules[index];
209
- if (module && module.withInstance !== undefined) {
296
+ if (module && module.whereInstanceOf !== undefined) {
210
297
  // Validate that it's a Constructor or array of Constructors
211
- const withInstance = module.withInstance;
212
- const constructors = arr(withInstance);
298
+ const whereInstanceOf = module.whereInstanceOf;
299
+ const constructors = arr(whereInstanceOf);
213
300
  for (const constructor of constructors) {
214
301
  if (typeof constructor !== 'function') {
215
- throw new Error(`Referenced module at index ${index} exports invalid withInstance: must be a Constructor or array of Constructors`);
302
+ throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
216
303
  }
217
304
  }
218
305
  }
@@ -224,7 +311,12 @@ export class MountObserver extends EventTarget {
224
311
  // If it's an element node, check if it matches
225
312
  if (node.nodeType === Node.ELEMENT_NODE) {
226
313
  const element = node;
227
- if (this.#matchesSelector(element)) {
314
+ // If intersection observer is active, start observing the element
315
+ // The intersection callback will handle mounting when it intersects
316
+ if (this.#intersectionObserver) {
317
+ this.#intersectionObserver.observe(element);
318
+ }
319
+ else if (this.#matchesSelector(element)) {
228
320
  this.#handleMatch(element);
229
321
  }
230
322
  }
@@ -233,7 +325,11 @@ export class MountObserver extends EventTarget {
233
325
  const root = node;
234
326
  // Get all elements matching the CSS selector first
235
327
  root.querySelectorAll(this.#init.matching).forEach(child => {
236
- if (this.#matchesSelector(child)) {
328
+ // If intersection observer is active, start observing the element
329
+ if (this.#intersectionObserver) {
330
+ this.#intersectionObserver.observe(child);
331
+ }
332
+ else if (this.#matchesSelector(child)) {
237
333
  this.#handleMatch(child);
238
334
  }
239
335
  });
@@ -256,22 +352,26 @@ export class MountObserver extends EventTarget {
256
352
  return false;
257
353
  }
258
354
  }
259
- // Check withInstance condition if specified
260
- if (this.#init.withInstance) {
261
- const constructors = arr(this.#init.withInstance);
355
+ // Check whereObservedRootSizeMatches condition if specified
356
+ if (this.#init.whereObservedRootSizeMatches && !this.#rootSizeMatches) {
357
+ return false;
358
+ }
359
+ // Check whereInstanceOf condition if specified
360
+ if (this.#init.whereInstanceOf) {
361
+ const constructors = arr(this.#init.whereInstanceOf);
262
362
  // Element must be an instance of at least one constructor (OR logic for array)
263
363
  const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
264
364
  if (!matchesInstanceOf) {
265
365
  return false;
266
366
  }
267
367
  }
268
- // Check referenced withInstance if imports are loaded and reference is specified
368
+ // Check referenced whereInstanceOf if imports are loaded and reference is specified
269
369
  if (this.#importsLoaded && this.#init.reference !== undefined) {
270
370
  const references = arr(this.#init.reference);
271
371
  for (const index of references) {
272
372
  const module = this.#modules[index];
273
- if (module && module.withInstance !== undefined) {
274
- const constructors = arr(module.withInstance);
373
+ if (module && module.whereInstanceOf !== undefined) {
374
+ const constructors = arr(module.whereInstanceOf);
275
375
  // Element must be an instance of at least one constructor (OR logic within this module)
276
376
  const matchesInstanceOf = constructors.some((constructor) => element instanceof constructor);
277
377
  if (!matchesInstanceOf) {