mount-observer 0.1.18 → 0.1.19

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 CHANGED
@@ -568,6 +568,104 @@ The handler automatically hoists templates that:
568
568
 
569
569
  </details>
570
570
 
571
+ ## Element Mount Configuration (EMC) Scripts
572
+
573
+ The `builtIns.emcScript` handler provides declarative element enhancement using `<script type="emc">` elements. EMC scripts combine mount observation with the [assign-gingerly](https://github.com/bahrus/assign-gingerly) enhancement system to apply behaviors, properties, and classes to elements as they mount.
574
+
575
+ **Why use EMC scripts?**
576
+
577
+ - Declaratively enhance elements without writing JavaScript
578
+ - Lazy load enhancement classes only when needed
579
+ - Automatically register and spawn enhancements
580
+ - Works with scoped custom element registries
581
+ - Supports attribute-based element matching
582
+ - Reuses enhancement definitions across multiple elements
583
+
584
+ **Basic usage:**
585
+
586
+ ```html
587
+ <!-- Define enhancement configuration -->
588
+ <script type="emc">
589
+ {
590
+ "matching": ".interactive",
591
+ "enhConfig": {
592
+ "spawn": "./my-enhancement.js",
593
+ "enhKey": "myEnhancement"
594
+ }
595
+ }
596
+ </script>
597
+
598
+ <!-- Elements matching the selector get enhanced -->
599
+ <div class="interactive">This will be enhanced</div>
600
+ <div class="interactive">This too</div>
601
+ ```
602
+
603
+ **External JSON configuration:**
604
+
605
+ ```html
606
+ <!-- Load configuration from external file -->
607
+ <script type="emc" src="./enh-config.json"></script>
608
+ ```
609
+
610
+ **With attribute matching:**
611
+
612
+ ```html
613
+ <script type="emc">
614
+ {
615
+ "matching": "button",
616
+ "enhConfig": {
617
+ "spawn": "./button-enhancement.js",
618
+ "enhKey": "fancyButton",
619
+ "withAttrs": {
620
+ "base": "variant"
621
+ }
622
+ }
623
+ }
624
+ </script>
625
+
626
+ <!-- Only buttons with variant="primary" get enhanced -->
627
+ <button variant="primary">Enhanced</button>
628
+ <button>Not enhanced</button>
629
+ ```
630
+
631
+ **How it works:**
632
+
633
+ 1. EMC script is parsed (inline JSON or external via `src`)
634
+ 2. Configuration is stored on `scriptElement.export`
635
+ 3. `resolved` event is dispatched
636
+ 4. Script ID is auto-generated as `${parentElement.localName}.${enhKey}` if not specified
637
+ 5. MountObserver watches for elements matching the configuration
638
+ 6. When element mounts:
639
+ - Checks if already enhanced (via `element.enh[enhKey]`)
640
+ - Registers enhancement class if not already registered
641
+ - Spawns enhancement instance via `element.enh.get(enhancementConfig)`
642
+
643
+ **Enhancement class example:**
644
+
645
+ ```javascript
646
+ // my-enhancement.js
647
+ export default class MyEnhancement {
648
+ constructor(element, ctx, initVals) {
649
+ this.element = element;
650
+ // Apply enhancement
651
+ this.element.classList.add('enhanced');
652
+ }
653
+
654
+ dispose() {
655
+ // Cleanup
656
+ this.element.classList.remove('enhanced');
657
+ }
658
+ }
659
+ ```
660
+
661
+ **Requirements:**
662
+
663
+ - Must import `ElementMountExtension.js` to enable `element.enh` property
664
+ - Must import `assign-gingerly/object-extension.js` for enhancement registry
665
+ - Enhancement classes should be constructors that accept `(element, ctx, initVals)`
666
+
667
+ [Implemented as EMCScript requirement](requirements/Done/EMCScript.md)
668
+
571
669
  ## Intra-Document HTML Includes with HTMLInclude
572
670
 
573
671
  The `builtIns.HTMLInclude` handler enables declarative HTML fragment reuse within a document using `<template src="#id">` syntax. Think of it as "constants for HTML" - define content once with an ID, then reference it multiple times throughout your document.
@@ -1,4 +1,53 @@
1
1
  import { EvtRt } from '../EvtRt.js';
2
+ /**
3
+ * Find a suitable HTMLElement class from a module.
4
+ * Checks the default export first, then searches all exports.
5
+ * @param module - The imported module
6
+ * @returns The HTMLElement class constructor
7
+ * @throws Error if no suitable class is found or multiple classes are found
8
+ */
9
+ function findSuitableClass(module) {
10
+ // Check default export first
11
+ const defaultExport = module.default;
12
+ if (defaultExport && extendsHTMLElement(defaultExport)) {
13
+ return defaultExport;
14
+ }
15
+ // Find all exports that extend HTMLElement
16
+ const htmlElementClasses = Object.values(module)
17
+ .filter(exp => typeof exp === 'function' && extendsHTMLElement(exp));
18
+ if (htmlElementClasses.length === 0) {
19
+ throw new Error('No suitable class found in module');
20
+ }
21
+ if (htmlElementClasses.length > 1) {
22
+ throw new Error('More than one class found in module');
23
+ }
24
+ return htmlElementClasses[0];
25
+ }
26
+ /**
27
+ * Check if a class extends HTMLElement.
28
+ * @param cls - The class to check
29
+ * @returns true if the class extends HTMLElement
30
+ */
31
+ function extendsHTMLElement(cls) {
32
+ try {
33
+ // Must be a function
34
+ if (typeof cls !== 'function') {
35
+ return false;
36
+ }
37
+ // Handle direct HTMLElement export
38
+ if (cls === HTMLElement) {
39
+ return true;
40
+ }
41
+ // Check if it has a prototype and extends HTMLElement
42
+ if (cls.prototype && cls.prototype instanceof HTMLElement) {
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
2
51
  export class DefineCustomElementHandler extends EvtRt {
3
52
  mount(mountedElement, MountConfig, context) {
4
53
  this.abort();
@@ -12,8 +61,8 @@ export class DefineCustomElementHandler extends EvtRt {
12
61
  if (customElements.get(tagName)) {
13
62
  return;
14
63
  }
15
- // Find suitable class
16
- const ElementClass = this.findSuitableClass(module);
64
+ // Find suitable class using shared utility
65
+ const ElementClass = findSuitableClass(module);
17
66
  // Validate that ElementClass is a constructor
18
67
  if (typeof ElementClass !== 'function') {
19
68
  throw new Error(`Found class is not a constructor: ${typeof ElementClass}`);
@@ -35,43 +84,6 @@ export class DefineCustomElementHandler extends EvtRt {
35
84
  define(tagName, ElementClass, mountedElement) {
36
85
  customElements.define(tagName, ElementClass);
37
86
  }
38
- findSuitableClass(module) {
39
- // Check default export first
40
- const defaultExport = module.default;
41
- if (defaultExport && this.extendsHTMLElement(defaultExport)) {
42
- return defaultExport;
43
- }
44
- // Find all exports that extend HTMLElement
45
- const htmlElementClasses = Object.values(module)
46
- .filter(exp => typeof exp === 'function' && this.extendsHTMLElement(exp));
47
- if (htmlElementClasses.length === 0) {
48
- throw new Error('No suitable class found in module');
49
- }
50
- if (htmlElementClasses.length > 1) {
51
- throw new Error('More than one class found in module');
52
- }
53
- return htmlElementClasses[0];
54
- }
55
- extendsHTMLElement(cls) {
56
- try {
57
- // Must be a function
58
- if (typeof cls !== 'function') {
59
- return false;
60
- }
61
- // Handle direct HTMLElement export
62
- if (cls === HTMLElement) {
63
- return true;
64
- }
65
- // Check if it has a prototype and extends HTMLElement
66
- if (cls.prototype && cls.prototype instanceof HTMLElement) {
67
- return true;
68
- }
69
- return false;
70
- }
71
- catch {
72
- return false;
73
- }
74
- }
75
87
  }
76
88
  /**
77
89
  * Handler for defining custom elements in scoped registries.
@@ -1,6 +1,61 @@
1
1
  import { EvtRt } from '../EvtRt.js';
2
2
  import { MountConfig, MountContext } from '../types/mount-observer/types.js';
3
3
 
4
+ /**
5
+ * Find a suitable HTMLElement class from a module.
6
+ * Checks the default export first, then searches all exports.
7
+ * @param module - The imported module
8
+ * @returns The HTMLElement class constructor
9
+ * @throws Error if no suitable class is found or multiple classes are found
10
+ */
11
+ function findSuitableClass(module: any): typeof HTMLElement {
12
+ // Check default export first
13
+ const defaultExport = module.default;
14
+
15
+ if (defaultExport && extendsHTMLElement(defaultExport)) {
16
+ return defaultExport;
17
+ }
18
+
19
+ // Find all exports that extend HTMLElement
20
+ const htmlElementClasses = Object.values(module)
21
+ .filter(exp => typeof exp === 'function' && extendsHTMLElement(exp));
22
+
23
+ if (htmlElementClasses.length === 0) {
24
+ throw new Error('No suitable class found in module');
25
+ }
26
+
27
+ if (htmlElementClasses.length > 1) {
28
+ throw new Error('More than one class found in module');
29
+ }
30
+
31
+ return htmlElementClasses[0] as typeof HTMLElement;
32
+ }
33
+
34
+ /**
35
+ * Check if a class extends HTMLElement.
36
+ * @param cls - The class to check
37
+ * @returns true if the class extends HTMLElement
38
+ */
39
+ function extendsHTMLElement(cls: any): boolean {
40
+ try {
41
+ // Must be a function
42
+ if (typeof cls !== 'function') {
43
+ return false;
44
+ }
45
+ // Handle direct HTMLElement export
46
+ if (cls === HTMLElement) {
47
+ return true;
48
+ }
49
+ // Check if it has a prototype and extends HTMLElement
50
+ if (cls.prototype && cls.prototype instanceof HTMLElement) {
51
+ return true;
52
+ }
53
+ return false;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
4
59
  export class DefineCustomElementHandler extends EvtRt {
5
60
  mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): void {
6
61
  this.abort();
@@ -17,8 +72,8 @@ export class DefineCustomElementHandler extends EvtRt {
17
72
  return;
18
73
  }
19
74
 
20
- // Find suitable class
21
- const ElementClass = this.findSuitableClass(module);
75
+ // Find suitable class using shared utility
76
+ const ElementClass = findSuitableClass(module);
22
77
 
23
78
  // Validate that ElementClass is a constructor
24
79
  if (typeof ElementClass !== 'function') {
@@ -43,49 +98,6 @@ export class DefineCustomElementHandler extends EvtRt {
43
98
  protected define(tagName: string, ElementClass: CustomElementConstructor, mountedElement: Element): void {
44
99
  customElements.define(tagName, ElementClass);
45
100
  }
46
-
47
- private findSuitableClass(module: any): typeof HTMLElement {
48
- // Check default export first
49
- const defaultExport = module.default;
50
-
51
- if (defaultExport && this.extendsHTMLElement(defaultExport)) {
52
- return defaultExport;
53
- }
54
-
55
- // Find all exports that extend HTMLElement
56
- const htmlElementClasses = Object.values(module)
57
- .filter(exp => typeof exp === 'function' && this.extendsHTMLElement(exp));
58
-
59
- if (htmlElementClasses.length === 0) {
60
- throw new Error('No suitable class found in module');
61
- }
62
-
63
- if (htmlElementClasses.length > 1) {
64
- throw new Error('More than one class found in module');
65
- }
66
-
67
- return htmlElementClasses[0] as typeof HTMLElement;
68
- }
69
-
70
- private extendsHTMLElement(cls: any): boolean {
71
- try {
72
- // Must be a function
73
- if (typeof cls !== 'function') {
74
- return false;
75
- }
76
- // Handle direct HTMLElement export
77
- if (cls === HTMLElement) {
78
- return true;
79
- }
80
- // Check if it has a prototype and extends HTMLElement
81
- if (cls.prototype && cls.prototype instanceof HTMLElement) {
82
- return true;
83
- }
84
- return false;
85
- } catch {
86
- return false;
87
- }
88
- }
89
101
  }
90
102
 
91
103
  /**
@@ -0,0 +1,178 @@
1
+ import { EvtRt } from '../EvtRt.js';
2
+ import { MountObserver } from '../MountObserver.js';
3
+ import '../ElementMountExtension.js';
4
+ import 'assign-gingerly/object-extension.js';
5
+ /**
6
+ * Handler for EMC (Element Mount Configuration) Script Elements.
7
+ * Processes script[type="emc"] elements to declaratively configure element enhancements.
8
+ *
9
+ * Supports two modes:
10
+ * 1. External JSON: <script type="emc" src="./config.json"></script>
11
+ * 2. Inline JSON: <script type="emc">{ "matching": "div", "enhConfig": {...} }</script>
12
+ *
13
+ * Unlike MountObserverScript, EMC scripts only support single config objects (not arrays).
14
+ */
15
+ export class EMCScriptHandler extends EvtRt {
16
+ // Static properties define default MountConfig constraints
17
+ static matching = 'script[type="emc"]';
18
+ static whereInstanceOf = HTMLScriptElement;
19
+ async mount(mountedElement, MountConfig, context) {
20
+ this.abort(); // Clean up event listeners (one-time operation)
21
+ const scriptElement = mountedElement;
22
+ let emcConfig = scriptElement.export;
23
+ if (!emcConfig) {
24
+ // Check if script has src attribute
25
+ const srcAttr = scriptElement.getAttribute('src');
26
+ if (srcAttr) {
27
+ // External JSON mode: import from src
28
+ try {
29
+ const module = await import(srcAttr, { with: { type: 'json' } });
30
+ emcConfig = module.default;
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to import JSON from '${srcAttr}': ${error instanceof Error ? error.message : String(error)}`);
34
+ }
35
+ }
36
+ else {
37
+ // Inline JSON mode: parse textContent
38
+ const jsonText = scriptElement.textContent?.trim();
39
+ if (!jsonText) {
40
+ throw new Error('Script element must have either src attribute or JSON content');
41
+ }
42
+ try {
43
+ emcConfig = JSON.parse(jsonText);
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Failed to parse JSON content: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
48
+ }
49
+ // Validate that config is an object (not array)
50
+ if (typeof emcConfig !== 'object' || emcConfig === null || Array.isArray(emcConfig)) {
51
+ throw new Error('EMC config must be an object (not an array)');
52
+ }
53
+ // Store the parsed config on the script element's export property
54
+ scriptElement.export = emcConfig;
55
+ // Dispatch resolved event
56
+ const { ResolvedEvent } = await import('../Events.js');
57
+ scriptElement.dispatchEvent(new ResolvedEvent(emcConfig));
58
+ }
59
+ // Validate EMC config has required properties
60
+ if (!emcConfig.enhConfig) {
61
+ throw new Error('EMC config must have enhConfig property');
62
+ }
63
+ const enhKey = emcConfig.enhConfig.enhKey;
64
+ if (!enhKey) {
65
+ throw new Error('EMC config enhConfig must have enhKey property');
66
+ }
67
+ // Set ID if not specified
68
+ if (!scriptElement.id && scriptElement.parentElement) {
69
+ scriptElement.id = `${scriptElement.parentElement.localName}.${enhKey}`;
70
+ }
71
+ // Construct MountConfig from EMC config
72
+ const mountConfig = await this.buildMountConfig(emcConfig);
73
+ // Create a MountObserver to watch for elements matching the config
74
+ const observer = new MountObserver(mountConfig);
75
+ // Store observer reference for cleanup
76
+ scriptElement.emcObserver = observer;
77
+ // Observe from the script element's parent or root node
78
+ const observeTarget = scriptElement.parentElement || scriptElement.getRootNode();
79
+ await observer.observe(observeTarget);
80
+ }
81
+ /**
82
+ * Build a MountConfig from an EMC config.
83
+ * Combines the matching selector with withAttrs if present.
84
+ */
85
+ async buildMountConfig(emcConfig) {
86
+ const { enhConfig, ...mountConfigBase } = emcConfig;
87
+ let matching = mountConfigBase.matching || '';
88
+ // If withAttrs is defined, use buildCSSQuery to combine with matching
89
+ if (enhConfig.withAttrs) {
90
+ const { buildCSSQuery } = await import('assign-gingerly/buildCSSQuery.js');
91
+ // Cast to any to avoid type mismatch with spawn property
92
+ const attrQuery = buildCSSQuery(enhConfig);
93
+ // Combine matching with attribute query
94
+ if (matching) {
95
+ matching = `${matching}${attrQuery}`;
96
+ }
97
+ else {
98
+ matching = attrQuery;
99
+ }
100
+ }
101
+ // Create the mount config with a custom handler
102
+ const mountConfig = {
103
+ ...mountConfigBase,
104
+ matching,
105
+ do: (mountedElement) => {
106
+ return this.handleMount(mountedElement, emcConfig);
107
+ }
108
+ };
109
+ return mountConfig;
110
+ }
111
+ /**
112
+ * Handle when an element mounts that matches the EMC config.
113
+ */
114
+ async handleMount(mountedElement, emcConfig) {
115
+ const enhKey = emcConfig.enhConfig.enhKey;
116
+ // Step 1: Check if element already has this enhancement
117
+ const enh = mountedElement.enh;
118
+ if (enh && enh[enhKey]) {
119
+ // Already enhanced, do nothing
120
+ return;
121
+ }
122
+ // Step 2: Get enhancement registry from the element's custom element registry
123
+ const customElementRegistry = mountedElement.customElementRegistry || customElements;
124
+ const enhancementRegistry = customElementRegistry.enhancementRegistry;
125
+ if (!enhancementRegistry) {
126
+ throw new Error('Enhancement registry not found on custom element registry');
127
+ }
128
+ // Check if enhancement is already registered using findByEnhKey method
129
+ let enhancementConfig = enhancementRegistry.findByEnhKey(enhKey);
130
+ // Step 3: If not registered, register it
131
+ if (!enhancementConfig) {
132
+ enhancementConfig = await this.registerEnhancement(emcConfig, enhancementRegistry);
133
+ }
134
+ // Step 4: Spawn enhancement instance
135
+ if (!enh) {
136
+ throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
137
+ }
138
+ await enh.get(enhancementConfig);
139
+ }
140
+ /**
141
+ * Register an enhancement in the enhancement registry.
142
+ */
143
+ async registerEnhancement(emcConfig, enhancementRegistry) {
144
+ const { enhConfig } = emcConfig;
145
+ const { spawn } = enhConfig;
146
+ if (!spawn) {
147
+ throw new Error('EMC enhConfig must have spawn property');
148
+ }
149
+ // Step 3.1: Import the module
150
+ const module = await import(spawn);
151
+ // Get the enhancement class - it should be the default export or any exported class
152
+ let ElementClass = module.default;
153
+ // If no default export, try to find a suitable class
154
+ if (!ElementClass) {
155
+ // Look for any exported constructor function
156
+ for (const key of Object.keys(module)) {
157
+ if (typeof module[key] === 'function') {
158
+ ElementClass = module[key];
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ if (!ElementClass) {
164
+ throw new Error(`No suitable class found in module ${spawn}`);
165
+ }
166
+ // Step 3.2: Construct enhancement config
167
+ const enhancementConfig = {
168
+ ...enhConfig,
169
+ spawn: ElementClass
170
+ };
171
+ // Step 3.3: Register in enhancement registry
172
+ enhancementRegistry.push(enhancementConfig);
173
+ return enhancementConfig;
174
+ }
175
+ }
176
+ // Register built-in handler
177
+ export const emc = 'builtIns.emcScript';
178
+ MountObserver.define(emc, EMCScriptHandler);
@@ -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);
package/index.js CHANGED
@@ -8,6 +8,7 @@ export { DefineCustomElementHandler, DefineScopedCustomElementHandler } from './
8
8
  export { EnhanceMountedElementHandler } from './handlers/EnhanceMountedElement.js';
9
9
  export { ScriptExportHandler } from './handlers/ScriptExport.js';
10
10
  export { MountObserverScriptHandler } from './handlers/MountObserverScript.js';
11
+ export { EMCScriptHandler } from './handlers/EMCScript.js';
11
12
  export { HoistTemplateHandler } from './handlers/HoistTemplate.js';
12
13
  export { HTMLIncludeHandler } from './handlers/HTMLInclude.js';
13
14
  export { upShadowSearch } from './upShadowSearch.js';
@@ -19,5 +20,6 @@ import './handlers/EnhanceMountedElement.js';
19
20
  import './handlers/GenIds.js'; // Temporarily disabled due to missing dependency
20
21
  import './handlers/ScriptExport.js';
21
22
  import './handlers/MountObserverScript.js';
23
+ import './handlers/EMCScript.js';
22
24
  import './handlers/HoistTemplate.js';
23
25
  import './handlers/HTMLInclude.js';
package/index.ts CHANGED
@@ -8,6 +8,7 @@ export { DefineCustomElementHandler, DefineScopedCustomElementHandler } from './
8
8
  export { EnhanceMountedElementHandler } from './handlers/EnhanceMountedElement.js';
9
9
  export { ScriptExportHandler } from './handlers/ScriptExport.js';
10
10
  export { MountObserverScriptHandler } from './handlers/MountObserverScript.js';
11
+ export { EMCScriptHandler } from './handlers/EMCScript.js';
11
12
  export { HoistTemplateHandler } from './handlers/HoistTemplate.js';
12
13
  export { HTMLIncludeHandler } from './handlers/HTMLInclude.js';
13
14
  export { upShadowSearch } from './upShadowSearch.js';
@@ -19,7 +20,8 @@ export type {
19
20
  DoCallback,
20
21
  ImportSpec,
21
22
  IMountEvent,
22
- IDismountEvent
23
+ IDismountEvent,
24
+ EMC
23
25
  } from './types/mount-observer/types.js';
24
26
  export {
25
27
  mountEventName,
@@ -37,6 +39,6 @@ import './handlers/EnhanceMountedElement.js';
37
39
  import './handlers/GenIds.js'; // Temporarily disabled due to missing dependency
38
40
  import './handlers/ScriptExport.js';
39
41
  import './handlers/MountObserverScript.js';
42
+ import './handlers/EMCScript.js';
40
43
  import './handlers/HoistTemplate.js';
41
44
  import './handlers/HTMLInclude.js';
42
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
@@ -30,35 +30,40 @@ export type Spawner<T = any, Obj = Element> = {
30
30
  canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
31
31
  }
32
32
 
33
+ export interface EnhancementConfigBase<T = any> {
34
+ //Allow unprefixed attributes for custom elements and SVG when element tag name matches pattern
35
+ allowUnprefixed?: string | RegExp;
36
+
37
+ //keys of type symbol are used for dependency injection
38
+ //and are used by assign-gingerly
39
+ symlinks?: { [key: symbol]: keyof T };
40
+
41
+ lifecycleKeys?:
42
+ | true // Use standard names: "dispose" method, "resolved" property/event
43
+ | {
44
+ dispose?: string | symbol,
45
+ resolved?: string | symbol
46
+ }
47
+ //used by mount-observer, not by assign-gingerly
48
+ //impossible to polyfill, but will always be disposed
49
+ //when oElement's reference count goes to zero
50
+ disposeOn?: DisposeEvent | DisposeEvent[]
51
+ }
52
+
33
53
  /**
34
54
  * Configuration for enhancing elements with class instances
35
55
  * Defines how to spawn and initialize enhancement classes
36
56
  */
37
- export interface EnhancementConfig<T = any, Obj = Element> {
57
+ export interface EnhancementConfig<T = any, Obj = Element> extends EnhancementConfigBase<T> {
38
58
 
39
59
  spawn: Spawner<T, Obj>;
40
60
 
41
61
  //Applicable to passing in the initVals during the spawn lifecycle event
42
62
  withAttrs?: AttrPatterns<T>;
43
63
 
44
- //Allow unprefixed attributes for custom elements and SVG when element tag name matches pattern
45
- allowUnprefixed?: string | RegExp;
46
-
47
- //keys of type symbol are used for dependency injection
48
- //and are used by assign-gingerly
49
- symlinks?: { [key: symbol]: keyof T };
64
+
50
65
  //only applicable when spawning from a DOM Element reference
51
66
  enhKey?: EnhKey;
52
- lifecycleKeys?:
53
- | true // Use standard names: "dispose" method, "resolved" property/event
54
- | {
55
- dispose?: string | symbol,
56
- resolved?: string | symbol
57
- }
58
- //used by mount-observer, not by assign-gingerly
59
- //impossible to polyfill, but will always be disposed
60
- //when oElement's reference count goes to zero
61
- disposeOn?: DisposeEvent | DisposeEvent[]
62
67
 
63
68
  }
64
69
 
@@ -1,5 +1,7 @@
1
1
  // Core types for MountObserver v2 - Polyfill Supported Scenario I
2
2
 
3
+ import {Spawner, EnhancementConfigBase, EnhKey} from '../assign-gingerly/types';
4
+
3
5
  export type Constructor = new (...args: any[]) => any;
4
6
 
5
7
  export type EventConstructor = {new(...args: any[]): Event};
@@ -25,6 +27,26 @@ export interface ConnectionCondition {
25
27
  rttMax?: number;
26
28
  }
27
29
 
30
+ /**
31
+ * Configuration for enhancing elements with class instances
32
+ * Defines how to spawn and initialize enhancement classes
33
+ */
34
+ export interface EnhConfig<T = any, Obj = Element> extends EnhancementConfigBase<T, Obj> {
35
+
36
+ // bare import specifier path
37
+ spawn: string;
38
+
39
+ enhKey: EnhKey;
40
+
41
+ //Applicable to passing in the initVals during the spawn lifecycle event
42
+ withAttrs?: AttrPatterns<T>;
43
+
44
+ }
45
+
46
+
47
+
48
+
49
+
28
50
  /**
29
51
  * Configuration object for MountObserver that defines what elements to observe and how to handle them.
30
52
  * All `where*` properties form an AND condition - elements must satisfy ALL specified conditions to mount.
@@ -218,6 +240,15 @@ export interface MountConfig<TKeys extends string = string> {
218
240
  * ```
219
241
  */
220
242
  with?: {[K in TKeys]: MountConfig};
243
+
244
+
245
+ }
246
+
247
+ export interface EMC<
248
+ TKeys extends string = string,
249
+ T = any, Obj = Element
250
+ > extends MountConfig<TKeys>{
251
+ enhConfig: EnhConfig<T, Obj>
221
252
  }
222
253
 
223
254