mount-observer-script-element 0.0.1
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/.hintrc +15 -0
- package/.kiro/steering/tech.md +251 -0
- package/.vscode/settings.json +2 -0
- package/LICENSE +21 -0
- package/MOSE.js +189 -0
- package/MOSE.ts +228 -0
- package/README.md +303 -0
- package/demo/scopedCustomElementRegistry.html +23 -0
- package/getHighestCERNode.js +37 -0
- package/getHighestCERNode.ts +41 -0
- package/imports.html +9 -0
- package/index.js +2 -0
- package/index.ts +2 -0
- package/package.json +50 -0
- package/requirements/Requirement1.md +14 -0
- package/requirements/Requirement2.md +7 -0
- package/requirements/Requirement3.md +15 -0
- package/requirements/Requirement4.md +9 -0
- package/requirements/Requirement5.md +16 -0
- package/requirements/Requirement6.md +23 -0
- package/requirements/Requirement7.md +3 -0
- package/tests/MOSE.html +173 -0
- package/tests/Requirement3.html +239 -0
- package/tests/buttonMatch.json +12 -0
- package/tests/exclude.html +33 -0
- package/tests/getHighestCERNode.html +119 -0
- package/tests/inheritance.html +33 -0
- package/tests/simple.html +25 -0
- package/tsconfig.json +20 -0
package/MOSE.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { getHighestCERNode } from './getHighestCERNode.js';
|
|
2
|
+
import { MountObserver } from 'mount-observer/MountObserver.js';
|
|
3
|
+
import { MountEvent, mountEventName } from 'mount-observer/Events.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type for a constructor that can be extended
|
|
7
|
+
*/
|
|
8
|
+
type Constructor<T = HTMLElement> = new (...args: any[]) => T;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Symbol to track if MountObserver has been set up for an element
|
|
12
|
+
*/
|
|
13
|
+
const MOUNT_OBSERVER_SETUP = Symbol.for('cteH9dMG-UWwxVaMwFgvQA');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* MOSE (Mount Observer Script Element) Mixin
|
|
17
|
+
*
|
|
18
|
+
* This mixin adds functionality to:
|
|
19
|
+
* 1. Check for duplicate custom element registrations within the same custom element registry scope
|
|
20
|
+
* 2. Monitor for <script type="mountobserver"> elements and apply their configurations
|
|
21
|
+
*
|
|
22
|
+
* @param Base - The base class to extend
|
|
23
|
+
* @returns Extended class with MOSE functionality
|
|
24
|
+
*/
|
|
25
|
+
export function MOSE<T extends Constructor<HTMLElement>>(Base: T) {
|
|
26
|
+
return class extends Base {
|
|
27
|
+
#mountObserver: MountObserver | undefined;
|
|
28
|
+
|
|
29
|
+
constructor(...args: any[]) {
|
|
30
|
+
super(...args);
|
|
31
|
+
this.#checkForDuplicateRegistration();
|
|
32
|
+
this.#setupMountObserver();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#checkForDuplicateRegistration() {
|
|
36
|
+
// Get the tag name of this element
|
|
37
|
+
// const tagName = this.tagName.toLowerCase();
|
|
38
|
+
const {localName} = this;
|
|
39
|
+
|
|
40
|
+
// Find the highest node with the same custom element registry
|
|
41
|
+
const highestCERNode = getHighestCERNode(this) as DocumentFragment;
|
|
42
|
+
|
|
43
|
+
if (!highestCERNode) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get the custom element registry for this scope
|
|
48
|
+
// const registry = (highestCERNode as any).customElementRegistry as CustomElementRegistry | undefined;
|
|
49
|
+
|
|
50
|
+
// // If there's no custom registry, use the global one
|
|
51
|
+
// const targetRegistry = registry || customElements;
|
|
52
|
+
|
|
53
|
+
// Check if an element with this name is already defined in this registry
|
|
54
|
+
try {
|
|
55
|
+
const existingTags =Array.from(highestCERNode.querySelectorAll(localName)).filter(x => x !== this);
|
|
56
|
+
|
|
57
|
+
if (existingTags.length > 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Custom element "${localName}" is already defined in this custom element registry scope.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// If get() throws, it means the element isn't registered yet, which is fine
|
|
64
|
+
// But if it's our error, re-throw it
|
|
65
|
+
if (error instanceof Error && error.message.includes('already defined')) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy mountobserver script elements from container
|
|
71
|
+
this.#getContainerMOSEs(highestCERNode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#getContainerMOSEs(highestCERNode: Node) {
|
|
75
|
+
// Step 1: Don't do anything if highestCERNode is the document root
|
|
76
|
+
if (highestCERNode === document) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 2: Get parent element or host
|
|
81
|
+
let parentNode: Element | null = null;
|
|
82
|
+
if (highestCERNode instanceof Element) {
|
|
83
|
+
parentNode = highestCERNode.parentElement;
|
|
84
|
+
} else if (highestCERNode instanceof ShadowRoot) {
|
|
85
|
+
parentNode = highestCERNode.host;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!parentNode) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 3: Find the highestCERNode of the parent
|
|
93
|
+
const parentHighestCERNode = getHighestCERNode(parentNode);
|
|
94
|
+
|
|
95
|
+
// Find parent custom element with same localName
|
|
96
|
+
if (!parentHighestCERNode || !('querySelector' in parentHighestCERNode)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parentCE = (parentHighestCERNode as DocumentFragment).querySelector(this.localName);
|
|
101
|
+
if (!parentCE) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If highestCERNode contains parentCE, exit
|
|
106
|
+
if ((highestCERNode as DocumentFragment).contains?.(parentCE)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Clone and append script elements
|
|
111
|
+
this.#cloneAndAppendScripts(parentCE);
|
|
112
|
+
|
|
113
|
+
// Add event listener for future mount events
|
|
114
|
+
parentCE.addEventListener(mountEventName, (e: Event) => {
|
|
115
|
+
const mountedElement = (e as MountEvent).mountedElement;
|
|
116
|
+
if (mountedElement instanceof HTMLScriptElement && mountedElement.type === 'mountobserver') {
|
|
117
|
+
this.#cloneAndAppendScripts(parentCE);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#cloneAndAppendScripts(sourceElement: Element) {
|
|
123
|
+
const scripts = Array.from(sourceElement.querySelectorAll('script[type="mountobserver"]')) as HTMLScriptElement[];
|
|
124
|
+
|
|
125
|
+
// Get exclude value from property or attribute
|
|
126
|
+
const exclude = (this as any).exclude ?? this.getAttribute('exclude');
|
|
127
|
+
|
|
128
|
+
for (const script of scripts) {
|
|
129
|
+
// Check if script matches exclude criteria
|
|
130
|
+
if (exclude && script.matches(exclude)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const src = script.getAttribute('src');
|
|
135
|
+
|
|
136
|
+
// Check if we already have a script with the same src
|
|
137
|
+
const existingScripts = Array.from(this.querySelectorAll('script[type="mountobserver"]')) as HTMLScriptElement[];
|
|
138
|
+
const alreadyExists = existingScripts.some(existing => {
|
|
139
|
+
const existingSrc = existing.getAttribute('src');
|
|
140
|
+
return existingSrc === src;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!alreadyExists) {
|
|
144
|
+
const clonedScript = script.cloneNode(true) as HTMLScriptElement;
|
|
145
|
+
this.appendChild(clonedScript);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #setupMountObserver() {
|
|
151
|
+
// Find the highest node with the same custom element registry
|
|
152
|
+
const highestCERNode = getHighestCERNode(this);
|
|
153
|
+
|
|
154
|
+
if (!highestCERNode) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if MountObserver has already been set up for this element
|
|
159
|
+
const existingObserver = (highestCERNode as any)[MOUNT_OBSERVER_SETUP];
|
|
160
|
+
if (existingObserver) {
|
|
161
|
+
// Subscribe to the existing observer's mount event and re-dispatch from this element
|
|
162
|
+
existingObserver.addEventListener(mountEventName, (e: MountEvent) => {
|
|
163
|
+
const {mountedElement} = e;
|
|
164
|
+
if(this.contains(mountedElement)){
|
|
165
|
+
this.dispatchEvent(e);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set up MountObserver to watch for <script type="mountobserver"> elements
|
|
172
|
+
this.#mountObserver = new MountObserver({
|
|
173
|
+
whereElementMatches: 'script[type="mountobserver"]',
|
|
174
|
+
do: async (scriptElement: Element) => {
|
|
175
|
+
await this.#processScriptElement(scriptElement as HTMLScriptElement, highestCERNode);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Mark that we've set up the MountObserver for this element
|
|
180
|
+
(highestCERNode as any)[MOUNT_OBSERVER_SETUP] = this.#mountObserver;
|
|
181
|
+
|
|
182
|
+
await this.#mountObserver.observe(highestCERNode);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async #processScriptElement(scriptElement: HTMLScriptElement, rootNode: Node) {
|
|
186
|
+
let config: any = {};
|
|
187
|
+
|
|
188
|
+
// Step 1: Check if script has src attribute and load JSON
|
|
189
|
+
const src = scriptElement.getAttribute('src')
|
|
190
|
+
if (src) {
|
|
191
|
+
try {
|
|
192
|
+
const response = await import(src, {with: {type: 'json'}});
|
|
193
|
+
config = structuredClone(response.default);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`Failed to load JSON from ${scriptElement.src}:`, error);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Step 2: If innerHTML is non-trivial, parse and merge it
|
|
201
|
+
const innerHTML = scriptElement.innerHTML.trim();
|
|
202
|
+
if (innerHTML) {
|
|
203
|
+
try {
|
|
204
|
+
const parsedJSON = JSON.parse(innerHTML);
|
|
205
|
+
|
|
206
|
+
// Step 3: Import assignGingerly and merge
|
|
207
|
+
const { assignGingerly } = await import('assign-gingerly/assignGingerly.js');
|
|
208
|
+
config = assignGingerly(config, parsedJSON, {
|
|
209
|
+
registry: (<any>scriptElement).customElementRegistry.assignGingerlyRegistry
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('Failed to parse script innerHTML as JSON:', error);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Step 4: Apply MountObserver with the merged config
|
|
218
|
+
if (Object.keys(config).length > 0) {
|
|
219
|
+
try {
|
|
220
|
+
const observer = new MountObserver(config);
|
|
221
|
+
observer.observe(rootNode);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Failed to create MountObserver with config:', error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# mount-observer-script-element
|
|
2
|
+
|
|
3
|
+
A TypeScript mixin (MOSE) that enables declarative configuration of [MountObserver](https://www.npmjs.com/package/mount-observer) instances through `<script type="mountobserver">` elements, with support for Chrome's Scoped Custom Element Registries.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The MOSE (Mount Observer Script Element) mixin provides:
|
|
8
|
+
|
|
9
|
+
1. **Scoped Custom Element Registry Support**: Works with Chrome's scoped custom element registries
|
|
10
|
+
2. **Declarative MountObserver Configuration**: Configure MountObserver instances using JSON in script elements
|
|
11
|
+
3. **Script Inheritance**: Child custom elements automatically inherit mountobserver scripts from parent elements
|
|
12
|
+
4. **Duplicate Prevention**: Ensures only one MountObserver is created per registry scope
|
|
13
|
+
5. **Event Propagation**: Re-dispatches mount events from child elements
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install mount-observer-script-element
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { MOSE } from 'mount-observer-script-element/MOSE.js';
|
|
25
|
+
|
|
26
|
+
class MyElement extends MOSE(HTMLElement) {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
customElements.define('my-element', MyElement);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<my-element>
|
|
37
|
+
<script type="mountobserver">
|
|
38
|
+
{
|
|
39
|
+
"whereElementMatches": "button",
|
|
40
|
+
"assignGingerly": {
|
|
41
|
+
"disabled": false,
|
|
42
|
+
"?.dataset?.action": "submit",
|
|
43
|
+
"?.style?.color": "green"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
<button>Click me</button>
|
|
48
|
+
</my-element>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### 1. Declarative MountObserver Configuration
|
|
54
|
+
|
|
55
|
+
Place `<script type="mountobserver">` elements inside your custom element to configure MountObserver behavior:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<my-element>
|
|
59
|
+
<script type="mountobserver">
|
|
60
|
+
{
|
|
61
|
+
"whereElementMatches": "input",
|
|
62
|
+
"assignGingerly": {
|
|
63
|
+
"placeholder": "Enter text...",
|
|
64
|
+
"?.style?.backgroundColor": "#f0f0f0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
</my-element>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. External JSON Configuration
|
|
72
|
+
|
|
73
|
+
Load configuration from external JSON files:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<script type="mountobserver" src="./config.json"></script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The JSON file will be loaded using JSON import with `import(src, {with: {type: 'json'}})`.
|
|
80
|
+
|
|
81
|
+
### 3. Merging Configurations
|
|
82
|
+
|
|
83
|
+
You can combine external and inline configurations. The inline JSON will be merged with the external configuration using [assignGingerly](https://www.npmjs.com/package/assign-gingerly):
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<script type="mountobserver" src="./base-config.json">
|
|
87
|
+
{
|
|
88
|
+
"assignGingerly": {
|
|
89
|
+
"?.style?.color": "red"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Script Inheritance
|
|
96
|
+
|
|
97
|
+
Child custom elements automatically inherit mountobserver scripts from parent elements of the same type:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<my-element>
|
|
101
|
+
<script type="mountobserver" src="./shared-config.json"></script>
|
|
102
|
+
|
|
103
|
+
<my-element>
|
|
104
|
+
<!-- This child element automatically inherits the parent's script -->
|
|
105
|
+
<button>I inherit the configuration</button>
|
|
106
|
+
</my-element>
|
|
107
|
+
</my-element>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**How it works:**
|
|
111
|
+
- When a MOSE element is created, it searches for a parent element with the same `localName`
|
|
112
|
+
- It clones all `<script type="mountobserver">` elements from the parent
|
|
113
|
+
- Scripts with duplicate `src` attributes are not cloned (avoiding duplicates)
|
|
114
|
+
- The child element listens for mount events on the parent to inherit dynamically added scripts
|
|
115
|
+
|
|
116
|
+
### 5. Excluding Inherited Scripts
|
|
117
|
+
|
|
118
|
+
Use the `exclude` property or attribute to prevent specific scripts from being inherited:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<my-element exclude="script[src*='unwanted']">
|
|
122
|
+
<!-- Scripts matching the exclude selector won't be inherited -->
|
|
123
|
+
</my-element>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
class MyElement extends MOSE(HTMLElement) {
|
|
128
|
+
exclude = "script[data-skip]";
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 6. Scoped Custom Element Registry Support
|
|
133
|
+
|
|
134
|
+
The mixin works with Chrome's scoped custom element registries:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const customRegistry = new CustomElementRegistry();
|
|
138
|
+
|
|
139
|
+
class ScopedElement extends MOSE(HTMLElement) {
|
|
140
|
+
constructor() {
|
|
141
|
+
super();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
customRegistry.define('scoped-element', ScopedElement);
|
|
146
|
+
|
|
147
|
+
const element = document.createElement('scoped-element', {
|
|
148
|
+
customElementRegistry: customRegistry
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 7. Duplicate Element Detection
|
|
153
|
+
|
|
154
|
+
The mixin prevents multiple instances of the same custom element within the same registry scope:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// This will throw an error if another element with the same localName
|
|
158
|
+
// already exists in the same highestCERNode scope
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 8. Event Propagation
|
|
162
|
+
|
|
163
|
+
When multiple MOSE elements share the same highestCERNode, mount events are propagated:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
myElement.addEventListener('mount', (e) => {
|
|
167
|
+
// Receives mount events for elements within this element
|
|
168
|
+
console.log('Element mounted:', e.mountedElement);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## API
|
|
173
|
+
|
|
174
|
+
### MOSE Mixin
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
function MOSE<T extends Constructor<HTMLElement>>(Base: T): T
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A TypeScript mixin that adds MountObserver script element functionality to any HTMLElement class.
|
|
181
|
+
|
|
182
|
+
### getHighestCERNode
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
function getHighestCERNode(node: Node): Node | null
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Utility function that finds the highest node in the DOM tree that shares the same `customElementRegistry` as the provided node.
|
|
189
|
+
|
|
190
|
+
## How It Works
|
|
191
|
+
|
|
192
|
+
1. **Initialization**: When a MOSE element is constructed:
|
|
193
|
+
- Checks for duplicate elements in the same registry scope
|
|
194
|
+
- Copies mountobserver scripts from parent elements (inheritance)
|
|
195
|
+
- Sets up a MountObserver to watch for `<script type="mountobserver">` elements
|
|
196
|
+
|
|
197
|
+
2. **Script Processing**: When a mountobserver script is found:
|
|
198
|
+
- Loads external JSON if `src` attribute is present
|
|
199
|
+
- Parses inline JSON from `innerHTML`
|
|
200
|
+
- Merges configurations using assignGingerly
|
|
201
|
+
- Creates a MountObserver with the merged configuration
|
|
202
|
+
|
|
203
|
+
3. **Registry Scoping**:
|
|
204
|
+
- Uses `getHighestCERNode()` to find the appropriate scope
|
|
205
|
+
- Ensures only one MountObserver per highestCERNode
|
|
206
|
+
- Stores observer reference using `Symbol.for('cteH9dMG-UWwxVaMwFgvQA')`
|
|
207
|
+
|
|
208
|
+
## Example: Complete Setup
|
|
209
|
+
|
|
210
|
+
```html
|
|
211
|
+
<!DOCTYPE html>
|
|
212
|
+
<html>
|
|
213
|
+
<head>
|
|
214
|
+
<script type="importmap">
|
|
215
|
+
{
|
|
216
|
+
"imports": {
|
|
217
|
+
"mount-observer/": "/node_modules/mount-observer/",
|
|
218
|
+
"assign-gingerly/": "/node_modules/assign-gingerly/"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
<script type="module">
|
|
225
|
+
import { MOSE } from './MOSE.js';
|
|
226
|
+
|
|
227
|
+
class MyElement extends MOSE(HTMLElement) {
|
|
228
|
+
constructor() {
|
|
229
|
+
super();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
customElements.define('my-element', MyElement);
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
<my-element>
|
|
237
|
+
<script type="mountobserver">
|
|
238
|
+
{
|
|
239
|
+
"whereElementMatches": "button",
|
|
240
|
+
"assignGingerly": {
|
|
241
|
+
"disabled": false,
|
|
242
|
+
"?.dataset?.action": "submit",
|
|
243
|
+
"?.style": {
|
|
244
|
+
"color": "green",
|
|
245
|
+
"height": "25px"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
</script>
|
|
250
|
+
<button>I am here</button>
|
|
251
|
+
</my-element>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Result:
|
|
257
|
+
```html
|
|
258
|
+
<button
|
|
259
|
+
data-action="submit"
|
|
260
|
+
style="color: green; height: 25px;">
|
|
261
|
+
I am here
|
|
262
|
+
</button>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Dependencies
|
|
266
|
+
|
|
267
|
+
- [mount-observer](https://www.npmjs.com/package/mount-observer) - Core MountObserver functionality
|
|
268
|
+
- [assign-gingerly](https://www.npmjs.com/package/assign-gingerly) - Property assignment with nested support
|
|
269
|
+
|
|
270
|
+
## Browser Support
|
|
271
|
+
|
|
272
|
+
Requires Chrome 146+ with Scoped Custom Element Registry support enabled.
|
|
273
|
+
|
|
274
|
+
## Testing
|
|
275
|
+
|
|
276
|
+
Manual test files are available in the `tests/` directory:
|
|
277
|
+
|
|
278
|
+
- `tests/simple.html` - Basic usage example
|
|
279
|
+
- `tests/inheritance.html` - Script inheritance example
|
|
280
|
+
- `tests/exclude.html` - Exclude functionality example
|
|
281
|
+
- `tests/getHighestCERNode.html` - Registry scoping tests
|
|
282
|
+
|
|
283
|
+
Run the development server:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm run serve
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Then open test files in Chrome Canary with scoped custom element registries enabled.
|
|
290
|
+
|
|
291
|
+
## TypeScript
|
|
292
|
+
|
|
293
|
+
The package is written in TypeScript and includes type definitions. Both `.ts` and compiled `.js` files are included in the repository.
|
|
294
|
+
|
|
295
|
+
To compile:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
tsc
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Scope Custom Element Registry</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<my-element>
|
|
10
|
+
<template shadowrootmode="open">
|
|
11
|
+
<div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
</my-element>
|
|
15
|
+
<script>
|
|
16
|
+
const div = document.querySelector('my-element').shadowRoot.querySelector('div');
|
|
17
|
+
const section = document.createElement('section', {
|
|
18
|
+
customElementRegistry: new CustomElementRegistry()
|
|
19
|
+
});
|
|
20
|
+
div.appendChild(section);
|
|
21
|
+
</script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively traverses up the DOM tree to find the highest node
|
|
3
|
+
* that shares the same customElementRegistry as the passed in element.
|
|
4
|
+
*
|
|
5
|
+
* @param {Node} node - The starting node to check
|
|
6
|
+
* @returns {Node | null} The highest node with matching customElementRegistry, or null if node is invalid
|
|
7
|
+
*/
|
|
8
|
+
export function getHighestCERNode(node) {
|
|
9
|
+
if (!node) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const startRegistry = node.customElementRegistry;
|
|
13
|
+
let currentNode = node;
|
|
14
|
+
let highestMatch = node;
|
|
15
|
+
while (currentNode) {
|
|
16
|
+
// Check if current node has matching customElementRegistry
|
|
17
|
+
if (currentNode.customElementRegistry === startRegistry) {
|
|
18
|
+
highestMatch = currentNode;
|
|
19
|
+
}
|
|
20
|
+
// Try to get parent element first
|
|
21
|
+
const parent = currentNode.parentElement;
|
|
22
|
+
if (parent) {
|
|
23
|
+
currentNode = parent;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// If no parent element, check for rootNode (shadow root case)
|
|
27
|
+
const root = currentNode.getRootNode();
|
|
28
|
+
if (root && root !== currentNode && root !== document) {
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Reached the top
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return highestMatch;
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively traverses up the DOM tree to find the highest node
|
|
3
|
+
* that shares the same customElementRegistry as the passed in element.
|
|
4
|
+
*
|
|
5
|
+
* @param {Node} node - The starting node to check
|
|
6
|
+
* @returns {Node | null} The highest node with matching customElementRegistry, or null if node is invalid
|
|
7
|
+
*/
|
|
8
|
+
export function getHighestCERNode(node: Node): Node | null {
|
|
9
|
+
if (!node) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const startRegistry = (node as any).customElementRegistry;
|
|
14
|
+
let currentNode: Node | null = node;
|
|
15
|
+
let highestMatch: Node = node;
|
|
16
|
+
|
|
17
|
+
while (currentNode) {
|
|
18
|
+
// Check if current node has matching customElementRegistry
|
|
19
|
+
if ((currentNode as any).customElementRegistry === startRegistry) {
|
|
20
|
+
highestMatch = currentNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Try to get parent element first
|
|
24
|
+
const parent = (currentNode as any).parentElement as Element | null;
|
|
25
|
+
if (parent) {
|
|
26
|
+
currentNode = parent;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If no parent element, check for rootNode (shadow root case)
|
|
31
|
+
const root = currentNode.getRootNode();
|
|
32
|
+
if (root && root !== currentNode && root !== document) {
|
|
33
|
+
return root;
|
|
34
|
+
} else {
|
|
35
|
+
// Reached the top
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return highestMatch;
|
|
41
|
+
}
|
package/imports.html
ADDED
package/index.js
ADDED
package/index.ts
ADDED