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.
@@ -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');
@@ -1,18 +1,21 @@
1
1
  import { EvtRt } from './EvtRt.js';
2
+ //import { buildCSSQuery } from 'assign-gingerly/buildCSSQuery.js';
3
+ import 'assign-gingerly/object-extension.js';
2
4
  /**
3
5
  * Handler for automatically enhancing mounted elements using assign-gingerly.
4
6
  * Searches the first imported module for an export with a "spawn" property
5
7
  * and uses element.enh.get() to spawn the enhancement.
6
8
  */
7
9
  export class EnhanceMountedElementHandler extends EvtRt {
8
- mount(mountedElement, MountConfig, context) {
10
+ async mount(mountedElement, MountConfig, context) {
11
+ this.abort();
9
12
  // Check if modules are specified
10
13
  if (!context.modules || context.modules.length === 0) {
11
14
  throw new Error('Must specify an ES Module with import property');
12
15
  }
13
16
  const module = context.modules[0];
14
17
  // Find registry item (object with spawn property)
15
- const registryItem = this.findRegistryItem(module);
18
+ const registryItem = await this._findRegistryItem(module, mountedElement);
16
19
  if (!registryItem) {
17
20
  throw new Error('No registry item found in module. Expected an export with a "spawn" property.');
18
21
  }
@@ -21,15 +24,13 @@ export class EnhanceMountedElementHandler extends EvtRt {
21
24
  throw new Error('Registry item "spawn" property must be a constructor function');
22
25
  }
23
26
  // Spawn the enhancement
24
- this.spawnEnhancement(mountedElement, registryItem, context);
27
+ this.#spawnEnhancement(mountedElement, registryItem, context);
25
28
  }
26
29
  /**
27
30
  * Spawn the enhancement using element.enh.get().
28
31
  * Polyfills customElementRegistry if needed for browsers without scoped registry support.
29
32
  */
30
- async spawnEnhancement(element, registryItem, context) {
31
- // Import assign-gingerly object-extension to enable enh property
32
- await import('assign-gingerly/object-extension.js');
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', {
@@ -52,14 +53,19 @@ export class EnhanceMountedElementHandler extends EvtRt {
52
53
  * @param module - The imported module
53
54
  * @returns The registry item or null if not found
54
55
  */
55
- findRegistryItem(module) {
56
+ async _findRegistryItem(module, el) {
56
57
  // Check default export first
57
- if (module.default && this.isRegistryItem(module.default)) {
58
+ if (module.default && await this._isRegistryItem(module.default, el)) {
58
59
  return module.default;
59
60
  }
60
61
  // Search all exports for a registry item
61
- const registryItems = Object.values(module)
62
- .filter(exp => this.isRegistryItem(exp));
62
+ const exports = Object.values(module);
63
+ const registryItems = [];
64
+ for (const e of exports) {
65
+ const isRegistryItem = await this._isRegistryItem(e, el);
66
+ if (isRegistryItem)
67
+ registryItems.push(e);
68
+ }
63
69
  if (registryItems.length === 0) {
64
70
  return null;
65
71
  }
@@ -73,10 +79,18 @@ export class EnhanceMountedElementHandler extends EvtRt {
73
79
  * @param exp - The export to check
74
80
  * @returns True if the export is a registry item
75
81
  */
76
- isRegistryItem(exp) {
77
- return exp !== null
82
+ async _isRegistryItem(exp, mountedElement) {
83
+ let test = exp !== null
78
84
  && typeof exp === 'object'
79
85
  && 'spawn' in exp
80
86
  && typeof exp.spawn === 'function';
87
+ if (!test)
88
+ return false;
89
+ const emc = exp;
90
+ if (emc.withAttrs !== undefined) {
91
+ const cssQuery = (await import('assign-gingerly/buildCSSQuery.js')).buildCSSQuery(emc);
92
+ return mountedElement.matches(cssQuery);
93
+ }
94
+ return true;
81
95
  }
82
96
  }
@@ -1,5 +1,8 @@
1
1
  import { EvtRt } from './EvtRt.js';
2
+ import {EnhancementConfig} from './types/assign-gingerly/types.js';
2
3
  import { MountConfig, MountContext } from './types/mount-observer/types.js';
4
+ //import { buildCSSQuery } from 'assign-gingerly/buildCSSQuery.js';
5
+ import 'assign-gingerly/object-extension.js';
3
6
 
4
7
  /**
5
8
  * Handler for automatically enhancing mounted elements using assign-gingerly.
@@ -7,7 +10,8 @@ import { MountConfig, MountContext } from './types/mount-observer/types.js';
7
10
  * and uses element.enh.get() to spawn the enhancement.
8
11
  */
9
12
  export class EnhanceMountedElementHandler extends EvtRt {
10
- mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): void {
13
+ async mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext){
14
+ this.abort();
11
15
  // Check if modules are specified
12
16
  if (!context.modules || context.modules.length === 0) {
13
17
  throw new Error('Must specify an ES Module with import property');
@@ -16,7 +20,7 @@ export class EnhanceMountedElementHandler extends EvtRt {
16
20
  const module = context.modules[0];
17
21
 
18
22
  // Find registry item (object with spawn property)
19
- const registryItem = this.findRegistryItem(module);
23
+ const registryItem = await this._findRegistryItem(module, mountedElement);
20
24
 
21
25
  if (!registryItem) {
22
26
  throw new Error('No registry item found in module. Expected an export with a "spawn" property.');
@@ -28,17 +32,14 @@ export class EnhanceMountedElementHandler extends EvtRt {
28
32
  }
29
33
 
30
34
  // Spawn the enhancement
31
- this.spawnEnhancement(mountedElement, registryItem, context);
35
+ this.#spawnEnhancement(mountedElement, registryItem, context);
32
36
  }
33
37
 
34
38
  /**
35
39
  * Spawn the enhancement using element.enh.get().
36
40
  * Polyfills customElementRegistry if needed for browsers without scoped registry support.
37
41
  */
38
- private async spawnEnhancement(element: Element, registryItem: any, context: MountContext): Promise<void> {
39
- // Import assign-gingerly object-extension to enable enh property
40
- await import('assign-gingerly/object-extension.js');
41
-
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', {
@@ -64,15 +65,19 @@ export class EnhanceMountedElementHandler extends EvtRt {
64
65
  * @param module - The imported module
65
66
  * @returns The registry item or null if not found
66
67
  */
67
- private findRegistryItem(module: any): any | null {
68
+ protected async _findRegistryItem(module: any, el: Element): Promise<any | null> {
68
69
  // Check default export first
69
- if (module.default && this.isRegistryItem(module.default)) {
70
+ if (module.default && await this._isRegistryItem(module.default, el)) {
70
71
  return module.default;
71
72
  }
72
73
 
73
74
  // Search all exports for a registry item
74
- const registryItems = Object.values(module)
75
- .filter(exp => this.isRegistryItem(exp));
75
+ const exports = Object.values(module);
76
+ const registryItems = [];
77
+ for(const e of exports){
78
+ const isRegistryItem = await this._isRegistryItem(e, el);
79
+ if(isRegistryItem) registryItems.push(e);
80
+ }
76
81
 
77
82
  if (registryItems.length === 0) {
78
83
  return null;
@@ -90,10 +95,17 @@ export class EnhanceMountedElementHandler extends EvtRt {
90
95
  * @param exp - The export to check
91
96
  * @returns True if the export is a registry item
92
97
  */
93
- private isRegistryItem(exp: any): boolean {
94
- return exp !== null
98
+ protected async _isRegistryItem(exp: any, mountedElement: Element): Promise<boolean> {
99
+ let test = exp !== null
95
100
  && typeof exp === 'object'
96
101
  && 'spawn' in exp
97
102
  && typeof exp.spawn === 'function';
103
+ if(!test) return false;
104
+ const emc = exp as EnhancementConfig;
105
+ if(emc.withAttrs !== undefined){
106
+ const cssQuery = (await import('assign-gingerly/buildCSSQuery.js')).buildCSSQuery(emc);
107
+ return mountedElement.matches(cssQuery);
108
+ }
109
+ return true;
98
110
  }
99
111
  }
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) {