mount-observer 0.1.12 → 0.1.13
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 +99 -99
- package/ElementMountExtension.js +183 -8
- package/ElementMountExtension.ts +218 -11
- package/EnhanceMountedElementHandler.js +96 -96
- package/Events.js +18 -18
- package/Events.ts +6 -6
- package/EvtRt.js +16 -14
- package/EvtRt.ts +18 -15
- package/MountObserver.js +186 -71
- package/MountObserver.ts +207 -85
- package/README.md +1345 -151
- package/RegistryMountCoordinator.js +125 -0
- package/RegistryMountCoordinator.ts +181 -0
- package/connectionMonitor.js +1 -1
- package/connectionMonitor.ts +1 -1
- package/{getRootRegistryContainer.js → getRegistryRoot.js} +1 -1
- package/{getRootRegistryContainer.ts → getRegistryRoot.ts} +1 -1
- package/index.js +15 -10
- package/index.ts +15 -10
- package/mediaQuery.js +1 -1
- package/mediaQuery.ts +1 -1
- package/observedRootHas.js +87 -87
- package/package.json +67 -61
- package/playwright.config.ts +1 -0
- package/rootSizeObserver.js +1 -1
- package/rootSizeObserver.ts +1 -1
- package/upShadowSearch.js +64 -0
- package/upShadowSearch.ts +62 -0
- package/DefineCustomElementHandler.ts +0 -117
- package/EnhanceMountedElementHandler.ts +0 -111
package/MountObserver.js
CHANGED
|
@@ -15,6 +15,7 @@ export class MountObserver extends EventTarget {
|
|
|
15
15
|
#options;
|
|
16
16
|
#abortController;
|
|
17
17
|
#modules = [];
|
|
18
|
+
#configFromPromise;
|
|
18
19
|
#mountedElements = {
|
|
19
20
|
weakSet: new WeakSet(),
|
|
20
21
|
setWeak: new Set()
|
|
@@ -39,6 +40,7 @@ export class MountObserver extends EventTarget {
|
|
|
39
40
|
#assignTentatively;
|
|
40
41
|
#elementNotifiers = new WeakMap();
|
|
41
42
|
#notifierMountedElements = new WeakSet();
|
|
43
|
+
#subObservers;
|
|
42
44
|
#mergeHandlerDefaults(config) {
|
|
43
45
|
const doValue = config.do;
|
|
44
46
|
// Only process if do is a string (single handler reference)
|
|
@@ -71,7 +73,7 @@ export class MountObserver extends EventTarget {
|
|
|
71
73
|
this.#init = mergedConfig;
|
|
72
74
|
this.#options = options;
|
|
73
75
|
this.#abortController = new AbortController();
|
|
74
|
-
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue,
|
|
76
|
+
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, loadingEagerness, import: imp, configFrom } = mergedConfig;
|
|
75
77
|
// Make a copy of assignOnMount config using structuredClone
|
|
76
78
|
if (assignOnMount !== undefined) {
|
|
77
79
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -91,9 +93,9 @@ export class MountObserver extends EventTarget {
|
|
|
91
93
|
if (doValue !== undefined) {
|
|
92
94
|
this.#validateDoHandlers();
|
|
93
95
|
}
|
|
94
|
-
//
|
|
95
|
-
if (
|
|
96
|
-
this.#
|
|
96
|
+
// Load configFrom modules if specified
|
|
97
|
+
if (configFrom !== undefined) {
|
|
98
|
+
this.#configFromPromise = this.#loadConfigFrom();
|
|
97
99
|
}
|
|
98
100
|
// Start loading imports if eager
|
|
99
101
|
if (loadingEagerness === 'eager' && imp) {
|
|
@@ -113,28 +115,73 @@ export class MountObserver extends EventTarget {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Normalize
|
|
125
|
-
const
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
throw new Error(`
|
|
118
|
+
/**
|
|
119
|
+
* Loads configuration from external modules specified in configFrom property.
|
|
120
|
+
* Merges multiple configs left-to-right, with inline config taking final precedence.
|
|
121
|
+
*/
|
|
122
|
+
async #loadConfigFrom() {
|
|
123
|
+
const { configFrom } = this.#init;
|
|
124
|
+
if (!configFrom)
|
|
125
|
+
return;
|
|
126
|
+
// Normalize to array
|
|
127
|
+
const configPaths = Array.isArray(configFrom) ? configFrom : [configFrom];
|
|
128
|
+
// Check for duplicates
|
|
129
|
+
const pathSet = new Set();
|
|
130
|
+
for (const path of configPaths) {
|
|
131
|
+
if (pathSet.has(path)) {
|
|
132
|
+
throw new Error(`Duplicate configFrom module: '${path}'`);
|
|
133
|
+
}
|
|
134
|
+
pathSet.add(path);
|
|
135
|
+
}
|
|
136
|
+
// Load all modules
|
|
137
|
+
const loadedConfigs = [];
|
|
138
|
+
for (const path of configPaths) {
|
|
139
|
+
try {
|
|
140
|
+
const module = await import(path);
|
|
141
|
+
if (!module.mountConfig) {
|
|
142
|
+
throw new Error(`Module '${path}' does not export 'mountConfig'`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof module.mountConfig !== 'object' || module.mountConfig === null) {
|
|
145
|
+
throw new Error(`Module '${path}' exports invalid mountConfig: must be an object`);
|
|
146
|
+
}
|
|
147
|
+
loadedConfigs.push(module.mountConfig);
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Re-throw with better context if it's not already our error
|
|
151
|
+
if (error instanceof Error && !error.message.includes(path)) {
|
|
152
|
+
throw new Error(`Failed to load config from '${path}': ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
136
155
|
}
|
|
137
156
|
}
|
|
157
|
+
// Merge configs: loaded configs first (left-to-right), then inline config
|
|
158
|
+
// Save the original inline config
|
|
159
|
+
const inlineConfig = { ...this.#init };
|
|
160
|
+
// Start with empty object, merge all loaded configs, then merge inline
|
|
161
|
+
let mergedConfig = {};
|
|
162
|
+
for (const loadedConfig of loadedConfigs) {
|
|
163
|
+
mergedConfig = Object.assign(mergedConfig, loadedConfig);
|
|
164
|
+
}
|
|
165
|
+
// Inline config takes final precedence
|
|
166
|
+
mergedConfig = Object.assign(mergedConfig, inlineConfig);
|
|
167
|
+
// Update the init config with merged result
|
|
168
|
+
this.#init = mergedConfig;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Creates and initializes sub-observers from the `with` property.
|
|
172
|
+
* Each sub-observer observes the same root node as the parent.
|
|
173
|
+
* Sub-observers are stored in #subObservers Map for lifecycle management.
|
|
174
|
+
*/
|
|
175
|
+
async #createSubObservers(rootNode) {
|
|
176
|
+
const withConfig = this.#init.with;
|
|
177
|
+
if (!withConfig)
|
|
178
|
+
return;
|
|
179
|
+
this.#subObservers = new Map();
|
|
180
|
+
for (const [key, subConfig] of Object.entries(withConfig)) {
|
|
181
|
+
const subObserver = new MountObserver(subConfig);
|
|
182
|
+
this.#subObservers.set(key, subObserver);
|
|
183
|
+
await subObserver.observe(rootNode);
|
|
184
|
+
}
|
|
138
185
|
}
|
|
139
186
|
async #setupMediaQuery() {
|
|
140
187
|
if (!this.#rootNode) {
|
|
@@ -175,6 +222,16 @@ export class MountObserver extends EventTarget {
|
|
|
175
222
|
get disconnectedSignal() {
|
|
176
223
|
return this.#abortController.signal;
|
|
177
224
|
}
|
|
225
|
+
get mountedElements() {
|
|
226
|
+
const elements = [];
|
|
227
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
228
|
+
const element = ref.deref();
|
|
229
|
+
if (element !== undefined) {
|
|
230
|
+
elements.push(element);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return elements;
|
|
234
|
+
}
|
|
178
235
|
getNotifier(element) {
|
|
179
236
|
// Return cached notifier if it exists
|
|
180
237
|
let notifier = this.#elementNotifiers.get(element);
|
|
@@ -186,10 +243,27 @@ export class MountObserver extends EventTarget {
|
|
|
186
243
|
this.#elementNotifiers.set(element, notifier);
|
|
187
244
|
return notifier;
|
|
188
245
|
}
|
|
189
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Begins observing elements within the provided node.
|
|
248
|
+
*
|
|
249
|
+
* @param observedNode - The node to observe for matching elements. This is the root
|
|
250
|
+
* of the observation scope where the mutation observer will be
|
|
251
|
+
* registered. All matching elements within this node (and its
|
|
252
|
+
* descendants) will trigger mount callbacks.
|
|
253
|
+
*
|
|
254
|
+
* Common values:
|
|
255
|
+
* - `document` - Observe the entire document
|
|
256
|
+
* - `element` - Observe a specific subtree
|
|
257
|
+
* - `shadowRoot` - Observe within a shadow DOM
|
|
258
|
+
*/
|
|
259
|
+
async observe(observedNode) {
|
|
190
260
|
if (this.#rootNode) {
|
|
191
261
|
throw new Error('Already observing');
|
|
192
262
|
}
|
|
263
|
+
// Wait for configFrom loading to complete if it was started
|
|
264
|
+
if (this.#configFromPromise) {
|
|
265
|
+
await this.#configFromPromise;
|
|
266
|
+
}
|
|
193
267
|
if (this.#asgMtSource || this.#asgDisMtSource) {
|
|
194
268
|
await import('assign-gingerly/object-extension.js');
|
|
195
269
|
}
|
|
@@ -197,7 +271,9 @@ export class MountObserver extends EventTarget {
|
|
|
197
271
|
const { assignTentatively } = await import('assign-gingerly/assignTentatively.js');
|
|
198
272
|
this.#assignTentatively = assignTentatively;
|
|
199
273
|
}
|
|
200
|
-
this.#rootNode = new WeakRef(
|
|
274
|
+
this.#rootNode = new WeakRef(observedNode);
|
|
275
|
+
// Create sub-observers from `with` property
|
|
276
|
+
await this.#createSubObservers(observedNode);
|
|
201
277
|
// Set up media query if specified (needs rootNode to be set first)
|
|
202
278
|
if (this.#init.withMediaMatching) {
|
|
203
279
|
await this.#setupMediaQuery();
|
|
@@ -220,7 +296,7 @@ export class MountObserver extends EventTarget {
|
|
|
220
296
|
}
|
|
221
297
|
// Process existing elements only if all conditions match
|
|
222
298
|
if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
|
|
223
|
-
this.#processNode(
|
|
299
|
+
this.#processNode(observedNode);
|
|
224
300
|
}
|
|
225
301
|
// Create mutation callback
|
|
226
302
|
this.#mutationCallback = (mutations) => {
|
|
@@ -248,10 +324,18 @@ export class MountObserver extends EventTarget {
|
|
|
248
324
|
subtree: true
|
|
249
325
|
};
|
|
250
326
|
// Register with shared mutation observer
|
|
251
|
-
registerSharedObserver(
|
|
327
|
+
registerSharedObserver(observedNode, this.#mutationCallback, observerConfig);
|
|
252
328
|
}
|
|
253
329
|
disconnect() {
|
|
254
330
|
const rootNode = this.#rootNode?.deref();
|
|
331
|
+
// Disconnect all sub-observers first (recursive)
|
|
332
|
+
if (this.#subObservers) {
|
|
333
|
+
for (const subObserver of this.#subObservers.values()) {
|
|
334
|
+
subObserver.disconnect();
|
|
335
|
+
}
|
|
336
|
+
this.#subObservers.clear();
|
|
337
|
+
this.#subObservers = undefined;
|
|
338
|
+
}
|
|
255
339
|
// Unregister from shared mutation observer
|
|
256
340
|
if (rootNode && this.#mutationCallback) {
|
|
257
341
|
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
@@ -288,23 +372,6 @@ export class MountObserver extends EventTarget {
|
|
|
288
372
|
const { loadImports } = await import('./loadImports.js');
|
|
289
373
|
this.#modules = await loadImports(this.#init.import);
|
|
290
374
|
this.#importsLoaded = true;
|
|
291
|
-
// Validate referenced whereInstanceOf if reference is specified
|
|
292
|
-
if (this.#init.reference !== undefined) {
|
|
293
|
-
const references = arr(this.#init.reference);
|
|
294
|
-
for (const index of references) {
|
|
295
|
-
const module = this.#modules[index];
|
|
296
|
-
if (module && module.whereInstanceOf !== undefined) {
|
|
297
|
-
// Validate that it's a Constructor or array of Constructors
|
|
298
|
-
const whereInstanceOf = module.whereInstanceOf;
|
|
299
|
-
const constructors = arr(whereInstanceOf);
|
|
300
|
-
for (const constructor of constructors) {
|
|
301
|
-
if (typeof constructor !== 'function') {
|
|
302
|
-
throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
375
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
309
376
|
}
|
|
310
377
|
#processNode(node) {
|
|
@@ -324,7 +391,8 @@ export class MountObserver extends EventTarget {
|
|
|
324
391
|
if ('querySelectorAll' in node && this.#init.matching) {
|
|
325
392
|
const root = node;
|
|
326
393
|
// Get all elements matching the CSS selector first
|
|
327
|
-
root.querySelectorAll(this.#init.matching)
|
|
394
|
+
const matches = root.querySelectorAll(this.#init.matching);
|
|
395
|
+
matches.forEach(child => {
|
|
328
396
|
// If intersection observer is active, start observing the element
|
|
329
397
|
if (this.#intersectionObserver) {
|
|
330
398
|
this.#intersectionObserver.observe(child);
|
|
@@ -345,9 +413,23 @@ export class MountObserver extends EventTarget {
|
|
|
345
413
|
if (!matchesElement) {
|
|
346
414
|
return false;
|
|
347
415
|
}
|
|
416
|
+
// Check that element's customElementRegistry matches root node's registry
|
|
417
|
+
const rootNode = this.#rootNode?.deref();
|
|
418
|
+
if (rootNode) {
|
|
419
|
+
const registriesMatch = rootNode.customElementRegistry === element.customElementRegistry;
|
|
420
|
+
// If whereDifferentCustomElementRegistry is true, exclude matching registries
|
|
421
|
+
if (this.#init.whereDifferentCustomElementRegistry) {
|
|
422
|
+
if (registriesMatch)
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
// Default behavior: exclude non-matching registries
|
|
427
|
+
if (!registriesMatch)
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
348
431
|
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
349
432
|
if (this.#init.withScopePerimeter) {
|
|
350
|
-
const rootNode = this.#rootNode?.deref();
|
|
351
433
|
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
352
434
|
return false;
|
|
353
435
|
}
|
|
@@ -365,19 +447,13 @@ export class MountObserver extends EventTarget {
|
|
|
365
447
|
return false;
|
|
366
448
|
}
|
|
367
449
|
}
|
|
368
|
-
// Check
|
|
369
|
-
if (this.#
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
376
|
-
const matchesInstanceOf = constructors.some((constructor) => element instanceof constructor);
|
|
377
|
-
if (!matchesInstanceOf) {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
450
|
+
// Check whereLocalNameMatches condition if specified
|
|
451
|
+
if (this.#init.whereLocalNameMatches) {
|
|
452
|
+
const pattern = typeof this.#init.whereLocalNameMatches === 'string'
|
|
453
|
+
? new RegExp(this.#init.whereLocalNameMatches)
|
|
454
|
+
: this.#init.whereLocalNameMatches;
|
|
455
|
+
if (!pattern.test(element.localName)) {
|
|
456
|
+
return false;
|
|
381
457
|
}
|
|
382
458
|
}
|
|
383
459
|
// All conditions passed
|
|
@@ -406,8 +482,50 @@ export class MountObserver extends EventTarget {
|
|
|
406
482
|
modules: this.#modules,
|
|
407
483
|
observer: this,
|
|
408
484
|
rootNode,
|
|
409
|
-
|
|
485
|
+
mountConfig: this.#init,
|
|
410
486
|
};
|
|
487
|
+
// Add withObservers if sub-observers exist
|
|
488
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
489
|
+
context.withObservers = {};
|
|
490
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
491
|
+
context.withObservers[key] = subObserver;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Check shouldMount condition if specified (final gate before mounting)
|
|
495
|
+
if (this.#init.shouldMount) {
|
|
496
|
+
try {
|
|
497
|
+
const shouldMount = this.#init.shouldMount(element, context);
|
|
498
|
+
if (!shouldMount) {
|
|
499
|
+
// shouldMount returned false - don't mount this element
|
|
500
|
+
// Remove from processed set so it can be re-evaluated later
|
|
501
|
+
this.#processedDoForElement.delete(element);
|
|
502
|
+
// Remove from mounted elements tracking
|
|
503
|
+
this.#mountedElements.weakSet.delete(element);
|
|
504
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
505
|
+
if (ref.deref() === element) {
|
|
506
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
// shouldMount threw an error - treat as false and log
|
|
515
|
+
console.error('shouldMount check failed:', error);
|
|
516
|
+
// Remove from processed set so it can be re-evaluated later
|
|
517
|
+
this.#processedDoForElement.delete(element);
|
|
518
|
+
// Remove from mounted elements tracking
|
|
519
|
+
this.#mountedElements.weakSet.delete(element);
|
|
520
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
521
|
+
if (ref.deref() === element) {
|
|
522
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
411
529
|
// Apply assignGingerly if specified
|
|
412
530
|
if (this.#asgMtSource) {
|
|
413
531
|
element.assignGingerly(this.#asgMtSource);
|
|
@@ -437,16 +555,6 @@ export class MountObserver extends EventTarget {
|
|
|
437
555
|
}
|
|
438
556
|
}
|
|
439
557
|
}
|
|
440
|
-
// Call referenced do functions from imported modules
|
|
441
|
-
if (this.#init.reference !== undefined) {
|
|
442
|
-
const references = arr(this.#init.reference);
|
|
443
|
-
for (const index of references) {
|
|
444
|
-
const module = this.#modules[index];
|
|
445
|
-
if (module && typeof module.do === 'function') {
|
|
446
|
-
module.do(element, context);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
558
|
// Dispatch mount event
|
|
451
559
|
const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
|
|
452
560
|
this.dispatchEvent(mountEvent);
|
|
@@ -529,8 +637,15 @@ export class MountObserver extends EventTarget {
|
|
|
529
637
|
modules: this.#modules,
|
|
530
638
|
observer: this,
|
|
531
639
|
rootNode,
|
|
532
|
-
|
|
640
|
+
mountConfig: this.#init,
|
|
533
641
|
};
|
|
642
|
+
// Add withObservers if sub-observers exist
|
|
643
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
644
|
+
context.withObservers = {};
|
|
645
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
646
|
+
context.withObservers[key] = subObserver;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
534
649
|
// Dispatch dismount event
|
|
535
650
|
const dismountEvent = new DismountEvent(element, 'with-matching-failed', this.#init);
|
|
536
651
|
this.dispatchEvent(dismountEvent);
|