mount-observer 0.1.4 → 0.1.5
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 +45 -0
- package/ElementMountExtension.ts +61 -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 +27 -0
- package/getRootRegistryContainer.ts +32 -0
- package/index.js +2 -3
- 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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { arr } from './arr.js';
|
|
2
|
-
import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent,
|
|
2
|
+
import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent, } from './Events.js';
|
|
3
3
|
import { registerSharedObserver, unregisterSharedObserver } from './SharedMutationObserver.js';
|
|
4
|
-
import {
|
|
4
|
+
import { withScopePerimeter } from './withScopePerimeter.js';
|
|
5
5
|
export class MountObserver extends EventTarget {
|
|
6
6
|
// Static registry for registered handlers
|
|
7
7
|
static #handlerRegistry = new Map();
|
|
@@ -24,23 +24,32 @@ export class MountObserver extends EventTarget {
|
|
|
24
24
|
#mutationCallback;
|
|
25
25
|
#rootNode;
|
|
26
26
|
#importsLoaded = false;
|
|
27
|
-
#elementAttrStates = new WeakMap();
|
|
28
|
-
#elementOnceAttrs = new WeakMap();
|
|
29
|
-
#matchesWhereAttrFn = null;
|
|
30
|
-
#buildAttrCoordinateMapFn = null;
|
|
31
|
-
#checkAttrChangesFn = null;
|
|
32
27
|
#mediaQueryCleanup;
|
|
33
28
|
#mediaMatches = true;
|
|
34
29
|
#asgMtSource;
|
|
35
30
|
#asgDisMtSource;
|
|
31
|
+
#stageMtSource;
|
|
32
|
+
#stageReversals = new WeakMap();
|
|
33
|
+
#assignTentatively;
|
|
36
34
|
#elementNotifiers = new WeakMap();
|
|
37
35
|
#notifierMountedElements = new WeakSet();
|
|
38
|
-
constructor(
|
|
36
|
+
constructor(config, options = {}) {
|
|
39
37
|
super();
|
|
38
|
+
// Handle array shorthand - convert EnhancementConfig[] to MountConfig
|
|
39
|
+
let init;
|
|
40
|
+
if (Array.isArray(config)) {
|
|
41
|
+
init = {
|
|
42
|
+
matching: '*', // Match all elements, let withAttrs do the filtering
|
|
43
|
+
enhancementConfig: config
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
init = config;
|
|
48
|
+
}
|
|
40
49
|
this.#init = init;
|
|
41
50
|
this.#options = options;
|
|
42
51
|
this.#abortController = new AbortController();
|
|
43
|
-
const { assignOnMount, assignOnDismount, do: doValue, reference,
|
|
52
|
+
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } = init;
|
|
44
53
|
// Make a copy of assignOnMount config using structuredClone
|
|
45
54
|
if (assignOnMount !== undefined) {
|
|
46
55
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -48,6 +57,9 @@ export class MountObserver extends EventTarget {
|
|
|
48
57
|
if (assignOnDismount !== undefined) {
|
|
49
58
|
this.#asgDisMtSource = structuredClone(assignOnDismount);
|
|
50
59
|
}
|
|
60
|
+
if (stageOnMount !== undefined) {
|
|
61
|
+
this.#stageMtSource = structuredClone(stageOnMount);
|
|
62
|
+
}
|
|
51
63
|
if (options.disconnectedSignal) {
|
|
52
64
|
options.disconnectedSignal.addEventListener('abort', () => {
|
|
53
65
|
this.disconnect();
|
|
@@ -61,10 +73,6 @@ export class MountObserver extends EventTarget {
|
|
|
61
73
|
if (reference !== undefined) {
|
|
62
74
|
this.#validateReference();
|
|
63
75
|
}
|
|
64
|
-
// Preload whereAttr utilities if needed
|
|
65
|
-
if (whereAttr) {
|
|
66
|
-
this.#preloadWhereAttrUtilities();
|
|
67
|
-
}
|
|
68
76
|
// Start loading imports if eager
|
|
69
77
|
if (loadingEagerness === 'eager' && imp) {
|
|
70
78
|
this.#loadImports();
|
|
@@ -106,23 +114,6 @@ export class MountObserver extends EventTarget {
|
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
|
-
async #preloadWhereAttrUtilities() {
|
|
110
|
-
if (!this.#matchesWhereAttrFn) {
|
|
111
|
-
const { matchesWhereAttr } = await import('./whereAttr.js');
|
|
112
|
-
this.#matchesWhereAttrFn = matchesWhereAttr;
|
|
113
|
-
}
|
|
114
|
-
if (!this.#buildAttrCoordinateMapFn) {
|
|
115
|
-
const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
|
|
116
|
-
this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
|
|
117
|
-
}
|
|
118
|
-
if (!this.#checkAttrChangesFn) {
|
|
119
|
-
const { checkAttrChanges } = await import('./attrChanges.js');
|
|
120
|
-
// Create a bound function that passes the required parameters
|
|
121
|
-
this.#checkAttrChangesFn = (element) => {
|
|
122
|
-
return checkAttrChanges(element, this.#init, this.#buildAttrCoordinateMapFn, this.#elementAttrStates, this.#elementOnceAttrs);
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
117
|
async #setupMediaQuery() {
|
|
127
118
|
if (!this.#rootNode) {
|
|
128
119
|
throw new Error('Cannot setup media query before observe() is called');
|
|
@@ -153,19 +144,24 @@ export class MountObserver extends EventTarget {
|
|
|
153
144
|
if (this.#asgMtSource || this.#asgDisMtSource) {
|
|
154
145
|
await import('assign-gingerly/object-extension.js');
|
|
155
146
|
}
|
|
147
|
+
if (this.#stageMtSource) {
|
|
148
|
+
const { assignTentatively } = await import('assign-gingerly/assignTentatively.js');
|
|
149
|
+
this.#assignTentatively = assignTentatively;
|
|
150
|
+
}
|
|
156
151
|
this.#rootNode = new WeakRef(rootNode);
|
|
157
152
|
// Set up media query if specified (needs rootNode to be set first)
|
|
158
|
-
if (this.#init.
|
|
153
|
+
if (this.#init.withMediaMatching) {
|
|
159
154
|
await this.#setupMediaQuery();
|
|
160
155
|
}
|
|
161
|
-
// Wait for whereAttr utilities to load if needed
|
|
162
|
-
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
163
|
-
await this.#preloadWhereAttrUtilities();
|
|
164
|
-
}
|
|
165
156
|
// Wait for eager imports to complete if they were started in constructor
|
|
166
157
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
167
158
|
await this.#loadImports();
|
|
168
159
|
}
|
|
160
|
+
// Register enhancement configs if no imports (inline only)
|
|
161
|
+
// If imports exist, registration happens in #loadImports after modules are loaded
|
|
162
|
+
if (!this.#init.import && this.#init.enhancementConfig) {
|
|
163
|
+
await this.#registerEnhancementConfigs();
|
|
164
|
+
}
|
|
169
165
|
// Process existing elements only if media matches
|
|
170
166
|
if (this.#mediaMatches) {
|
|
171
167
|
this.#processNode(rootNode);
|
|
@@ -176,7 +172,6 @@ export class MountObserver extends EventTarget {
|
|
|
176
172
|
if (!this.#mediaMatches) {
|
|
177
173
|
return;
|
|
178
174
|
}
|
|
179
|
-
const attrChanges = [];
|
|
180
175
|
for (const mutation of mutations) {
|
|
181
176
|
if (mutation.type === 'childList') {
|
|
182
177
|
for (const node of mutation.addedNodes) {
|
|
@@ -190,43 +185,12 @@ export class MountObserver extends EventTarget {
|
|
|
190
185
|
}
|
|
191
186
|
});
|
|
192
187
|
}
|
|
193
|
-
else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
|
|
194
|
-
// Handle attribute changes for mounted elements
|
|
195
|
-
const element = mutation.target;
|
|
196
|
-
if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
|
|
197
|
-
const changes = this.#checkAttrChangesFn(element);
|
|
198
|
-
attrChanges.push(...changes);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Batch and dispatch attribute changes
|
|
203
|
-
if (attrChanges.length > 0) {
|
|
204
|
-
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
205
|
-
// Dispatch filtered attrchange events to element-specific notifiers
|
|
206
|
-
const changesByElement = new Map();
|
|
207
|
-
for (const change of attrChanges) {
|
|
208
|
-
if (!changesByElement.has(change.element)) {
|
|
209
|
-
changesByElement.set(change.element, []);
|
|
210
|
-
}
|
|
211
|
-
changesByElement.get(change.element).push(change);
|
|
212
|
-
}
|
|
213
|
-
for (const [element, changes] of changesByElement) {
|
|
214
|
-
const notifier = this.#elementNotifiers.get(element);
|
|
215
|
-
if (notifier) {
|
|
216
|
-
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
188
|
}
|
|
220
189
|
};
|
|
221
190
|
const observerConfig = {
|
|
222
191
|
childList: true,
|
|
223
192
|
subtree: true
|
|
224
193
|
};
|
|
225
|
-
// Add attribute observation if whereAttr is configured
|
|
226
|
-
if (this.#init.whereAttr) {
|
|
227
|
-
observerConfig.attributes = true;
|
|
228
|
-
observerConfig.attributeOldValue = true;
|
|
229
|
-
}
|
|
230
194
|
// Register with shared mutation observer
|
|
231
195
|
registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
|
|
232
196
|
}
|
|
@@ -253,25 +217,111 @@ export class MountObserver extends EventTarget {
|
|
|
253
217
|
const { loadImports } = await import('./loadImports.js');
|
|
254
218
|
this.#modules = await loadImports(this.#init.import);
|
|
255
219
|
this.#importsLoaded = true;
|
|
256
|
-
// Validate referenced
|
|
220
|
+
// Validate referenced withInstance if reference is specified
|
|
257
221
|
if (this.#init.reference !== undefined) {
|
|
258
222
|
const references = arr(this.#init.reference);
|
|
259
223
|
for (const index of references) {
|
|
260
224
|
const module = this.#modules[index];
|
|
261
|
-
if (module && module.
|
|
225
|
+
if (module && module.withInstance !== undefined) {
|
|
262
226
|
// Validate that it's a Constructor or array of Constructors
|
|
263
|
-
const
|
|
264
|
-
const constructors = arr(
|
|
227
|
+
const withInstance = module.withInstance;
|
|
228
|
+
const constructors = arr(withInstance);
|
|
265
229
|
for (const constructor of constructors) {
|
|
266
230
|
if (typeof constructor !== 'function') {
|
|
267
|
-
throw new Error(`Referenced module at index ${index} exports invalid
|
|
231
|
+
throw new Error(`Referenced module at index ${index} exports invalid withInstance: must be a Constructor or array of Constructors`);
|
|
268
232
|
}
|
|
269
233
|
}
|
|
270
234
|
}
|
|
271
235
|
}
|
|
272
236
|
}
|
|
237
|
+
// Register enhancement configs after imports are loaded
|
|
238
|
+
await this.#registerEnhancementConfigs();
|
|
273
239
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
274
240
|
}
|
|
241
|
+
async #registerEnhancementConfigs() {
|
|
242
|
+
const rootNode = this.#rootNode?.deref();
|
|
243
|
+
if (!rootNode || !(rootNode instanceof Element)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const registry = rootNode.customElementRegistry?.enhancementRegistry;
|
|
247
|
+
if (!registry) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const items = registry.getItems();
|
|
251
|
+
// Collect all enhancement configs to register
|
|
252
|
+
const configsToRegister = [];
|
|
253
|
+
// First, add inline enhancementConfig(s)
|
|
254
|
+
if (this.#init.enhancementConfig) {
|
|
255
|
+
const inlineConfigs = arr(this.#init.enhancementConfig);
|
|
256
|
+
configsToRegister.push(...inlineConfigs);
|
|
257
|
+
}
|
|
258
|
+
// Then, add referenced enhancementConfig(s) from imported modules
|
|
259
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
260
|
+
const references = arr(this.#init.reference);
|
|
261
|
+
for (const index of references) {
|
|
262
|
+
const module = this.#modules[index];
|
|
263
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
264
|
+
const referencedConfigs = arr(module.enhancementConfig);
|
|
265
|
+
configsToRegister.push(...referencedConfigs);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Register each config if not already registered (using reference equality)
|
|
270
|
+
for (const config of configsToRegister) {
|
|
271
|
+
if (!items.includes(config)) {
|
|
272
|
+
registry.push(config);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Resolves template variables in a string recursively
|
|
278
|
+
* @param template - Template string with ${var} placeholders
|
|
279
|
+
* @param patterns - The patterns object containing variable values
|
|
280
|
+
* @returns Resolved string
|
|
281
|
+
*/
|
|
282
|
+
#resolveAttrTemplate(template, patterns) {
|
|
283
|
+
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
284
|
+
const value = patterns[varName];
|
|
285
|
+
if (value === undefined) {
|
|
286
|
+
throw new Error(`Undefined template variable: ${varName}`);
|
|
287
|
+
}
|
|
288
|
+
if (typeof value === 'string') {
|
|
289
|
+
// Recursively resolve
|
|
290
|
+
return this.#resolveAttrTemplate(value, patterns);
|
|
291
|
+
}
|
|
292
|
+
return String(value);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Checks if element has attribute with enh- prefix handling
|
|
297
|
+
* @param element - The element to check
|
|
298
|
+
* @param attrName - The attribute name (without enh- prefix)
|
|
299
|
+
* @param allowUnprefixed - Pattern that element tag name must match to allow unprefixed attributes
|
|
300
|
+
* @returns true if element has the attribute
|
|
301
|
+
*/
|
|
302
|
+
#hasAttributeWithEnhPrefix(element, attrName, allowUnprefixed) {
|
|
303
|
+
const isCustomElement = element.tagName.includes('-');
|
|
304
|
+
const isSVGElement = element instanceof SVGElement;
|
|
305
|
+
// For custom elements and SVG - strict enh- requirement
|
|
306
|
+
if (isCustomElement || isSVGElement) {
|
|
307
|
+
if (element.hasAttribute(`enh-${attrName}`)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
// Only check unprefixed if tag name matches allowUnprefixed pattern
|
|
311
|
+
if (allowUnprefixed) {
|
|
312
|
+
const pattern = typeof allowUnprefixed === 'string'
|
|
313
|
+
? new RegExp(allowUnprefixed)
|
|
314
|
+
: allowUnprefixed;
|
|
315
|
+
const tagName = element.tagName.toLowerCase();
|
|
316
|
+
if (pattern.test(tagName)) {
|
|
317
|
+
return element.hasAttribute(attrName);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
// For built-in elements - enh- is alias (check both)
|
|
323
|
+
return element.hasAttribute(`enh-${attrName}`) || element.hasAttribute(attrName);
|
|
324
|
+
}
|
|
275
325
|
#processNode(node) {
|
|
276
326
|
// If it's an element node, check if it matches
|
|
277
327
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -281,10 +331,10 @@ export class MountObserver extends EventTarget {
|
|
|
281
331
|
}
|
|
282
332
|
}
|
|
283
333
|
// Process children
|
|
284
|
-
if ('querySelectorAll' in node) {
|
|
334
|
+
if ('querySelectorAll' in node && this.#init.matching) {
|
|
285
335
|
const root = node;
|
|
286
336
|
// Get all elements matching the CSS selector first
|
|
287
|
-
root.querySelectorAll(this.#init.
|
|
337
|
+
root.querySelectorAll(this.#init.matching).forEach(child => {
|
|
288
338
|
if (this.#matchesSelector(child)) {
|
|
289
339
|
this.#handleMatch(child);
|
|
290
340
|
}
|
|
@@ -293,45 +343,37 @@ export class MountObserver extends EventTarget {
|
|
|
293
343
|
}
|
|
294
344
|
#matchesSelector(element) {
|
|
295
345
|
//TODO: reduce redundncy with this.#init?
|
|
296
|
-
// Check
|
|
297
|
-
|
|
346
|
+
// Check matching condition
|
|
347
|
+
if (!this.#init.matching) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
const matchesElement = element.matches(this.#init.matching);
|
|
298
351
|
if (!matchesElement) {
|
|
299
352
|
return false;
|
|
300
353
|
}
|
|
301
|
-
// Check
|
|
302
|
-
if (this.#init.
|
|
354
|
+
// Check withScopePerimeter condition if specified (donut hole scoping)
|
|
355
|
+
if (this.#init.withScopePerimeter) {
|
|
303
356
|
const rootNode = this.#rootNode?.deref();
|
|
304
|
-
if (!rootNode || !
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Check whereAttr condition if specified
|
|
309
|
-
if (this.#init.whereAttr) {
|
|
310
|
-
// Use cached function (should be loaded by now from constructor)
|
|
311
|
-
if (!this.#matchesWhereAttrFn) {
|
|
312
|
-
console.warn('whereAttr utilities not loaded yet');
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
if (!this.#matchesWhereAttrFn(element, this.#init.whereAttr)) {
|
|
357
|
+
if (!rootNode || !withScopePerimeter(rootNode, element, this.#init.withScopePerimeter)) {
|
|
316
358
|
return false;
|
|
317
359
|
}
|
|
318
360
|
}
|
|
319
|
-
// Check
|
|
320
|
-
if (this.#init.
|
|
321
|
-
const constructors = arr(this.#init.
|
|
361
|
+
// Check withInstance condition if specified
|
|
362
|
+
if (this.#init.withInstance) {
|
|
363
|
+
const constructors = arr(this.#init.withInstance);
|
|
322
364
|
// Element must be an instance of at least one constructor (OR logic for array)
|
|
323
365
|
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
324
366
|
if (!matchesInstanceOf) {
|
|
325
367
|
return false;
|
|
326
368
|
}
|
|
327
369
|
}
|
|
328
|
-
// Check referenced
|
|
370
|
+
// Check referenced withInstance if imports are loaded and reference is specified
|
|
329
371
|
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
330
372
|
const references = arr(this.#init.reference);
|
|
331
373
|
for (const index of references) {
|
|
332
374
|
const module = this.#modules[index];
|
|
333
|
-
if (module && module.
|
|
334
|
-
const constructors = arr(module.
|
|
375
|
+
if (module && module.withInstance !== undefined) {
|
|
376
|
+
const constructors = arr(module.withInstance);
|
|
335
377
|
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
336
378
|
const matchesInstanceOf = constructors.some((constructor) => element instanceof constructor);
|
|
337
379
|
if (!matchesInstanceOf) {
|
|
@@ -340,6 +382,65 @@ export class MountObserver extends EventTarget {
|
|
|
340
382
|
}
|
|
341
383
|
}
|
|
342
384
|
}
|
|
385
|
+
//TODO: move to a separate file?
|
|
386
|
+
// Check withAttrs condition if specified (attribute-based matching)
|
|
387
|
+
// Check ALL enhancementConfigs (inline + referenced)
|
|
388
|
+
const enhancementConfigs = [];
|
|
389
|
+
// Add inline configs
|
|
390
|
+
if (this.#init.enhancementConfig) {
|
|
391
|
+
enhancementConfigs.push(...arr(this.#init.enhancementConfig));
|
|
392
|
+
}
|
|
393
|
+
// Add referenced configs if imports are loaded
|
|
394
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
395
|
+
const references = arr(this.#init.reference);
|
|
396
|
+
for (const index of references) {
|
|
397
|
+
const module = this.#modules[index];
|
|
398
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
399
|
+
enhancementConfigs.push(...arr(module.enhancementConfig));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Check if ANY enhancementConfig has withAttrs - if so, element must match at least ONE
|
|
404
|
+
let hasAnyWithAttrs = false;
|
|
405
|
+
let matchesAnyWithAttrs = false;
|
|
406
|
+
for (const config of enhancementConfigs) {
|
|
407
|
+
if (!config.withAttrs) {
|
|
408
|
+
continue; // Skip configs without withAttrs
|
|
409
|
+
}
|
|
410
|
+
hasAnyWithAttrs = true;
|
|
411
|
+
const withAttrs = config.withAttrs;
|
|
412
|
+
const allowUnprefixed = config.allowUnprefixed;
|
|
413
|
+
// Collect all attribute names to check for this config
|
|
414
|
+
const attrNames = [];
|
|
415
|
+
for (const key in withAttrs) {
|
|
416
|
+
// Skip base and underscore-prefixed config keys
|
|
417
|
+
if (key === 'base' || key.startsWith('_')) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const value = withAttrs[key];
|
|
421
|
+
if (typeof value === 'string') {
|
|
422
|
+
// Resolve template string to get actual attribute name
|
|
423
|
+
const attrName = this.#resolveAttrTemplate(value, withAttrs);
|
|
424
|
+
attrNames.push(attrName);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Handle base attribute specially if present
|
|
428
|
+
if ('base' in withAttrs && typeof withAttrs.base === 'string') {
|
|
429
|
+
attrNames.push(withAttrs.base);
|
|
430
|
+
}
|
|
431
|
+
// Check if element has at least ONE of the specified attributes (OR logic within config)
|
|
432
|
+
if (attrNames.length > 0) {
|
|
433
|
+
const hasAnyAttribute = attrNames.some(attrName => this.#hasAttributeWithEnhPrefix(element, attrName, allowUnprefixed));
|
|
434
|
+
if (hasAnyAttribute) {
|
|
435
|
+
matchesAnyWithAttrs = true;
|
|
436
|
+
break; // Found a matching config, no need to check others
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// If any config has withAttrs but element doesn't match any of them, reject
|
|
441
|
+
if (hasAnyWithAttrs && !matchesAnyWithAttrs) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
343
444
|
// All conditions passed
|
|
344
445
|
return true;
|
|
345
446
|
}
|
|
@@ -366,12 +467,44 @@ export class MountObserver extends EventTarget {
|
|
|
366
467
|
modules: this.#modules,
|
|
367
468
|
observer: this,
|
|
368
469
|
rootNode,
|
|
369
|
-
|
|
470
|
+
MountConfig: this.#init,
|
|
370
471
|
};
|
|
371
472
|
// Apply assignGingerly if specified
|
|
372
473
|
if (this.#asgMtSource) {
|
|
373
474
|
element.assignGingerly(this.#asgMtSource);
|
|
374
475
|
}
|
|
476
|
+
// Apply assignTentatively if specified (staged assignments)
|
|
477
|
+
if (this.#stageMtSource && this.#assignTentatively) {
|
|
478
|
+
const reversal = {};
|
|
479
|
+
this.#assignTentatively(element, this.#stageMtSource, { reversal });
|
|
480
|
+
this.#stageReversals.set(element, reversal);
|
|
481
|
+
}
|
|
482
|
+
// Spawn enhancements if configured
|
|
483
|
+
// Process inline configs first, then referenced configs
|
|
484
|
+
const enhancementConfigs = [];
|
|
485
|
+
// Add inline configs
|
|
486
|
+
if (this.#init.enhancementConfig) {
|
|
487
|
+
enhancementConfigs.push(...arr(this.#init.enhancementConfig));
|
|
488
|
+
}
|
|
489
|
+
// Add referenced configs if imports are loaded
|
|
490
|
+
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
491
|
+
const references = arr(this.#init.reference);
|
|
492
|
+
for (const index of references) {
|
|
493
|
+
const module = this.#modules[index];
|
|
494
|
+
if (module && module.enhancementConfig !== undefined) {
|
|
495
|
+
enhancementConfigs.push(...arr(module.enhancementConfig));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Spawn each enhancement that has a spawn property
|
|
500
|
+
if (enhancementConfigs.length > 0) {
|
|
501
|
+
await import('assign-gingerly/object-extension.js');
|
|
502
|
+
for (const config of enhancementConfigs) {
|
|
503
|
+
if (config.spawn) {
|
|
504
|
+
element.enh.get(config, context);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
375
508
|
// Check if notifier exists BEFORE calling do callback
|
|
376
509
|
const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
|
|
377
510
|
// Call do callback(s) - can be string, function, or array
|
|
@@ -419,18 +552,6 @@ export class MountObserver extends EventTarget {
|
|
|
419
552
|
const { emitMountedElementEvents } = await import('./emitEvents.js');
|
|
420
553
|
await emitMountedElementEvents(element, this.#init, this.#processedEventsForElement);
|
|
421
554
|
}
|
|
422
|
-
// Check for initial attribute changes if whereAttr is configured
|
|
423
|
-
if (this.#checkAttrChangesFn) {
|
|
424
|
-
const changes = this.#checkAttrChangesFn(element);
|
|
425
|
-
if (changes.length > 0) {
|
|
426
|
-
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
427
|
-
// Also dispatch to element-specific notifier
|
|
428
|
-
const notifier = this.#elementNotifiers.get(element);
|
|
429
|
-
if (notifier) {
|
|
430
|
-
notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
555
|
}
|
|
435
556
|
async assignGingerly(config) {
|
|
436
557
|
// Handle undefined case
|
|
@@ -462,6 +583,14 @@ export class MountObserver extends EventTarget {
|
|
|
462
583
|
if (!this.#mountedElements.weakSet.has(element)) {
|
|
463
584
|
return;
|
|
464
585
|
}
|
|
586
|
+
// Reverse tentative assignments first (restore original values)
|
|
587
|
+
if (this.#stageMtSource && this.#assignTentatively) {
|
|
588
|
+
const reversal = this.#stageReversals.get(element);
|
|
589
|
+
if (reversal) {
|
|
590
|
+
this.#assignTentatively(element, reversal);
|
|
591
|
+
this.#stageReversals.delete(element);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
465
594
|
// Apply assignGingerly if specified for dismount
|
|
466
595
|
if (this.#asgDisMtSource) {
|
|
467
596
|
element.assignGingerly(this.#asgDisMtSource);
|
|
@@ -487,10 +616,10 @@ export class MountObserver extends EventTarget {
|
|
|
487
616
|
modules: this.#modules,
|
|
488
617
|
observer: this,
|
|
489
618
|
rootNode,
|
|
490
|
-
|
|
619
|
+
MountConfig: this.#init,
|
|
491
620
|
};
|
|
492
621
|
// Dispatch dismount event
|
|
493
|
-
const dismountEvent = new DismountEvent(element, '
|
|
622
|
+
const dismountEvent = new DismountEvent(element, 'with-matching-failed', this.#init);
|
|
494
623
|
this.dispatchEvent(dismountEvent);
|
|
495
624
|
// Dispatch to element-specific notifier
|
|
496
625
|
const notifier = this.#elementNotifiers.get(element);
|