mount-observer 0.1.12 → 0.1.14
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/DefineCustomElementHandler.js +99 -99
- package/ElementMountExtension.js +183 -8
- package/ElementMountExtension.ts +218 -11
- package/EnhanceMountedElementHandler.js +96 -96
- package/Events.js +27 -18
- package/Events.ts +18 -6
- package/EvtRt.js +16 -14
- package/EvtRt.ts +18 -15
- package/MountObserver.js +207 -71
- package/MountObserver.ts +234 -85
- package/README.md +1782 -251
- package/RegistryMountCoordinator.js +125 -0
- package/RegistryMountCoordinator.ts +181 -0
- package/connectionMonitor.js +1 -1
- package/connectionMonitor.ts +1 -1
- package/{getRootRegistryContainer.js → getRegistryRoot.js} +1 -1
- package/{getRootRegistryContainer.ts → getRegistryRoot.ts} +1 -1
- package/index.js +15 -10
- package/index.ts +15 -10
- package/mediaQuery.js +1 -1
- package/mediaQuery.ts +1 -1
- package/observedRootHas.js +87 -87
- package/package.json +67 -61
- package/playwright.config.ts +1 -0
- package/rootSizeObserver.js +1 -1
- package/rootSizeObserver.ts +1 -1
- package/upShadowSearch.js +67 -0
- package/upShadowSearch.ts +65 -0
- package/DefineCustomElementHandler.ts +0 -117
- package/EnhanceMountedElementHandler.ts +0 -111
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import { EvtRt } from './EvtRt.js';
|
|
2
|
-
export class DefineCustomElementHandler extends EvtRt {
|
|
3
|
-
mount(mountedElement, MountConfig, context) {
|
|
4
|
-
this.abort();
|
|
5
|
-
// Check if modules are specified
|
|
6
|
-
if (!context.modules || context.modules.length === 0) {
|
|
7
|
-
throw new Error('Must specify an ES Module');
|
|
8
|
-
}
|
|
9
|
-
const module = context.modules[0];
|
|
10
|
-
const tagName = mountedElement.localName;
|
|
11
|
-
// Check if already defined
|
|
12
|
-
if (customElements.get(tagName)) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
// Find suitable class
|
|
16
|
-
const ElementClass = this.findSuitableClass(module);
|
|
17
|
-
// Validate that ElementClass is a constructor
|
|
18
|
-
if (typeof ElementClass !== 'function') {
|
|
19
|
-
throw new Error(`Found class is not a constructor: ${typeof ElementClass}`);
|
|
20
|
-
}
|
|
21
|
-
// Create wrapper class to allow reuse
|
|
22
|
-
// Use anonymous class expression which works across all browsers
|
|
23
|
-
const WrapperClass = class extends ElementClass {
|
|
24
|
-
};
|
|
25
|
-
// Define the custom element using the define method
|
|
26
|
-
this.define(tagName, WrapperClass, mountedElement);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Define the custom element in the appropriate registry.
|
|
30
|
-
* Override this method in subclasses to use scoped registries.
|
|
31
|
-
* @param tagName - The custom element tag name
|
|
32
|
-
* @param ElementClass - The element class constructor
|
|
33
|
-
* @param mountedElement - The mounted element (used for scoped registry access)
|
|
34
|
-
*/
|
|
35
|
-
define(tagName, ElementClass, mountedElement) {
|
|
36
|
-
customElements.define(tagName, ElementClass);
|
|
37
|
-
}
|
|
38
|
-
findSuitableClass(module) {
|
|
39
|
-
// Check default export first
|
|
40
|
-
const defaultExport = module.default;
|
|
41
|
-
if (defaultExport && this.extendsHTMLElement(defaultExport)) {
|
|
42
|
-
return defaultExport;
|
|
43
|
-
}
|
|
44
|
-
// Find all exports that extend HTMLElement
|
|
45
|
-
const htmlElementClasses = Object.values(module)
|
|
46
|
-
.filter(exp => typeof exp === 'function' && this.extendsHTMLElement(exp));
|
|
47
|
-
if (htmlElementClasses.length === 0) {
|
|
48
|
-
throw new Error('No suitable class found in module');
|
|
49
|
-
}
|
|
50
|
-
if (htmlElementClasses.length > 1) {
|
|
51
|
-
throw new Error('More than one class found in module');
|
|
52
|
-
}
|
|
53
|
-
return htmlElementClasses[0];
|
|
54
|
-
}
|
|
55
|
-
extendsHTMLElement(cls) {
|
|
56
|
-
try {
|
|
57
|
-
// Must be a function
|
|
58
|
-
if (typeof cls !== 'function') {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
// Handle direct HTMLElement export
|
|
62
|
-
if (cls === HTMLElement) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
// Check if it has a prototype and extends HTMLElement
|
|
66
|
-
if (cls.prototype && cls.prototype instanceof HTMLElement) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Handler for defining custom elements in scoped registries.
|
|
78
|
-
* Uses the element's customElementRegistry property to define elements
|
|
79
|
-
* in the appropriate scoped registry instead of the global registry.
|
|
80
|
-
*/
|
|
81
|
-
export class DefineScopedCustomElementHandler extends DefineCustomElementHandler {
|
|
82
|
-
/**
|
|
83
|
-
* Define the custom element in the element's scoped registry.
|
|
84
|
-
* @param tagName - The custom element tag name
|
|
85
|
-
* @param ElementClass - The element class constructor
|
|
86
|
-
* @param mountedElement - The mounted element with customElementRegistry
|
|
87
|
-
*/
|
|
88
|
-
define(tagName, ElementClass, mountedElement) {
|
|
89
|
-
const registry = mountedElement.customElementRegistry;
|
|
90
|
-
if (!registry) {
|
|
91
|
-
throw new Error('Element does not have a customElementRegistry. Scoped registries require Chrome 146+ or latest WebKit/Safari.');
|
|
92
|
-
}
|
|
93
|
-
// Check if already defined in this scoped registry
|
|
94
|
-
if (registry.get(tagName)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
registry.define(tagName, ElementClass);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
1
|
+
import { EvtRt } from './EvtRt.js';
|
|
2
|
+
export class DefineCustomElementHandler extends EvtRt {
|
|
3
|
+
mount(mountedElement, MountConfig, context) {
|
|
4
|
+
this.abort();
|
|
5
|
+
// Check if modules are specified
|
|
6
|
+
if (!context.modules || context.modules.length === 0) {
|
|
7
|
+
throw new Error('Must specify an ES Module');
|
|
8
|
+
}
|
|
9
|
+
const module = context.modules[0];
|
|
10
|
+
const tagName = mountedElement.localName;
|
|
11
|
+
// Check if already defined
|
|
12
|
+
if (customElements.get(tagName)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Find suitable class
|
|
16
|
+
const ElementClass = this.findSuitableClass(module);
|
|
17
|
+
// Validate that ElementClass is a constructor
|
|
18
|
+
if (typeof ElementClass !== 'function') {
|
|
19
|
+
throw new Error(`Found class is not a constructor: ${typeof ElementClass}`);
|
|
20
|
+
}
|
|
21
|
+
// Create wrapper class to allow reuse
|
|
22
|
+
// Use anonymous class expression which works across all browsers
|
|
23
|
+
const WrapperClass = class extends ElementClass {
|
|
24
|
+
};
|
|
25
|
+
// Define the custom element using the define method
|
|
26
|
+
this.define(tagName, WrapperClass, mountedElement);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Define the custom element in the appropriate registry.
|
|
30
|
+
* Override this method in subclasses to use scoped registries.
|
|
31
|
+
* @param tagName - The custom element tag name
|
|
32
|
+
* @param ElementClass - The element class constructor
|
|
33
|
+
* @param mountedElement - The mounted element (used for scoped registry access)
|
|
34
|
+
*/
|
|
35
|
+
define(tagName, ElementClass, mountedElement) {
|
|
36
|
+
customElements.define(tagName, ElementClass);
|
|
37
|
+
}
|
|
38
|
+
findSuitableClass(module) {
|
|
39
|
+
// Check default export first
|
|
40
|
+
const defaultExport = module.default;
|
|
41
|
+
if (defaultExport && this.extendsHTMLElement(defaultExport)) {
|
|
42
|
+
return defaultExport;
|
|
43
|
+
}
|
|
44
|
+
// Find all exports that extend HTMLElement
|
|
45
|
+
const htmlElementClasses = Object.values(module)
|
|
46
|
+
.filter(exp => typeof exp === 'function' && this.extendsHTMLElement(exp));
|
|
47
|
+
if (htmlElementClasses.length === 0) {
|
|
48
|
+
throw new Error('No suitable class found in module');
|
|
49
|
+
}
|
|
50
|
+
if (htmlElementClasses.length > 1) {
|
|
51
|
+
throw new Error('More than one class found in module');
|
|
52
|
+
}
|
|
53
|
+
return htmlElementClasses[0];
|
|
54
|
+
}
|
|
55
|
+
extendsHTMLElement(cls) {
|
|
56
|
+
try {
|
|
57
|
+
// Must be a function
|
|
58
|
+
if (typeof cls !== 'function') {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// Handle direct HTMLElement export
|
|
62
|
+
if (cls === HTMLElement) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Check if it has a prototype and extends HTMLElement
|
|
66
|
+
if (cls.prototype && cls.prototype instanceof HTMLElement) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Handler for defining custom elements in scoped registries.
|
|
78
|
+
* Uses the element's customElementRegistry property to define elements
|
|
79
|
+
* in the appropriate scoped registry instead of the global registry.
|
|
80
|
+
*/
|
|
81
|
+
export class DefineScopedCustomElementHandler extends DefineCustomElementHandler {
|
|
82
|
+
/**
|
|
83
|
+
* Define the custom element in the element's scoped registry.
|
|
84
|
+
* @param tagName - The custom element tag name
|
|
85
|
+
* @param ElementClass - The element class constructor
|
|
86
|
+
* @param mountedElement - The mounted element with customElementRegistry
|
|
87
|
+
*/
|
|
88
|
+
define(tagName, ElementClass, mountedElement) {
|
|
89
|
+
const registry = mountedElement.customElementRegistry;
|
|
90
|
+
if (!registry) {
|
|
91
|
+
throw new Error('Element does not have a customElementRegistry. Scoped registries require Chrome 146+ or latest WebKit/Safari.');
|
|
92
|
+
}
|
|
93
|
+
// Check if already defined in this scoped registry
|
|
94
|
+
if (registry.get(tagName)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
registry.define(tagName, ElementClass);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/ElementMountExtension.js
CHANGED
|
@@ -3,22 +3,100 @@
|
|
|
3
3
|
* This finds the appropriate scoped registry container and observes it.
|
|
4
4
|
*/
|
|
5
5
|
import { MountObserver } from './MountObserver.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getRegistryRoot } from './getRegistryRoot.js';
|
|
7
|
+
import { getOrInsertObserverEntry } from './RegistryMountCoordinator.js';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Registry for tracking MountConfig objects associated with a CustomElementRegistry.
|
|
10
|
+
* This enables coordination of mount observers across multiple DOM scopes that share
|
|
11
|
+
* the same registry.
|
|
12
|
+
*/
|
|
13
|
+
export class MountConfigRegistry extends EventTarget {
|
|
14
|
+
#items = new Set();
|
|
15
|
+
get items() {
|
|
16
|
+
return Array.from(this.#items);
|
|
17
|
+
}
|
|
18
|
+
push(items) {
|
|
19
|
+
if (Array.isArray(items)) {
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
this.#items.add(item);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
this.#items.add(items);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Add mountConfigRegistry property to CustomElementRegistry prototype
|
|
30
|
+
if (typeof CustomElementRegistry !== 'undefined') {
|
|
31
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'mountConfigRegistry', {
|
|
32
|
+
get: function () {
|
|
33
|
+
// Create a new MountConfigRegistry instance on first access and cache it
|
|
34
|
+
const registry = new MountConfigRegistry();
|
|
35
|
+
// Replace the getter with the actual value
|
|
36
|
+
Object.defineProperty(this, 'mountConfigRegistry', {
|
|
37
|
+
value: registry,
|
|
38
|
+
writable: true,
|
|
39
|
+
enumerable: false,
|
|
40
|
+
configurable: true,
|
|
41
|
+
});
|
|
42
|
+
return registry;
|
|
43
|
+
},
|
|
44
|
+
enumerable: false,
|
|
45
|
+
configurable: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Adds a mount method to Node.prototype that works for Element, ShadowRoot, and Document.
|
|
50
|
+
* This provides a unified API for mounting observers on any node type.
|
|
51
|
+
*
|
|
52
|
+
* For Elements:
|
|
9
53
|
* 1. Determines the observation scope based on options.scope
|
|
10
54
|
* 2. Creates a MountObserver with the provided config
|
|
11
|
-
* 3. Observes that scope
|
|
12
|
-
* 4. Returns the
|
|
55
|
+
* 3. Observes that scope with registry coordination
|
|
56
|
+
* 4. Returns the node for chaining
|
|
57
|
+
*
|
|
58
|
+
* For ShadowRoot and Document:
|
|
59
|
+
* 1. Observes the node directly (no registry coordination)
|
|
60
|
+
* 2. Returns the node for chaining
|
|
13
61
|
*/
|
|
14
|
-
Object.defineProperty(
|
|
62
|
+
Object.defineProperty(Node.prototype, 'mount', {
|
|
15
63
|
value: async function (config, options = {}) {
|
|
16
|
-
|
|
64
|
+
// For ShadowRoot and Document, observe directly
|
|
65
|
+
if (this instanceof ShadowRoot || this instanceof Document) {
|
|
66
|
+
const mo = new MountObserver(config, options);
|
|
67
|
+
await mo.observe(this);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
// For Element, use the robust registry-aware logic
|
|
71
|
+
if (!(this instanceof Element)) {
|
|
72
|
+
throw new Error('mount() can only be called on Element, ShadowRoot, or Document');
|
|
73
|
+
}
|
|
74
|
+
const scope = options.scope ?? 'registry'; // NEW DEFAULT
|
|
17
75
|
let thingToObserve;
|
|
18
76
|
if (scope === 'registry') {
|
|
19
|
-
|
|
77
|
+
// Find this element's registry root
|
|
78
|
+
const registryContainer = getRegistryRoot(this);
|
|
79
|
+
if (!registryContainer) {
|
|
80
|
+
throw new Error('Could not find registry root');
|
|
81
|
+
}
|
|
82
|
+
thingToObserve = registryContainer;
|
|
83
|
+
// Get the registry for coordination
|
|
84
|
+
const registry = this.customElementRegistry;
|
|
85
|
+
// Register with coordinator if registry exists
|
|
86
|
+
if (registry) {
|
|
87
|
+
await getOrInsertObserverEntry(registry, config, thingToObserve);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// No registry, just create a standalone observer
|
|
91
|
+
const mo = new MountObserver(config, options);
|
|
92
|
+
await mo.observe(thingToObserve);
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
else if (scope === 'registryRoot') {
|
|
97
|
+
const registryContainer = getRegistryRoot(this);
|
|
20
98
|
if (!registryContainer) {
|
|
21
|
-
throw new Error('Could not find
|
|
99
|
+
throw new Error('Could not find registry root');
|
|
22
100
|
}
|
|
23
101
|
thingToObserve = registryContainer;
|
|
24
102
|
}
|
|
@@ -47,3 +125,100 @@ Object.defineProperty(Element.prototype, 'mount', {
|
|
|
47
125
|
enumerable: false,
|
|
48
126
|
configurable: true,
|
|
49
127
|
});
|
|
128
|
+
/**
|
|
129
|
+
* Adds a mountScope method to Element.prototype that:
|
|
130
|
+
* 1. Finds the registry root for this element
|
|
131
|
+
* 2. Gets all active configs for this registry
|
|
132
|
+
* 3. Creates new MountObservers for each config to observe this scope
|
|
133
|
+
*/
|
|
134
|
+
Object.defineProperty(Element.prototype, 'mountScope', {
|
|
135
|
+
value: async function () {
|
|
136
|
+
const registry = this.customElementRegistry;
|
|
137
|
+
if (!registry) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Find the root of this scope
|
|
141
|
+
const registryRoot = getRegistryRoot(this);
|
|
142
|
+
if (!registryRoot) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Get all configs for this registry
|
|
146
|
+
const configs = registry.mountConfigRegistry.items;
|
|
147
|
+
// For each config, ensure an observer exists for this registry root
|
|
148
|
+
for (const config of configs) {
|
|
149
|
+
await getOrInsertObserverEntry(registry, config, registryRoot);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
writable: true,
|
|
153
|
+
enumerable: false,
|
|
154
|
+
configurable: true,
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* Adds a mountGlobally method to Node.prototype that works for Element, ShadowRoot, and Document.
|
|
158
|
+
*
|
|
159
|
+
* For Elements:
|
|
160
|
+
* 1. Mounts the config in the current registry
|
|
161
|
+
* 2. Creates propagators to automatically mount in:
|
|
162
|
+
* - Elements with different custom element registries
|
|
163
|
+
* - Shadow roots within the same registry
|
|
164
|
+
*
|
|
165
|
+
* For ShadowRoot and Document:
|
|
166
|
+
* 1. Mounts in the current node
|
|
167
|
+
* 2. Creates propagators for child registries and shadow roots
|
|
168
|
+
*
|
|
169
|
+
* This enables "viral" propagation of mount observers across registry boundaries,
|
|
170
|
+
* useful for bootstrapping core handlers like builtIns.mountObserverScript.
|
|
171
|
+
*/
|
|
172
|
+
Object.defineProperty(Node.prototype, 'mountGlobally', {
|
|
173
|
+
value: async function (config, options = {}) {
|
|
174
|
+
// Mount in current node first
|
|
175
|
+
await this.mount(config, options);
|
|
176
|
+
// Propagator 1: Watch for elements in different registries
|
|
177
|
+
const crossCustomElementRegistryPropagator = new MountObserver({
|
|
178
|
+
matching: '*',
|
|
179
|
+
whereDifferentCustomElementRegistry: true,
|
|
180
|
+
do: async (el) => {
|
|
181
|
+
// Wait for custom element to be defined so it has the chance to add shadowRoot
|
|
182
|
+
const { localName } = el;
|
|
183
|
+
if (localName.includes('-')) {
|
|
184
|
+
const registry = el.customElementRegistry;
|
|
185
|
+
if (registry && typeof registry.whenDefined === 'function') {
|
|
186
|
+
await registry.whenDefined(localName);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const shadowRoot = el.shadowRoot;
|
|
190
|
+
if (shadowRoot) {
|
|
191
|
+
// Use mountGlobally to propagate recursively
|
|
192
|
+
await shadowRoot.mountGlobally(config, options);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// No shadow root, mount on element
|
|
196
|
+
await el.mount(config, options);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, options);
|
|
200
|
+
await crossCustomElementRegistryPropagator.observe(this);
|
|
201
|
+
// Propagator 2: Watch for shadow roots within the same registry
|
|
202
|
+
const crossShadowRootPropagator = new MountObserver({
|
|
203
|
+
matching: '*',
|
|
204
|
+
whereLocalNameMatches: /-/,
|
|
205
|
+
do: async (el) => {
|
|
206
|
+
const { localName } = el;
|
|
207
|
+
const registry = el.customElementRegistry;
|
|
208
|
+
if (registry && typeof registry.whenDefined === 'function') {
|
|
209
|
+
await registry.whenDefined(localName);
|
|
210
|
+
}
|
|
211
|
+
const shadowRoot = el.shadowRoot;
|
|
212
|
+
if (shadowRoot === null)
|
|
213
|
+
return;
|
|
214
|
+
// Use mountGlobally to propagate recursively
|
|
215
|
+
await shadowRoot.mountGlobally(config, options);
|
|
216
|
+
}
|
|
217
|
+
}, options);
|
|
218
|
+
await crossShadowRootPropagator.observe(this);
|
|
219
|
+
return this;
|
|
220
|
+
},
|
|
221
|
+
writable: true,
|
|
222
|
+
enumerable: false,
|
|
223
|
+
configurable: true,
|
|
224
|
+
});
|
package/ElementMountExtension.ts
CHANGED
|
@@ -4,39 +4,137 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { MountObserver } from './MountObserver.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getRegistryRoot } from './getRegistryRoot.js';
|
|
8
|
+
import { getOrInsertObserverEntry } from './RegistryMountCoordinator.js';
|
|
8
9
|
import type { MountConfig, MountObserverOptions } from './types/mount-observer/types.js';
|
|
9
10
|
|
|
10
11
|
declare global {
|
|
11
|
-
interface
|
|
12
|
-
mount<T extends
|
|
12
|
+
interface Node {
|
|
13
|
+
mount<T extends Node>(
|
|
13
14
|
this: T,
|
|
14
15
|
config: MountConfig,
|
|
15
16
|
options?: MountObserverOptions
|
|
16
17
|
): Promise<T>;
|
|
18
|
+
|
|
19
|
+
mountGlobally<T extends Node>(
|
|
20
|
+
this: T,
|
|
21
|
+
config: MountConfig,
|
|
22
|
+
options?: MountObserverOptions
|
|
23
|
+
): Promise<T>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Element {
|
|
27
|
+
mountScope(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CustomElementRegistry {
|
|
31
|
+
mountConfigRegistry: MountConfigRegistry;
|
|
17
32
|
}
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
/**
|
|
21
|
-
*
|
|
36
|
+
* Registry for tracking MountConfig objects associated with a CustomElementRegistry.
|
|
37
|
+
* This enables coordination of mount observers across multiple DOM scopes that share
|
|
38
|
+
* the same registry.
|
|
39
|
+
*/
|
|
40
|
+
export class MountConfigRegistry extends EventTarget {
|
|
41
|
+
#items: Set<MountConfig> = new Set();
|
|
42
|
+
|
|
43
|
+
get items(): MountConfig[] {
|
|
44
|
+
return Array.from(this.#items);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
push(items: MountConfig | MountConfig[]): void {
|
|
48
|
+
if (Array.isArray(items)) {
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
this.#items.add(item);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
this.#items.add(items);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add mountConfigRegistry property to CustomElementRegistry prototype
|
|
59
|
+
if (typeof CustomElementRegistry !== 'undefined') {
|
|
60
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'mountConfigRegistry', {
|
|
61
|
+
get: function () {
|
|
62
|
+
// Create a new MountConfigRegistry instance on first access and cache it
|
|
63
|
+
const registry = new MountConfigRegistry();
|
|
64
|
+
// Replace the getter with the actual value
|
|
65
|
+
Object.defineProperty(this, 'mountConfigRegistry', {
|
|
66
|
+
value: registry,
|
|
67
|
+
writable: true,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
configurable: true,
|
|
70
|
+
});
|
|
71
|
+
return registry;
|
|
72
|
+
},
|
|
73
|
+
enumerable: false,
|
|
74
|
+
configurable: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adds a mount method to Node.prototype that works for Element, ShadowRoot, and Document.
|
|
80
|
+
* This provides a unified API for mounting observers on any node type.
|
|
81
|
+
*
|
|
82
|
+
* For Elements:
|
|
22
83
|
* 1. Determines the observation scope based on options.scope
|
|
23
84
|
* 2. Creates a MountObserver with the provided config
|
|
24
|
-
* 3. Observes that scope
|
|
25
|
-
* 4. Returns the
|
|
85
|
+
* 3. Observes that scope with registry coordination
|
|
86
|
+
* 4. Returns the node for chaining
|
|
87
|
+
*
|
|
88
|
+
* For ShadowRoot and Document:
|
|
89
|
+
* 1. Observes the node directly (no registry coordination)
|
|
90
|
+
* 2. Returns the node for chaining
|
|
26
91
|
*/
|
|
27
|
-
Object.defineProperty(
|
|
28
|
-
value: async function <T extends
|
|
92
|
+
Object.defineProperty(Node.prototype, 'mount', {
|
|
93
|
+
value: async function <T extends Node>(
|
|
29
94
|
this: T,
|
|
30
95
|
config: MountConfig,
|
|
31
96
|
options: MountObserverOptions = {}
|
|
32
97
|
): Promise<T> {
|
|
33
|
-
|
|
98
|
+
// For ShadowRoot and Document, observe directly
|
|
99
|
+
if (this instanceof ShadowRoot || this instanceof Document) {
|
|
100
|
+
const mo = new MountObserver(config, options);
|
|
101
|
+
await mo.observe(this);
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For Element, use the robust registry-aware logic
|
|
106
|
+
if (!(this instanceof Element)) {
|
|
107
|
+
throw new Error('mount() can only be called on Element, ShadowRoot, or Document');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const scope = options.scope ?? 'registry'; // NEW DEFAULT
|
|
34
111
|
let thingToObserve: Node;
|
|
35
112
|
|
|
36
113
|
if (scope === 'registry') {
|
|
37
|
-
|
|
114
|
+
// Find this element's registry root
|
|
115
|
+
const registryContainer = getRegistryRoot(this);
|
|
38
116
|
if (!registryContainer) {
|
|
39
|
-
throw new Error('Could not find
|
|
117
|
+
throw new Error('Could not find registry root');
|
|
118
|
+
}
|
|
119
|
+
thingToObserve = registryContainer;
|
|
120
|
+
|
|
121
|
+
// Get the registry for coordination
|
|
122
|
+
const registry = (this as any).customElementRegistry;
|
|
123
|
+
|
|
124
|
+
// Register with coordinator if registry exists
|
|
125
|
+
if (registry) {
|
|
126
|
+
await getOrInsertObserverEntry(registry, config, thingToObserve);
|
|
127
|
+
} else {
|
|
128
|
+
// No registry, just create a standalone observer
|
|
129
|
+
const mo = new MountObserver(config, options);
|
|
130
|
+
await mo.observe(thingToObserve);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return this;
|
|
134
|
+
} else if (scope === 'registryRoot') {
|
|
135
|
+
const registryContainer = getRegistryRoot(this);
|
|
136
|
+
if (!registryContainer) {
|
|
137
|
+
throw new Error('Could not find registry root');
|
|
40
138
|
}
|
|
41
139
|
thingToObserve = registryContainer;
|
|
42
140
|
} else if (scope === 'self') {
|
|
@@ -62,3 +160,112 @@ Object.defineProperty(Element.prototype, 'mount', {
|
|
|
62
160
|
enumerable: false,
|
|
63
161
|
configurable: true,
|
|
64
162
|
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Adds a mountScope method to Element.prototype that:
|
|
166
|
+
* 1. Finds the registry root for this element
|
|
167
|
+
* 2. Gets all active configs for this registry
|
|
168
|
+
* 3. Creates new MountObservers for each config to observe this scope
|
|
169
|
+
*/
|
|
170
|
+
Object.defineProperty(Element.prototype, 'mountScope', {
|
|
171
|
+
value: async function(): Promise<void> {
|
|
172
|
+
const registry = (this as any).customElementRegistry;
|
|
173
|
+
if (!registry) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find the root of this scope
|
|
178
|
+
const registryRoot = getRegistryRoot(this);
|
|
179
|
+
if (!registryRoot) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Get all configs for this registry
|
|
184
|
+
const configs = registry.mountConfigRegistry.items;
|
|
185
|
+
|
|
186
|
+
// For each config, ensure an observer exists for this registry root
|
|
187
|
+
for (const config of configs) {
|
|
188
|
+
await getOrInsertObserverEntry(registry, config, registryRoot);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
writable: true,
|
|
192
|
+
enumerable: false,
|
|
193
|
+
configurable: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Adds a mountGlobally method to Node.prototype that works for Element, ShadowRoot, and Document.
|
|
198
|
+
*
|
|
199
|
+
* For Elements:
|
|
200
|
+
* 1. Mounts the config in the current registry
|
|
201
|
+
* 2. Creates propagators to automatically mount in:
|
|
202
|
+
* - Elements with different custom element registries
|
|
203
|
+
* - Shadow roots within the same registry
|
|
204
|
+
*
|
|
205
|
+
* For ShadowRoot and Document:
|
|
206
|
+
* 1. Mounts in the current node
|
|
207
|
+
* 2. Creates propagators for child registries and shadow roots
|
|
208
|
+
*
|
|
209
|
+
* This enables "viral" propagation of mount observers across registry boundaries,
|
|
210
|
+
* useful for bootstrapping core handlers like builtIns.mountObserverScript.
|
|
211
|
+
*/
|
|
212
|
+
Object.defineProperty(Node.prototype, 'mountGlobally', {
|
|
213
|
+
value: async function <T extends Node>(
|
|
214
|
+
this: T,
|
|
215
|
+
config: MountConfig,
|
|
216
|
+
options: MountObserverOptions = {}
|
|
217
|
+
): Promise<T> {
|
|
218
|
+
// Mount in current node first
|
|
219
|
+
await this.mount(config, options);
|
|
220
|
+
|
|
221
|
+
// Propagator 1: Watch for elements in different registries
|
|
222
|
+
const crossCustomElementRegistryPropagator = new MountObserver({
|
|
223
|
+
matching: '*',
|
|
224
|
+
whereDifferentCustomElementRegistry: true,
|
|
225
|
+
do: async (el: Element) => {
|
|
226
|
+
// Wait for custom element to be defined so it has the chance to add shadowRoot
|
|
227
|
+
const { localName } = el;
|
|
228
|
+
if (localName.includes('-')) {
|
|
229
|
+
const registry = (el as any).customElementRegistry;
|
|
230
|
+
if (registry && typeof registry.whenDefined === 'function') {
|
|
231
|
+
await registry.whenDefined(localName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const shadowRoot = (el as any).shadowRoot;
|
|
235
|
+
if (shadowRoot) {
|
|
236
|
+
// Use mountGlobally to propagate recursively
|
|
237
|
+
await shadowRoot.mountGlobally(config, options);
|
|
238
|
+
} else {
|
|
239
|
+
// No shadow root, mount on element
|
|
240
|
+
await el.mount(config, options);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}, options);
|
|
244
|
+
|
|
245
|
+
await crossCustomElementRegistryPropagator.observe(this);
|
|
246
|
+
|
|
247
|
+
// Propagator 2: Watch for shadow roots within the same registry
|
|
248
|
+
const crossShadowRootPropagator = new MountObserver({
|
|
249
|
+
matching: '*',
|
|
250
|
+
whereLocalNameMatches: /-/,
|
|
251
|
+
do: async (el: Element) => {
|
|
252
|
+
const { localName } = el;
|
|
253
|
+
const registry = (el as any).customElementRegistry;
|
|
254
|
+
if (registry && typeof registry.whenDefined === 'function') {
|
|
255
|
+
await registry.whenDefined(localName);
|
|
256
|
+
}
|
|
257
|
+
const shadowRoot = (el as any).shadowRoot;
|
|
258
|
+
if (shadowRoot === null) return;
|
|
259
|
+
// Use mountGlobally to propagate recursively
|
|
260
|
+
await shadowRoot.mountGlobally(config, options);
|
|
261
|
+
}
|
|
262
|
+
}, options);
|
|
263
|
+
|
|
264
|
+
await crossShadowRootPropagator.observe(this);
|
|
265
|
+
|
|
266
|
+
return this;
|
|
267
|
+
},
|
|
268
|
+
writable: true,
|
|
269
|
+
enumerable: false,
|
|
270
|
+
configurable: true,
|
|
271
|
+
});
|