mount-observer 0.1.12 → 0.1.14
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 +27 -18
- package/Events.ts +18 -6
- package/EvtRt.js +16 -14
- package/EvtRt.ts +18 -15
- package/MountObserver.js +207 -71
- package/MountObserver.ts +234 -85
- package/README.md +1782 -251
- 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 +67 -0
- package/upShadowSearch.ts +65 -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,8 @@ export class MountObserver extends EventTarget {
|
|
|
39
40
|
#assignTentatively;
|
|
40
41
|
#elementNotifiers = new WeakMap();
|
|
41
42
|
#notifierMountedElements = new WeakSet();
|
|
43
|
+
#subObservers;
|
|
44
|
+
#whenDefinedResolved = false;
|
|
42
45
|
#mergeHandlerDefaults(config) {
|
|
43
46
|
const doValue = config.do;
|
|
44
47
|
// Only process if do is a string (single handler reference)
|
|
@@ -71,7 +74,7 @@ export class MountObserver extends EventTarget {
|
|
|
71
74
|
this.#init = mergedConfig;
|
|
72
75
|
this.#options = options;
|
|
73
76
|
this.#abortController = new AbortController();
|
|
74
|
-
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue,
|
|
77
|
+
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, loadingEagerness, import: imp, configFrom } = mergedConfig;
|
|
75
78
|
// Make a copy of assignOnMount config using structuredClone
|
|
76
79
|
if (assignOnMount !== undefined) {
|
|
77
80
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -91,9 +94,9 @@ export class MountObserver extends EventTarget {
|
|
|
91
94
|
if (doValue !== undefined) {
|
|
92
95
|
this.#validateDoHandlers();
|
|
93
96
|
}
|
|
94
|
-
//
|
|
95
|
-
if (
|
|
96
|
-
this.#
|
|
97
|
+
// Load configFrom modules if specified
|
|
98
|
+
if (configFrom !== undefined) {
|
|
99
|
+
this.#configFromPromise = this.#loadConfigFrom();
|
|
97
100
|
}
|
|
98
101
|
// Start loading imports if eager
|
|
99
102
|
if (loadingEagerness === 'eager' && imp) {
|
|
@@ -113,28 +116,91 @@ export class MountObserver extends EventTarget {
|
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Normalize
|
|
125
|
-
const
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
throw new Error(`
|
|
119
|
+
/**
|
|
120
|
+
* Loads configuration from external modules specified in configFrom property.
|
|
121
|
+
* Merges multiple configs left-to-right, with inline config taking final precedence.
|
|
122
|
+
*/
|
|
123
|
+
async #loadConfigFrom() {
|
|
124
|
+
const { configFrom } = this.#init;
|
|
125
|
+
if (!configFrom)
|
|
126
|
+
return;
|
|
127
|
+
// Normalize to array
|
|
128
|
+
const configPaths = Array.isArray(configFrom) ? configFrom : [configFrom];
|
|
129
|
+
// Check for duplicates
|
|
130
|
+
const pathSet = new Set();
|
|
131
|
+
for (const path of configPaths) {
|
|
132
|
+
if (pathSet.has(path)) {
|
|
133
|
+
throw new Error(`Duplicate configFrom module: '${path}'`);
|
|
134
|
+
}
|
|
135
|
+
pathSet.add(path);
|
|
136
|
+
}
|
|
137
|
+
// Load all modules
|
|
138
|
+
const loadedConfigs = [];
|
|
139
|
+
for (const path of configPaths) {
|
|
140
|
+
try {
|
|
141
|
+
const module = await import(path);
|
|
142
|
+
if (!module.mountConfig) {
|
|
143
|
+
throw new Error(`Module '${path}' does not export 'mountConfig'`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof module.mountConfig !== 'object' || module.mountConfig === null) {
|
|
146
|
+
throw new Error(`Module '${path}' exports invalid mountConfig: must be an object`);
|
|
147
|
+
}
|
|
148
|
+
loadedConfigs.push(module.mountConfig);
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
catch (error) {
|
|
151
|
+
// Re-throw with better context if it's not already our error
|
|
152
|
+
if (error instanceof Error && !error.message.includes(path)) {
|
|
153
|
+
throw new Error(`Failed to load config from '${path}': ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
136
156
|
}
|
|
137
157
|
}
|
|
158
|
+
// Merge configs: loaded configs first (left-to-right), then inline config
|
|
159
|
+
// Save the original inline config
|
|
160
|
+
const inlineConfig = { ...this.#init };
|
|
161
|
+
// Start with empty object, merge all loaded configs, then merge inline
|
|
162
|
+
let mergedConfig = {};
|
|
163
|
+
for (const loadedConfig of loadedConfigs) {
|
|
164
|
+
mergedConfig = Object.assign(mergedConfig, loadedConfig);
|
|
165
|
+
}
|
|
166
|
+
// Inline config takes final precedence
|
|
167
|
+
mergedConfig = Object.assign(mergedConfig, inlineConfig);
|
|
168
|
+
// Update the init config with merged result
|
|
169
|
+
this.#init = mergedConfig;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Waits for custom elements to be defined before mounting.
|
|
173
|
+
* Only runs once per observer instance.
|
|
174
|
+
*/
|
|
175
|
+
async #waitForWhenDefined(rootNode) {
|
|
176
|
+
// Skip if already resolved or not configured
|
|
177
|
+
if (this.#whenDefinedResolved || !this.#init.whenDefined) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Get the custom element registry from the root node
|
|
181
|
+
const registry = rootNode.customElementRegistry || customElements;
|
|
182
|
+
// Normalize to array
|
|
183
|
+
const tagNames = arr(this.#init.whenDefined);
|
|
184
|
+
// Wait for all tags to be defined
|
|
185
|
+
await Promise.all(tagNames.map(tag => registry.whenDefined(tag)));
|
|
186
|
+
// Mark as resolved so we don't check again
|
|
187
|
+
this.#whenDefinedResolved = true;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Creates and initializes sub-observers from the `with` property.
|
|
191
|
+
* Each sub-observer observes the same root node as the parent.
|
|
192
|
+
* Sub-observers are stored in #subObservers Map for lifecycle management.
|
|
193
|
+
*/
|
|
194
|
+
async #createSubObservers(rootNode) {
|
|
195
|
+
const withConfig = this.#init.with;
|
|
196
|
+
if (!withConfig)
|
|
197
|
+
return;
|
|
198
|
+
this.#subObservers = new Map();
|
|
199
|
+
for (const [key, subConfig] of Object.entries(withConfig)) {
|
|
200
|
+
const subObserver = new MountObserver(subConfig);
|
|
201
|
+
this.#subObservers.set(key, subObserver);
|
|
202
|
+
await subObserver.observe(rootNode);
|
|
203
|
+
}
|
|
138
204
|
}
|
|
139
205
|
async #setupMediaQuery() {
|
|
140
206
|
if (!this.#rootNode) {
|
|
@@ -175,6 +241,16 @@ export class MountObserver extends EventTarget {
|
|
|
175
241
|
get disconnectedSignal() {
|
|
176
242
|
return this.#abortController.signal;
|
|
177
243
|
}
|
|
244
|
+
get mountedElements() {
|
|
245
|
+
const elements = [];
|
|
246
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
247
|
+
const element = ref.deref();
|
|
248
|
+
if (element !== undefined) {
|
|
249
|
+
elements.push(element);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return elements;
|
|
253
|
+
}
|
|
178
254
|
getNotifier(element) {
|
|
179
255
|
// Return cached notifier if it exists
|
|
180
256
|
let notifier = this.#elementNotifiers.get(element);
|
|
@@ -186,10 +262,27 @@ export class MountObserver extends EventTarget {
|
|
|
186
262
|
this.#elementNotifiers.set(element, notifier);
|
|
187
263
|
return notifier;
|
|
188
264
|
}
|
|
189
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Begins observing elements within the provided node.
|
|
267
|
+
*
|
|
268
|
+
* @param observedNode - The node to observe for matching elements. This is the root
|
|
269
|
+
* of the observation scope where the mutation observer will be
|
|
270
|
+
* registered. All matching elements within this node (and its
|
|
271
|
+
* descendants) will trigger mount callbacks.
|
|
272
|
+
*
|
|
273
|
+
* Common values:
|
|
274
|
+
* - `document` - Observe the entire document
|
|
275
|
+
* - `element` - Observe a specific subtree
|
|
276
|
+
* - `shadowRoot` - Observe within a shadow DOM
|
|
277
|
+
*/
|
|
278
|
+
async observe(observedNode) {
|
|
190
279
|
if (this.#rootNode) {
|
|
191
280
|
throw new Error('Already observing');
|
|
192
281
|
}
|
|
282
|
+
// Wait for configFrom loading to complete if it was started
|
|
283
|
+
if (this.#configFromPromise) {
|
|
284
|
+
await this.#configFromPromise;
|
|
285
|
+
}
|
|
193
286
|
if (this.#asgMtSource || this.#asgDisMtSource) {
|
|
194
287
|
await import('assign-gingerly/object-extension.js');
|
|
195
288
|
}
|
|
@@ -197,7 +290,11 @@ export class MountObserver extends EventTarget {
|
|
|
197
290
|
const { assignTentatively } = await import('assign-gingerly/assignTentatively.js');
|
|
198
291
|
this.#assignTentatively = assignTentatively;
|
|
199
292
|
}
|
|
200
|
-
this.#rootNode = new WeakRef(
|
|
293
|
+
this.#rootNode = new WeakRef(observedNode);
|
|
294
|
+
// Wait for whenDefined if specified (must be first check)
|
|
295
|
+
await this.#waitForWhenDefined(observedNode);
|
|
296
|
+
// Create sub-observers from `with` property
|
|
297
|
+
await this.#createSubObservers(observedNode);
|
|
201
298
|
// Set up media query if specified (needs rootNode to be set first)
|
|
202
299
|
if (this.#init.withMediaMatching) {
|
|
203
300
|
await this.#setupMediaQuery();
|
|
@@ -220,7 +317,7 @@ export class MountObserver extends EventTarget {
|
|
|
220
317
|
}
|
|
221
318
|
// Process existing elements only if all conditions match
|
|
222
319
|
if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
|
|
223
|
-
this.#processNode(
|
|
320
|
+
this.#processNode(observedNode);
|
|
224
321
|
}
|
|
225
322
|
// Create mutation callback
|
|
226
323
|
this.#mutationCallback = (mutations) => {
|
|
@@ -248,10 +345,18 @@ export class MountObserver extends EventTarget {
|
|
|
248
345
|
subtree: true
|
|
249
346
|
};
|
|
250
347
|
// Register with shared mutation observer
|
|
251
|
-
registerSharedObserver(
|
|
348
|
+
registerSharedObserver(observedNode, this.#mutationCallback, observerConfig);
|
|
252
349
|
}
|
|
253
350
|
disconnect() {
|
|
254
351
|
const rootNode = this.#rootNode?.deref();
|
|
352
|
+
// Disconnect all sub-observers first (recursive)
|
|
353
|
+
if (this.#subObservers) {
|
|
354
|
+
for (const subObserver of this.#subObservers.values()) {
|
|
355
|
+
subObserver.disconnect();
|
|
356
|
+
}
|
|
357
|
+
this.#subObservers.clear();
|
|
358
|
+
this.#subObservers = undefined;
|
|
359
|
+
}
|
|
255
360
|
// Unregister from shared mutation observer
|
|
256
361
|
if (rootNode && this.#mutationCallback) {
|
|
257
362
|
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
@@ -288,23 +393,6 @@ export class MountObserver extends EventTarget {
|
|
|
288
393
|
const { loadImports } = await import('./loadImports.js');
|
|
289
394
|
this.#modules = await loadImports(this.#init.import);
|
|
290
395
|
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
396
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
309
397
|
}
|
|
310
398
|
#processNode(node) {
|
|
@@ -324,7 +412,8 @@ export class MountObserver extends EventTarget {
|
|
|
324
412
|
if ('querySelectorAll' in node && this.#init.matching) {
|
|
325
413
|
const root = node;
|
|
326
414
|
// Get all elements matching the CSS selector first
|
|
327
|
-
root.querySelectorAll(this.#init.matching)
|
|
415
|
+
const matches = root.querySelectorAll(this.#init.matching);
|
|
416
|
+
matches.forEach(child => {
|
|
328
417
|
// If intersection observer is active, start observing the element
|
|
329
418
|
if (this.#intersectionObserver) {
|
|
330
419
|
this.#intersectionObserver.observe(child);
|
|
@@ -345,9 +434,23 @@ export class MountObserver extends EventTarget {
|
|
|
345
434
|
if (!matchesElement) {
|
|
346
435
|
return false;
|
|
347
436
|
}
|
|
437
|
+
// Check that element's customElementRegistry matches root node's registry
|
|
438
|
+
const rootNode = this.#rootNode?.deref();
|
|
439
|
+
if (rootNode) {
|
|
440
|
+
const registriesMatch = rootNode.customElementRegistry === element.customElementRegistry;
|
|
441
|
+
// If whereDifferentCustomElementRegistry is true, exclude matching registries
|
|
442
|
+
if (this.#init.whereDifferentCustomElementRegistry) {
|
|
443
|
+
if (registriesMatch)
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Default behavior: exclude non-matching registries
|
|
448
|
+
if (!registriesMatch)
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
348
452
|
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
349
453
|
if (this.#init.withScopePerimeter) {
|
|
350
|
-
const rootNode = this.#rootNode?.deref();
|
|
351
454
|
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
352
455
|
return false;
|
|
353
456
|
}
|
|
@@ -365,19 +468,13 @@ export class MountObserver extends EventTarget {
|
|
|
365
468
|
return false;
|
|
366
469
|
}
|
|
367
470
|
}
|
|
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
|
-
}
|
|
471
|
+
// Check whereLocalNameMatches condition if specified
|
|
472
|
+
if (this.#init.whereLocalNameMatches) {
|
|
473
|
+
const pattern = typeof this.#init.whereLocalNameMatches === 'string'
|
|
474
|
+
? new RegExp(this.#init.whereLocalNameMatches)
|
|
475
|
+
: this.#init.whereLocalNameMatches;
|
|
476
|
+
if (!pattern.test(element.localName)) {
|
|
477
|
+
return false;
|
|
381
478
|
}
|
|
382
479
|
}
|
|
383
480
|
// All conditions passed
|
|
@@ -406,8 +503,50 @@ export class MountObserver extends EventTarget {
|
|
|
406
503
|
modules: this.#modules,
|
|
407
504
|
observer: this,
|
|
408
505
|
rootNode,
|
|
409
|
-
|
|
506
|
+
mountConfig: this.#init,
|
|
410
507
|
};
|
|
508
|
+
// Add withObservers if sub-observers exist
|
|
509
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
510
|
+
context.withObservers = {};
|
|
511
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
512
|
+
context.withObservers[key] = subObserver;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Check shouldMount condition if specified (final gate before mounting)
|
|
516
|
+
if (this.#init.shouldMount) {
|
|
517
|
+
try {
|
|
518
|
+
const shouldMount = this.#init.shouldMount(element, context);
|
|
519
|
+
if (!shouldMount) {
|
|
520
|
+
// shouldMount returned false - don't mount this element
|
|
521
|
+
// Remove from processed set so it can be re-evaluated later
|
|
522
|
+
this.#processedDoForElement.delete(element);
|
|
523
|
+
// Remove from mounted elements tracking
|
|
524
|
+
this.#mountedElements.weakSet.delete(element);
|
|
525
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
526
|
+
if (ref.deref() === element) {
|
|
527
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
// shouldMount threw an error - treat as false and log
|
|
536
|
+
console.error('shouldMount check failed:', error);
|
|
537
|
+
// Remove from processed set so it can be re-evaluated later
|
|
538
|
+
this.#processedDoForElement.delete(element);
|
|
539
|
+
// Remove from mounted elements tracking
|
|
540
|
+
this.#mountedElements.weakSet.delete(element);
|
|
541
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
542
|
+
if (ref.deref() === element) {
|
|
543
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
411
550
|
// Apply assignGingerly if specified
|
|
412
551
|
if (this.#asgMtSource) {
|
|
413
552
|
element.assignGingerly(this.#asgMtSource);
|
|
@@ -437,16 +576,6 @@ export class MountObserver extends EventTarget {
|
|
|
437
576
|
}
|
|
438
577
|
}
|
|
439
578
|
}
|
|
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
579
|
// Dispatch mount event
|
|
451
580
|
const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
|
|
452
581
|
this.dispatchEvent(mountEvent);
|
|
@@ -529,8 +658,15 @@ export class MountObserver extends EventTarget {
|
|
|
529
658
|
modules: this.#modules,
|
|
530
659
|
observer: this,
|
|
531
660
|
rootNode,
|
|
532
|
-
|
|
661
|
+
mountConfig: this.#init,
|
|
533
662
|
};
|
|
663
|
+
// Add withObservers if sub-observers exist
|
|
664
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
665
|
+
context.withObservers = {};
|
|
666
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
667
|
+
context.withObservers[key] = subObserver;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
534
670
|
// Dispatch dismount event
|
|
535
671
|
const dismountEvent = new DismountEvent(element, 'with-matching-failed', this.#init);
|
|
536
672
|
this.dispatchEvent(dismountEvent);
|