mount-observer 0.1.8 → 0.1.10
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/ElementMountExtension.ts +2 -3
- package/EnhanceMountedElementHandler.js +82 -0
- package/EnhanceMountedElementHandler.ts +99 -0
- package/MountObserver.js +2 -189
- package/MountObserver.ts +4 -229
- package/README.md +50 -285
- package/index.js +3 -0
- package/index.ts +3 -0
- package/package.json +7 -3
package/ElementMountExtension.ts
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
import { MountObserver } from './MountObserver.js';
|
|
7
7
|
import { getRootRegistryContainer } from './getRootRegistryContainer.js';
|
|
8
8
|
import type { MountConfig, MountObserverOptions } from './types/mount-observer/types.js';
|
|
9
|
-
import type { EnhancementConfig } from './types/assign-gingerly/types.js';
|
|
10
9
|
|
|
11
10
|
declare global {
|
|
12
11
|
interface Element {
|
|
13
12
|
mount<T extends Element>(
|
|
14
13
|
this: T,
|
|
15
|
-
config: MountConfig
|
|
14
|
+
config: MountConfig,
|
|
16
15
|
options?: MountObserverOptions
|
|
17
16
|
): Promise<T>;
|
|
18
17
|
}
|
|
@@ -28,7 +27,7 @@ declare global {
|
|
|
28
27
|
Object.defineProperty(Element.prototype, 'mount', {
|
|
29
28
|
value: async function <T extends Element>(
|
|
30
29
|
this: T,
|
|
31
|
-
config: MountConfig
|
|
30
|
+
config: MountConfig,
|
|
32
31
|
options: MountObserverOptions = {}
|
|
33
32
|
): Promise<T> {
|
|
34
33
|
const scope = options.scope ?? 'registry';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EvtRt } from './EvtRt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Handler for automatically enhancing mounted elements using assign-gingerly.
|
|
4
|
+
* Searches the first imported module for an export with a "spawn" property
|
|
5
|
+
* and uses element.enh.get() to spawn the enhancement.
|
|
6
|
+
*/
|
|
7
|
+
export class EnhanceMountedElementHandler extends EvtRt {
|
|
8
|
+
mount(mountedElement, MountConfig, context) {
|
|
9
|
+
// Check if modules are specified
|
|
10
|
+
if (!context.modules || context.modules.length === 0) {
|
|
11
|
+
throw new Error('Must specify an ES Module with import property');
|
|
12
|
+
}
|
|
13
|
+
const module = context.modules[0];
|
|
14
|
+
// Find registry item (object with spawn property)
|
|
15
|
+
const registryItem = this.findRegistryItem(module);
|
|
16
|
+
if (!registryItem) {
|
|
17
|
+
throw new Error('No registry item found in module. Expected an export with a "spawn" property.');
|
|
18
|
+
}
|
|
19
|
+
// Validate spawn is a constructor
|
|
20
|
+
if (typeof registryItem.spawn !== 'function') {
|
|
21
|
+
throw new Error('Registry item "spawn" property must be a constructor function');
|
|
22
|
+
}
|
|
23
|
+
// Spawn the enhancement
|
|
24
|
+
this.spawnEnhancement(mountedElement, registryItem, context);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Spawn the enhancement using element.enh.get().
|
|
28
|
+
* Polyfills customElementRegistry if needed for browsers without scoped registry support.
|
|
29
|
+
*/
|
|
30
|
+
async spawnEnhancement(element, registryItem, context) {
|
|
31
|
+
// Import assign-gingerly object-extension to enable enh property
|
|
32
|
+
await import('assign-gingerly/object-extension.js');
|
|
33
|
+
// Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
|
|
34
|
+
if (!element.customElementRegistry) {
|
|
35
|
+
Object.defineProperty(element, 'customElementRegistry', {
|
|
36
|
+
value: customElements,
|
|
37
|
+
writable: true,
|
|
38
|
+
enumerable: false,
|
|
39
|
+
configurable: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Use element.enh.get() to spawn the enhancement
|
|
43
|
+
const enh = element.enh;
|
|
44
|
+
if (!enh || typeof enh.get !== 'function') {
|
|
45
|
+
throw new Error('Element does not have enh.get() method. Make sure assign-gingerly/object-extension.js is loaded.');
|
|
46
|
+
}
|
|
47
|
+
enh.get(registryItem, context);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Find a registry item in the module exports.
|
|
51
|
+
* A registry item is an object with a "spawn" property.
|
|
52
|
+
* @param module - The imported module
|
|
53
|
+
* @returns The registry item or null if not found
|
|
54
|
+
*/
|
|
55
|
+
findRegistryItem(module) {
|
|
56
|
+
// Check default export first
|
|
57
|
+
if (module.default && this.isRegistryItem(module.default)) {
|
|
58
|
+
return module.default;
|
|
59
|
+
}
|
|
60
|
+
// Search all exports for a registry item
|
|
61
|
+
const registryItems = Object.values(module)
|
|
62
|
+
.filter(exp => this.isRegistryItem(exp));
|
|
63
|
+
if (registryItems.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (registryItems.length > 1) {
|
|
67
|
+
throw new Error('More than one registry item found in module. Expected exactly one export with a "spawn" property.');
|
|
68
|
+
}
|
|
69
|
+
return registryItems[0];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if an export is a registry item (has a spawn property).
|
|
73
|
+
* @param exp - The export to check
|
|
74
|
+
* @returns True if the export is a registry item
|
|
75
|
+
*/
|
|
76
|
+
isRegistryItem(exp) {
|
|
77
|
+
return exp !== null
|
|
78
|
+
&& typeof exp === 'object'
|
|
79
|
+
&& 'spawn' in exp
|
|
80
|
+
&& typeof exp.spawn === 'function';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EvtRt } from './EvtRt.js';
|
|
2
|
+
import { MountConfig, MountContext } from './types/mount-observer/types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for automatically enhancing mounted elements using assign-gingerly.
|
|
6
|
+
* Searches the first imported module for an export with a "spawn" property
|
|
7
|
+
* and uses element.enh.get() to spawn the enhancement.
|
|
8
|
+
*/
|
|
9
|
+
export class EnhanceMountedElementHandler extends EvtRt {
|
|
10
|
+
mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): void {
|
|
11
|
+
// Check if modules are specified
|
|
12
|
+
if (!context.modules || context.modules.length === 0) {
|
|
13
|
+
throw new Error('Must specify an ES Module with import property');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const module = context.modules[0];
|
|
17
|
+
|
|
18
|
+
// Find registry item (object with spawn property)
|
|
19
|
+
const registryItem = this.findRegistryItem(module);
|
|
20
|
+
|
|
21
|
+
if (!registryItem) {
|
|
22
|
+
throw new Error('No registry item found in module. Expected an export with a "spawn" property.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate spawn is a constructor
|
|
26
|
+
if (typeof registryItem.spawn !== 'function') {
|
|
27
|
+
throw new Error('Registry item "spawn" property must be a constructor function');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Spawn the enhancement
|
|
31
|
+
this.spawnEnhancement(mountedElement, registryItem, context);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Spawn the enhancement using element.enh.get().
|
|
36
|
+
* Polyfills customElementRegistry if needed for browsers without scoped registry support.
|
|
37
|
+
*/
|
|
38
|
+
private async spawnEnhancement(element: Element, registryItem: any, context: MountContext): Promise<void> {
|
|
39
|
+
// Import assign-gingerly object-extension to enable enh property
|
|
40
|
+
await import('assign-gingerly/object-extension.js');
|
|
41
|
+
|
|
42
|
+
// Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
|
|
43
|
+
if (!(element as any).customElementRegistry) {
|
|
44
|
+
Object.defineProperty(element, 'customElementRegistry', {
|
|
45
|
+
value: customElements,
|
|
46
|
+
writable: true,
|
|
47
|
+
enumerable: false,
|
|
48
|
+
configurable: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Use element.enh.get() to spawn the enhancement
|
|
53
|
+
const enh = (element as any).enh;
|
|
54
|
+
if (!enh || typeof enh.get !== 'function') {
|
|
55
|
+
throw new Error('Element does not have enh.get() method. Make sure assign-gingerly/object-extension.js is loaded.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enh.get(registryItem, context);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find a registry item in the module exports.
|
|
63
|
+
* A registry item is an object with a "spawn" property.
|
|
64
|
+
* @param module - The imported module
|
|
65
|
+
* @returns The registry item or null if not found
|
|
66
|
+
*/
|
|
67
|
+
private findRegistryItem(module: any): any | null {
|
|
68
|
+
// Check default export first
|
|
69
|
+
if (module.default && this.isRegistryItem(module.default)) {
|
|
70
|
+
return module.default;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Search all exports for a registry item
|
|
74
|
+
const registryItems = Object.values(module)
|
|
75
|
+
.filter(exp => this.isRegistryItem(exp));
|
|
76
|
+
|
|
77
|
+
if (registryItems.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (registryItems.length > 1) {
|
|
82
|
+
throw new Error('More than one registry item found in module. Expected exactly one export with a "spawn" property.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return registryItems[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if an export is a registry item (has a spawn property).
|
|
90
|
+
* @param exp - The export to check
|
|
91
|
+
* @returns True if the export is a registry item
|
|
92
|
+
*/
|
|
93
|
+
private isRegistryItem(exp: any): boolean {
|
|
94
|
+
return exp !== null
|
|
95
|
+
&& typeof exp === 'object'
|
|
96
|
+
&& 'spawn' in exp
|
|
97
|
+
&& typeof exp.spawn === 'function';
|
|
98
|
+
}
|
|
99
|
+
}
|
package/MountObserver.js
CHANGED
|
@@ -35,21 +35,10 @@ export class MountObserver extends EventTarget {
|
|
|
35
35
|
#notifierMountedElements = new WeakSet();
|
|
36
36
|
constructor(config, options = {}) {
|
|
37
37
|
super();
|
|
38
|
-
|
|
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
|
-
}
|
|
49
|
-
this.#init = init;
|
|
38
|
+
this.#init = config;
|
|
50
39
|
this.#options = options;
|
|
51
40
|
this.#abortController = new AbortController();
|
|
52
|
-
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } =
|
|
41
|
+
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } = config;
|
|
53
42
|
// Make a copy of assignOnMount config using structuredClone
|
|
54
43
|
if (assignOnMount !== undefined) {
|
|
55
44
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -157,11 +146,6 @@ export class MountObserver extends EventTarget {
|
|
|
157
146
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
158
147
|
await this.#loadImports();
|
|
159
148
|
}
|
|
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
|
-
}
|
|
165
149
|
// Process existing elements only if media matches
|
|
166
150
|
if (this.#mediaMatches) {
|
|
167
151
|
this.#processNode(rootNode);
|
|
@@ -234,94 +218,8 @@ export class MountObserver extends EventTarget {
|
|
|
234
218
|
}
|
|
235
219
|
}
|
|
236
220
|
}
|
|
237
|
-
// Register enhancement configs after imports are loaded
|
|
238
|
-
await this.#registerEnhancementConfigs();
|
|
239
221
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
240
222
|
}
|
|
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
|
-
}
|
|
325
223
|
#processNode(node) {
|
|
326
224
|
// If it's an element node, check if it matches
|
|
327
225
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -382,65 +280,6 @@ export class MountObserver extends EventTarget {
|
|
|
382
280
|
}
|
|
383
281
|
}
|
|
384
282
|
}
|
|
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
|
-
}
|
|
444
283
|
// All conditions passed
|
|
445
284
|
return true;
|
|
446
285
|
}
|
|
@@ -479,32 +318,6 @@ export class MountObserver extends EventTarget {
|
|
|
479
318
|
this.#assignTentatively(element, this.#stageMtSource, { reversal });
|
|
480
319
|
this.#stageReversals.set(element, reversal);
|
|
481
320
|
}
|
|
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
|
-
}
|
|
508
321
|
// Check if notifier exists BEFORE calling do callback
|
|
509
322
|
const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
|
|
510
323
|
// Call do callback(s) - can be string, function, or array
|
package/MountObserver.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
} from './SharedMutationObserver.js';
|
|
23
23
|
import { withScopePerimeter } from './withScopePerimeter.js';
|
|
24
24
|
import type { assignTentatively as AssignTentativelyType } from 'assign-gingerly/assignTentatively.js';
|
|
25
|
-
import type { BaseRegistry, EnhancementConfig } from './types/assign-gingerly/types.js';
|
|
26
25
|
|
|
27
26
|
export class MountObserver extends EventTarget implements IMountObserver {
|
|
28
27
|
// Static registry for registered handlers
|
|
@@ -58,28 +57,17 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
58
57
|
#elementNotifiers = new WeakMap<Element, EventTarget>();
|
|
59
58
|
#notifierMountedElements = new WeakSet<Element>();
|
|
60
59
|
|
|
61
|
-
constructor(config: MountConfig
|
|
60
|
+
constructor(config: MountConfig, options: MountObserverOptions = {}) {
|
|
62
61
|
super();
|
|
63
62
|
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
this.#init = init;
|
|
63
|
+
this.#init = config;
|
|
76
64
|
this.#options = options;
|
|
77
65
|
this.#abortController = new AbortController();
|
|
78
66
|
|
|
79
67
|
const {
|
|
80
68
|
assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness,
|
|
81
69
|
import: imp
|
|
82
|
-
} =
|
|
70
|
+
} = config;
|
|
83
71
|
// Make a copy of assignOnMount config using structuredClone
|
|
84
72
|
if (assignOnMount !== undefined) {
|
|
85
73
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -218,12 +206,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
218
206
|
await this.#loadImports();
|
|
219
207
|
}
|
|
220
208
|
|
|
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
|
-
|
|
227
209
|
// Process existing elements only if media matches
|
|
228
210
|
if (this.#mediaMatches) {
|
|
229
211
|
this.#processNode(rootNode);
|
|
@@ -310,113 +292,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
310
292
|
}
|
|
311
293
|
}
|
|
312
294
|
|
|
313
|
-
// Register enhancement configs after imports are loaded
|
|
314
|
-
await this.#registerEnhancementConfigs();
|
|
315
|
-
|
|
316
295
|
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
317
296
|
}
|
|
318
297
|
|
|
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
|
-
|
|
420
298
|
#processNode(node: Node): void {
|
|
421
299
|
// If it's an element node, check if it matches
|
|
422
300
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -490,80 +368,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
490
368
|
}
|
|
491
369
|
}
|
|
492
370
|
}
|
|
493
|
-
|
|
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
|
-
}
|
|
566
|
-
|
|
371
|
+
|
|
567
372
|
// All conditions passed
|
|
568
373
|
return true;
|
|
569
374
|
}
|
|
@@ -611,36 +416,6 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
611
416
|
this.#stageReversals.set(element, reversal);
|
|
612
417
|
}
|
|
613
418
|
|
|
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
|
-
|
|
644
419
|
// Check if notifier exists BEFORE calling do callback
|
|
645
420
|
const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
|
|
646
421
|
|
package/README.md
CHANGED
|
@@ -25,9 +25,7 @@ The following features have been implemented and tested:
|
|
|
25
25
|
- ✅ **assignOnMount**: Property assignment when elements mount
|
|
26
26
|
- ✅ **assignOnDismount**: Property assignment when elements dismount
|
|
27
27
|
- ✅ **stageOnMount**: Reversible property assignment (auto-restores on dismount)
|
|
28
|
-
- ✅ **spawn**: Automatic enhancement spawning via assign-gingerly integration
|
|
29
28
|
- ✅ **do callbacks**: Mount/dismount/disconnect/reconnect lifecycle hooks
|
|
30
|
-
- ✅ **Array argument shorthand**: Pass EnhancementConfig[] directly to constructor
|
|
31
29
|
- ✅ **Element mount extension**: element.mount() method for scoped registry observation
|
|
32
30
|
- ✅ **Shared MutationObserver**: Efficient observer sharing across instances
|
|
33
31
|
- ✅ **Code splitting**: Conditional features loaded on-demand
|
|
@@ -165,6 +163,56 @@ element.mount({
|
|
|
165
163
|
});
|
|
166
164
|
```
|
|
167
165
|
|
|
166
|
+
## Enhancing Elements with assign-gingerly
|
|
167
|
+
|
|
168
|
+
The `builtIns.enhanceMountedElement` handler automatically enhances mounted elements using the [assign-gingerly](https://www.npmjs.com/package/assign-gingerly) enhancement system. This allows you to attach behavior and state to elements without subclassing.
|
|
169
|
+
|
|
170
|
+
```JavaScript
|
|
171
|
+
// MyEnhancement.js
|
|
172
|
+
class ButtonEnhancement {
|
|
173
|
+
constructor(element, ctx, initVals) {
|
|
174
|
+
this.element = element;
|
|
175
|
+
this.ctx = ctx;
|
|
176
|
+
this.clickCount = 0;
|
|
177
|
+
|
|
178
|
+
element.addEventListener('click', () => {
|
|
179
|
+
this.clickCount++;
|
|
180
|
+
element.setAttribute('data-clicks', this.clickCount);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default {
|
|
186
|
+
spawn: ButtonEnhancement,
|
|
187
|
+
enhKey: 'buttonEnh'
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// main.js
|
|
191
|
+
import 'mount-observer/ElementMountExtension.js';
|
|
192
|
+
|
|
193
|
+
document.mount({
|
|
194
|
+
matching: '.enhance-me',
|
|
195
|
+
import: './MyEnhancement.js',
|
|
196
|
+
do: 'builtIns.enhanceMountedElement'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// HTML
|
|
200
|
+
<button class="enhance-me">Click me</button>
|
|
201
|
+
|
|
202
|
+
// Access the enhancement
|
|
203
|
+
const button = document.querySelector('.enhance-me');
|
|
204
|
+
console.log(button.enh.buttonEnh.clickCount); // 0
|
|
205
|
+
button.click();
|
|
206
|
+
console.log(button.enh.buttonEnh.clickCount); // 1
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The handler:
|
|
210
|
+
1. Searches the imported module for an export with a `spawn` property (the enhancement class)
|
|
211
|
+
2. Calls `element.enh.get(registryItem, context)` to spawn the enhancement
|
|
212
|
+
3. Stores the enhancement instance on `element.enh[enhKey]` if an `enhKey` is provided
|
|
213
|
+
|
|
214
|
+
This works with browsers that don't support scoped custom element registries by polyfilling the `customElementRegistry` property on elements.
|
|
215
|
+
|
|
168
216
|
|
|
169
217
|
# Thorough Exposition Begins Here
|
|
170
218
|
|
|
@@ -414,51 +462,6 @@ This optimization ensures that with lazy loading, elements that don't match the
|
|
|
414
462
|
|
|
415
463
|
[Implemented as [Requirement12](requirements/Done/Requirement12.md)]
|
|
416
464
|
|
|
417
|
-
## Simplified API: Array Argument Shorthand
|
|
418
|
-
|
|
419
|
-
For simple use cases where you just want to enhance elements based on attributes without needing the full `MountConfig` object, you can pass an array of [EnhancementConfig` objects](https://github.com/bahrus/assign-gingerly) directly to the constructor:
|
|
420
|
-
|
|
421
|
-
```JavaScript
|
|
422
|
-
import { MountObserver } from 'mount-observer';
|
|
423
|
-
|
|
424
|
-
// Instead of wrapping in MountConfig:
|
|
425
|
-
// const observer = new MountObserver({
|
|
426
|
-
// enhancementConfig: [config1, config2]
|
|
427
|
-
// });
|
|
428
|
-
|
|
429
|
-
// You can use the shorthand:
|
|
430
|
-
const observer = new MountObserver([
|
|
431
|
-
{
|
|
432
|
-
spawn: Enhancement1,
|
|
433
|
-
enhKey: 'enh1',
|
|
434
|
-
withAttrs: {
|
|
435
|
-
base: 'data-',
|
|
436
|
-
action: '${base}action'
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
spawn: Enhancement2,
|
|
441
|
-
enhKey: 'enh2',
|
|
442
|
-
withAttrs: {
|
|
443
|
-
base: 'data-',
|
|
444
|
-
theme: '${base}theme'
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
]);
|
|
448
|
-
|
|
449
|
-
await observer.observe(document.body);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
When you pass an array directly:
|
|
453
|
-
- The array is automatically converted to `{ matching: '*', enhancementConfig: [...] }`
|
|
454
|
-
- All elements are considered (matching: '*'), with filtering done by `withAttrs` in each config
|
|
455
|
-
- This is perfect for attribute-based progressive enhancement scenarios
|
|
456
|
-
- You can still use all `EnhancementConfig` features like `spawn`, `withAttrs`, `canSpawn`, etc.
|
|
457
|
-
|
|
458
|
-
This "lite" API makes it easier to do the right thing by reducing boilerplate for common enhancement patterns.
|
|
459
|
-
|
|
460
|
-
[Implemented as ArrayArgument requirement](requirements/Done/ArrayArgument.md).
|
|
461
|
-
|
|
462
465
|
## Element Mount Extension
|
|
463
466
|
|
|
464
467
|
For even more convenience, you can use the `element.mount()` method to observe elements within their scoped custom element registry context. This is particularly useful with scoped custom element registries (Chrome 146+, latest WebKit/Safari).
|
|
@@ -473,18 +476,6 @@ await document.body.mount({
|
|
|
473
476
|
element.classList.add('enhanced');
|
|
474
477
|
}
|
|
475
478
|
});
|
|
476
|
-
|
|
477
|
-
// Or use the array shorthand directly
|
|
478
|
-
await document.body.mount([
|
|
479
|
-
{
|
|
480
|
-
spawn: ButtonEnhancement,
|
|
481
|
-
enhKey: 'btn-enh',
|
|
482
|
-
withAttrs: {
|
|
483
|
-
base: 'data-',
|
|
484
|
-
action: '${base}action'
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
]);
|
|
488
479
|
```
|
|
489
480
|
|
|
490
481
|
The `mount()` method:
|
|
@@ -492,7 +483,6 @@ The `mount()` method:
|
|
|
492
483
|
- Creates a `MountObserver` with the provided config
|
|
493
484
|
- Observes the determined scope
|
|
494
485
|
- Returns the element for chaining (as a Promise)
|
|
495
|
-
- Accepts both `MountConfig` objects and `EnhancementConfig[]` arrays
|
|
496
486
|
|
|
497
487
|
Scope options (via `options.scope`):
|
|
498
488
|
- `'registry'` (default): Observes the root registry container (highest element with same customElementRegistry)
|
|
@@ -1097,231 +1087,6 @@ button.classList.remove('loading'); // Dismount: disabled restored to true (the
|
|
|
1097
1087
|
|
|
1098
1088
|
[Implemented as [Requirement13](requirements/Done/Requirement13.md)]
|
|
1099
1089
|
|
|
1100
|
-
## Spawning enhancements with assign-gingerly integration
|
|
1101
|
-
|
|
1102
|
-
MountObserver integrates with the [assign-gingerly](https://github.com/bahrus/assign-gingerly) enhancement system to automatically spawn enhancement instances when elements mount. This provides a powerful way to attach behaviors and functionality to elements using the enhancement registry pattern.
|
|
1103
|
-
|
|
1104
|
-
### What is spawn?
|
|
1105
|
-
|
|
1106
|
-
In the assign-gingerly enhancement system, `spawn` is a class constructor (not a boolean) that defines what enhancement instance to create. The `enhancementConfig` object is a registry item that gets registered with the element's enhancement registry.
|
|
1107
|
-
|
|
1108
|
-
### Basic spawn usage
|
|
1109
|
-
|
|
1110
|
-
```JavaScript
|
|
1111
|
-
// Define an enhancement class
|
|
1112
|
-
class ButtonEnhancement {
|
|
1113
|
-
constructor(element, ctx, initVals) {
|
|
1114
|
-
this.element = element;
|
|
1115
|
-
this.onClick = this.onClick.bind(this);
|
|
1116
|
-
element.addEventListener('click', this.onClick);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
onClick(e) {
|
|
1120
|
-
console.log('Button clicked!', this.element);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
const observer = new MountObserver({
|
|
1125
|
-
matching: 'button[data-enhance]',
|
|
1126
|
-
enhancementConfig: {
|
|
1127
|
-
spawn: ButtonEnhancement, // The class constructor
|
|
1128
|
-
enhKey: 'buttonEnh'
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
observer.observe(document);
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
When an element mounts, if `enhancementConfig.spawn` is defined, MountObserver will:
|
|
1135
|
-
1. Import the assign-gingerly object extension module
|
|
1136
|
-
2. Call `element.enh.get(enhancementConfig, mountContext)` to spawn the enhancement
|
|
1137
|
-
3. Pass the mount context as the second parameter, making it available to the enhancement constructor
|
|
1138
|
-
|
|
1139
|
-
### How spawn works
|
|
1140
|
-
|
|
1141
|
-
The spawn feature leverages the `element.enh` property from assign-gingerly, which provides access to the enhancement registry. The `enhancementConfig` is a registry item with this structure:
|
|
1142
|
-
|
|
1143
|
-
```TypeScript
|
|
1144
|
-
interface IBaseRegistryItem<T> {
|
|
1145
|
-
spawn: {new(element?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T};
|
|
1146
|
-
symlinks?: {[key: symbol]: keyof T};
|
|
1147
|
-
enhKey?: string;
|
|
1148
|
-
withAttrs?: AttrPatterns<T>;
|
|
1149
|
-
canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
|
|
1150
|
-
}
|
|
1151
|
-
```
|
|
1152
|
-
|
|
1153
|
-
When you call `element.enh.get(enhancementConfig, mountContext)`:
|
|
1154
|
-
- If an enhancement matching the config already exists for this element, it returns the existing instance
|
|
1155
|
-
- If no enhancement exists, it creates a new one by calling `new enhancementConfig.spawn(element, ctx, initVals)`
|
|
1156
|
-
- The enhancement is registered in the element's custom element registry's enhancement registry
|
|
1157
|
-
- The mount context is passed to the enhancement constructor via `ctx.mountCtx`
|
|
1158
|
-
|
|
1159
|
-
### Spawn happens once per element
|
|
1160
|
-
|
|
1161
|
-
The spawn operation only occurs the first time an element mounts. If the element is removed and re-added to the DOM:
|
|
1162
|
-
- The spawn code won't run again (element already in `#processedDoForElement`)
|
|
1163
|
-
- The existing enhancement instance persists with the element
|
|
1164
|
-
- This ensures enhancements are singletons per element instance
|
|
1165
|
-
|
|
1166
|
-
### Mount context in enhancements
|
|
1167
|
-
|
|
1168
|
-
The mount context passed to spawned enhancements includes:
|
|
1169
|
-
|
|
1170
|
-
```TypeScript
|
|
1171
|
-
interface MountContext {
|
|
1172
|
-
modules: any[]; // Imported modules (if import was specified)
|
|
1173
|
-
observer: MountObserver; // The MountObserver instance
|
|
1174
|
-
rootNode: Node; // The observed root node
|
|
1175
|
-
MountConfig: MountConfig; // The full configuration object
|
|
1176
|
-
}
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
This allows enhancements to access imported dependencies, communicate with the observer, and understand their mounting context.
|
|
1180
|
-
|
|
1181
|
-
### Combining spawn with other features
|
|
1182
|
-
|
|
1183
|
-
Spawn works seamlessly with other MountObserver features:
|
|
1184
|
-
|
|
1185
|
-
```JavaScript
|
|
1186
|
-
class WidgetEnhancement {
|
|
1187
|
-
constructor(element, ctx, initVals) {
|
|
1188
|
-
this.element = element;
|
|
1189
|
-
this.modules = ctx.mountCtx?.modules || [];
|
|
1190
|
-
console.log('Widget enhanced with', this.modules);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
theme = 'light';
|
|
1194
|
-
mode = 'default';
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const observer = new MountObserver({
|
|
1198
|
-
matching: 'my-widget',
|
|
1199
|
-
import: './widget-helpers.js',
|
|
1200
|
-
assignOnMount: {
|
|
1201
|
-
dataset: { initialized: 'true' }
|
|
1202
|
-
},
|
|
1203
|
-
stageOnMount: {
|
|
1204
|
-
disabled: true // Temporarily disable during setup
|
|
1205
|
-
},
|
|
1206
|
-
enhancementConfig: {
|
|
1207
|
-
spawn: WidgetEnhancement,
|
|
1208
|
-
enhKey: 'widget',
|
|
1209
|
-
withAttrs: {
|
|
1210
|
-
base: 'data-config',
|
|
1211
|
-
theme: '${base}-theme',
|
|
1212
|
-
mode: '${base}-mode'
|
|
1213
|
-
}
|
|
1214
|
-
},
|
|
1215
|
-
do: (element, ctx) => {
|
|
1216
|
-
console.log('Additional setup after spawn');
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
```
|
|
1220
|
-
|
|
1221
|
-
**Execution order on mount:**
|
|
1222
|
-
1. `assignOnMount` properties applied
|
|
1223
|
-
2. `stageOnMount` properties applied
|
|
1224
|
-
3. **Spawn enhancement** (if configured)
|
|
1225
|
-
4. `do` callbacks executed
|
|
1226
|
-
5. Mount event dispatched
|
|
1227
|
-
|
|
1228
|
-
### Attribute-based enhancement spawning
|
|
1229
|
-
|
|
1230
|
-
When combined with `withAttrs`, spawn only occurs for elements that have the specified attributes:
|
|
1231
|
-
|
|
1232
|
-
```JavaScript
|
|
1233
|
-
class ActionEnhancement {
|
|
1234
|
-
constructor(element, ctx, initVals) {
|
|
1235
|
-
this.element = element;
|
|
1236
|
-
this.onClick = this.onClick.bind(this);
|
|
1237
|
-
element.addEventListener('click', this.onClick);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
onClick(e) {
|
|
1241
|
-
const action = this.element.dataset.action;
|
|
1242
|
-
console.log(`Action: ${action}`);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const observer = new MountObserver({
|
|
1247
|
-
matching: 'button',
|
|
1248
|
-
enhancementConfig: {
|
|
1249
|
-
spawn: ActionEnhancement,
|
|
1250
|
-
enhKey: 'action',
|
|
1251
|
-
withAttrs: {
|
|
1252
|
-
base: 'data-action'
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
```
|
|
1257
|
-
|
|
1258
|
-
Only buttons with a `data-action` attribute (or `enh-data-action` for custom elements) will have the enhancement spawned.
|
|
1259
|
-
|
|
1260
|
-
### Guard conditions with canSpawn
|
|
1261
|
-
|
|
1262
|
-
The `canSpawn` property in `enhancementConfig` provides conditional spawning:
|
|
1263
|
-
|
|
1264
|
-
```JavaScript
|
|
1265
|
-
class InputEnhancement {
|
|
1266
|
-
constructor(element, ctx, initVals) {
|
|
1267
|
-
this.element = element;
|
|
1268
|
-
this.onInput = this.onInput.bind(this);
|
|
1269
|
-
element.addEventListener('input', this.onInput);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
onInput(e) {
|
|
1273
|
-
console.log('Input changed:', e.target.value);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
static canSpawn(element) {
|
|
1277
|
-
// Only spawn for inputs that aren't readonly
|
|
1278
|
-
return !element.readOnly;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
const observer = new MountObserver({
|
|
1283
|
-
matching: 'input',
|
|
1284
|
-
enhancementConfig: {
|
|
1285
|
-
spawn: InputEnhancement,
|
|
1286
|
-
enhKey: 'inputEnh'
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
```
|
|
1290
|
-
|
|
1291
|
-
If `canSpawn` returns `false`, the enhancement won't be spawned for that element.
|
|
1292
|
-
|
|
1293
|
-
### Browser compatibility
|
|
1294
|
-
|
|
1295
|
-
The spawn feature requires:
|
|
1296
|
-
- `Element.prototype.customElementRegistry` (Chrome 146+)
|
|
1297
|
-
- `customElementRegistry.enhancementRegistry` (Chrome 146+)
|
|
1298
|
-
|
|
1299
|
-
For older browsers, you'll need to polyfill these features or the spawn functionality won't work. The test suite includes a polyfill example:
|
|
1300
|
-
|
|
1301
|
-
```JavaScript
|
|
1302
|
-
// Polyfill for browsers without customElementRegistry
|
|
1303
|
-
if (!Element.prototype.hasOwnProperty('customElementRegistry')) {
|
|
1304
|
-
Object.defineProperty(Element.prototype, 'customElementRegistry', {
|
|
1305
|
-
get() {
|
|
1306
|
-
if (!this._customElementRegistry) {
|
|
1307
|
-
this._customElementRegistry = {
|
|
1308
|
-
enhancementRegistry: new BaseRegistry()
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
return this._customElementRegistry;
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
```
|
|
1316
|
-
|
|
1317
|
-
### Performance considerations
|
|
1318
|
-
|
|
1319
|
-
- The assign-gingerly object extension module is only loaded when `spawn` is configured
|
|
1320
|
-
- Enhancements are created once per element (singleton pattern)
|
|
1321
|
-
- The enhancement registry uses weak references to allow garbage collection
|
|
1322
|
-
|
|
1323
|
-
[Implemented as [SpawnOnMount](requirements/Done/SpawnOnMount.md)]
|
|
1324
|
-
|
|
1325
1090
|
## Emitting events from mounted elements
|
|
1326
1091
|
|
|
1327
1092
|
MountObserver can automatically dispatch custom events from elements when they mount. This is useful for:
|
package/index.js
CHANGED
|
@@ -5,11 +5,14 @@ export { emitMountedElementEvents } from './emitEvents.js';
|
|
|
5
5
|
export { arr } from './arr.js';
|
|
6
6
|
export { EvtRt } from './EvtRt.js';
|
|
7
7
|
export { DefineCustomElementHandler } from './DefineCustomElementHandler.js';
|
|
8
|
+
export { EnhanceMountedElementHandler } from './EnhanceMountedElementHandler.js';
|
|
8
9
|
export { mountEventName, dismountEventName, disconnectEventName, loadEventName, mediamatchEventName, mediaunmatchEventName } from './Events.js';
|
|
9
10
|
// Register built-in handlers
|
|
10
11
|
import { MountObserver } from './MountObserver.js';
|
|
11
12
|
import { EvtRt } from './EvtRt.js';
|
|
12
13
|
import { DefineCustomElementHandler, DefineScopedCustomElementHandler } from './DefineCustomElementHandler.js';
|
|
14
|
+
import { EnhanceMountedElementHandler } from './EnhanceMountedElementHandler.js';
|
|
13
15
|
MountObserver.define('builtIns.logToConsole', EvtRt);
|
|
14
16
|
MountObserver.define('builtIns.defineCustomElement', DefineCustomElementHandler);
|
|
15
17
|
MountObserver.define('buildIns.defineScopedCustomElement', DefineScopedCustomElementHandler);
|
|
18
|
+
MountObserver.define('builtIns.enhanceMountedElement', EnhanceMountedElementHandler);
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { emitMountedElementEvents } from './emitEvents.js';
|
|
|
5
5
|
export { arr } from './arr.js';
|
|
6
6
|
export { EvtRt } from './EvtRt.js';
|
|
7
7
|
export { DefineCustomElementHandler } from './DefineCustomElementHandler.js';
|
|
8
|
+
export { EnhanceMountedElementHandler } from './EnhanceMountedElementHandler.js';
|
|
8
9
|
export type {
|
|
9
10
|
MountConfig,
|
|
10
11
|
MountObserverOptions,
|
|
@@ -28,7 +29,9 @@ export {
|
|
|
28
29
|
import { MountObserver } from './MountObserver.js';
|
|
29
30
|
import { EvtRt } from './EvtRt.js';
|
|
30
31
|
import { DefineCustomElementHandler, DefineScopedCustomElementHandler } from './DefineCustomElementHandler.js';
|
|
32
|
+
import { EnhanceMountedElementHandler } from './EnhanceMountedElementHandler.js';
|
|
31
33
|
|
|
32
34
|
MountObserver.define('builtIns.logToConsole', EvtRt);
|
|
33
35
|
MountObserver.define('builtIns.defineCustomElement', DefineCustomElementHandler);
|
|
34
36
|
MountObserver.define('buildIns.defineScopedCustomElement', DefineScopedCustomElementHandler);
|
|
37
|
+
MountObserver.define('builtIns.enhanceMountedElement', EnhanceMountedElementHandler);
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mount-observer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Observe and act on css matches.",
|
|
5
5
|
"main": "MountObserver.js",
|
|
6
6
|
"module": "MountObserver.js",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"assign-gingerly": "0.0.
|
|
8
|
+
"assign-gingerly": "0.0.14"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@playwright/test": "1.58.2",
|
|
12
|
-
"spa-ssi": "0.0.
|
|
12
|
+
"spa-ssi": "0.0.27"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"./DefineCustomElementHandler.js": {
|
|
40
40
|
"default": "./DefineCustomElementHandler.js",
|
|
41
41
|
"types": "./DefineCustomElementHandler.ts"
|
|
42
|
+
},
|
|
43
|
+
"./EnhanceMountedElementHandler.js": {
|
|
44
|
+
"default": "./EnhanceMountedElementHandler.js",
|
|
45
|
+
"types": "./EnhanceMountedElementHandler.ts"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"files": [
|