mount-observer 0.1.20 → 0.1.22
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/Events.js +9 -0
- package/Events.ts +10 -0
- package/README.md +197 -0
- package/Synthesizer.js +180 -0
- package/Synthesizer.ts +207 -0
- package/handlers/EMCScript.js +3 -9
- package/handlers/EMCScript.ts +4 -12
- package/index.js +2 -1
- package/index.ts +3 -1
- package/package.json +1 -1
package/Events.js
CHANGED
|
@@ -76,3 +76,12 @@ export class ResolvedEvent extends Event {
|
|
|
76
76
|
this.export = exportValue;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
export const addedScriptElementEventName = 'addedscriptelement';
|
|
80
|
+
export class AddedScriptElementEvent extends Event {
|
|
81
|
+
scriptElement;
|
|
82
|
+
static eventName = addedScriptElementEventName;
|
|
83
|
+
constructor(scriptElement) {
|
|
84
|
+
super(AddedScriptElementEvent.eventName, { bubbles: false, composed: false });
|
|
85
|
+
this.scriptElement = scriptElement;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/Events.ts
CHANGED
|
@@ -73,3 +73,13 @@ export class ResolvedEvent extends Event {
|
|
|
73
73
|
this.export = exportValue;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export const addedScriptElementEventName = 'addedscriptelement';
|
|
78
|
+
|
|
79
|
+
export class AddedScriptElementEvent extends Event {
|
|
80
|
+
static eventName: typeof addedScriptElementEventName = addedScriptElementEventName;
|
|
81
|
+
|
|
82
|
+
constructor(public scriptElement: HTMLScriptElement) {
|
|
83
|
+
super(AddedScriptElementEvent.eventName, { bubbles: false, composed: false });
|
|
84
|
+
}
|
|
85
|
+
}
|
package/README.md
CHANGED
|
@@ -666,6 +666,203 @@ export default class MyEnhancement {
|
|
|
666
666
|
|
|
667
667
|
[Implemented as EMCScript requirement](requirements/Done/EMCScript.md)
|
|
668
668
|
|
|
669
|
+
## Syndicating Mount Observers with Synthesizer
|
|
670
|
+
|
|
671
|
+
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.
|
|
672
|
+
|
|
673
|
+
**Why use Synthesizer?**
|
|
674
|
+
|
|
675
|
+
- Automatically share mount observer configurations across shadow roots
|
|
676
|
+
- Eliminates manual observer setup in each shadow root
|
|
677
|
+
- Ensures consistent behavior across component boundaries
|
|
678
|
+
- Works with both MOSE and EMC script elements
|
|
679
|
+
- Provides a declarative, inheritance-based approach
|
|
680
|
+
|
|
681
|
+
**How it works:**
|
|
682
|
+
|
|
683
|
+
1. **Syndicator** (in document root): Watches for `script[type="mountobserver"]` and `script[type="emc"]` elements and broadcasts them to subscribers
|
|
684
|
+
2. **Subscriber** (in shadow roots): Receives and clones script elements from the syndicator
|
|
685
|
+
3. **Automatic activation**: Both syndicator and subscriber activate 5 built-in handlers in their respective root nodes
|
|
686
|
+
|
|
687
|
+
**Basic usage:**
|
|
688
|
+
|
|
689
|
+
```html
|
|
690
|
+
<!-- Define your Synthesizer custom element -->
|
|
691
|
+
<script type="module">
|
|
692
|
+
import { Synthesizer } from 'mount-observer/Synthesizer.js';
|
|
693
|
+
|
|
694
|
+
class AppSynthesizer extends Synthesizer {}
|
|
695
|
+
customElements.define('app-synthesizer', AppSynthesizer);
|
|
696
|
+
</script>
|
|
697
|
+
|
|
698
|
+
<!-- Syndicator in document root with mount observer scripts -->
|
|
699
|
+
<app-synthesizer>
|
|
700
|
+
<script type="mountobserver">
|
|
701
|
+
{
|
|
702
|
+
"matching": "button.primary",
|
|
703
|
+
"import": "./primary-button.js",
|
|
704
|
+
"do": "builtIns.defineCustomElement"
|
|
705
|
+
}
|
|
706
|
+
</script>
|
|
707
|
+
|
|
708
|
+
<script type="emc">
|
|
709
|
+
{
|
|
710
|
+
"matching": ".interactive",
|
|
711
|
+
"enhConfig": {
|
|
712
|
+
"spawn": "./interactive.js",
|
|
713
|
+
"enhKey": "interactive"
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
</script>
|
|
717
|
+
</app-synthesizer>
|
|
718
|
+
|
|
719
|
+
<!-- Component with shadow root -->
|
|
720
|
+
<my-component>
|
|
721
|
+
#shadow
|
|
722
|
+
<!-- Subscriber automatically receives scripts from syndicator -->
|
|
723
|
+
<app-synthesizer></app-synthesizer>
|
|
724
|
+
|
|
725
|
+
<!-- These elements will be enhanced by the syndicated observers -->
|
|
726
|
+
<button class="primary">Click me</button>
|
|
727
|
+
<div class="interactive">Interactive content</div>
|
|
728
|
+
</my-component>
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**What happens:**
|
|
732
|
+
|
|
733
|
+
1. The syndicator (`<app-synthesizer>` in document root) activates 5 built-in handlers:
|
|
734
|
+
- `builtIns.mountObserverScript`
|
|
735
|
+
- `builtIns.scriptExport`
|
|
736
|
+
- `builtIns.HTMLInclude`
|
|
737
|
+
- `builtIns.hoistTemplate`
|
|
738
|
+
- `builtIns.emcScript`
|
|
739
|
+
|
|
740
|
+
2. The syndicator watches for script elements being added to its light children
|
|
741
|
+
|
|
742
|
+
3. When a script is added, it waits for the `resolved` event (ensuring the script is parsed)
|
|
743
|
+
|
|
744
|
+
4. The syndicator dispatches an `AddedScriptElementEvent` with the script element
|
|
745
|
+
|
|
746
|
+
5. Subscribers in shadow roots:
|
|
747
|
+
- Find the syndicator in the document root (matching localName)
|
|
748
|
+
- Process existing scripts from the syndicator
|
|
749
|
+
- Subscribe to `addedscriptelement` events for new scripts
|
|
750
|
+
- Clone each script element and copy its `export` property
|
|
751
|
+
- Append cloned scripts to their own light children
|
|
752
|
+
- Activate the same 5 built-in handlers in their shadow root
|
|
753
|
+
|
|
754
|
+
**Syndicator vs Subscriber:**
|
|
755
|
+
|
|
756
|
+
The Synthesizer automatically determines its role based on its root node:
|
|
757
|
+
- **Document root** → Acts as syndicator (broadcasts scripts)
|
|
758
|
+
- **Shadow root** → Acts as subscriber (receives scripts)
|
|
759
|
+
|
|
760
|
+
**Activation of built-in handlers:**
|
|
761
|
+
|
|
762
|
+
Both syndicator and subscriber call `element.mount()` to activate handlers in their respective scopes:
|
|
763
|
+
|
|
764
|
+
```javascript
|
|
765
|
+
// Activated in both syndicator and subscriber root nodes
|
|
766
|
+
await this.getRootNode().mount({
|
|
767
|
+
do: 'builtIns.mountObserverScript'
|
|
768
|
+
});
|
|
769
|
+
await this.getRootNode().mount({
|
|
770
|
+
do: 'builtIns.scriptExport'
|
|
771
|
+
});
|
|
772
|
+
await this.getRootNode().mount({
|
|
773
|
+
do: 'builtIns.HTMLInclude'
|
|
774
|
+
});
|
|
775
|
+
await this.getRootNode().mount({
|
|
776
|
+
do: 'builtIns.hoistTemplate'
|
|
777
|
+
});
|
|
778
|
+
await this.getRootNode().mount({
|
|
779
|
+
do: 'builtIns.emcScript'
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
This ensures that:
|
|
784
|
+
- MOSE scripts are processed in each scope
|
|
785
|
+
- Script exports are available
|
|
786
|
+
- HTML includes work within each shadow root
|
|
787
|
+
- Templates are hoisted for performance
|
|
788
|
+
- EMC scripts enhance elements in each scope
|
|
789
|
+
|
|
790
|
+
**Script processing:**
|
|
791
|
+
|
|
792
|
+
When a subscriber receives a script element:
|
|
793
|
+
|
|
794
|
+
1. Checks if the script has an `export` property (parsed configuration)
|
|
795
|
+
2. If not, waits for the `resolved` event (with 5-second timeout)
|
|
796
|
+
3. Clones the script element
|
|
797
|
+
4. Copies the `export` property from source to clone (by reference)
|
|
798
|
+
5. Appends the cloned script to the subscriber's light children
|
|
799
|
+
6. The activated handlers process the cloned script in the shadow root's scope
|
|
800
|
+
|
|
801
|
+
**Benefits:**
|
|
802
|
+
|
|
803
|
+
- **Declarative**: Define observers once in the document root
|
|
804
|
+
- **Automatic**: Scripts propagate to all shadow roots automatically
|
|
805
|
+
- **Scoped**: Each shadow root gets its own observer instances
|
|
806
|
+
- **Efficient**: Parsed configurations are shared (not re-parsed)
|
|
807
|
+
- **Maintainable**: Update observers in one place, changes propagate everywhere
|
|
808
|
+
|
|
809
|
+
**Example - Multiple components:**
|
|
810
|
+
|
|
811
|
+
```html
|
|
812
|
+
<!-- Syndicator with shared observers -->
|
|
813
|
+
<app-synthesizer>
|
|
814
|
+
<script type="mountobserver">
|
|
815
|
+
{
|
|
816
|
+
"matching": "button",
|
|
817
|
+
"import": "./button-enhancement.js",
|
|
818
|
+
"do": "builtIns.enhanceMountedElement"
|
|
819
|
+
}
|
|
820
|
+
</script>
|
|
821
|
+
</app-synthesizer>
|
|
822
|
+
|
|
823
|
+
<!-- Component 1 -->
|
|
824
|
+
<my-header>
|
|
825
|
+
#shadow
|
|
826
|
+
<app-synthesizer></app-synthesizer>
|
|
827
|
+
<button>Header Button</button> <!-- Enhanced -->
|
|
828
|
+
</my-header>
|
|
829
|
+
|
|
830
|
+
<!-- Component 2 -->
|
|
831
|
+
<my-footer>
|
|
832
|
+
#shadow
|
|
833
|
+
<app-synthesizer></app-synthesizer>
|
|
834
|
+
<button>Footer Button</button> <!-- Enhanced -->
|
|
835
|
+
</my-footer>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Both components receive the button enhancement observer automatically.
|
|
839
|
+
|
|
840
|
+
**Error handling:**
|
|
841
|
+
|
|
842
|
+
- Logs errors if handler activation fails
|
|
843
|
+
- Logs errors if script processing fails
|
|
844
|
+
- Continues processing other scripts even if one fails
|
|
845
|
+
- Provides 5-second timeout for waiting on `resolved` events
|
|
846
|
+
|
|
847
|
+
**Requirements:**
|
|
848
|
+
|
|
849
|
+
- Must extend the `Synthesizer` abstract class
|
|
850
|
+
- Must be defined as a custom element
|
|
851
|
+
- Syndicator must be in the document root
|
|
852
|
+
- Subscribers must be in shadow roots
|
|
853
|
+
- All handlers must be imported and registered before use
|
|
854
|
+
|
|
855
|
+
**Comparison with `mountGlobally()`:**
|
|
856
|
+
|
|
857
|
+
Unlike `mountGlobally()`, which discovers shadow roots by observing custom elements, Synthesizer:
|
|
858
|
+
- Uses explicit syndicator-subscriber pattern
|
|
859
|
+
- Provides more control over which scripts are syndicated
|
|
860
|
+
- Works with any shadow root structure
|
|
861
|
+
- Doesn't rely on custom element discovery
|
|
862
|
+
- Allows for selective script propagation
|
|
863
|
+
|
|
864
|
+
[Implemented as Syndicating Mount Observers With Synthesizer requirement](requirements/Done/Syndicating Mount Observers With Synthesizer.md)
|
|
865
|
+
|
|
669
866
|
## Intra-Document HTML Includes with HTMLInclude
|
|
670
867
|
|
|
671
868
|
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.
|
package/Synthesizer.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import './ElementMountExtension.js';
|
|
2
|
+
import { waitForEvent } from 'assign-gingerly/waitForEvent.js';
|
|
3
|
+
import { AddedScriptElementEvent } from './Events.js';
|
|
4
|
+
/**
|
|
5
|
+
* Track which root nodes have already had handlers activated.
|
|
6
|
+
* Uses WeakSet to avoid memory leaks when nodes are garbage collected.
|
|
7
|
+
*/
|
|
8
|
+
const activatedRootNodes = new WeakSet();
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class for syndicating mount observer and EMC script elements across shadow roots.
|
|
11
|
+
*
|
|
12
|
+
* Synthesizer instances act as either:
|
|
13
|
+
* - Syndicator (in document root): Broadcasts script elements to subscribers
|
|
14
|
+
* - Subscriber (in shadow roots): Receives and clones script elements from syndicator
|
|
15
|
+
*
|
|
16
|
+
* Ensures that handlers are only activated once per root node, even if multiple
|
|
17
|
+
* Synthesizer instances exist in the same root.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```javascript
|
|
21
|
+
* class MySynthesizer extends Synthesizer {}
|
|
22
|
+
* customElements.define('my-synthesizer', MySynthesizer);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* ```html
|
|
26
|
+
* <!-- Syndicator in document -->
|
|
27
|
+
* <my-synthesizer>
|
|
28
|
+
* <script type="mountobserver">...</script>
|
|
29
|
+
* <script type="emc">...</script>
|
|
30
|
+
* </my-synthesizer>
|
|
31
|
+
*
|
|
32
|
+
* <!-- Subscriber in shadow root -->
|
|
33
|
+
* <my-component>
|
|
34
|
+
* #shadow-root
|
|
35
|
+
* <my-synthesizer></my-synthesizer>
|
|
36
|
+
* </my-component>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class Synthesizer extends HTMLElement {
|
|
40
|
+
#mutationObserver;
|
|
41
|
+
#isSyndicator = false;
|
|
42
|
+
/**
|
|
43
|
+
* List of built-in handlers to activate.
|
|
44
|
+
*/
|
|
45
|
+
static builtInHandlers = [
|
|
46
|
+
'builtIns.mountObserverScript',
|
|
47
|
+
'builtIns.scriptExport',
|
|
48
|
+
'builtIns.HTMLInclude',
|
|
49
|
+
'builtIns.hoistTemplate',
|
|
50
|
+
'builtIns.emcScript'
|
|
51
|
+
];
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
// Synthesizer elements are infrastructure, not UI
|
|
54
|
+
this.hidden = true;
|
|
55
|
+
// Identify the root node
|
|
56
|
+
const rootNode = this.getRootNode();
|
|
57
|
+
// Determine if this is a syndicator or subscriber
|
|
58
|
+
this.#isSyndicator = rootNode === document;
|
|
59
|
+
// Activate handlers on the root node
|
|
60
|
+
this.#activateHandlers(rootNode);
|
|
61
|
+
if (this.#isSyndicator) {
|
|
62
|
+
// Act as syndicator
|
|
63
|
+
this.#initializeSyndicator();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Act as subscriber
|
|
67
|
+
this.#initializeSubscriber();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
disconnectedCallback() {
|
|
71
|
+
if (this.#mutationObserver) {
|
|
72
|
+
this.#mutationObserver.disconnect();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Activate mount observer handlers in the specified root node.
|
|
77
|
+
* Only activates once per root node, even if multiple Synthesizer instances exist.
|
|
78
|
+
*/
|
|
79
|
+
async #activateHandlers(rootNode) {
|
|
80
|
+
// Check if handlers have already been activated for this root node
|
|
81
|
+
if (activatedRootNodes.has(rootNode)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Mark this root node as activated
|
|
85
|
+
activatedRootNodes.add(rootNode);
|
|
86
|
+
const constructor = this.constructor;
|
|
87
|
+
for (const handlerName of constructor.builtInHandlers) {
|
|
88
|
+
try {
|
|
89
|
+
await rootNode.mount({
|
|
90
|
+
do: handlerName
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(`Synthesizer: Failed to activate handler ${handlerName}:`, error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize as syndicator (in document root).
|
|
100
|
+
* Watches for script elements and broadcasts them to subscribers.
|
|
101
|
+
*/
|
|
102
|
+
#initializeSyndicator() {
|
|
103
|
+
// Process existing script elements
|
|
104
|
+
const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
|
|
105
|
+
scripts.forEach(script => {
|
|
106
|
+
this.#broadcastScript(script);
|
|
107
|
+
});
|
|
108
|
+
// Watch for new script elements
|
|
109
|
+
this.#mutationObserver = new MutationObserver((mutations) => {
|
|
110
|
+
for (const mutation of mutations) {
|
|
111
|
+
for (const node of mutation.addedNodes) {
|
|
112
|
+
if (node instanceof HTMLScriptElement) {
|
|
113
|
+
const type = node.getAttribute('type');
|
|
114
|
+
if (type === 'mountobserver' || type === 'emc') {
|
|
115
|
+
this.#broadcastScript(node);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
this.#mutationObserver.observe(this, {
|
|
122
|
+
childList: true,
|
|
123
|
+
subtree: false
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Broadcast a script element to subscribers.
|
|
128
|
+
*/
|
|
129
|
+
#broadcastScript(scriptElement) {
|
|
130
|
+
this.dispatchEvent(new AddedScriptElementEvent(scriptElement));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Initialize as subscriber (in shadow root).
|
|
134
|
+
* Subscribes to syndicator and processes script elements.
|
|
135
|
+
*/
|
|
136
|
+
#initializeSubscriber() {
|
|
137
|
+
// Find the syndicator in document root
|
|
138
|
+
const syndicator = document.querySelector(this.localName);
|
|
139
|
+
if (!syndicator) {
|
|
140
|
+
console.warn(`Synthesizer: No syndicator found in document for ${this.localName}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Process existing scripts from syndicator
|
|
144
|
+
const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
|
|
145
|
+
scripts.forEach(script => {
|
|
146
|
+
this.#processScript(script);
|
|
147
|
+
});
|
|
148
|
+
// Subscribe to new scripts
|
|
149
|
+
syndicator.addEventListener(AddedScriptElementEvent.eventName, (e) => {
|
|
150
|
+
const event = e;
|
|
151
|
+
this.#processScript(event.scriptElement);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Process a script element from the syndicator.
|
|
156
|
+
* Waits for export property, then clones and appends.
|
|
157
|
+
*/
|
|
158
|
+
async #processScript(scriptElement) {
|
|
159
|
+
try {
|
|
160
|
+
// Check if export property exists
|
|
161
|
+
let exportValue = scriptElement.export;
|
|
162
|
+
if (!exportValue) {
|
|
163
|
+
// Wait for resolved event with timeout
|
|
164
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for resolved event')), 5000));
|
|
165
|
+
const eventPromise = waitForEvent(scriptElement, 'resolved');
|
|
166
|
+
const event = await Promise.race([eventPromise, timeoutPromise]);
|
|
167
|
+
exportValue = event.export;
|
|
168
|
+
}
|
|
169
|
+
// Clone the script element
|
|
170
|
+
const clonedScript = scriptElement.cloneNode(true);
|
|
171
|
+
// Copy the export property
|
|
172
|
+
clonedScript.export = exportValue;
|
|
173
|
+
// Append to this element's children
|
|
174
|
+
this.appendChild(clonedScript);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('Synthesizer: Failed to process script element:', error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
package/Synthesizer.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import './ElementMountExtension.js';
|
|
2
|
+
import { waitForEvent } from 'assign-gingerly/waitForEvent.js';
|
|
3
|
+
import { AddedScriptElementEvent } from './Events.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Track which root nodes have already had handlers activated.
|
|
7
|
+
* Uses WeakSet to avoid memory leaks when nodes are garbage collected.
|
|
8
|
+
*/
|
|
9
|
+
const activatedRootNodes = new WeakSet<Node>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Abstract base class for syndicating mount observer and EMC script elements across shadow roots.
|
|
13
|
+
*
|
|
14
|
+
* Synthesizer instances act as either:
|
|
15
|
+
* - Syndicator (in document root): Broadcasts script elements to subscribers
|
|
16
|
+
* - Subscriber (in shadow roots): Receives and clones script elements from syndicator
|
|
17
|
+
*
|
|
18
|
+
* Ensures that handlers are only activated once per root node, even if multiple
|
|
19
|
+
* Synthesizer instances exist in the same root.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```javascript
|
|
23
|
+
* class MySynthesizer extends Synthesizer {}
|
|
24
|
+
* customElements.define('my-synthesizer', MySynthesizer);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* ```html
|
|
28
|
+
* <!-- Syndicator in document -->
|
|
29
|
+
* <my-synthesizer>
|
|
30
|
+
* <script type="mountobserver">...</script>
|
|
31
|
+
* <script type="emc">...</script>
|
|
32
|
+
* </my-synthesizer>
|
|
33
|
+
*
|
|
34
|
+
* <!-- Subscriber in shadow root -->
|
|
35
|
+
* <my-component>
|
|
36
|
+
* #shadow-root
|
|
37
|
+
* <my-synthesizer></my-synthesizer>
|
|
38
|
+
* </my-component>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export abstract class Synthesizer extends HTMLElement {
|
|
42
|
+
#mutationObserver: MutationObserver | undefined;
|
|
43
|
+
#isSyndicator: boolean = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List of built-in handlers to activate.
|
|
47
|
+
*/
|
|
48
|
+
protected static builtInHandlers = [
|
|
49
|
+
'builtIns.mountObserverScript',
|
|
50
|
+
'builtIns.scriptExport',
|
|
51
|
+
'builtIns.HTMLInclude',
|
|
52
|
+
'builtIns.hoistTemplate',
|
|
53
|
+
'builtIns.emcScript'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
connectedCallback(): void {
|
|
57
|
+
// Synthesizer elements are infrastructure, not UI
|
|
58
|
+
this.hidden = true;
|
|
59
|
+
|
|
60
|
+
// Identify the root node
|
|
61
|
+
const rootNode = this.getRootNode();
|
|
62
|
+
|
|
63
|
+
// Determine if this is a syndicator or subscriber
|
|
64
|
+
this.#isSyndicator = rootNode === document;
|
|
65
|
+
|
|
66
|
+
// Activate handlers on the root node
|
|
67
|
+
this.#activateHandlers(rootNode);
|
|
68
|
+
|
|
69
|
+
if (this.#isSyndicator) {
|
|
70
|
+
// Act as syndicator
|
|
71
|
+
this.#initializeSyndicator();
|
|
72
|
+
} else {
|
|
73
|
+
// Act as subscriber
|
|
74
|
+
this.#initializeSubscriber();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
disconnectedCallback(): void {
|
|
79
|
+
if (this.#mutationObserver) {
|
|
80
|
+
this.#mutationObserver.disconnect();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Activate mount observer handlers in the specified root node.
|
|
86
|
+
* Only activates once per root node, even if multiple Synthesizer instances exist.
|
|
87
|
+
*/
|
|
88
|
+
async #activateHandlers(rootNode: Node): Promise<void> {
|
|
89
|
+
// Check if handlers have already been activated for this root node
|
|
90
|
+
if (activatedRootNodes.has(rootNode)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Mark this root node as activated
|
|
95
|
+
activatedRootNodes.add(rootNode);
|
|
96
|
+
|
|
97
|
+
const constructor = this.constructor as typeof Synthesizer;
|
|
98
|
+
|
|
99
|
+
for (const handlerName of constructor.builtInHandlers) {
|
|
100
|
+
try {
|
|
101
|
+
await (rootNode as any).mount({
|
|
102
|
+
do: handlerName
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`Synthesizer: Failed to activate handler ${handlerName}:`, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize as syndicator (in document root).
|
|
112
|
+
* Watches for script elements and broadcasts them to subscribers.
|
|
113
|
+
*/
|
|
114
|
+
#initializeSyndicator(): void {
|
|
115
|
+
// Process existing script elements
|
|
116
|
+
const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
|
|
117
|
+
scripts.forEach(script => {
|
|
118
|
+
this.#broadcastScript(script as HTMLScriptElement);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Watch for new script elements
|
|
122
|
+
this.#mutationObserver = new MutationObserver((mutations) => {
|
|
123
|
+
for (const mutation of mutations) {
|
|
124
|
+
for (const node of mutation.addedNodes) {
|
|
125
|
+
if (node instanceof HTMLScriptElement) {
|
|
126
|
+
const type = node.getAttribute('type');
|
|
127
|
+
if (type === 'mountobserver' || type === 'emc') {
|
|
128
|
+
this.#broadcastScript(node);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.#mutationObserver.observe(this, {
|
|
136
|
+
childList: true,
|
|
137
|
+
subtree: false
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Broadcast a script element to subscribers.
|
|
143
|
+
*/
|
|
144
|
+
#broadcastScript(scriptElement: HTMLScriptElement): void {
|
|
145
|
+
this.dispatchEvent(new AddedScriptElementEvent(scriptElement));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Initialize as subscriber (in shadow root).
|
|
150
|
+
* Subscribes to syndicator and processes script elements.
|
|
151
|
+
*/
|
|
152
|
+
#initializeSubscriber(): void {
|
|
153
|
+
// Find the syndicator in document root
|
|
154
|
+
const syndicator = document.querySelector(this.localName) as Synthesizer | null;
|
|
155
|
+
|
|
156
|
+
if (!syndicator) {
|
|
157
|
+
console.warn(`Synthesizer: No syndicator found in document for ${this.localName}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Process existing scripts from syndicator
|
|
162
|
+
const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
|
|
163
|
+
scripts.forEach(script => {
|
|
164
|
+
this.#processScript(script as HTMLScriptElement);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Subscribe to new scripts
|
|
168
|
+
syndicator.addEventListener(AddedScriptElementEvent.eventName, (e) => {
|
|
169
|
+
const event = e as AddedScriptElementEvent;
|
|
170
|
+
this.#processScript(event.scriptElement);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Process a script element from the syndicator.
|
|
176
|
+
* Waits for export property, then clones and appends.
|
|
177
|
+
*/
|
|
178
|
+
async #processScript(scriptElement: HTMLScriptElement): Promise<void> {
|
|
179
|
+
try {
|
|
180
|
+
// Check if export property exists
|
|
181
|
+
let exportValue = (scriptElement as any).export;
|
|
182
|
+
|
|
183
|
+
if (!exportValue) {
|
|
184
|
+
// Wait for resolved event with timeout
|
|
185
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
186
|
+
setTimeout(() => reject(new Error('Timeout waiting for resolved event')), 5000)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const eventPromise = waitForEvent(scriptElement, 'resolved');
|
|
190
|
+
|
|
191
|
+
const event = await Promise.race([eventPromise, timeoutPromise]);
|
|
192
|
+
exportValue = (event as any).export;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Clone the script element
|
|
196
|
+
const clonedScript = scriptElement.cloneNode(true) as HTMLScriptElement;
|
|
197
|
+
|
|
198
|
+
// Copy the export property
|
|
199
|
+
(clonedScript as any).export = exportValue;
|
|
200
|
+
|
|
201
|
+
// Append to this element's children
|
|
202
|
+
this.appendChild(clonedScript);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Synthesizer: Failed to process script element:', error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
package/handlers/EMCScript.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { EvtRt } from '../EvtRt.js';
|
|
2
|
-
import { MountObserver } from '../MountObserver.js';
|
|
3
2
|
import '../ElementMountExtension.js';
|
|
4
3
|
import 'assign-gingerly/object-extension.js';
|
|
5
4
|
/**
|
|
@@ -68,15 +67,9 @@ export class EMCScriptHandler extends EvtRt {
|
|
|
68
67
|
if (!scriptElement.id && scriptElement.parentElement) {
|
|
69
68
|
scriptElement.id = `${scriptElement.parentElement.localName}.${enhKey}`;
|
|
70
69
|
}
|
|
71
|
-
// Construct MountConfig from EMC config
|
|
70
|
+
// Construct MountConfig from EMC config and mount it
|
|
72
71
|
const mountConfig = await this.buildMountConfig(emcConfig);
|
|
73
|
-
|
|
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);
|
|
72
|
+
await scriptElement.mount(mountConfig);
|
|
80
73
|
}
|
|
81
74
|
/**
|
|
82
75
|
* Build a MountConfig from an EMC config.
|
|
@@ -174,5 +167,6 @@ export class EMCScriptHandler extends EvtRt {
|
|
|
174
167
|
}
|
|
175
168
|
}
|
|
176
169
|
// Register built-in handler
|
|
170
|
+
import { MountObserver } from '../MountObserver.js';
|
|
177
171
|
export const emc = 'builtIns.emcScript';
|
|
178
172
|
MountObserver.define(emc, EMCScriptHandler);
|
package/handlers/EMCScript.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { EvtRt } from '../EvtRt.js';
|
|
2
2
|
import { EMC, MountConfig, MountContext } from '../types/mount-observer/types.js';
|
|
3
|
-
import { MountObserver } from '../MountObserver.js';
|
|
4
3
|
import '../ElementMountExtension.js';
|
|
5
4
|
import 'assign-gingerly/object-extension.js';
|
|
6
5
|
|
|
@@ -80,18 +79,9 @@ export class EMCScriptHandler extends EvtRt {
|
|
|
80
79
|
scriptElement.id = `${scriptElement.parentElement.localName}.${enhKey}`;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
// Construct MountConfig from EMC config
|
|
82
|
+
// Construct MountConfig from EMC config and mount it
|
|
84
83
|
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);
|
|
84
|
+
await scriptElement.mount(mountConfig);
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
/**
|
|
@@ -211,6 +201,8 @@ export class EMCScriptHandler extends EvtRt {
|
|
|
211
201
|
}
|
|
212
202
|
|
|
213
203
|
// Register built-in handler
|
|
204
|
+
import { MountObserver } from '../MountObserver.js';
|
|
205
|
+
|
|
214
206
|
export const emc = 'builtIns.emcScript';
|
|
215
207
|
|
|
216
208
|
MountObserver.define(emc, EMCScriptHandler);
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Main entry point for MountObserver v2
|
|
2
2
|
export { MountObserver } from './MountObserver.js';
|
|
3
|
+
export { Synthesizer } from './Synthesizer.js';
|
|
3
4
|
export { withScopePerimeter } from './withScopePerimeter.js';
|
|
4
5
|
export { emitMountedElementEvents } from './emitEvents.js';
|
|
5
6
|
export { arr } from './arr.js';
|
|
@@ -12,7 +13,7 @@ export { EMCScriptHandler } from './handlers/EMCScript.js';
|
|
|
12
13
|
export { HoistTemplateHandler } from './handlers/HoistTemplate.js';
|
|
13
14
|
export { HTMLIncludeHandler } from './handlers/HTMLInclude.js';
|
|
14
15
|
export { upShadowSearch } from './upShadowSearch.js';
|
|
15
|
-
export { mountEventName, dismountEventName, disconnectEventName, loadEventName, mediamatchEventName, mediaunmatchEventName } from './Events.js';
|
|
16
|
+
export { mountEventName, dismountEventName, disconnectEventName, loadEventName, mediamatchEventName, mediaunmatchEventName, addedScriptElementEventName } from './Events.js';
|
|
16
17
|
// Register built-in handlers
|
|
17
18
|
import './EvtRt.js';
|
|
18
19
|
import './handlers/DefineCustomElement.js';
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Main entry point for MountObserver v2
|
|
2
2
|
export { MountObserver } from './MountObserver.js';
|
|
3
|
+
export { Synthesizer } from './Synthesizer.js';
|
|
3
4
|
export { withScopePerimeter } from './withScopePerimeter.js';
|
|
4
5
|
export { emitMountedElementEvents } from './emitEvents.js';
|
|
5
6
|
export { arr } from './arr.js';
|
|
@@ -29,7 +30,8 @@ export {
|
|
|
29
30
|
disconnectEventName,
|
|
30
31
|
loadEventName,
|
|
31
32
|
mediamatchEventName,
|
|
32
|
-
mediaunmatchEventName
|
|
33
|
+
mediaunmatchEventName,
|
|
34
|
+
addedScriptElementEventName
|
|
33
35
|
} from './Events.js';
|
|
34
36
|
|
|
35
37
|
// Register built-in handlers
|