mount-observer 0.1.11 → 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 -98
- package/ElementMountExtension.js +183 -8
- package/ElementMountExtension.ts +218 -11
- package/EnhanceMountedElementHandler.js +96 -95
- package/Events.js +18 -18
- package/Events.ts +6 -6
- package/EvtRt.js +24 -17
- package/EvtRt.ts +30 -18
- package/MountObserver.js +296 -81
- package/MountObserver.ts +387 -121
- package/README.md +1508 -235
- package/RegistryMountCoordinator.js +125 -0
- package/RegistryMountCoordinator.ts +181 -0
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- 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 -0
- package/package.json +67 -61
- package/playwright.config.ts +1 -0
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
- package/upShadowSearch.js +64 -0
- package/upShadowSearch.ts +62 -0
- package/DefineCustomElementHandler.ts +0 -116
- package/EnhanceMountedElementHandler.ts +0 -110
package/MountObserver.ts
CHANGED
|
@@ -21,9 +21,10 @@ import {
|
|
|
21
21
|
type MutationCallback
|
|
22
22
|
} from './SharedMutationObserver.js';
|
|
23
23
|
import { withScopePerimeter } from './withScopePerimeter.js';
|
|
24
|
+
import { getRegistryRoot } from './getRegistryRoot.js';
|
|
24
25
|
import type { assignTentatively as AssignTentativelyType } from 'assign-gingerly/assignTentatively.js';
|
|
25
26
|
|
|
26
|
-
export class MountObserver extends EventTarget implements IMountObserver {
|
|
27
|
+
export class MountObserver<TKeys extends string = string> extends EventTarget implements IMountObserver {
|
|
27
28
|
// Static registry for registered handlers
|
|
28
29
|
static #handlerRegistry = new Map<string, Constructor>();
|
|
29
30
|
|
|
@@ -38,6 +39,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
38
39
|
#options: MountObserverOptions;
|
|
39
40
|
#abortController: AbortController;
|
|
40
41
|
#modules: any[] = [];
|
|
42
|
+
#configFromPromise: Promise<void> | undefined;
|
|
41
43
|
#mountedElements: WeakDual<Element> = {
|
|
42
44
|
weakSet: new WeakSet(),
|
|
43
45
|
setWeak: new Set()
|
|
@@ -48,7 +50,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
48
50
|
#rootNode: WeakRef<Node> | undefined;
|
|
49
51
|
#importsLoaded = false;
|
|
50
52
|
#mediaQueryCleanup?: () => void;
|
|
53
|
+
#rootSizeCleanup?: () => void;
|
|
54
|
+
#intersectionCleanup?: () => void;
|
|
55
|
+
#connectionCleanup?: () => void;
|
|
56
|
+
#intersectionObserver?: IntersectionObserver;
|
|
51
57
|
#mediaMatches: boolean = true;
|
|
58
|
+
#rootSizeMatches: boolean = true;
|
|
59
|
+
#connectionMatches: boolean = true;
|
|
52
60
|
#asgMtSource: Record<string, any> | undefined;
|
|
53
61
|
#asgDisMtSource: Record<string, any> | undefined;
|
|
54
62
|
#stageMtSource: Record<string, any> | undefined;
|
|
@@ -56,18 +64,53 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
56
64
|
#assignTentatively: typeof AssignTentativelyType | undefined;
|
|
57
65
|
#elementNotifiers = new WeakMap<Element, EventTarget>();
|
|
58
66
|
#notifierMountedElements = new WeakSet<Element>();
|
|
67
|
+
#subObservers: Map<string, MountObserver> | undefined;
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
#mergeHandlerDefaults(config: MountConfig): MountConfig {
|
|
70
|
+
const doValue = config.do;
|
|
71
|
+
|
|
72
|
+
// Only process if do is a string (single handler reference)
|
|
73
|
+
if (typeof doValue !== 'string') {
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Look up the handler class
|
|
78
|
+
const HandlerClass = MountObserver.#handlerRegistry.get(doValue);
|
|
79
|
+
if (!HandlerClass) {
|
|
80
|
+
// Validation will catch this later
|
|
81
|
+
return config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract static properties from the handler class
|
|
85
|
+
const handlerDefaults: Partial<MountConfig> = {};
|
|
86
|
+
const proto = HandlerClass as any;
|
|
87
|
+
|
|
88
|
+
// Get all static properties
|
|
89
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
90
|
+
if (key !== 'prototype' && key !== 'length' && key !== 'name') {
|
|
91
|
+
handlerDefaults[key as keyof MountConfig] = proto[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Merge: handler defaults first, then inline config (inline trumps)
|
|
96
|
+
// Using object spread - inline config overwrites handler defaults
|
|
97
|
+
return { ...handlerDefaults, ...config };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
constructor(config: MountConfig<TKeys>, options: MountObserverOptions = {}) {
|
|
61
101
|
super();
|
|
62
102
|
|
|
63
|
-
|
|
103
|
+
// Merge handler defaults if do is a string reference
|
|
104
|
+
const mergedConfig = this.#mergeHandlerDefaults(config);
|
|
105
|
+
|
|
106
|
+
this.#init = mergedConfig;
|
|
64
107
|
this.#options = options;
|
|
65
108
|
this.#abortController = new AbortController();
|
|
66
109
|
|
|
67
110
|
const {
|
|
68
|
-
assignOnMount, assignOnDismount, stageOnMount, do: doValue,
|
|
69
|
-
import: imp
|
|
70
|
-
} =
|
|
111
|
+
assignOnMount, assignOnDismount, stageOnMount, do: doValue, loadingEagerness,
|
|
112
|
+
import: imp, configFrom
|
|
113
|
+
} = mergedConfig;
|
|
71
114
|
// Make a copy of assignOnMount config using structuredClone
|
|
72
115
|
if (assignOnMount !== undefined) {
|
|
73
116
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -90,9 +133,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
90
133
|
this.#validateDoHandlers();
|
|
91
134
|
}
|
|
92
135
|
|
|
93
|
-
//
|
|
94
|
-
if (
|
|
95
|
-
this.#
|
|
136
|
+
// Load configFrom modules if specified
|
|
137
|
+
if (configFrom !== undefined) {
|
|
138
|
+
this.#configFromPromise = this.#loadConfigFrom();
|
|
96
139
|
}
|
|
97
140
|
|
|
98
141
|
// Start loading imports if eager
|
|
@@ -115,35 +158,87 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Loads configuration from external modules specified in configFrom property.
|
|
164
|
+
* Merges multiple configs left-to-right, with inline config taking final precedence.
|
|
165
|
+
*/
|
|
166
|
+
async #loadConfigFrom(): Promise<void> {
|
|
167
|
+
const { configFrom } = this.#init;
|
|
168
|
+
if (!configFrom) return;
|
|
169
|
+
|
|
170
|
+
// Normalize to array
|
|
171
|
+
const configPaths = Array.isArray(configFrom) ? configFrom : [configFrom];
|
|
172
|
+
|
|
173
|
+
// Check for duplicates
|
|
174
|
+
const pathSet = new Set<string>();
|
|
175
|
+
for (const path of configPaths) {
|
|
176
|
+
if (pathSet.has(path)) {
|
|
177
|
+
throw new Error(`Duplicate configFrom module: '${path}'`);
|
|
178
|
+
}
|
|
179
|
+
pathSet.add(path);
|
|
122
180
|
}
|
|
123
181
|
|
|
124
|
-
//
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
182
|
+
// Load all modules
|
|
183
|
+
const loadedConfigs: MountConfig[] = [];
|
|
184
|
+
for (const path of configPaths) {
|
|
185
|
+
try {
|
|
186
|
+
const module = await import(path);
|
|
128
187
|
|
|
129
|
-
|
|
130
|
-
|
|
188
|
+
if (!module.mountConfig) {
|
|
189
|
+
throw new Error(`Module '${path}' does not export 'mountConfig'`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof module.mountConfig !== 'object' || module.mountConfig === null) {
|
|
193
|
+
throw new Error(`Module '${path}' exports invalid mountConfig: must be an object`);
|
|
194
|
+
}
|
|
131
195
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
196
|
+
loadedConfigs.push(module.mountConfig);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// Re-throw with better context if it's not already our error
|
|
199
|
+
if (error instanceof Error && !error.message.includes(path)) {
|
|
200
|
+
throw new Error(`Failed to load config from '${path}': ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
137
203
|
}
|
|
204
|
+
}
|
|
138
205
|
|
|
139
|
-
|
|
206
|
+
// Merge configs: loaded configs first (left-to-right), then inline config
|
|
207
|
+
// Save the original inline config
|
|
208
|
+
const inlineConfig = { ...this.#init };
|
|
140
209
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
210
|
+
// Start with empty object, merge all loaded configs, then merge inline
|
|
211
|
+
let mergedConfig: MountConfig = {};
|
|
212
|
+
for (const loadedConfig of loadedConfigs) {
|
|
213
|
+
mergedConfig = Object.assign(mergedConfig, loadedConfig);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Inline config takes final precedence
|
|
217
|
+
mergedConfig = Object.assign(mergedConfig, inlineConfig);
|
|
218
|
+
|
|
219
|
+
// Update the init config with merged result
|
|
220
|
+
this.#init = mergedConfig;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Creates and initializes sub-observers from the `with` property.
|
|
225
|
+
* Each sub-observer observes the same root node as the parent.
|
|
226
|
+
* Sub-observers are stored in #subObservers Map for lifecycle management.
|
|
227
|
+
*/
|
|
228
|
+
async #createSubObservers(rootNode: Node): Promise<void> {
|
|
229
|
+
const withConfig = this.#init.with;
|
|
230
|
+
if (!withConfig) return;
|
|
231
|
+
|
|
232
|
+
this.#subObservers = new Map();
|
|
233
|
+
|
|
234
|
+
for (const [key, subConfig] of Object.entries(withConfig)) {
|
|
235
|
+
const subObserver = new MountObserver(subConfig as MountConfig);
|
|
236
|
+
this.#subObservers.set(key, subObserver);
|
|
237
|
+
await subObserver.observe(rootNode);
|
|
145
238
|
}
|
|
146
239
|
}
|
|
240
|
+
|
|
241
|
+
|
|
147
242
|
|
|
148
243
|
|
|
149
244
|
async #setupMediaQuery(): Promise<void> {
|
|
@@ -165,10 +260,79 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
165
260
|
this.#mediaQueryCleanup = result.cleanup;
|
|
166
261
|
}
|
|
167
262
|
|
|
263
|
+
async #setupRootSizeObserver(): Promise<void> {
|
|
264
|
+
if (!this.#rootNode) {
|
|
265
|
+
throw new Error('Cannot setup root size observer before observe() is called');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const { setupRootSizeObserver } = await import('./rootSizeObserver.js');
|
|
269
|
+
const result = setupRootSizeObserver(
|
|
270
|
+
this.#init,
|
|
271
|
+
this.#rootNode,
|
|
272
|
+
this.#mountedElements,
|
|
273
|
+
this.#modules,
|
|
274
|
+
this,
|
|
275
|
+
(node) => this.#processNode(node)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
this.#rootSizeMatches = result.conditionMatches;
|
|
279
|
+
this.#rootSizeCleanup = result.cleanup;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async #setupElementIntersection(): Promise<void> {
|
|
283
|
+
if (!this.#rootNode) {
|
|
284
|
+
throw new Error('Cannot setup element intersection before observe() is called');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { setupElementIntersection } = await import('./elementIntersection.js');
|
|
288
|
+
const result = setupElementIntersection(
|
|
289
|
+
this.#init,
|
|
290
|
+
this.#rootNode,
|
|
291
|
+
this.#mountedElements,
|
|
292
|
+
this.#modules,
|
|
293
|
+
this,
|
|
294
|
+
(element) => this.#matchesSelector(element),
|
|
295
|
+
(element) => this.#handleMatch(element)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
this.#intersectionObserver = result.intersectionObserver;
|
|
299
|
+
this.#intersectionCleanup = result.cleanup;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async #setupConnectionMonitor(): Promise<void> {
|
|
303
|
+
if (!this.#rootNode) {
|
|
304
|
+
throw new Error('Cannot setup connection monitor before observe() is called');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { setupConnectionMonitor } = await import('./connectionMonitor.js');
|
|
308
|
+
const result = setupConnectionMonitor(
|
|
309
|
+
this.#init,
|
|
310
|
+
this.#rootNode,
|
|
311
|
+
this.#mountedElements,
|
|
312
|
+
this.#modules,
|
|
313
|
+
this,
|
|
314
|
+
(node) => this.#processNode(node)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
this.#connectionMatches = result.conditionMatches;
|
|
318
|
+
this.#connectionCleanup = result.cleanup;
|
|
319
|
+
}
|
|
320
|
+
|
|
168
321
|
get disconnectedSignal(): AbortSignal {
|
|
169
322
|
return this.#abortController.signal;
|
|
170
323
|
}
|
|
171
324
|
|
|
325
|
+
get mountedElements(): Element[] {
|
|
326
|
+
const elements: Element[] = [];
|
|
327
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
328
|
+
const element = ref.deref();
|
|
329
|
+
if (element !== undefined) {
|
|
330
|
+
elements.push(element);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return elements;
|
|
334
|
+
}
|
|
335
|
+
|
|
172
336
|
getNotifier(element: Element): EventTarget {
|
|
173
337
|
// Return cached notifier if it exists
|
|
174
338
|
let notifier = this.#elementNotifiers.get(element);
|
|
@@ -182,10 +346,29 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
182
346
|
return notifier;
|
|
183
347
|
}
|
|
184
348
|
|
|
185
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Begins observing elements within the provided node.
|
|
351
|
+
*
|
|
352
|
+
* @param observedNode - The node to observe for matching elements. This is the root
|
|
353
|
+
* of the observation scope where the mutation observer will be
|
|
354
|
+
* registered. All matching elements within this node (and its
|
|
355
|
+
* descendants) will trigger mount callbacks.
|
|
356
|
+
*
|
|
357
|
+
* Common values:
|
|
358
|
+
* - `document` - Observe the entire document
|
|
359
|
+
* - `element` - Observe a specific subtree
|
|
360
|
+
* - `shadowRoot` - Observe within a shadow DOM
|
|
361
|
+
*/
|
|
362
|
+
async observe(observedNode: Node): Promise<void> {
|
|
186
363
|
if (this.#rootNode) {
|
|
187
364
|
throw new Error('Already observing');
|
|
188
365
|
}
|
|
366
|
+
|
|
367
|
+
// Wait for configFrom loading to complete if it was started
|
|
368
|
+
if (this.#configFromPromise) {
|
|
369
|
+
await this.#configFromPromise;
|
|
370
|
+
}
|
|
371
|
+
|
|
189
372
|
if(this.#asgMtSource || this.#asgDisMtSource){
|
|
190
373
|
await import('assign-gingerly/object-extension.js');
|
|
191
374
|
}
|
|
@@ -194,27 +377,45 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
194
377
|
this.#assignTentatively = assignTentatively;
|
|
195
378
|
}
|
|
196
379
|
|
|
197
|
-
this.#rootNode = new WeakRef(
|
|
380
|
+
this.#rootNode = new WeakRef(observedNode);
|
|
381
|
+
|
|
382
|
+
// Create sub-observers from `with` property
|
|
383
|
+
await this.#createSubObservers(observedNode);
|
|
198
384
|
|
|
199
385
|
// Set up media query if specified (needs rootNode to be set first)
|
|
200
386
|
if (this.#init.withMediaMatching) {
|
|
201
387
|
await this.#setupMediaQuery();
|
|
202
388
|
}
|
|
203
389
|
|
|
390
|
+
// Set up root size observer if specified (needs rootNode to be set first)
|
|
391
|
+
if (this.#init.whereObservedRootSizeMatches) {
|
|
392
|
+
await this.#setupRootSizeObserver();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Set up element intersection observer if specified (needs rootNode to be set first)
|
|
396
|
+
if (this.#init.whereElementIntersectsWith) {
|
|
397
|
+
await this.#setupElementIntersection();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Set up connection monitor if specified (needs rootNode to be set first)
|
|
401
|
+
if (this.#init.whereConnectionHas) {
|
|
402
|
+
await this.#setupConnectionMonitor();
|
|
403
|
+
}
|
|
404
|
+
|
|
204
405
|
// Wait for eager imports to complete if they were started in constructor
|
|
205
406
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
206
407
|
await this.#loadImports();
|
|
207
408
|
}
|
|
208
409
|
|
|
209
|
-
// Process existing elements only if
|
|
210
|
-
if (this.#mediaMatches) {
|
|
211
|
-
this.#processNode(
|
|
410
|
+
// Process existing elements only if all conditions match
|
|
411
|
+
if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
|
|
412
|
+
this.#processNode(observedNode);
|
|
212
413
|
}
|
|
213
414
|
|
|
214
415
|
// Create mutation callback
|
|
215
416
|
this.#mutationCallback = (mutations) => {
|
|
216
|
-
// Skip processing if
|
|
217
|
-
if (!this.#mediaMatches) {
|
|
417
|
+
// Skip processing if any condition doesn't match
|
|
418
|
+
if (!this.#mediaMatches || !this.#rootSizeMatches || !this.#connectionMatches) {
|
|
218
419
|
return;
|
|
219
420
|
}
|
|
220
421
|
|
|
@@ -240,12 +441,21 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
240
441
|
};
|
|
241
442
|
|
|
242
443
|
// Register with shared mutation observer
|
|
243
|
-
registerSharedObserver(
|
|
444
|
+
registerSharedObserver(observedNode, this.#mutationCallback, observerConfig);
|
|
244
445
|
}
|
|
245
446
|
|
|
246
447
|
disconnect(): void {
|
|
247
448
|
const rootNode = this.#rootNode?.deref();
|
|
248
449
|
|
|
450
|
+
// Disconnect all sub-observers first (recursive)
|
|
451
|
+
if (this.#subObservers) {
|
|
452
|
+
for (const subObserver of this.#subObservers.values()) {
|
|
453
|
+
subObserver.disconnect();
|
|
454
|
+
}
|
|
455
|
+
this.#subObservers.clear();
|
|
456
|
+
this.#subObservers = undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
249
459
|
// Unregister from shared mutation observer
|
|
250
460
|
if (rootNode && this.#mutationCallback) {
|
|
251
461
|
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
@@ -258,6 +468,24 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
258
468
|
this.#mediaQueryCleanup = undefined;
|
|
259
469
|
}
|
|
260
470
|
|
|
471
|
+
// Remove root size observer
|
|
472
|
+
if (this.#rootSizeCleanup) {
|
|
473
|
+
this.#rootSizeCleanup();
|
|
474
|
+
this.#rootSizeCleanup = undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Remove intersection observer
|
|
478
|
+
if (this.#intersectionCleanup) {
|
|
479
|
+
this.#intersectionCleanup();
|
|
480
|
+
this.#intersectionCleanup = undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Remove connection monitor
|
|
484
|
+
if (this.#connectionCleanup) {
|
|
485
|
+
this.#connectionCleanup();
|
|
486
|
+
this.#connectionCleanup = undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
261
489
|
this.#abortController.abort();
|
|
262
490
|
this.#rootNode = undefined;
|
|
263
491
|
}
|
|
@@ -272,26 +500,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
272
500
|
this.#modules = await loadImports(this.#init.import);
|
|
273
501
|
this.#importsLoaded = true;
|
|
274
502
|
|
|
275
|
-
// Validate referenced withInstance if reference is specified
|
|
276
|
-
if (this.#init.reference !== undefined) {
|
|
277
|
-
const references = arr(this.#init.reference);
|
|
278
|
-
|
|
279
|
-
for (const index of references) {
|
|
280
|
-
const module = this.#modules[index];
|
|
281
|
-
if (module && module.withInstance !== undefined) {
|
|
282
|
-
// Validate that it's a Constructor or array of Constructors
|
|
283
|
-
const withInstance = module.withInstance;
|
|
284
|
-
const constructors = arr(withInstance);
|
|
285
|
-
|
|
286
|
-
for (const constructor of constructors) {
|
|
287
|
-
if (typeof constructor !== 'function') {
|
|
288
|
-
throw new Error(`Referenced module at index ${index} exports invalid withInstance: must be a Constructor or array of Constructors`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
503
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
296
504
|
}
|
|
297
505
|
|
|
@@ -300,7 +508,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
300
508
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
301
509
|
const element = node as Element;
|
|
302
510
|
|
|
303
|
-
|
|
511
|
+
// If intersection observer is active, start observing the element
|
|
512
|
+
// The intersection callback will handle mounting when it intersects
|
|
513
|
+
if (this.#intersectionObserver) {
|
|
514
|
+
this.#intersectionObserver.observe(element);
|
|
515
|
+
} else if (this.#matchesSelector(element)) {
|
|
304
516
|
this.#handleMatch(element);
|
|
305
517
|
}
|
|
306
518
|
}
|
|
@@ -310,8 +522,12 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
310
522
|
const root = node as DocumentFragment;
|
|
311
523
|
|
|
312
524
|
// Get all elements matching the CSS selector first
|
|
313
|
-
root.querySelectorAll(this.#init.matching)
|
|
314
|
-
|
|
525
|
+
const matches = root.querySelectorAll(this.#init.matching);
|
|
526
|
+
matches.forEach(child => {
|
|
527
|
+
// If intersection observer is active, start observing the element
|
|
528
|
+
if (this.#intersectionObserver) {
|
|
529
|
+
this.#intersectionObserver.observe(child);
|
|
530
|
+
} else if (this.#matchesSelector(child)) {
|
|
315
531
|
this.#handleMatch(child);
|
|
316
532
|
}
|
|
317
533
|
});
|
|
@@ -319,59 +535,70 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
319
535
|
}
|
|
320
536
|
|
|
321
537
|
#matchesSelector(element: Element): boolean {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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)) {
|
|
538
|
+
//TODO: reduce redundncy with this.#init?
|
|
539
|
+
// Check matching condition
|
|
540
|
+
if (!this.#init.matching) {
|
|
337
541
|
return false;
|
|
338
542
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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) {
|
|
543
|
+
|
|
544
|
+
const matchesElement = element.matches(this.#init.matching);
|
|
545
|
+
if (!matchesElement) {
|
|
349
546
|
return false;
|
|
350
547
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
|
|
364
|
-
|
|
365
|
-
if (!matchesInstanceOf) {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
548
|
+
|
|
549
|
+
// Check that element's customElementRegistry matches root node's registry
|
|
550
|
+
const rootNode = this.#rootNode?.deref();
|
|
551
|
+
if (rootNode) {
|
|
552
|
+
const registriesMatch = (rootNode as any).customElementRegistry === (element as any).customElementRegistry;
|
|
553
|
+
|
|
554
|
+
// If whereDifferentCustomElementRegistry is true, exclude matching registries
|
|
555
|
+
if (this.#init.whereDifferentCustomElementRegistry) {
|
|
556
|
+
if (registriesMatch) return false;
|
|
557
|
+
} else {
|
|
558
|
+
// Default behavior: exclude non-matching registries
|
|
559
|
+
if (!registriesMatch) return false;
|
|
368
560
|
}
|
|
369
561
|
}
|
|
562
|
+
|
|
563
|
+
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
564
|
+
if (this.#init.withScopePerimeter) {
|
|
565
|
+
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check whereObservedRootSizeMatches condition if specified
|
|
571
|
+
if (this.#init.whereObservedRootSizeMatches && !this.#rootSizeMatches) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check whereInstanceOf condition if specified
|
|
576
|
+
if (this.#init.whereInstanceOf) {
|
|
577
|
+
const constructors = arr(this.#init.whereInstanceOf);
|
|
578
|
+
|
|
579
|
+
// Element must be an instance of at least one constructor (OR logic for array)
|
|
580
|
+
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
581
|
+
|
|
582
|
+
if (!matchesInstanceOf) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check whereLocalNameMatches condition if specified
|
|
588
|
+
if (this.#init.whereLocalNameMatches) {
|
|
589
|
+
const pattern = typeof this.#init.whereLocalNameMatches === 'string'
|
|
590
|
+
? new RegExp(this.#init.whereLocalNameMatches)
|
|
591
|
+
: this.#init.whereLocalNameMatches;
|
|
592
|
+
|
|
593
|
+
if (!pattern.test(element.localName)) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// All conditions passed
|
|
599
|
+
return true;
|
|
370
600
|
}
|
|
371
601
|
|
|
372
|
-
// All conditions passed
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
602
|
|
|
376
603
|
async #handleMatch(element: Element): Promise<void> {
|
|
377
604
|
if (this.#processedDoForElement.has(element)) {
|
|
@@ -397,12 +624,55 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
397
624
|
return;
|
|
398
625
|
}
|
|
399
626
|
|
|
400
|
-
const context: MountContext = {
|
|
627
|
+
const context: MountContext<TKeys> = {
|
|
401
628
|
modules: this.#modules,
|
|
402
629
|
observer: this,
|
|
403
630
|
rootNode,
|
|
404
|
-
|
|
631
|
+
mountConfig: this.#init,
|
|
405
632
|
};
|
|
633
|
+
|
|
634
|
+
// Add withObservers if sub-observers exist
|
|
635
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
636
|
+
context.withObservers = {} as {[K in TKeys]: IMountObserver};
|
|
637
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
638
|
+
(context.withObservers as any)[key] = subObserver;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Check shouldMount condition if specified (final gate before mounting)
|
|
643
|
+
if (this.#init.shouldMount) {
|
|
644
|
+
try {
|
|
645
|
+
const shouldMount = this.#init.shouldMount(element, context);
|
|
646
|
+
if (!shouldMount) {
|
|
647
|
+
// shouldMount returned false - don't mount this element
|
|
648
|
+
// Remove from processed set so it can be re-evaluated later
|
|
649
|
+
this.#processedDoForElement.delete(element);
|
|
650
|
+
// Remove from mounted elements tracking
|
|
651
|
+
this.#mountedElements.weakSet.delete(element);
|
|
652
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
653
|
+
if (ref.deref() === element) {
|
|
654
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
// shouldMount threw an error - treat as false and log
|
|
662
|
+
console.error('shouldMount check failed:', error);
|
|
663
|
+
// Remove from processed set so it can be re-evaluated later
|
|
664
|
+
this.#processedDoForElement.delete(element);
|
|
665
|
+
// Remove from mounted elements tracking
|
|
666
|
+
this.#mountedElements.weakSet.delete(element);
|
|
667
|
+
for (const ref of this.#mountedElements.setWeak) {
|
|
668
|
+
if (ref.deref() === element) {
|
|
669
|
+
this.#mountedElements.setWeak.delete(ref);
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
406
676
|
|
|
407
677
|
// Apply assignGingerly if specified
|
|
408
678
|
if (this.#asgMtSource) {
|
|
@@ -437,18 +707,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
437
707
|
}
|
|
438
708
|
}
|
|
439
709
|
|
|
440
|
-
// Call referenced do functions from imported modules
|
|
441
|
-
if (this.#init.reference !== undefined) {
|
|
442
|
-
const references = arr(this.#init.reference);
|
|
443
|
-
|
|
444
|
-
for (const index of references) {
|
|
445
|
-
const module = this.#modules[index];
|
|
446
|
-
if (module && typeof module.do === 'function') {
|
|
447
|
-
module.do(element, context);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
710
|
// Dispatch mount event
|
|
453
711
|
const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
|
|
454
712
|
this.dispatchEvent(mountEvent);
|
|
@@ -539,12 +797,20 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
539
797
|
return;
|
|
540
798
|
}
|
|
541
799
|
|
|
542
|
-
const context: MountContext = {
|
|
800
|
+
const context: MountContext<TKeys> = {
|
|
543
801
|
modules: this.#modules,
|
|
544
802
|
observer: this,
|
|
545
803
|
rootNode,
|
|
546
|
-
|
|
804
|
+
mountConfig: this.#init,
|
|
547
805
|
};
|
|
806
|
+
|
|
807
|
+
// Add withObservers if sub-observers exist
|
|
808
|
+
if (this.#subObservers && this.#subObservers.size > 0) {
|
|
809
|
+
context.withObservers = {} as {[K in TKeys]: IMountObserver};
|
|
810
|
+
for (const [key, subObserver] of this.#subObservers.entries()) {
|
|
811
|
+
(context.withObservers as any)[key] = subObserver;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
548
814
|
|
|
549
815
|
|
|
550
816
|
// Dispatch dismount event
|