mount-observer 0.1.4 → 0.1.6
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 +37 -3
- package/DefineCustomElementHandler.ts +43 -4
- package/ElementMountExtension.js +49 -0
- package/ElementMountExtension.ts +65 -0
- package/Events.js +18 -29
- package/Events.ts +7 -16
- package/EvtRt.js +14 -14
- package/EvtRt.ts +15 -15
- package/MountObserver.js +240 -111
- package/MountObserver.ts +283 -131
- package/README.md +667 -447
- package/emitEvents.js +13 -13
- package/emitEvents.ts +14 -14
- package/getRootRegistryContainer.js +49 -0
- package/getRootRegistryContainer.ts +56 -0
- package/index.js +4 -4
- package/index.ts +4 -5
- package/loadImports.js +1 -1
- package/loadImports.ts +1 -1
- package/mediaQuery.js +5 -5
- package/mediaQuery.ts +7 -7
- package/package.json +5 -9
- package/playwright.config.ts +8 -8
- package/types.d.ts +22 -46
- package/{whereOutside.js → withScopePerimeter.js} +1 -1
- package/{whereOutside.ts → withScopePerimeter.ts} +1 -1
- package/attrChanges.js +0 -70
- package/attrChanges.ts +0 -90
- package/attrCoordinates.js +0 -93
- package/attrCoordinates.ts +0 -122
- package/whereAttr.js +0 -174
- package/whereAttr.ts +0 -221
package/MountObserver.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
MountConfig,
|
|
3
3
|
MountObserverOptions,
|
|
4
4
|
IMountObserver,
|
|
5
5
|
MountContext,
|
|
6
|
-
AttrChange,
|
|
7
6
|
WeakDual,
|
|
8
7
|
EventConfig,
|
|
9
8
|
EventConstructor,
|
|
@@ -15,14 +14,15 @@ import {
|
|
|
15
14
|
DismountEvent,
|
|
16
15
|
DisconnectEvent,
|
|
17
16
|
LoadEvent,
|
|
18
|
-
AttrChangeEvent,
|
|
19
17
|
} from './Events.js';
|
|
20
18
|
import {
|
|
21
19
|
registerSharedObserver,
|
|
22
20
|
unregisterSharedObserver,
|
|
23
21
|
type MutationCallback
|
|
24
22
|
} from './SharedMutationObserver.js';
|
|
25
|
-
import {
|
|
23
|
+
import { withScopePerimeter } from './withScopePerimeter.js';
|
|
24
|
+
import type { assignTentatively as AssignTentativelyType } from 'assign-gingerly/assignTentatively.js';
|
|
25
|
+
import type { BaseRegistry, EnhancementConfig } from 'assign-gingerly/types.js';
|
|
26
26
|
|
|
27
27
|
export class MountObserver extends EventTarget implements IMountObserver {
|
|
28
28
|
// Static registry for registered handlers
|
|
@@ -35,7 +35,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
35
35
|
this.#handlerRegistry.set(name, handler);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
#init:
|
|
38
|
+
#init: MountConfig;
|
|
39
39
|
#options: MountObserverOptions;
|
|
40
40
|
#abortController: AbortController;
|
|
41
41
|
#modules: any[] = [];
|
|
@@ -48,26 +48,36 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
48
48
|
#mutationCallback: MutationCallback | undefined;
|
|
49
49
|
#rootNode: WeakRef<Node> | undefined;
|
|
50
50
|
#importsLoaded = false;
|
|
51
|
-
#elementAttrStates = new WeakMap<Element, Map<string, string | null>>();
|
|
52
|
-
#elementOnceAttrs = new WeakMap<Element, Set<string>>();
|
|
53
|
-
#matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
|
|
54
|
-
#buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
|
|
55
|
-
#checkAttrChangesFn: ((element: Element) => AttrChange[]) | null = null;
|
|
56
51
|
#mediaQueryCleanup?: () => void;
|
|
57
52
|
#mediaMatches: boolean = true;
|
|
58
53
|
#asgMtSource: Record<string, any> | undefined;
|
|
59
54
|
#asgDisMtSource: Record<string, any> | undefined;
|
|
55
|
+
#stageMtSource: Record<string, any> | undefined;
|
|
56
|
+
#stageReversals = new WeakMap<Element, Record<string, any>>();
|
|
57
|
+
#assignTentatively: typeof AssignTentativelyType | undefined;
|
|
60
58
|
#elementNotifiers = new WeakMap<Element, EventTarget>();
|
|
61
59
|
#notifierMountedElements = new WeakSet<Element>();
|
|
62
60
|
|
|
63
|
-
constructor(
|
|
61
|
+
constructor(config: MountConfig | EnhancementConfig[], options: MountObserverOptions = {}) {
|
|
64
62
|
super();
|
|
63
|
+
|
|
64
|
+
// Handle array shorthand - convert EnhancementConfig[] to MountConfig
|
|
65
|
+
let init: MountConfig;
|
|
66
|
+
if (Array.isArray(config)) {
|
|
67
|
+
init = {
|
|
68
|
+
matching: '*', // Match all elements, let withAttrs do the filtering
|
|
69
|
+
enhancementConfig: config
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
init = config;
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
this.#init = init;
|
|
66
76
|
this.#options = options;
|
|
67
77
|
this.#abortController = new AbortController();
|
|
68
78
|
|
|
69
79
|
const {
|
|
70
|
-
assignOnMount, assignOnDismount, do: doValue, reference,
|
|
80
|
+
assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness,
|
|
71
81
|
import: imp
|
|
72
82
|
} = init;
|
|
73
83
|
// Make a copy of assignOnMount config using structuredClone
|
|
@@ -77,6 +87,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
77
87
|
if (assignOnDismount !== undefined) {
|
|
78
88
|
this.#asgDisMtSource = structuredClone(assignOnDismount);
|
|
79
89
|
}
|
|
90
|
+
if (stageOnMount !== undefined) {
|
|
91
|
+
this.#stageMtSource = structuredClone(stageOnMount);
|
|
92
|
+
}
|
|
80
93
|
|
|
81
94
|
if (options.disconnectedSignal) {
|
|
82
95
|
options.disconnectedSignal.addEventListener('abort', () => {
|
|
@@ -94,11 +107,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
94
107
|
this.#validateReference();
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
// Preload whereAttr utilities if needed
|
|
98
|
-
if (whereAttr) {
|
|
99
|
-
this.#preloadWhereAttrUtilities();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
110
|
// Start loading imports if eager
|
|
103
111
|
if (loadingEagerness === 'eager' && imp) {
|
|
104
112
|
this.#loadImports();
|
|
@@ -149,29 +157,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
async #preloadWhereAttrUtilities(): Promise<void> {
|
|
153
|
-
if (!this.#matchesWhereAttrFn) {
|
|
154
|
-
const { matchesWhereAttr } = await import('./whereAttr.js');
|
|
155
|
-
this.#matchesWhereAttrFn = matchesWhereAttr;
|
|
156
|
-
}
|
|
157
|
-
if (!this.#buildAttrCoordinateMapFn) {
|
|
158
|
-
const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
|
|
159
|
-
this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
|
|
160
|
-
}
|
|
161
|
-
if (!this.#checkAttrChangesFn) {
|
|
162
|
-
const { checkAttrChanges } = await import('./attrChanges.js');
|
|
163
|
-
// Create a bound function that passes the required parameters
|
|
164
|
-
this.#checkAttrChangesFn = (element: Element) => {
|
|
165
|
-
return checkAttrChanges(
|
|
166
|
-
element,
|
|
167
|
-
this.#init,
|
|
168
|
-
this.#buildAttrCoordinateMapFn!,
|
|
169
|
-
this.#elementAttrStates,
|
|
170
|
-
this.#elementOnceAttrs
|
|
171
|
-
);
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
160
|
|
|
176
161
|
async #setupMediaQuery(): Promise<void> {
|
|
177
162
|
if (!this.#rootNode) {
|
|
@@ -216,24 +201,29 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
216
201
|
if(this.#asgMtSource || this.#asgDisMtSource){
|
|
217
202
|
await import('assign-gingerly/object-extension.js');
|
|
218
203
|
}
|
|
204
|
+
if(this.#stageMtSource){
|
|
205
|
+
const { assignTentatively } = await import('assign-gingerly/assignTentatively.js');
|
|
206
|
+
this.#assignTentatively = assignTentatively;
|
|
207
|
+
}
|
|
219
208
|
|
|
220
209
|
this.#rootNode = new WeakRef(rootNode);
|
|
221
210
|
|
|
222
211
|
// Set up media query if specified (needs rootNode to be set first)
|
|
223
|
-
if (this.#init.
|
|
212
|
+
if (this.#init.withMediaMatching) {
|
|
224
213
|
await this.#setupMediaQuery();
|
|
225
214
|
}
|
|
226
|
-
|
|
227
|
-
// Wait for whereAttr utilities to load if needed
|
|
228
|
-
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
229
|
-
await this.#preloadWhereAttrUtilities();
|
|
230
|
-
}
|
|
231
215
|
|
|
232
216
|
// Wait for eager imports to complete if they were started in constructor
|
|
233
217
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
234
218
|
await this.#loadImports();
|
|
235
219
|
}
|
|
236
220
|
|
|
221
|
+
// Register enhancement configs if no imports (inline only)
|
|
222
|
+
// If imports exist, registration happens in #loadImports after modules are loaded
|
|
223
|
+
if (!this.#init.import && this.#init.enhancementConfig) {
|
|
224
|
+
await this.#registerEnhancementConfigs();
|
|
225
|
+
}
|
|
226
|
+
|
|
237
227
|
// Process existing elements only if media matches
|
|
238
228
|
if (this.#mediaMatches) {
|
|
239
229
|
this.#processNode(rootNode);
|
|
@@ -246,8 +236,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
246
236
|
return;
|
|
247
237
|
}
|
|
248
238
|
|
|
249
|
-
const attrChanges: AttrChange[] = [];
|
|
250
|
-
|
|
251
239
|
for (const mutation of mutations) {
|
|
252
240
|
if (mutation.type === 'childList') {
|
|
253
241
|
for (const node of mutation.addedNodes) {
|
|
@@ -260,34 +248,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
260
248
|
this.#handleRemoval(node as Element);
|
|
261
249
|
}
|
|
262
250
|
});
|
|
263
|
-
} else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
|
|
264
|
-
// Handle attribute changes for mounted elements
|
|
265
|
-
const element = mutation.target as Element;
|
|
266
|
-
if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
|
|
267
|
-
const changes = this.#checkAttrChangesFn(element);
|
|
268
|
-
attrChanges.push(...changes);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Batch and dispatch attribute changes
|
|
274
|
-
if (attrChanges.length > 0) {
|
|
275
|
-
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
276
|
-
|
|
277
|
-
// Dispatch filtered attrchange events to element-specific notifiers
|
|
278
|
-
const changesByElement = new Map<Element, AttrChange[]>();
|
|
279
|
-
for (const change of attrChanges) {
|
|
280
|
-
if (!changesByElement.has(change.element)) {
|
|
281
|
-
changesByElement.set(change.element, []);
|
|
282
|
-
}
|
|
283
|
-
changesByElement.get(change.element)!.push(change);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (const [element, changes] of changesByElement) {
|
|
287
|
-
const notifier = this.#elementNotifiers.get(element);
|
|
288
|
-
if (notifier) {
|
|
289
|
-
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
290
|
-
}
|
|
291
251
|
}
|
|
292
252
|
}
|
|
293
253
|
};
|
|
@@ -296,12 +256,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
296
256
|
childList: true,
|
|
297
257
|
subtree: true
|
|
298
258
|
};
|
|
299
|
-
|
|
300
|
-
// Add attribute observation if whereAttr is configured
|
|
301
|
-
if (this.#init.whereAttr) {
|
|
302
|
-
observerConfig.attributes = true;
|
|
303
|
-
observerConfig.attributeOldValue = true;
|
|
304
|
-
}
|
|
305
259
|
|
|
306
260
|
// Register with shared mutation observer
|
|
307
261
|
registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
|
|
@@ -336,29 +290,133 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
336
290
|
this.#modules = await loadImports(this.#init.import);
|
|
337
291
|
this.#importsLoaded = true;
|
|
338
292
|
|
|
339
|
-
// Validate referenced
|
|
293
|
+
// Validate referenced withInstance if reference is specified
|
|
340
294
|
if (this.#init.reference !== undefined) {
|
|
341
295
|
const references = arr(this.#init.reference);
|
|
342
296
|
|
|
343
297
|
for (const index of references) {
|
|
344
298
|
const module = this.#modules[index];
|
|
345
|
-
if (module && module.
|
|
299
|
+
if (module && module.withInstance !== undefined) {
|
|
346
300
|
// Validate that it's a Constructor or array of Constructors
|
|
347
|
-
const
|
|
348
|
-
const constructors = arr(
|
|
301
|
+
const withInstance = module.withInstance;
|
|
302
|
+
const constructors = arr(withInstance);
|
|
349
303
|
|
|
350
304
|
for (const constructor of constructors) {
|
|
351
305
|
if (typeof constructor !== 'function') {
|
|
352
|
-
throw new Error(`Referenced module at index ${index} exports invalid
|
|
306
|
+
throw new Error(`Referenced module at index ${index} exports invalid withInstance: must be a Constructor or array of Constructors`);
|
|
353
307
|
}
|
|
354
308
|
}
|
|
355
309
|
}
|
|
356
310
|
}
|
|
357
311
|
}
|
|
358
312
|
|
|
313
|
+
// Register enhancement configs after imports are loaded
|
|
314
|
+
await this.#registerEnhancementConfigs();
|
|
315
|
+
|
|
359
316
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
360
317
|
}
|
|
361
318
|
|
|
319
|
+
async #registerEnhancementConfigs(): Promise<void> {
|
|
320
|
+
const rootNode = this.#rootNode?.deref();
|
|
321
|
+
if (!rootNode || !(rootNode instanceof Element)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const registry = (rootNode as any).customElementRegistry?.enhancementRegistry as BaseRegistry | undefined;
|
|
326
|
+
if (!registry) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const items = registry.getItems();
|
|
331
|
+
|
|
332
|
+
// Collect all enhancement configs to register
|
|
333
|
+
const configsToRegister: EnhancementConfig[] = [];
|
|
334
|
+
|
|
335
|
+
// First, add inline enhancementConfig(s)
|
|
336
|
+
if (this.#init.enhancementConfig) {
|
|
337
|
+
const inlineConfigs = arr(this.#init.enhancementConfig);
|
|
338
|
+
configsToRegister.push(...inlineConfigs);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Then, add referenced enhancementConfig(s) from imported modules
|
|
342
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
343
|
+
const references = arr(this.#init.reference);
|
|
344
|
+
|
|
345
|
+
for (const index of references) {
|
|
346
|
+
const module = this.#modules[index];
|
|
347
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
348
|
+
const referencedConfigs = arr(module.enhancementConfig);
|
|
349
|
+
configsToRegister.push(...referencedConfigs);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Register each config if not already registered (using reference equality)
|
|
355
|
+
for (const config of configsToRegister) {
|
|
356
|
+
if (!items.includes(config)) {
|
|
357
|
+
registry.push(config);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Resolves template variables in a string recursively
|
|
364
|
+
* @param template - Template string with ${var} placeholders
|
|
365
|
+
* @param patterns - The patterns object containing variable values
|
|
366
|
+
* @returns Resolved string
|
|
367
|
+
*/
|
|
368
|
+
#resolveAttrTemplate(template: string, patterns: Record<string, any>): string {
|
|
369
|
+
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
370
|
+
const value = patterns[varName];
|
|
371
|
+
if (value === undefined) {
|
|
372
|
+
throw new Error(`Undefined template variable: ${varName}`);
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === 'string') {
|
|
375
|
+
// Recursively resolve
|
|
376
|
+
return this.#resolveAttrTemplate(value, patterns);
|
|
377
|
+
}
|
|
378
|
+
return String(value);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Checks if element has attribute with enh- prefix handling
|
|
384
|
+
* @param element - The element to check
|
|
385
|
+
* @param attrName - The attribute name (without enh- prefix)
|
|
386
|
+
* @param allowUnprefixed - Pattern that element tag name must match to allow unprefixed attributes
|
|
387
|
+
* @returns true if element has the attribute
|
|
388
|
+
*/
|
|
389
|
+
#hasAttributeWithEnhPrefix(
|
|
390
|
+
element: Element,
|
|
391
|
+
attrName: string,
|
|
392
|
+
allowUnprefixed?: string | RegExp
|
|
393
|
+
): boolean {
|
|
394
|
+
const isCustomElement = element.tagName.includes('-');
|
|
395
|
+
const isSVGElement = element instanceof SVGElement;
|
|
396
|
+
|
|
397
|
+
// For custom elements and SVG - strict enh- requirement
|
|
398
|
+
if (isCustomElement || isSVGElement) {
|
|
399
|
+
if (element.hasAttribute(`enh-${attrName}`)) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Only check unprefixed if tag name matches allowUnprefixed pattern
|
|
404
|
+
if (allowUnprefixed) {
|
|
405
|
+
const pattern = typeof allowUnprefixed === 'string'
|
|
406
|
+
? new RegExp(allowUnprefixed)
|
|
407
|
+
: allowUnprefixed;
|
|
408
|
+
const tagName = element.tagName.toLowerCase();
|
|
409
|
+
if (pattern.test(tagName)) {
|
|
410
|
+
return element.hasAttribute(attrName);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// For built-in elements - enh- is alias (check both)
|
|
417
|
+
return element.hasAttribute(`enh-${attrName}`) || element.hasAttribute(attrName);
|
|
418
|
+
}
|
|
419
|
+
|
|
362
420
|
#processNode(node: Node): void {
|
|
363
421
|
// If it's an element node, check if it matches
|
|
364
422
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -370,11 +428,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
370
428
|
}
|
|
371
429
|
|
|
372
430
|
// Process children
|
|
373
|
-
if ('querySelectorAll' in node) {
|
|
431
|
+
if ('querySelectorAll' in node && this.#init.matching) {
|
|
374
432
|
const root = node as DocumentFragment;
|
|
375
433
|
|
|
376
434
|
// Get all elements matching the CSS selector first
|
|
377
|
-
root.querySelectorAll(this.#init.
|
|
435
|
+
root.querySelectorAll(this.#init.matching).forEach(child => {
|
|
378
436
|
if (this.#matchesSelector(child)) {
|
|
379
437
|
this.#handleMatch(child);
|
|
380
438
|
}
|
|
@@ -384,36 +442,27 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
384
442
|
|
|
385
443
|
#matchesSelector(element: Element): boolean {
|
|
386
444
|
//TODO: reduce redundncy with this.#init?
|
|
387
|
-
// Check
|
|
388
|
-
|
|
389
|
-
if (!matchesElement) {
|
|
445
|
+
// Check matching condition
|
|
446
|
+
if (!this.#init.matching) {
|
|
390
447
|
return false;
|
|
391
448
|
}
|
|
392
449
|
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
if (!rootNode || !whereOutside(rootNode, element, this.#init.whereOutside)) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
450
|
+
const matchesElement = element.matches(this.#init.matching);
|
|
451
|
+
if (!matchesElement) {
|
|
452
|
+
return false;
|
|
399
453
|
}
|
|
400
454
|
|
|
401
|
-
// Check
|
|
402
|
-
if (this.#init.
|
|
403
|
-
|
|
404
|
-
if (!this.#
|
|
405
|
-
console.warn('whereAttr utilities not loaded yet');
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (!this.#matchesWhereAttrFn(element, this.#init.whereAttr)) {
|
|
455
|
+
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
456
|
+
if (this.#init.withScopePerimeter) {
|
|
457
|
+
const rootNode = this.#rootNode?.deref();
|
|
458
|
+
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
410
459
|
return false;
|
|
411
460
|
}
|
|
412
461
|
}
|
|
413
462
|
|
|
414
|
-
// Check
|
|
415
|
-
if (this.#init.
|
|
416
|
-
const constructors = arr(this.#init.
|
|
463
|
+
// Check withInstance condition if specified
|
|
464
|
+
if (this.#init.withInstance) {
|
|
465
|
+
const constructors = arr(this.#init.withInstance);
|
|
417
466
|
|
|
418
467
|
// Element must be an instance of at least one constructor (OR logic for array)
|
|
419
468
|
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
@@ -423,14 +472,14 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
423
472
|
}
|
|
424
473
|
}
|
|
425
474
|
|
|
426
|
-
// Check referenced
|
|
475
|
+
// Check referenced withInstance if imports are loaded and reference is specified
|
|
427
476
|
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
428
477
|
const references = arr(this.#init.reference);
|
|
429
478
|
|
|
430
479
|
for (const index of references) {
|
|
431
480
|
const module = this.#modules[index];
|
|
432
|
-
if (module && module.
|
|
433
|
-
const constructors = arr(module.
|
|
481
|
+
if (module && module.withInstance !== undefined) {
|
|
482
|
+
const constructors = arr(module.withInstance);
|
|
434
483
|
|
|
435
484
|
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
436
485
|
const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
|
|
@@ -441,6 +490,79 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
441
490
|
}
|
|
442
491
|
}
|
|
443
492
|
}
|
|
493
|
+
//TODO: move to a separate file?
|
|
494
|
+
// Check withAttrs condition if specified (attribute-based matching)
|
|
495
|
+
// Check ALL enhancementConfigs (inline + referenced)
|
|
496
|
+
const enhancementConfigs: EnhancementConfig[] = [];
|
|
497
|
+
|
|
498
|
+
// Add inline configs
|
|
499
|
+
if (this.#init.enhancementConfig) {
|
|
500
|
+
enhancementConfigs.push(...arr(this.#init.enhancementConfig));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Add referenced configs if imports are loaded
|
|
504
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
505
|
+
const references = arr(this.#init.reference);
|
|
506
|
+
for (const index of references) {
|
|
507
|
+
const module = this.#modules[index];
|
|
508
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
509
|
+
enhancementConfigs.push(...arr(module.enhancementConfig));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check if ANY enhancementConfig has withAttrs - if so, element must match at least ONE
|
|
515
|
+
let hasAnyWithAttrs = false;
|
|
516
|
+
let matchesAnyWithAttrs = false;
|
|
517
|
+
|
|
518
|
+
for (const config of enhancementConfigs) {
|
|
519
|
+
if (!config.withAttrs) {
|
|
520
|
+
continue; // Skip configs without withAttrs
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
hasAnyWithAttrs = true;
|
|
524
|
+
const withAttrs = config.withAttrs;
|
|
525
|
+
const allowUnprefixed = config.allowUnprefixed;
|
|
526
|
+
|
|
527
|
+
// Collect all attribute names to check for this config
|
|
528
|
+
const attrNames: string[] = [];
|
|
529
|
+
|
|
530
|
+
for (const key in withAttrs) {
|
|
531
|
+
// Skip base and underscore-prefixed config keys
|
|
532
|
+
if (key === 'base' || key.startsWith('_')) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const value = withAttrs[key];
|
|
537
|
+
if (typeof value === 'string') {
|
|
538
|
+
// Resolve template string to get actual attribute name
|
|
539
|
+
const attrName = this.#resolveAttrTemplate(value, withAttrs);
|
|
540
|
+
attrNames.push(attrName);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Handle base attribute specially if present
|
|
545
|
+
if ('base' in withAttrs && typeof withAttrs.base === 'string') {
|
|
546
|
+
attrNames.push(withAttrs.base);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check if element has at least ONE of the specified attributes (OR logic within config)
|
|
550
|
+
if (attrNames.length > 0) {
|
|
551
|
+
const hasAnyAttribute = attrNames.some(attrName =>
|
|
552
|
+
this.#hasAttributeWithEnhPrefix(element, attrName, allowUnprefixed)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (hasAnyAttribute) {
|
|
556
|
+
matchesAnyWithAttrs = true;
|
|
557
|
+
break; // Found a matching config, no need to check others
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If any config has withAttrs but element doesn't match any of them, reject
|
|
563
|
+
if (hasAnyWithAttrs && !matchesAnyWithAttrs) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
444
566
|
|
|
445
567
|
// All conditions passed
|
|
446
568
|
return true;
|
|
@@ -474,7 +596,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
474
596
|
modules: this.#modules,
|
|
475
597
|
observer: this,
|
|
476
598
|
rootNode,
|
|
477
|
-
|
|
599
|
+
MountConfig: this.#init,
|
|
478
600
|
};
|
|
479
601
|
|
|
480
602
|
// Apply assignGingerly if specified
|
|
@@ -482,6 +604,43 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
482
604
|
element.assignGingerly(this.#asgMtSource);
|
|
483
605
|
}
|
|
484
606
|
|
|
607
|
+
// Apply assignTentatively if specified (staged assignments)
|
|
608
|
+
if (this.#stageMtSource && this.#assignTentatively) {
|
|
609
|
+
const reversal = {};
|
|
610
|
+
this.#assignTentatively(element, this.#stageMtSource, { reversal });
|
|
611
|
+
this.#stageReversals.set(element, reversal);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Spawn enhancements if configured
|
|
615
|
+
// Process inline configs first, then referenced configs
|
|
616
|
+
const enhancementConfigs: EnhancementConfig[] = [];
|
|
617
|
+
|
|
618
|
+
// Add inline configs
|
|
619
|
+
if (this.#init.enhancementConfig) {
|
|
620
|
+
enhancementConfigs.push(...arr(this.#init.enhancementConfig));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Add referenced configs if imports are loaded
|
|
624
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
625
|
+
const references = arr(this.#init.reference);
|
|
626
|
+
for (const index of references) {
|
|
627
|
+
const module = this.#modules[index];
|
|
628
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
629
|
+
enhancementConfigs.push(...arr(module.enhancementConfig));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Spawn each enhancement that has a spawn property
|
|
635
|
+
if (enhancementConfigs.length > 0) {
|
|
636
|
+
await import('assign-gingerly/object-extension.js');
|
|
637
|
+
for (const config of enhancementConfigs) {
|
|
638
|
+
if (config.spawn) {
|
|
639
|
+
(element as any).enh.get(config, context);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
485
644
|
// Check if notifier exists BEFORE calling do callback
|
|
486
645
|
const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
|
|
487
646
|
|
|
@@ -535,20 +694,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
535
694
|
const { emitMountedElementEvents } = await import('./emitEvents.js');
|
|
536
695
|
await emitMountedElementEvents(element, this.#init, this.#processedEventsForElement);
|
|
537
696
|
}
|
|
538
|
-
|
|
539
|
-
// Check for initial attribute changes if whereAttr is configured
|
|
540
|
-
if (this.#checkAttrChangesFn) {
|
|
541
|
-
const changes = this.#checkAttrChangesFn(element);
|
|
542
|
-
if (changes.length > 0) {
|
|
543
|
-
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
544
|
-
|
|
545
|
-
// Also dispatch to element-specific notifier
|
|
546
|
-
const notifier = this.#elementNotifiers.get(element);
|
|
547
|
-
if (notifier) {
|
|
548
|
-
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
697
|
}
|
|
553
698
|
|
|
554
699
|
async assignGingerly(config: Record<string, any> | undefined): Promise<void> {
|
|
@@ -585,7 +730,14 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
585
730
|
return;
|
|
586
731
|
}
|
|
587
732
|
|
|
588
|
-
|
|
733
|
+
// Reverse tentative assignments first (restore original values)
|
|
734
|
+
if (this.#stageMtSource && this.#assignTentatively) {
|
|
735
|
+
const reversal = this.#stageReversals.get(element);
|
|
736
|
+
if (reversal) {
|
|
737
|
+
this.#assignTentatively(element, reversal);
|
|
738
|
+
this.#stageReversals.delete(element);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
589
741
|
|
|
590
742
|
// Apply assignGingerly if specified for dismount
|
|
591
743
|
if (this.#asgDisMtSource) {
|
|
@@ -616,12 +768,12 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
616
768
|
modules: this.#modules,
|
|
617
769
|
observer: this,
|
|
618
770
|
rootNode,
|
|
619
|
-
|
|
771
|
+
MountConfig: this.#init,
|
|
620
772
|
};
|
|
621
773
|
|
|
622
774
|
|
|
623
775
|
// Dispatch dismount event
|
|
624
|
-
const dismountEvent = new DismountEvent(element, '
|
|
776
|
+
const dismountEvent = new DismountEvent(element, 'with-matching-failed', this.#init);
|
|
625
777
|
this.dispatchEvent(dismountEvent);
|
|
626
778
|
|
|
627
779
|
// Dispatch to element-specific notifier
|