mount-observer 0.1.35 → 0.1.37

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
@@ -37,6 +37,7 @@ The following features have been implemented and tested:
37
37
  - ✅ **Shared MutationObserver**: Efficient observer sharing across instances
38
38
  - ✅ **Code splitting**: Conditional features loaded on-demand
39
39
  - ✅ **Memory management**: WeakRef usage for DOM node references
40
+ - ✅ **Cede Scripts**: Declarative custom element definition via `<script type="cede">`
40
41
 
41
42
  ### Not Yet Implemented
42
43
  - ❌ Reconnect event handling
@@ -666,6 +667,138 @@ export default class MyEnhancement {
666
667
 
667
668
  [Implemented as EMCScript requirement](requirements/Done/EMCScript.md)
668
669
 
670
+ ## Custom Element Definition (Cede) Scripts
671
+
672
+ The `builtIns.cedeScript` handler enables declarative custom element definition using `<script type="cede">` elements. "Cede" stands for **C**ustom **E**lement **De**finition. It creates a new class that extends an existing custom element class and defines it in the appropriate registry — all without writing any JavaScript.
673
+
674
+ **Why use Cede Scripts?**
675
+
676
+ - Define custom elements declaratively in HTML
677
+ - Extend existing base classes without writing boilerplate
678
+ - Wire up features from JSON configuration (powered by [assign-gingerly's `defineWithFeatures`](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/defineWithFeatures.md))
679
+ - Works with scoped custom element registries
680
+ - Enables template-based custom elements where the parent's fragment becomes the template
681
+ - Supports external JSON config via `src` attribute (with import maps)
682
+ - Zero JavaScript required for element definition
683
+
684
+ **Basic usage (simple extension):**
685
+
686
+ ```html
687
+ <time-ticker>
688
+ <script type="cede" data-extends="xtal-element"></script>
689
+ </time-ticker>
690
+ ```
691
+
692
+ **With inline feature configuration:**
693
+
694
+ ```html
695
+ <time-ticker>
696
+ <script type="cede" data-extends="el-maker">{
697
+ "assignFeatures": {
698
+ "roundabout": {
699
+ "customData": {"template": "my-template"},
700
+ "withAttrs": {"base": "ra"},
701
+ "callbackForwarding": ["connectedCallback"]
702
+ },
703
+ "truthSourcer": {
704
+ "callbackForwarding": ["connectedCallback", "attributeChangedCallback"]
705
+ }
706
+ }
707
+ }</script>
708
+ </time-ticker>
709
+ ```
710
+
711
+ **With external JSON config:**
712
+
713
+ ```html
714
+ <time-ticker>
715
+ <script type="cede" data-extends="el-maker" src="./time-ticker-config.json"></script>
716
+ </time-ticker>
717
+ ```
718
+
719
+ The `src` attribute uses JSON import assertions, so it works with import maps for bare specifiers (e.g., `src="my-configs/time-ticker.json"`).
720
+
721
+ **What happens:**
722
+
723
+ 1. The handler finds the script element's `customElementRegistry` (falls back to global `customElements`)
724
+ 2. Parses configuration from `src` (JSON import), inline `textContent`, or a pre-existing `export` property
725
+ 3. Stores the parsed config on `scriptElement.export` and dispatches a `resolved` event
726
+ 4. Delegates to [`defineWithFeatures`](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/defineWithFeatures.md) which:
727
+ - Awaits `registry.whenDefined('el-maker')` to get the base class
728
+ - Resolves async feature spawns from the base class's `static supportedFeatures`
729
+ - Creates a subclass and sets `NewCtr.seedRef = new WeakRef(scriptEl)`
730
+ - Wires up features via `assignFeatures`
731
+ - Calls `registry.define('time-ticker', NewCtr)`
732
+ 5. If `registry.get('time-ticker')` already exists, does nothing (first definition wins)
733
+
734
+ The tag name comes from the `localName` of the script element's parent.
735
+
736
+ **Accessing the seed reference:**
737
+
738
+ The `seedRef` static property gives the custom element class access to the original script element. The primary use case is extracting the parent's (Shadow) Fragment to create a cloneable template for other instances:
739
+
740
+ ```javascript
741
+ class XtalElement extends HTMLElement {
742
+ connectedCallback() {
743
+ const seedScript = this.constructor.seedRef?.deref();
744
+ if (seedScript) {
745
+ const parentFragment = seedScript.parentElement.shadowRoot;
746
+ // Use parentFragment as a template for cloning
747
+ }
748
+ }
749
+ }
750
+ ```
751
+
752
+ **With scoped registries:**
753
+
754
+ ```html
755
+ <my-app>
756
+ #shadow (with scoped registry)
757
+ <time-ticker>
758
+ <script type="cede" data-extends="xtal-element"></script>
759
+ </time-ticker>
760
+ </my-app>
761
+ ```
762
+
763
+ The handler uses the script element's `customElementRegistry` property, so it naturally works with [scoped custom element registries](https://developer.chrome.com/blog/scoped-registries) (Chrome 146+, latest WebKit/Safari). If no scoped registry is present, it falls back to the global `customElements` registry.
764
+
765
+ **Bootstrapping:**
766
+
767
+ ```html
768
+ <script type="module">
769
+ import { MountObserver } from 'mount-observer/MountObserver.js';
770
+
771
+ // Handler provides matching and whereInstanceOf via static properties
772
+ const observer = new MountObserver({
773
+ do: 'builtIns.cedeScript'
774
+ });
775
+ observer.observe(document);
776
+ </script>
777
+ ```
778
+
779
+ Or declaratively via a MOSE:
780
+
781
+ ```html
782
+ <script type="mountobserver">
783
+ {
784
+ "do": "builtIns.cedeScript"
785
+ }
786
+ </script>
787
+ ```
788
+
789
+ **Key behaviors:**
790
+
791
+ - If the parent element's tag is already defined in the registry, the handler silently no-ops
792
+ - If multiple cede scripts exist under the same parent, the first one to resolve wins
793
+ - The script element must have a `parentElement` or the handler throws
794
+ - `whenDefined` awaits indefinitely — the base class must eventually be registered
795
+ - Empty scripts (no JSON, no `src`) work as simple class extension with no features
796
+ - Feature spawn resolution is cached per base class — defining 10 elements extending the same base only imports each feature once
797
+
798
+ For full details on the base class contract (`static supportedFeatures`, `fallbackSpawn`, spawn caching, and `assignFeatures` wiring), see the [defineWithFeatures documentation](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/defineWithFeatures.md).
799
+
800
+ [Implemented as SupportForCedeScripts requirement](requirements/SupportForCedeScripts.md)
801
+
669
802
  ## Syndicating Mount Observers with Synthesizer
670
803
 
671
804
  The `Synthesizer` abstract base class enables automatic propagation of mount observer configurations across shadow DOM boundaries. It acts as a "syndicator-subscriber" pattern where a syndicator in the document root broadcasts script elements to subscribers in shadow roots.
@@ -680,9 +813,9 @@ The `Synthesizer` abstract base class enables automatic propagation of mount obs
680
813
 
681
814
  **How it works:**
682
815
 
683
- 1. **Syndicator** (in document root): Watches for `script[type="mountobserver"]` and `script[type="emc"]` elements and broadcasts them to subscribers
816
+ 1. **Syndicator** (in document root): Watches for `script[type="mountobserver"]`, `script[type="emc"]`, and `script[type="cede"]` elements and broadcasts them to subscribers
684
817
  2. **Subscriber** (in shadow roots): Receives and clones script elements from the syndicator
685
- 3. **Automatic activation**: Both syndicator and subscriber activate 7 built-in handlers in their respective root nodes
818
+ 3. **Automatic activation**: Both syndicator and subscriber activate 8 built-in handlers in their respective root nodes
686
819
 
687
820
  **Basic usage:**
688
821
 
@@ -730,7 +863,7 @@ The `Synthesizer` abstract base class enables automatic propagation of mount obs
730
863
 
731
864
  **What happens:**
732
865
 
733
- 1. The syndicator (`<app-synthesizer>` in document root) activates 7 built-in handlers:
866
+ 1. The syndicator (`<app-synthesizer>` in document root) activates 8 built-in handlers:
734
867
  - `builtIns.mountObserverScript` - Process MOSE scripts
735
868
  - `builtIns.scriptExport` - Expose module exports from script elements
736
869
  - `builtIns.HTMLInclude` - Enable intra-document HTML includes
@@ -738,6 +871,7 @@ The `Synthesizer` abstract base class enables automatic propagation of mount obs
738
871
  - `builtIns.generateIds` - Auto-generate unique IDs
739
872
  - `builtIns.emcParserScript` - Load parsers for EMC scripts
740
873
  - `builtIns.emcScript` - Process EMC scripts
874
+ - `builtIns.cedeScript` - Declarative custom element definition
741
875
 
742
876
  <details>
743
877
  <summary>Why these handlers?</summary>
@@ -751,6 +885,7 @@ These handlers form the core infrastructure for declarative progressive enhancem
751
885
  - **generateIds**: Automates ID generation for forms and accessibility features
752
886
  - **emcParserScript**: Enables lazy-loading of complex parsers for enhancement attributes
753
887
  - **emcScript**: Processes enhancement configurations for progressive enhancement
888
+ - **cedeScript**: Declarative custom element definition via `<script type="cede">` — extends a base class and defines the parent's tag name
754
889
 
755
890
  Together, these handlers enable a complete HTML-first development workflow where behaviors, enhancements, and configurations can be declared in HTML and automatically propagated across shadow DOM boundaries.
756
891
 
@@ -768,7 +903,7 @@ Together, these handlers enable a complete HTML-first development workflow where
768
903
  - Subscribe to `addedscriptelement` events for new scripts
769
904
  - Clone each script element and copy its `export` property
770
905
  - Append cloned scripts to their own light children
771
- - Activate the same 7 built-in handlers in their shadow root
906
+ - Activate the same 8 built-in handlers in their shadow root
772
907
 
773
908
  **Syndicator vs Subscriber:**
774
909
 
@@ -4067,7 +4202,7 @@ The platform provides some nice help with managing forms, including IDREF depend
4067
4202
 
4068
4203
  This would be useful for other linkages as well, which the platform doesn't support currently.
4069
4204
 
4070
- Again, because of the mount-observer being the "first point of contact" with the DOM, this is supported by mount-observer as well.
4205
+ Again, because of the mountWhy these handlers-observer being the "first point of contact" with the DOM, this is supported by mount-observer as well.
4071
4206
 
4072
4207
  ```html
4073
4208
  <section id=section>
@@ -4164,3 +4299,34 @@ To keep the api uniform, we hide this discrepancy by pretending the form element
4164
4299
 
4165
4300
  </script>
4166
4301
  ```
4302
+
4303
+ ## Viewing Demos Locally
4304
+
4305
+ 1. Install git
4306
+ 2. Fork/clone this repo
4307
+ 3. Install node.js
4308
+ 4. Open command window to folder where you cloned this repo
4309
+ 5. > git submodule update --init --recursive
4310
+ 6. > npm install
4311
+ 7. > npm run serve
4312
+ 8. Open http://localhost:8000/ in a modern browser
4313
+
4314
+ ## Running Tests
4315
+
4316
+ ```
4317
+ > npm run test
4318
+ ```
4319
+
4320
+ ## Using from ESM Module:
4321
+
4322
+ ```JavaScript
4323
+ import {MountObserver} 'mount-observer/MountObserver.js';
4324
+ ```
4325
+
4326
+ ## Using from CDN:
4327
+
4328
+ ```html
4329
+ <script type=module crossorigin=anonymous>
4330
+ import 'https://esm.sh/mount-observer';
4331
+ </script>
4332
+ ```
package/Synthesizer.js CHANGED
@@ -15,6 +15,8 @@ import 'mount-observer/handlers/EMCParserScript.js';
15
15
  import { emcParser } from 'mount-observer/handlers/EMCParserScript.js';
16
16
  import 'mount-observer/handlers/GenIds.js';
17
17
  import { genIds } from 'mount-observer/handlers/GenIds.js';
18
+ import 'mount-observer/handlers/CedeScript.js';
19
+ import { cedeScript } from 'mount-observer/handlers/CedeScript.js';
18
20
  /**
19
21
  * Track which root nodes have already had handlers activated.
20
22
  * Uses WeakSet to avoid memory leaks when nodes are garbage collected.
@@ -57,7 +59,7 @@ export class Synthesizer extends HTMLElement {
57
59
  * List of built-in handlers to activate.
58
60
  */
59
61
  static builtInHandlers = [
60
- mos, scriptExport, include, hoist, genIds, emcParser, emc
62
+ mos, scriptExport, include, hoist, genIds, emcParser, emc, cedeScript
61
63
  ];
62
64
  connectedCallback() {
63
65
  // Synthesizer elements are infrastructure, not UI
@@ -149,7 +151,7 @@ export class Synthesizer extends HTMLElement {
149
151
  */
150
152
  #initializeSyndicator() {
151
153
  // Process existing script elements
152
- const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
154
+ const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"], script[type="cede"]');
153
155
  scripts.forEach(script => {
154
156
  if (this.checkIfAllowed(script)) {
155
157
  this.#broadcastScript(script);
@@ -161,7 +163,7 @@ export class Synthesizer extends HTMLElement {
161
163
  for (const node of mutation.addedNodes) {
162
164
  if (node instanceof HTMLScriptElement) {
163
165
  const type = node.getAttribute('type');
164
- if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser') {
166
+ if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser' || type === 'cede') {
165
167
  if (this.checkIfAllowed(node)) {
166
168
  this.#broadcastScript(node);
167
169
  }
@@ -194,7 +196,7 @@ export class Synthesizer extends HTMLElement {
194
196
  }
195
197
  // Process existing scripts from syndicator
196
198
  // Only process scripts that pass the syndicator's filtering
197
- const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
199
+ const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"], script[type="cede"]');
198
200
  scripts.forEach(script => {
199
201
  if (syndicator.checkIfAllowed(script)) {
200
202
  this.#processScript(script);
package/Synthesizer.ts CHANGED
@@ -16,6 +16,8 @@ import 'mount-observer/handlers/EMCParserScript.js';
16
16
  import {emcParser} from 'mount-observer/handlers/EMCParserScript.js';
17
17
  import 'mount-observer/handlers/GenIds.js';
18
18
  import {genIds} from 'mount-observer/handlers/GenIds.js';
19
+ import 'mount-observer/handlers/CedeScript.js';
20
+ import {cedeScript} from 'mount-observer/handlers/CedeScript.js';
19
21
 
20
22
  /**
21
23
  * Track which root nodes have already had handlers activated.
@@ -61,7 +63,7 @@ export abstract class Synthesizer extends HTMLElement {
61
63
  * List of built-in handlers to activate.
62
64
  */
63
65
  protected static builtInHandlers = [
64
- mos, scriptExport, include, hoist, genIds, emcParser, emc
66
+ mos, scriptExport, include, hoist, genIds, emcParser, emc, cedeScript
65
67
  ];
66
68
 
67
69
  connectedCallback(): void {
@@ -168,7 +170,7 @@ export abstract class Synthesizer extends HTMLElement {
168
170
  */
169
171
  #initializeSyndicator(): void {
170
172
  // Process existing script elements
171
- const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
173
+ const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"], script[type="cede"]');
172
174
  scripts.forEach(script => {
173
175
  if (this.checkIfAllowed(script as HTMLScriptElement)) {
174
176
  this.#broadcastScript(script as HTMLScriptElement);
@@ -181,7 +183,7 @@ export abstract class Synthesizer extends HTMLElement {
181
183
  for (const node of mutation.addedNodes) {
182
184
  if (node instanceof HTMLScriptElement) {
183
185
  const type = node.getAttribute('type');
184
- if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser') {
186
+ if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser' || type === 'cede') {
185
187
  if (this.checkIfAllowed(node)) {
186
188
  this.#broadcastScript(node);
187
189
  }
@@ -219,7 +221,7 @@ export abstract class Synthesizer extends HTMLElement {
219
221
 
220
222
  // Process existing scripts from syndicator
221
223
  // Only process scripts that pass the syndicator's filtering
222
- const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
224
+ const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"], script[type="cede"]');
223
225
  scripts.forEach(script => {
224
226
  if (syndicator.checkIfAllowed(script as HTMLScriptElement)) {
225
227
  this.#processScript(script as HTMLScriptElement);
@@ -0,0 +1,100 @@
1
+ import { EvtRt } from '../EvtRt.js';
2
+ import { MountObserver } from '../MountObserver.js';
3
+ /**
4
+ * Handler for `<script type="cede" data-extends="...">` elements.
5
+ * "Cede" stands for Custom Element Definition.
6
+ *
7
+ * Delegates to assign-gingerly's `defineWithFeatures` to create a custom element
8
+ * class extending the base specified by `data-extends`, optionally wiring up
9
+ * features from JSON configuration (inline or via `src`).
10
+ *
11
+ * The new class gets a static `seedRef` WeakRef pointing back to the script
12
+ * element, allowing the custom element to extract the parent's (Shadow) Fragment
13
+ * and create a cloneable template from it.
14
+ *
15
+ * Examples:
16
+ * ```html
17
+ * <!-- Simple (no features) -->
18
+ * <time-ticker>
19
+ * <script type="cede" data-extends="xtal-element"></script>
20
+ * </time-ticker>
21
+ *
22
+ * <!-- With inline feature config -->
23
+ * <time-ticker>
24
+ * <script type="cede" data-extends="el-maker">{
25
+ * "assignFeatures": {
26
+ * "roundabout": { "callbackForwarding": ["connectedCallback"] }
27
+ * }
28
+ * }</script>
29
+ * </time-ticker>
30
+ *
31
+ * <!-- With external JSON config -->
32
+ * <time-ticker>
33
+ * <script type="cede" data-extends="el-maker" src="./time-ticker-config.json"></script>
34
+ * </time-ticker>
35
+ * ```
36
+ */
37
+ export class CedeScriptHandler extends EvtRt {
38
+ static matching = 'script[type="cede"][data-extends]';
39
+ static whereInstanceOf = HTMLScriptElement;
40
+ async mount(mountedElement, mountConfig, context) {
41
+ this.abort();
42
+ const scriptEl = mountedElement;
43
+ const extendsName = scriptEl.dataset.extends;
44
+ if (!extendsName)
45
+ return;
46
+ const parentEl = scriptEl.parentElement;
47
+ if (!parentEl) {
48
+ throw new Error('CedeScript: script element must have a parentElement');
49
+ }
50
+ const tagName = parentEl.localName;
51
+ const registry = scriptEl.customElementRegistry || customElements;
52
+ // Already defined? Do nothing (first one prevails).
53
+ if (registry.get(tagName))
54
+ return;
55
+ // Parse config: from export, src, or inline JSON
56
+ let config = scriptEl.export;
57
+ if (!config) {
58
+ const srcAttr = scriptEl.getAttribute('src');
59
+ if (srcAttr) {
60
+ try {
61
+ const module = await import(srcAttr, { with: { type: 'json' } });
62
+ config = module.default;
63
+ }
64
+ catch (error) {
65
+ throw new Error(`Failed to import JSON from '${srcAttr}': ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ }
68
+ else {
69
+ const jsonText = scriptEl.textContent?.trim();
70
+ if (jsonText) {
71
+ try {
72
+ config = JSON.parse(jsonText);
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Failed to parse JSON content: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+ }
78
+ else {
79
+ config = {};
80
+ }
81
+ }
82
+ // Store parsed config and dispatch resolved event
83
+ scriptEl.export = config;
84
+ const { ResolvedEvent } = await import('../Events.js');
85
+ scriptEl.dispatchEvent(new ResolvedEvent(config));
86
+ }
87
+ // Delegate to defineWithFeatures
88
+ const { defineWithFeatures } = await import('assign-gingerly/defineWithFeatures.js');
89
+ // Race condition guard: check again after async operations
90
+ if (registry.get(tagName))
91
+ return;
92
+ await defineWithFeatures(tagName, extendsName, config, registry, {
93
+ onSubclassCreated(NewCtr) {
94
+ NewCtr.seedRef = new WeakRef(scriptEl);
95
+ }
96
+ });
97
+ }
98
+ }
99
+ MountObserver.define('builtIns.cedeScript', CedeScriptHandler);
100
+ export const cedeScript = 'builtIns.cedeScript';
@@ -0,0 +1,106 @@
1
+ import { EvtRt } from '../EvtRt.js';
2
+ import { MountConfig, MountContext } from '../types/mount-observer/types.js';
3
+ import { MountObserver } from '../MountObserver.js';
4
+
5
+ /**
6
+ * Handler for `<script type="cede" data-extends="...">` elements.
7
+ * "Cede" stands for Custom Element Definition.
8
+ *
9
+ * Delegates to assign-gingerly's `defineWithFeatures` to create a custom element
10
+ * class extending the base specified by `data-extends`, optionally wiring up
11
+ * features from JSON configuration (inline or via `src`).
12
+ *
13
+ * The new class gets a static `seedRef` WeakRef pointing back to the script
14
+ * element, allowing the custom element to extract the parent's (Shadow) Fragment
15
+ * and create a cloneable template from it.
16
+ *
17
+ * Examples:
18
+ * ```html
19
+ * <!-- Simple (no features) -->
20
+ * <time-ticker>
21
+ * <script type="cede" data-extends="xtal-element"></script>
22
+ * </time-ticker>
23
+ *
24
+ * <!-- With inline feature config -->
25
+ * <time-ticker>
26
+ * <script type="cede" data-extends="el-maker">{
27
+ * "assignFeatures": {
28
+ * "roundabout": { "callbackForwarding": ["connectedCallback"] }
29
+ * }
30
+ * }</script>
31
+ * </time-ticker>
32
+ *
33
+ * <!-- With external JSON config -->
34
+ * <time-ticker>
35
+ * <script type="cede" data-extends="el-maker" src="./time-ticker-config.json"></script>
36
+ * </time-ticker>
37
+ * ```
38
+ */
39
+ export class CedeScriptHandler extends EvtRt {
40
+ static matching = 'script[type="cede"][data-extends]';
41
+ static whereInstanceOf = HTMLScriptElement;
42
+
43
+ async mount(mountedElement: Element, mountConfig: MountConfig, context: MountContext): Promise<void> {
44
+ this.abort();
45
+ const scriptEl = mountedElement as HTMLScriptElement;
46
+ const extendsName = scriptEl.dataset.extends;
47
+ if (!extendsName) return;
48
+
49
+ const parentEl = scriptEl.parentElement;
50
+ if (!parentEl) {
51
+ throw new Error('CedeScript: script element must have a parentElement');
52
+ }
53
+
54
+ const tagName = parentEl.localName;
55
+ const registry = (scriptEl as any).customElementRegistry || customElements;
56
+
57
+ // Already defined? Do nothing (first one prevails).
58
+ if (registry.get(tagName)) return;
59
+
60
+ // Parse config: from export, src, or inline JSON
61
+ let config: Record<string, any> = (scriptEl as any).export;
62
+ if (!config) {
63
+ const srcAttr = scriptEl.getAttribute('src');
64
+ if (srcAttr) {
65
+ try {
66
+ const module = await import(srcAttr, { with: { type: 'json' } } as any);
67
+ config = module.default;
68
+ } catch (error) {
69
+ throw new Error(`Failed to import JSON from '${srcAttr}': ${error instanceof Error ? error.message : String(error)}`);
70
+ }
71
+ } else {
72
+ const jsonText = scriptEl.textContent?.trim();
73
+ if (jsonText) {
74
+ try {
75
+ config = JSON.parse(jsonText);
76
+ } catch (error) {
77
+ throw new Error(`Failed to parse JSON content: ${error instanceof Error ? error.message : String(error)}`);
78
+ }
79
+ } else {
80
+ config = {};
81
+ }
82
+ }
83
+
84
+ // Store parsed config and dispatch resolved event
85
+ (scriptEl as any).export = config;
86
+ const { ResolvedEvent } = await import('../Events.js');
87
+ scriptEl.dispatchEvent(new ResolvedEvent(config));
88
+ }
89
+
90
+ // Delegate to defineWithFeatures
91
+ const { defineWithFeatures } = await import('assign-gingerly/defineWithFeatures.js');
92
+
93
+ // Race condition guard: check again after async operations
94
+ if (registry.get(tagName)) return;
95
+
96
+ await defineWithFeatures(tagName, extendsName, config as any, registry, {
97
+ onSubclassCreated(NewCtr) {
98
+ (NewCtr as any).seedRef = new WeakRef(scriptEl);
99
+ }
100
+ });
101
+ }
102
+ }
103
+
104
+ MountObserver.define('builtIns.cedeScript', CedeScriptHandler);
105
+
106
+ export const cedeScript = 'builtIns.cedeScript';
@@ -152,8 +152,8 @@ export class EMCScriptHandler extends EvtRt {
152
152
  if (!enh) {
153
153
  throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
154
154
  }
155
- // Pass synthesizerElement through SpawnContext if available
156
- const spawnContext = synthesizerElement ? { synthesizerElement } : undefined;
155
+ // Pass synthesizerElement and full EMC config through SpawnContext
156
+ const spawnContext = { synthesizerElement, emc: emcConfig };
157
157
  await enh.get(enhancementConfig, spawnContext);
158
158
  }
159
159
  /**
@@ -180,8 +180,8 @@ export class EMCScriptHandler extends EvtRt {
180
180
  throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
181
181
  }
182
182
 
183
- // Pass synthesizerElement through SpawnContext if available
184
- const spawnContext = synthesizerElement ? { synthesizerElement } : undefined;
183
+ // Pass synthesizerElement and full EMC config through SpawnContext
184
+ const spawnContext = { synthesizerElement, emc: emcConfig };
185
185
  await enh.get(enhancementConfig, spawnContext);
186
186
  }
187
187
 
@@ -1,73 +1,73 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupElementIntersection = setupElementIntersection;
4
- exports.isElementIntersecting = isElementIntersecting;
5
- var Events_js_1 = require("./Events.js");
6
- function setupElementIntersection(init, rootNodeRef, mountedElements, modules, observer, matchesSelector, handleMatch) {
7
- var whereElementIntersectsWith = init.whereElementIntersectsWith;
8
- if (!whereElementIntersectsWith) {
9
- throw new Error('whereElementIntersectsWith is required');
10
- }
11
- // Track which elements are currently intersecting
12
- var intersectingElements = new WeakSet();
13
- // Create IntersectionObserver with the provided options
14
- var intersectionObserver = new IntersectionObserver(function (entries) {
15
- for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
16
- var entry = entries_1[_i];
17
- var element = entry.target;
18
- if (entry.isIntersecting) {
19
- // Element is now intersecting
20
- intersectingElements.add(element);
21
- // Check if element matches all other conditions and mount if so
22
- if (matchesSelector(element)) {
23
- handleMatch(element);
24
- }
25
- }
26
- else {
27
- // Element is no longer intersecting
28
- intersectingElements.delete(element);
29
- // Dismount if it was mounted
30
- if (mountedElements.weakSet.has(element)) {
31
- dismountElement(element);
32
- }
33
- }
34
- }
35
- }, whereElementIntersectsWith);
36
- function dismountElement(element) {
37
- // Remove from mounted elements
38
- mountedElements.weakSet.delete(element);
39
- for (var _i = 0, _a = mountedElements.setWeak; _i < _a.length; _i++) {
40
- var ref = _a[_i];
41
- if (ref.deref() === element) {
42
- mountedElements.setWeak.delete(ref);
43
- break;
44
- }
45
- }
46
- // Dispatch dismount event
47
- observer.dispatchEvent(new Events_js_1.DismountEvent(element, 'intersection-failed', init));
48
- }
49
- function observeElement(element) {
50
- intersectionObserver.observe(element);
51
- }
52
- return {
53
- intersectionObserver: intersectionObserver,
54
- observeElement: observeElement,
55
- cleanup: function () {
56
- intersectionObserver.disconnect();
57
- }
58
- };
59
- }
60
- /**
61
- * Check if an element is currently intersecting
62
- * This is called from #matchesSelector to determine if intersection condition is met
63
- */
64
- function isElementIntersecting(element, intersectionObserver) {
65
- // If no intersection observer is set up, consider all elements as intersecting
66
- if (!intersectionObserver) {
67
- return true;
68
- }
69
- // When intersection observer is active, we can't synchronously determine intersection state
70
- // The element will be observed and the callback will handle mounting when it intersects
71
- // Return false here to prevent immediate mounting
72
- return false;
73
- }
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupElementIntersection = setupElementIntersection;
4
+ exports.isElementIntersecting = isElementIntersecting;
5
+ var Events_js_1 = require("./Events.js");
6
+ function setupElementIntersection(init, rootNodeRef, mountedElements, modules, observer, matchesSelector, handleMatch) {
7
+ var whereElementIntersectsWith = init.whereElementIntersectsWith;
8
+ if (!whereElementIntersectsWith) {
9
+ throw new Error('whereElementIntersectsWith is required');
10
+ }
11
+ // Track which elements are currently intersecting
12
+ var intersectingElements = new WeakSet();
13
+ // Create IntersectionObserver with the provided options
14
+ var intersectionObserver = new IntersectionObserver(function (entries) {
15
+ for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
16
+ var entry = entries_1[_i];
17
+ var element = entry.target;
18
+ if (entry.isIntersecting) {
19
+ // Element is now intersecting
20
+ intersectingElements.add(element);
21
+ // Check if element matches all other conditions and mount if so
22
+ if (matchesSelector(element)) {
23
+ handleMatch(element);
24
+ }
25
+ }
26
+ else {
27
+ // Element is no longer intersecting
28
+ intersectingElements.delete(element);
29
+ // Dismount if it was mounted
30
+ if (mountedElements.weakSet.has(element)) {
31
+ dismountElement(element);
32
+ }
33
+ }
34
+ }
35
+ }, whereElementIntersectsWith);
36
+ function dismountElement(element) {
37
+ // Remove from mounted elements
38
+ mountedElements.weakSet.delete(element);
39
+ for (var _i = 0, _a = mountedElements.setWeak; _i < _a.length; _i++) {
40
+ var ref = _a[_i];
41
+ if (ref.deref() === element) {
42
+ mountedElements.setWeak.delete(ref);
43
+ break;
44
+ }
45
+ }
46
+ // Dispatch dismount event
47
+ observer.dispatchEvent(new Events_js_1.DismountEvent(element, 'intersection-failed', init));
48
+ }
49
+ function observeElement(element) {
50
+ intersectionObserver.observe(element);
51
+ }
52
+ return {
53
+ intersectionObserver: intersectionObserver,
54
+ observeElement: observeElement,
55
+ cleanup: function () {
56
+ intersectionObserver.disconnect();
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * Check if an element is currently intersecting
62
+ * This is called from #matchesSelector to determine if intersection condition is met
63
+ */
64
+ function isElementIntersecting(element, intersectionObserver) {
65
+ // If no intersection observer is set up, consider all elements as intersecting
66
+ if (!intersectionObserver) {
67
+ return true;
68
+ }
69
+ // When intersection observer is active, we can't synchronously determine intersection state
70
+ // The element will be observed and the callback will handle mounting when it intersects
71
+ // Return false here to prevent immediate mounting
72
+ return false;
73
+ }
package/index.ts CHANGED
@@ -12,6 +12,7 @@ export { MountObserverScriptHandler } from './handlers/MountObserverScript.js';
12
12
  export { EMCScriptHandler } from './handlers/EMCScript.js';
13
13
  export { HoistTemplateHandler } from './handlers/HoistTemplate.js';
14
14
  export { HTMLIncludeHandler } from './handlers/HTMLInclude.js';
15
+ export { CedeScriptHandler } from './handlers/CedeScript.js';
15
16
  export { upShadowSearch } from './upShadowSearch.js';
16
17
  export type {
17
18
  MountConfig,
@@ -44,3 +45,4 @@ import './handlers/MountObserverScript.js';
44
45
  import './handlers/EMCScript.js';
45
46
  import './handlers/HoistTemplate.js';
46
47
  import './handlers/HTMLInclude.js';
48
+ import './handlers/CedeScript.js';
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
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.30",
8
+ "assign-gingerly": "0.0.50",
9
9
  "id-generation": "0.0.4"
10
10
  },
11
11
  "devDependencies": {
12
- "@playwright/test": "1.59.1",
12
+ "@playwright/test": "1.60.0",
13
13
  "spa-ssi": "0.0.27"
14
14
  },
15
15
  "exports": {
@@ -0,0 +1,11 @@
1
+ import {RAConfig} from '../roundabout/types';
2
+ import {AttrPatterns} from '../assign-gingerly/types';
3
+
4
+ /**
5
+ * Assign Gingerly Roundabout Config
6
+ */
7
+ export interface AgraceConfig<TProps = unknown, TActions = TProps, ETProps = TProps, TCustomData = unknown> {
8
+ raConfig: RAConfig<TProps, TActions, ETProps, TCustomData>,
9
+ withAttrs?: AttrPatterns<TProps>,
10
+ template?: string | HTMLTemplateElement,
11
+ }
@@ -212,6 +212,13 @@ export interface SpawnContext<T = any, TMountContext = any> {
212
212
  * Used for scoped parser registry access during attribute parsing.
213
213
  */
214
214
  synthesizerElement?: Element;
215
+ /**
216
+ * The full EMC configuration object that triggered this spawn.
217
+ * Passed through so enhancement classes can access their full configuration
218
+ * (including customData) without needing to separately import the JSON file.
219
+ * This avoids duplicate JSON imports when using emoji shorthand aliases.
220
+ */
221
+ emc?: any;
215
222
  }
216
223
 
217
224
  /**
@@ -226,6 +233,14 @@ export interface IAssignGingerlyOptions {
226
233
  registry?: typeof EnhancementRegistry | EnhancementRegistry;
227
234
  bypassChecks?: boolean;
228
235
  withMethods?: string[] | Set<string>;
236
+ aka?: Record<string, string>;
237
+
238
+ /**
239
+ * AbortSignal for cleaning up reactive subscriptions (@eachTime)
240
+ * Required when using @eachTime symbol for reactive iteration
241
+ * When the signal is aborted, all event listeners are automatically removed
242
+ */
243
+ signal?: AbortSignal;
229
244
  }
230
245
 
231
246
  /**
@@ -242,7 +257,6 @@ export declare class EnhancementRegisteredEvent extends Event {
242
257
  * Extends EventTarget to dispatch events when configs are registered
243
258
  */
244
259
  export declare class EnhancementRegistry extends EventTarget {
245
- private items;
246
260
  push(items: EnhancementConfig | EnhancementConfig[]): void;
247
261
  getItems(): EnhancementConfig[];
248
262
  findBySymbol(symbol: symbol | string): EnhancementConfig | undefined;
@@ -303,10 +317,7 @@ export declare class ElementEnhancementGateway{
303
317
  }
304
318
 
305
319
  export interface ElementEnhancement{
306
- dispose(regItem: EnhancementConfig): void;
307
- }
308
-
309
- export interface ElementInfer{
310
- value: any;
311
- eventType: string
320
+ get(registryItem: EnhancementConfig | string | symbol, mountCtx?: any): any;
321
+ dispose(registryItem: EnhancementConfig | string | symbol): void;
322
+ whenResolved(registryItem: EnhancementConfig | string | symbol, mountCtx?: any): Promise<any>;
312
323
  }
@@ -19,7 +19,8 @@ export interface BindingRule {
19
19
 
20
20
  localProp?: string,
21
21
  localEvent?: string,
22
- remoteSpecifierString?: string,
22
+ remoteId?: string,
23
+ remoteProp?: string,
23
24
  remoteSpecifier?: Specifier,
24
25
 
25
26
 
@@ -1,4 +1,4 @@
1
- import { ElementEnhancementGateway } from "../assign-gingerly/types";
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
2
  import { StatementsResult } from "../nested-regex-groups/types";
3
3
 
4
4
  export interface Specifier {
@@ -23,7 +23,7 @@ export type ProPAP = Promise<PAP>
23
23
 
24
24
  export interface Actions{
25
25
  hydrate(self: AP): ProPAP;
26
- init(self: AP, enhancedElement: Element, initVals: PAP): Promise<void>
26
+ init(self: AP, enhancedElement: Element, ctx: SpawnContext, initVals: PAP): Promise<void>
27
27
  }
28
28
 
29
29
 
@@ -1,4 +1,4 @@
1
- import { ElementEnhancementGateway, ElementInfer } from "../assign-gingerly/types";
1
+ import { ElementEnhancementGateway } from "../assign-gingerly/types";
2
2
  import { StatementsResult } from "../nested-regex-groups/types";
3
3
 
4
4
  export interface EndUserProps{}
@@ -0,0 +1,46 @@
1
+ import type { EnhancementConfig } from "../assign-gingerly/types";
2
+
3
+ /**
4
+ * Symbol for smart value assignment
5
+ */
6
+ export declare const value: symbol;
7
+
8
+ /**
9
+ * Symbol for smart display assignment
10
+ */
11
+ export declare const display: symbol;
12
+
13
+ /**
14
+ * Enhancement class that provides smart value and display property inference
15
+ */
16
+ export declare class Infer<TValue = any, TDisplay = any> {
17
+ get enhancedElement(): Element;
18
+ constructor(enhancedElement?: Element);
19
+ get value(): TValue | undefined;
20
+ set value(nv: TValue);
21
+ get display(): TDisplay | undefined;
22
+ set display(nv: TDisplay);
23
+ get eventType(): string;
24
+ }
25
+
26
+ /**
27
+ * Registry item for the Infer enhancement
28
+ */
29
+ export declare const registryItem: EnhancementConfig;
30
+
31
+ /**
32
+ * Infer the most appropriate value property for an element
33
+ */
34
+ export declare function inferValueProperty(element: Element): string;
35
+
36
+ /**
37
+ * Infer the most appropriate display property for an element
38
+ */
39
+ export declare function inferDisplayProperty(element: Element): string;
40
+
41
+ /**
42
+ * Infer the most appropriate event type for an element
43
+ */
44
+ export declare function inferEventType(element: Element): string;
45
+
46
+ export default registryItem;
@@ -1,6 +1,6 @@
1
1
  // Core types for MountObserver v2 - Polyfill Supported Scenario I
2
2
 
3
- import {Spawner, EnhancementConfigBase, EnhKey, AttrPatterns} from '../assign-gingerly/types';
3
+ import {EnhancementConfigBase, EnhKey, AttrPatterns} from '../assign-gingerly/types';
4
4
 
5
5
  export type Constructor = new (...args: any[]) => any;
6
6
 
@@ -24,11 +24,17 @@ export interface LogicOp<Props = any, TActions = Props>{
24
24
 
25
25
  delay?: number,
26
26
 
27
+ }
28
+
29
+ /**
30
+ * Extends LogicOp with a `do` property for specifying which function to call.
31
+ * Used by positractions where the function is generic and view-model-neutral.
32
+ */
33
+ export interface LogicOpWithDo<Props = any, TActions = Props> extends LogicOp<Props, TActions>{
27
34
  do?:
28
35
  | Function
29
36
  | (keyof TActions & string)
30
37
  | PropsToProps<Props>
31
-
32
38
  }
33
39
 
34
40
  export type Actions<TProps = any, TActions = TProps> =
@@ -46,6 +52,8 @@ export type Compacts<TProps = any, TActions = TProps> =
46
52
  | Partial<{[key in `when_${keyof TProps & string}_changes_toggle_${keyof TProps & string}`]: number}>
47
53
  | Partial<{[key in `when_${keyof TProps & string}_changes_inc_${keyof TProps & string}_by`]: number}>
48
54
  | Partial<{[key in `when_${keyof TProps & string}_changes_dispatch`]: string}> //TODO
55
+ | Partial<{[key in `on_${string}_of_${keyof TProps & string}_inc_${keyof TProps & string}_by`]: number}>
56
+ | Partial<{[key in `on_${string}_of_${keyof TProps & string}_set_${keyof TProps & string}_to`]: any}>
49
57
  ;
50
58
 
51
59
  export type Hitches<TProps = any, TActions = TProps> =
@@ -58,7 +66,7 @@ export type Handlers<ETProps = any, TActions = ETProps> =
58
66
  export type Positractions<TProps = any, TActions = TProps> =
59
67
  | Array<Positraction<TProps, TActions>>;
60
68
 
61
- export interface Positraction<TProps = any, TActions = TProps> extends LogicOp<TProps, TActions> {
69
+ export interface Positraction<TProps = any, TActions = TProps> extends LogicOpWithDo<TProps, TActions> {
62
70
  do:
63
71
  | Function
64
72
  | (keyof TActions & string)
@@ -71,13 +79,30 @@ export interface Positraction<TProps = any, TActions = TProps> extends LogicOp<T
71
79
  assignTo?: Array<null | (keyof TProps & string)>
72
80
  }
73
81
 
82
+ /**
83
+ * A merge is a fully JSON-serializable reactive rule.
84
+ * When its conditions are met, it calls assignFrom(vm, assignFrom, { from: vm })
85
+ * to resolve RHS path strings against the vm and assign the results into the vm.
86
+ * No method or code is required.
87
+ */
88
+ export interface Merge<TProps = any> extends LogicOp<TProps> {
89
+ /**
90
+ * Pattern object whose keys are LHS assignGingerly paths and whose
91
+ * values are RHS `?.`-prefixed path strings resolved against the vm.
92
+ */
93
+ assign: Record<string, any>;
94
+ }
95
+
96
+ export type Merges<TProps = any> = Array<Merge<TProps>>;
97
+
74
98
  export interface RAConfig<TProps = unknown, TActions = TProps, ETProps = TProps, TCustomData = unknown> {
75
99
  actions?: Actions<TProps,TActions>,
76
100
  compacts?: Compacts<TProps, TActions>,
77
101
  //onsets?: Onsets<TProps, TActions>,
78
102
  handlers?: Handlers<ETProps, TActions>,
79
- hitch?: Hitches<TProps, TActions>,
103
+ hitches?: Hitches<TProps, TActions>,
80
104
  positractions?: Positractions<TProps>,
105
+ merges?: Merges<TProps>,
81
106
  /**
82
107
  * Configure automatic WeakRef wrapping for properties
83
108
  *
@@ -128,6 +153,12 @@ export interface RoundaboutOptions<TProps = unknown, TActions = TProps, ETProps
128
153
  * - Diamond dependencies (A→B, A→C, B→D, C→D)
129
154
  */
130
155
  internalRouting?: boolean,
156
+
157
+ /**
158
+ * Options passed to every internal assignGingerly call.
159
+ * See IAssignGingerlyOptions in assign-gingerly for details.
160
+ */
161
+ assignGingerlyOptions?: import('../assign-gingerly/types.js').IAssignGingerlyOptions,
131
162
 
132
163
 
133
164
  }