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.
@@ -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 (mose) {
28
- const se = mose.deref();
29
- const { parentElement } = se;
30
- const { enhKey } = registryItem;
31
- if (!se.id && enhKey) {
32
- se.id = `${parentElement?.localName}.${enhKey}`;
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
- 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
- }
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);
@@ -333,57 +333,58 @@ export class HTMLIncludeHandler extends EvtRt {
333
333
  if (sourceRootNode === templateRootNode) {
334
334
  return;
335
335
  }
336
- // Find all MOSE scripts in the source element
337
- const sourceScripts = sourceElement.querySelectorAll('script[type="mountobserver"]');
338
- if (sourceScripts.length === 0) {
339
- return;
340
- }
341
- // Find all MOSE scripts in the clone
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
- // Find matching clone script by ID
359
- const cloneScript = Array.from(cloneScripts).find(s => s.getAttribute('id') === sourceId);
360
- if (!cloneScript)
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
- // 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`);
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
  }