mount-observer 0.1.18 → 0.1.20
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/README.md +98 -0
- package/handlers/DefineCustomElement.js +51 -39
- package/handlers/DefineCustomElement.ts +57 -45
- package/handlers/EMCScript.js +178 -0
- package/handlers/EMCScript.ts +216 -0
- package/handlers/EnhanceMountedElement.js +9 -9
- package/handlers/EnhanceMountedElement.ts +9 -9
- package/handlers/HTMLInclude.js +48 -47
- package/handlers/HTMLInclude.ts +92 -87
- package/index.js +2 -0
- package/index.ts +4 -2
- package/package.json +1 -1
- package/types/assign-gingerly/types.d.ts +22 -17
- package/types/mount-observer/types.d.ts +31 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { EvtRt } from '../EvtRt.js';
|
|
2
|
+
import { EMC, MountConfig, MountContext } from '../types/mount-observer/types.js';
|
|
3
|
+
import { MountObserver } from '../MountObserver.js';
|
|
4
|
+
import '../ElementMountExtension.js';
|
|
5
|
+
import 'assign-gingerly/object-extension.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handler for EMC (Element Mount Configuration) Script Elements.
|
|
9
|
+
* Processes script[type="emc"] elements to declaratively configure element enhancements.
|
|
10
|
+
*
|
|
11
|
+
* Supports two modes:
|
|
12
|
+
* 1. External JSON: <script type="emc" src="./config.json"></script>
|
|
13
|
+
* 2. Inline JSON: <script type="emc">{ "matching": "div", "enhConfig": {...} }</script>
|
|
14
|
+
*
|
|
15
|
+
* Unlike MountObserverScript, EMC scripts only support single config objects (not arrays).
|
|
16
|
+
*/
|
|
17
|
+
export class EMCScriptHandler extends EvtRt {
|
|
18
|
+
// Static properties define default MountConfig constraints
|
|
19
|
+
static matching = 'script[type="emc"]';
|
|
20
|
+
static whereInstanceOf = HTMLScriptElement;
|
|
21
|
+
|
|
22
|
+
async mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): Promise<void> {
|
|
23
|
+
this.abort(); // Clean up event listeners (one-time operation)
|
|
24
|
+
|
|
25
|
+
const scriptElement = mountedElement as HTMLScriptElement;
|
|
26
|
+
|
|
27
|
+
let emcConfig = (scriptElement as any).export;
|
|
28
|
+
if (!emcConfig) {
|
|
29
|
+
// Check if script has src attribute
|
|
30
|
+
const srcAttr = scriptElement.getAttribute('src');
|
|
31
|
+
|
|
32
|
+
if (srcAttr) {
|
|
33
|
+
// External JSON mode: import from src
|
|
34
|
+
try {
|
|
35
|
+
const module = await import(srcAttr, { with: { type: 'json' } } as any);
|
|
36
|
+
emcConfig = module.default;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Failed to import JSON from '${srcAttr}': ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// Inline JSON mode: parse textContent
|
|
42
|
+
const jsonText = scriptElement.textContent?.trim();
|
|
43
|
+
|
|
44
|
+
if (!jsonText) {
|
|
45
|
+
throw new Error('Script element must have either src attribute or JSON content');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
emcConfig = JSON.parse(jsonText);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Failed to parse JSON content: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate that config is an object (not array)
|
|
56
|
+
if (typeof emcConfig !== 'object' || emcConfig === null || Array.isArray(emcConfig)) {
|
|
57
|
+
throw new Error('EMC config must be an object (not an array)');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Store the parsed config on the script element's export property
|
|
61
|
+
(scriptElement as any).export = emcConfig;
|
|
62
|
+
|
|
63
|
+
// Dispatch resolved event
|
|
64
|
+
const { ResolvedEvent } = await import('../Events.js');
|
|
65
|
+
scriptElement.dispatchEvent(new ResolvedEvent(emcConfig));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate EMC config has required properties
|
|
69
|
+
if (!emcConfig.enhConfig) {
|
|
70
|
+
throw new Error('EMC config must have enhConfig property');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const enhKey = emcConfig.enhConfig.enhKey;
|
|
74
|
+
if (!enhKey) {
|
|
75
|
+
throw new Error('EMC config enhConfig must have enhKey property');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set ID if not specified
|
|
79
|
+
if (!scriptElement.id && scriptElement.parentElement) {
|
|
80
|
+
scriptElement.id = `${scriptElement.parentElement.localName}.${enhKey}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Construct MountConfig from EMC config
|
|
84
|
+
const mountConfig = await this.buildMountConfig(emcConfig);
|
|
85
|
+
|
|
86
|
+
// Create a MountObserver to watch for elements matching the config
|
|
87
|
+
const observer = new MountObserver(mountConfig);
|
|
88
|
+
|
|
89
|
+
// Store observer reference for cleanup
|
|
90
|
+
(scriptElement as any).emcObserver = observer;
|
|
91
|
+
|
|
92
|
+
// Observe from the script element's parent or root node
|
|
93
|
+
const observeTarget = scriptElement.parentElement || scriptElement.getRootNode() as Node;
|
|
94
|
+
await observer.observe(observeTarget);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a MountConfig from an EMC config.
|
|
99
|
+
* Combines the matching selector with withAttrs if present.
|
|
100
|
+
*/
|
|
101
|
+
private async buildMountConfig(emcConfig: EMC): Promise<MountConfig> {
|
|
102
|
+
const { enhConfig, ...mountConfigBase } = emcConfig;
|
|
103
|
+
|
|
104
|
+
let matching = mountConfigBase.matching || '';
|
|
105
|
+
|
|
106
|
+
// If withAttrs is defined, use buildCSSQuery to combine with matching
|
|
107
|
+
if (enhConfig.withAttrs) {
|
|
108
|
+
const { buildCSSQuery } = await import('assign-gingerly/buildCSSQuery.js');
|
|
109
|
+
// Cast to any to avoid type mismatch with spawn property
|
|
110
|
+
const attrQuery = buildCSSQuery(enhConfig as any);
|
|
111
|
+
|
|
112
|
+
// Combine matching with attribute query
|
|
113
|
+
if (matching) {
|
|
114
|
+
matching = `${matching}${attrQuery}`;
|
|
115
|
+
} else {
|
|
116
|
+
matching = attrQuery;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create the mount config with a custom handler
|
|
121
|
+
const mountConfig: MountConfig = {
|
|
122
|
+
...mountConfigBase,
|
|
123
|
+
matching,
|
|
124
|
+
do: (mountedElement: Element) => {
|
|
125
|
+
return this.handleMount(mountedElement, emcConfig);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return mountConfig;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle when an element mounts that matches the EMC config.
|
|
134
|
+
*/
|
|
135
|
+
private async handleMount(mountedElement: Element, emcConfig: EMC): Promise<void> {
|
|
136
|
+
const enhKey = emcConfig.enhConfig.enhKey;
|
|
137
|
+
|
|
138
|
+
// Step 1: Check if element already has this enhancement
|
|
139
|
+
const enh = (mountedElement as any).enh;
|
|
140
|
+
if (enh && enh[enhKey]) {
|
|
141
|
+
// Already enhanced, do nothing
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Step 2: Get enhancement registry from the element's custom element registry
|
|
146
|
+
const customElementRegistry = (mountedElement as any).customElementRegistry || customElements;
|
|
147
|
+
const enhancementRegistry = (customElementRegistry as any).enhancementRegistry;
|
|
148
|
+
|
|
149
|
+
if (!enhancementRegistry) {
|
|
150
|
+
throw new Error('Enhancement registry not found on custom element registry');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if enhancement is already registered using findByEnhKey method
|
|
154
|
+
let enhancementConfig = enhancementRegistry.findByEnhKey(enhKey);
|
|
155
|
+
|
|
156
|
+
// Step 3: If not registered, register it
|
|
157
|
+
if (!enhancementConfig) {
|
|
158
|
+
enhancementConfig = await this.registerEnhancement(emcConfig, enhancementRegistry);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Step 4: Spawn enhancement instance
|
|
162
|
+
if (!enh) {
|
|
163
|
+
throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await enh.get(enhancementConfig);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register an enhancement in the enhancement registry.
|
|
171
|
+
*/
|
|
172
|
+
private async registerEnhancement(emcConfig: EMC, enhancementRegistry: any): Promise<any> {
|
|
173
|
+
const { enhConfig } = emcConfig;
|
|
174
|
+
const { spawn } = enhConfig;
|
|
175
|
+
|
|
176
|
+
if (!spawn) {
|
|
177
|
+
throw new Error('EMC enhConfig must have spawn property');
|
|
178
|
+
}
|
|
179
|
+
// Step 3.1: Import the module
|
|
180
|
+
const module = await import(spawn);
|
|
181
|
+
|
|
182
|
+
// Get the enhancement class - it should be the default export or any exported class
|
|
183
|
+
let ElementClass = module.default;
|
|
184
|
+
|
|
185
|
+
// If no default export, try to find a suitable class
|
|
186
|
+
if (!ElementClass) {
|
|
187
|
+
// Look for any exported constructor function
|
|
188
|
+
for (const key of Object.keys(module)) {
|
|
189
|
+
if (typeof module[key] === 'function') {
|
|
190
|
+
ElementClass = module[key];
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!ElementClass) {
|
|
197
|
+
throw new Error(`No suitable class found in module ${spawn}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Step 3.2: Construct enhancement config
|
|
201
|
+
const enhancementConfig = {
|
|
202
|
+
...enhConfig,
|
|
203
|
+
spawn: ElementClass
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Step 3.3: Register in enhancement registry
|
|
207
|
+
enhancementRegistry.push(enhancementConfig);
|
|
208
|
+
|
|
209
|
+
return enhancementConfig;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Register built-in handler
|
|
214
|
+
export const emc = 'builtIns.emcScript';
|
|
215
|
+
|
|
216
|
+
MountObserver.define(emc, EMCScriptHandler);
|
|
@@ -23,15 +23,15 @@ export class EnhanceMountedElementHandler extends EvtRt {
|
|
|
23
23
|
if (typeof registryItem.spawn !== 'function') {
|
|
24
24
|
throw new Error('Registry item "spawn" property must be a constructor function');
|
|
25
25
|
}
|
|
26
|
-
const mose = context?.observer?.options?.mose;
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
26
|
+
// const mose = context?.observer?.options?.mose;
|
|
27
|
+
// if(mose){
|
|
28
|
+
// const se = mose.deref() as HTMLScriptElement;
|
|
29
|
+
// const {parentElement} = se;
|
|
30
|
+
// const {enhKey} = registryItem;
|
|
31
|
+
// if(!se.id && enhKey){
|
|
32
|
+
// se.id = `${parentElement?.localName}.${ enhKey}`;
|
|
33
|
+
// }
|
|
34
|
+
// }
|
|
35
35
|
// Spawn the enhancement
|
|
36
36
|
this.#spawnEnhancement(mountedElement, registryItem, context);
|
|
37
37
|
}
|
|
@@ -30,15 +30,15 @@ export class EnhanceMountedElementHandler extends EvtRt {
|
|
|
30
30
|
if (typeof registryItem.spawn !== 'function') {
|
|
31
31
|
throw new Error('Registry item "spawn" property must be a constructor function');
|
|
32
32
|
}
|
|
33
|
-
const mose = context?.observer?.options?.mose;
|
|
34
|
-
if(mose){
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
33
|
+
// const mose = context?.observer?.options?.mose;
|
|
34
|
+
// if(mose){
|
|
35
|
+
// const se = mose.deref() as HTMLScriptElement;
|
|
36
|
+
// const {parentElement} = se;
|
|
37
|
+
// const {enhKey} = registryItem;
|
|
38
|
+
// if(!se.id && enhKey){
|
|
39
|
+
// se.id = `${parentElement?.localName}.${ enhKey}`;
|
|
40
|
+
// }
|
|
41
|
+
// }
|
|
42
42
|
|
|
43
43
|
// Spawn the enhancement
|
|
44
44
|
this.#spawnEnhancement(mountedElement, registryItem, context);
|
package/handlers/HTMLInclude.js
CHANGED
|
@@ -333,57 +333,58 @@ export class HTMLIncludeHandler extends EvtRt {
|
|
|
333
333
|
if (sourceRootNode === templateRootNode) {
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
let cloneScripts;
|
|
343
|
-
if (clone instanceof Element) {
|
|
344
|
-
cloneScripts = clone.querySelectorAll('script[type="mountobserver"]');
|
|
345
|
-
}
|
|
346
|
-
else if (clone instanceof DocumentFragment) {
|
|
347
|
-
cloneScripts = clone.querySelectorAll('script[type="mountobserver"]');
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
// Copy exports from source scripts to cloned scripts (matching by ID)
|
|
353
|
-
for (let i = 0; i < sourceScripts.length; i++) {
|
|
354
|
-
const sourceScript = sourceScripts[i];
|
|
355
|
-
const sourceId = sourceScript.getAttribute('id');
|
|
356
|
-
if (!sourceId)
|
|
336
|
+
const types = ['mountobserver', 'emc'];
|
|
337
|
+
for (const t of types) {
|
|
338
|
+
const qry = `script[type="${t}"]`;
|
|
339
|
+
// Find all MOSE scripts in the source element
|
|
340
|
+
const sourceScripts = sourceElement.querySelectorAll(qry);
|
|
341
|
+
if (sourceScripts.length === 0) {
|
|
357
342
|
continue;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
343
|
+
}
|
|
344
|
+
// Find all MOSE scripts in the clone
|
|
345
|
+
let cloneScripts;
|
|
346
|
+
if (clone instanceof Element || clone instanceof DocumentFragment) {
|
|
347
|
+
cloneScripts = clone.querySelectorAll('script[type="mountobserver"]');
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
361
350
|
continue;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const event = await new Promise((resolve, reject) => {
|
|
369
|
-
const timeout = setTimeout(() => {
|
|
370
|
-
reject(new Error('Timeout'));
|
|
371
|
-
}, 5000);
|
|
372
|
-
sourceScript.addEventListener('resolved', (e) => {
|
|
373
|
-
clearTimeout(timeout);
|
|
374
|
-
resolve(e);
|
|
375
|
-
}, { once: true });
|
|
376
|
-
});
|
|
377
|
-
sourceExport = event.export;
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
console.warn(`HTMLInclude: Timeout waiting for MOSE script #${sourceId} to resolve`);
|
|
351
|
+
}
|
|
352
|
+
// Copy exports from source scripts to cloned scripts (matching by ID)
|
|
353
|
+
for (let i = 0; i < sourceScripts.length; i++) {
|
|
354
|
+
const sourceScript = sourceScripts[i];
|
|
355
|
+
const sourceId = sourceScript.getAttribute('id');
|
|
356
|
+
if (!sourceId)
|
|
381
357
|
continue;
|
|
358
|
+
// Find matching clone script by ID
|
|
359
|
+
const cloneScript = Array.from(cloneScripts).find(s => s.getAttribute('id') === sourceId);
|
|
360
|
+
if (!cloneScript)
|
|
361
|
+
continue;
|
|
362
|
+
// Check if source script has export
|
|
363
|
+
let sourceExport = sourceScript.export;
|
|
364
|
+
if (!sourceExport) {
|
|
365
|
+
// Wait for the source script to resolve
|
|
366
|
+
try {
|
|
367
|
+
// Create a promise that waits for the resolved event
|
|
368
|
+
const event = await new Promise((resolve, reject) => {
|
|
369
|
+
const timeout = setTimeout(() => {
|
|
370
|
+
reject(new Error('Timeout'));
|
|
371
|
+
}, 5000);
|
|
372
|
+
sourceScript.addEventListener('resolved', (e) => {
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
resolve(e);
|
|
375
|
+
}, { once: true });
|
|
376
|
+
});
|
|
377
|
+
sourceExport = event.export;
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.warn(`HTMLInclude: Timeout waiting for MOSE script #${sourceId} to resolve`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Copy export to cloned script
|
|
385
|
+
if (sourceExport) {
|
|
386
|
+
cloneScript.export = sourceExport;
|
|
382
387
|
}
|
|
383
|
-
}
|
|
384
|
-
// Copy export to cloned script
|
|
385
|
-
if (sourceExport) {
|
|
386
|
-
cloneScript.export = sourceExport;
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
390
|
}
|