mount-observer 0.1.14 → 0.1.16
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/ElementMountExtension.js +5 -2
- package/ElementMountExtension.ts +7 -2
- package/MountObserver.js +3 -0
- package/MountObserver.ts +3 -0
- package/RegistryMountCoordinator.js +5 -5
- package/RegistryMountCoordinator.ts +8 -6
- package/{DefineCustomElementHandler.js → handlers/DefineCustomElement.js} +103 -99
- package/handlers/DefineCustomElement.ts +123 -0
- package/{EnhanceMountedElementHandler.js → handlers/EnhanceMountedElement.js} +109 -96
- package/handlers/EnhanceMountedElement.ts +126 -0
- package/handlers/Events.js +110 -0
- package/handlers/EvtRt.js +59 -0
- package/handlers/GenIds.js +37 -0
- package/handlers/GenIds.ts +45 -0
- package/handlers/HTMLInclude.js +393 -0
- package/handlers/HTMLInclude.ts +453 -0
- package/handlers/HoistTemplate.js +77 -0
- package/handlers/HoistTemplate.ts +89 -0
- package/handlers/MountObserver.js +941 -0
- package/handlers/MountObserverScript.js +78 -0
- package/handlers/MountObserverScript.ts +89 -0
- package/handlers/ScriptExport.js +83 -0
- package/handlers/ScriptExport.ts +97 -0
- package/handlers/SharedMutationObserver.js +78 -0
- package/handlers/arr.js +16 -0
- package/handlers/connectionMonitor.js +122 -0
- package/handlers/elementIntersection.js +73 -0
- package/handlers/emitEvents.js +187 -0
- package/handlers/getRegistryRoot.js +52 -0
- package/handlers/loadImports.js +129 -0
- package/handlers/mediaQuery.js +90 -0
- package/handlers/rootSizeObserver.js +131 -0
- package/handlers/upShadowSearch.js +70 -0
- package/handlers/withScopePerimeter.js +22 -0
- package/package.json +12 -2
- package/types/assign-gingerly/types.d.ts +244 -0
- package/types/be-a-beacon/types.d.ts +3 -0
- package/types/global.d.ts +29 -0
- package/types/id-generation/types.d.ts +26 -0
- package/types/mount-observer/types.d.ts +332 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { EvtRt } from '../EvtRt.js';
|
|
2
|
+
import type { MountContext, MountConfig } from '../types/mount-observer/types';
|
|
3
|
+
import { upShadowSearch } from '../upShadowSearch.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cache for element lookups by ID.
|
|
7
|
+
* Maps root nodes to a map of ID -> WeakRef<Element> for performance.
|
|
8
|
+
*/
|
|
9
|
+
const idCache = new WeakMap<Node, Map<string, WeakRef<Element>>>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tracks IDs currently being processed to detect circular references.
|
|
13
|
+
*/
|
|
14
|
+
const processingStack = new Set<string>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Splits a space-separated string of attribute names into an array.
|
|
18
|
+
*/
|
|
19
|
+
function splitRefs(refs: string): string[] {
|
|
20
|
+
return refs
|
|
21
|
+
.split(' ')
|
|
22
|
+
.map(s => s.trim())
|
|
23
|
+
.filter(s => !!s);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a CSS selector from an element's attributes, classes, and tag name.
|
|
28
|
+
* Excludes the -i attribute and any attributes listed in -i from the selector.
|
|
29
|
+
*/
|
|
30
|
+
function toQuery(el: Element): string {
|
|
31
|
+
// Get the list of attributes to exclude from the selector
|
|
32
|
+
const insertAttrs = el.getAttribute('-i');
|
|
33
|
+
const excludeAttrs = new Set(['-i']); // Always exclude -i itself
|
|
34
|
+
|
|
35
|
+
if (insertAttrs !== null) {
|
|
36
|
+
const attrs = splitRefs(insertAttrs);
|
|
37
|
+
attrs.forEach(attr => excludeAttrs.add(attr));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const classes = Array.from(el.classList).map(c => `.${c}`).join('');
|
|
41
|
+
const parts = Array.from(el.part).map(p => `[part~="${p}"]`).join('');
|
|
42
|
+
const attributes = Array.from(el.attributes)
|
|
43
|
+
.filter(attr => !excludeAttrs.has(attr.name))
|
|
44
|
+
.map(attr => `[${attr.name}="${attr.value}"]`)
|
|
45
|
+
.join('');
|
|
46
|
+
const {localName} = el;
|
|
47
|
+
return `${localName}${classes}${parts}${attributes}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prepares an element for insertion by extracting its children and insertion attributes.
|
|
52
|
+
* Returns a DocumentFragment with the children and a map of attributes to insert.
|
|
53
|
+
*/
|
|
54
|
+
function prepareForInsertion(el: Element): { fragment: DocumentFragment, attributeMap: {[key: string]: string} | null } {
|
|
55
|
+
const fragment = new DocumentFragment();
|
|
56
|
+
const clone = el.cloneNode(true) as Element;
|
|
57
|
+
|
|
58
|
+
// Move all children to the fragment
|
|
59
|
+
while (clone.firstChild) {
|
|
60
|
+
fragment.appendChild(clone.firstChild);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for -i attribute which specifies which attributes to insert
|
|
64
|
+
const insertAttrs = el.getAttribute('-i');
|
|
65
|
+
let attributeMap: {[key: string]: string} | null = null;
|
|
66
|
+
|
|
67
|
+
if (insertAttrs !== null) {
|
|
68
|
+
const attrs = splitRefs(insertAttrs);
|
|
69
|
+
attributeMap = {};
|
|
70
|
+
for (const attr of attrs) {
|
|
71
|
+
const value = el.getAttribute(attr);
|
|
72
|
+
if (value !== null) {
|
|
73
|
+
attributeMap[attr] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { fragment, attributeMap };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Applies insertion to a matched element by replacing its children and updating attributes.
|
|
83
|
+
*/
|
|
84
|
+
function applyInsertion(
|
|
85
|
+
targetElement: Element,
|
|
86
|
+
sourceFragment: DocumentFragment,
|
|
87
|
+
attributeMap: {[key: string]: string} | null
|
|
88
|
+
): void {
|
|
89
|
+
// Clone the fragment so it can be reused
|
|
90
|
+
const fragmentClone = sourceFragment.cloneNode(true) as DocumentFragment;
|
|
91
|
+
|
|
92
|
+
// Replace all children of the target element
|
|
93
|
+
targetElement.replaceChildren(fragmentClone);
|
|
94
|
+
|
|
95
|
+
// Update attributes if specified
|
|
96
|
+
if (attributeMap !== null) {
|
|
97
|
+
for (const key in attributeMap) {
|
|
98
|
+
const value = attributeMap[key];
|
|
99
|
+
targetElement.setAttribute(key, value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handler that enables HTML fragment reuse via template[src="#id"] syntax.
|
|
106
|
+
*
|
|
107
|
+
* This handler allows declarative reuse of HTML fragments by cloning content from
|
|
108
|
+
* any element with an ID. It's similar to JavaScript constants for HTML.
|
|
109
|
+
*
|
|
110
|
+
* Features:
|
|
111
|
+
* - Clones content from templates (including hoisted templates with remoteContent)
|
|
112
|
+
* - Clones any element with an ID
|
|
113
|
+
* - Supports matching insertions: template children can match and modify cloned content
|
|
114
|
+
* - Caches lookups for performance (useful for repeated references like periodic tables)
|
|
115
|
+
* - Detects circular references
|
|
116
|
+
* - Searches across shadow DOM boundaries
|
|
117
|
+
*
|
|
118
|
+
* Matching Insertions:
|
|
119
|
+
* When a template has children, they are used to match elements in the cloned content
|
|
120
|
+
* and replace their children/attributes. This enables partial updates and "nulling out" content.
|
|
121
|
+
*
|
|
122
|
+
* The -i attribute specifies which attributes to insert/update on matched elements.
|
|
123
|
+
*
|
|
124
|
+
* @example Basic usage
|
|
125
|
+
* ```html
|
|
126
|
+
* <div id="reusable">
|
|
127
|
+
* <p>This content can be reused</p>
|
|
128
|
+
* </div>
|
|
129
|
+
*
|
|
130
|
+
* <template src="#reusable"></template>
|
|
131
|
+
* <!-- Results in: <div><p>This content can be reused</p></div> -->
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example Matching insertions
|
|
135
|
+
* ```html
|
|
136
|
+
* <div itemscope id="love">
|
|
137
|
+
* <data value="false" itemprop="todayIsFriday">It's Thursday</data>
|
|
138
|
+
* </div>
|
|
139
|
+
*
|
|
140
|
+
* <template src="#love">
|
|
141
|
+
* <data value="true" itemprop="todayIsFriday" -i="value"></data>
|
|
142
|
+
* </template>
|
|
143
|
+
* <!-- Results in:
|
|
144
|
+
* <div itemscope>
|
|
145
|
+
* <data value="true" itemprop="todayIsFriday">It's Thursday</data>
|
|
146
|
+
* </div>
|
|
147
|
+
* The matched element's value attribute is updated, but children are replaced
|
|
148
|
+
* -->
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export class HTMLIncludeHandler extends EvtRt {
|
|
152
|
+
static matching = 'template[src^="#"]';
|
|
153
|
+
static whereInstanceOf = HTMLTemplateElement;
|
|
154
|
+
|
|
155
|
+
async mount(mountedElement: Element): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
const template = mountedElement as HTMLTemplateElement;
|
|
158
|
+
const src = template.getAttribute('src');
|
|
159
|
+
|
|
160
|
+
if (!src || !src.startsWith('#')) {
|
|
161
|
+
console.warn('HTMLInclude: Invalid src attribute, must start with #');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const id = src.substring(1);
|
|
166
|
+
|
|
167
|
+
// Try cache first
|
|
168
|
+
const rootNode = template.getRootNode() as Node;
|
|
169
|
+
let sourceElement = this.getCachedElement(rootNode, id);
|
|
170
|
+
|
|
171
|
+
if (!sourceElement) {
|
|
172
|
+
// Search up through shadow roots
|
|
173
|
+
sourceElement = upShadowSearch(template, id);
|
|
174
|
+
|
|
175
|
+
if (!sourceElement) {
|
|
176
|
+
const error = `Element with id="${id}" not found`;
|
|
177
|
+
template.setAttribute('data-include-error', error);
|
|
178
|
+
console.warn(`HTMLInclude: ${error}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Cache the result
|
|
183
|
+
this.cacheElement(rootNode, id, sourceElement);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for circular references only if source is also a template with src
|
|
187
|
+
if (sourceElement instanceof HTMLTemplateElement && sourceElement.hasAttribute('src')) {
|
|
188
|
+
const sourceId = sourceElement.getAttribute('id');
|
|
189
|
+
if (sourceId && processingStack.has(sourceId)) {
|
|
190
|
+
const error = `Circular reference detected: #${id}`;
|
|
191
|
+
template.setAttribute('data-include-error', error);
|
|
192
|
+
console.error(`HTMLInclude: ${error}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Mark this template as processing (for circular reference detection)
|
|
198
|
+
const templateId = template.getAttribute('id');
|
|
199
|
+
if (templateId) {
|
|
200
|
+
processingStack.add(templateId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Clone the content
|
|
205
|
+
const { clone, isLiveElement } = this.cloneContent(sourceElement);
|
|
206
|
+
|
|
207
|
+
if (!clone) {
|
|
208
|
+
const error = `Unable to clone content from #${id}`;
|
|
209
|
+
template.setAttribute('data-include-error', error);
|
|
210
|
+
console.warn(`HTMLInclude: ${error}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Optimization 4: Copy MOSE exports if cloning live element from different root
|
|
215
|
+
if (isLiveElement) {
|
|
216
|
+
await this.copyMoseExports(sourceElement, clone, rootNode);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if the template has children - if so, process matching insertions
|
|
220
|
+
const templateChildren = Array.from(template.content.children);
|
|
221
|
+
|
|
222
|
+
if (templateChildren.length > 0) {
|
|
223
|
+
// Process matching insertions for each child in the template
|
|
224
|
+
this.processMatchingInsertions(clone, templateChildren);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove ID from cloned element to avoid duplicate IDs in the DOM
|
|
228
|
+
if (clone instanceof Element && clone.hasAttribute('id')) {
|
|
229
|
+
clone.removeAttribute('id');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for shadowRootModeOnLoad attribute
|
|
233
|
+
const shadowRootMode = template.getAttribute('shadowrootmodeonload');
|
|
234
|
+
|
|
235
|
+
if (shadowRootMode) {
|
|
236
|
+
// Shadow DOM mode - attach to parent's shadow root
|
|
237
|
+
const parent = template.parentElement;
|
|
238
|
+
|
|
239
|
+
if (!parent) {
|
|
240
|
+
console.warn('HTMLInclude: Cannot attach shadow root - template has no parent element');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate shadow root mode
|
|
245
|
+
if (shadowRootMode !== 'open' && shadowRootMode !== 'closed') {
|
|
246
|
+
console.warn(`HTMLInclude: Invalid shadowRootModeOnLoad value "${shadowRootMode}", must be "open" or "closed"`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Get or create shadow root
|
|
251
|
+
let shadowRoot = parent.shadowRoot;
|
|
252
|
+
if (!shadowRoot) {
|
|
253
|
+
try {
|
|
254
|
+
shadowRoot = parent.attachShadow({ mode: shadowRootMode as ShadowRootMode });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('HTMLInclude: Failed to attach shadow root:', error);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Append clone to shadow root
|
|
262
|
+
shadowRoot.appendChild(clone);
|
|
263
|
+
template.remove();
|
|
264
|
+
} else {
|
|
265
|
+
// Normal mode - insert before template
|
|
266
|
+
template.parentNode?.insertBefore(clone, template);
|
|
267
|
+
template.remove();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
// Always remove from processing stack
|
|
272
|
+
if (templateId) {
|
|
273
|
+
processingStack.delete(templateId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('HTMLInclude: Unexpected error:', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Gets a cached element reference if available and still valid.
|
|
284
|
+
*/
|
|
285
|
+
getCachedElement(rootNode: Node, id: string): Element | null {
|
|
286
|
+
const rootCache = idCache.get(rootNode);
|
|
287
|
+
if (!rootCache) return null;
|
|
288
|
+
|
|
289
|
+
const weakRef = rootCache.get(id);
|
|
290
|
+
if (!weakRef) return null;
|
|
291
|
+
|
|
292
|
+
const element = weakRef.deref();
|
|
293
|
+
if (!element) {
|
|
294
|
+
// Element was garbage collected, remove from cache
|
|
295
|
+
rootCache.delete(id);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return element;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Caches an element reference for future lookups.
|
|
304
|
+
*/
|
|
305
|
+
cacheElement(rootNode: Node, id: string, element: Element): void {
|
|
306
|
+
let rootCache = idCache.get(rootNode);
|
|
307
|
+
if (!rootCache) {
|
|
308
|
+
rootCache = new Map();
|
|
309
|
+
idCache.set(rootNode, rootCache);
|
|
310
|
+
}
|
|
311
|
+
rootCache.set(id, new WeakRef(element));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Processes matching insertions by finding elements in the cloned content that match
|
|
316
|
+
* the selectors from template children and applying insertions to them.
|
|
317
|
+
*/
|
|
318
|
+
processMatchingInsertions(clonedContent: Node, templateChildren: Element[]): void {
|
|
319
|
+
// For each child in the template, find matching elements in the cloned content
|
|
320
|
+
for (const templateChild of templateChildren) {
|
|
321
|
+
// Generate a selector from the template child
|
|
322
|
+
const selector = toQuery(templateChild);
|
|
323
|
+
|
|
324
|
+
// Prepare the insertion content and attribute map
|
|
325
|
+
const { fragment, attributeMap } = prepareForInsertion(templateChild);
|
|
326
|
+
|
|
327
|
+
// Find all matching elements in the cloned content
|
|
328
|
+
let matchingElements: Element[] = [];
|
|
329
|
+
|
|
330
|
+
if (clonedContent instanceof Element) {
|
|
331
|
+
// Check if the cloned element itself matches
|
|
332
|
+
if (clonedContent.matches(selector)) {
|
|
333
|
+
matchingElements.push(clonedContent);
|
|
334
|
+
}
|
|
335
|
+
// Find matching descendants
|
|
336
|
+
const descendants = Array.from(clonedContent.querySelectorAll(selector));
|
|
337
|
+
matchingElements = [...matchingElements, ...descendants];
|
|
338
|
+
} else if (clonedContent instanceof DocumentFragment) {
|
|
339
|
+
// Search within the fragment
|
|
340
|
+
matchingElements = Array.from(clonedContent.querySelectorAll(selector));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Apply insertion to each matching element
|
|
344
|
+
for (const matchingElement of matchingElements) {
|
|
345
|
+
applyInsertion(matchingElement, fragment, attributeMap);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Clones content from the source element.
|
|
352
|
+
* Priority: remoteContent (hoisted templates) > content (templates) > element itself
|
|
353
|
+
* Returns an object with the cloned node and whether it was cloned from a live element
|
|
354
|
+
*/
|
|
355
|
+
cloneContent(sourceElement: Element): { clone: Node | null, isLiveElement: boolean } {
|
|
356
|
+
// Check for remoteContent property (hoisted templates)
|
|
357
|
+
if ('remoteContent' in sourceElement) {
|
|
358
|
+
try {
|
|
359
|
+
const remoteContent = (sourceElement as any).remoteContent as DocumentFragment;
|
|
360
|
+
return { clone: remoteContent.cloneNode(true), isLiveElement: false };
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.warn('HTMLInclude: Failed to access remoteContent', e);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check for content property (regular templates)
|
|
367
|
+
if (sourceElement instanceof HTMLTemplateElement && sourceElement.content) {
|
|
368
|
+
return { clone: sourceElement.content.cloneNode(true), isLiveElement: false };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Clone the element itself (live DOM element)
|
|
372
|
+
return { clone: sourceElement.cloneNode(true), isLiveElement: true };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Copies MOSE script exports from source to cloned scripts.
|
|
377
|
+
* This optimization avoids re-parsing JSON when cloning MOSE scripts across shadow boundaries.
|
|
378
|
+
*/
|
|
379
|
+
async copyMoseExports(sourceElement: Element, clone: Node, templateRootNode: Node): Promise<void> {
|
|
380
|
+
const sourceRootNode = sourceElement.getRootNode();
|
|
381
|
+
|
|
382
|
+
// Only process if source and template are in different root nodes
|
|
383
|
+
if (sourceRootNode === templateRootNode) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Find all MOSE scripts in the source element
|
|
388
|
+
const sourceScripts = sourceElement.querySelectorAll('script[type="mountobserver"]');
|
|
389
|
+
|
|
390
|
+
if (sourceScripts.length === 0) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Find all MOSE scripts in the clone
|
|
395
|
+
let cloneScripts: NodeListOf<Element>;
|
|
396
|
+
if (clone instanceof Element) {
|
|
397
|
+
cloneScripts = clone.querySelectorAll('script[type="mountobserver"]');
|
|
398
|
+
} else if (clone instanceof DocumentFragment) {
|
|
399
|
+
cloneScripts = clone.querySelectorAll('script[type="mountobserver"]');
|
|
400
|
+
} else {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Copy exports from source scripts to cloned scripts (matching by ID)
|
|
405
|
+
for (let i = 0; i < sourceScripts.length; i++) {
|
|
406
|
+
const sourceScript = sourceScripts[i] as HTMLScriptElement;
|
|
407
|
+
const sourceId = sourceScript.getAttribute('id');
|
|
408
|
+
|
|
409
|
+
if (!sourceId) continue;
|
|
410
|
+
|
|
411
|
+
// Find matching clone script by ID
|
|
412
|
+
const cloneScript = Array.from(cloneScripts).find(
|
|
413
|
+
s => s.getAttribute('id') === sourceId
|
|
414
|
+
) as HTMLScriptElement | undefined;
|
|
415
|
+
|
|
416
|
+
if (!cloneScript) continue;
|
|
417
|
+
|
|
418
|
+
// Check if source script has export
|
|
419
|
+
let sourceExport = (sourceScript as any).export;
|
|
420
|
+
|
|
421
|
+
if (!sourceExport) {
|
|
422
|
+
// Wait for the source script to resolve
|
|
423
|
+
try {
|
|
424
|
+
// Create a promise that waits for the resolved event
|
|
425
|
+
const event = await new Promise<Event>((resolve, reject) => {
|
|
426
|
+
const timeout = setTimeout(() => {
|
|
427
|
+
reject(new Error('Timeout'));
|
|
428
|
+
}, 5000);
|
|
429
|
+
|
|
430
|
+
sourceScript.addEventListener('resolved', (e) => {
|
|
431
|
+
clearTimeout(timeout);
|
|
432
|
+
resolve(e);
|
|
433
|
+
}, { once: true });
|
|
434
|
+
});
|
|
435
|
+
sourceExport = (event as any).export;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.warn(`HTMLInclude: Timeout waiting for MOSE script #${sourceId} to resolve`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Copy export to cloned script
|
|
443
|
+
if (sourceExport) {
|
|
444
|
+
(cloneScript as any).export = sourceExport;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Register the handler
|
|
451
|
+
import { MountObserver } from '../MountObserver.js';
|
|
452
|
+
|
|
453
|
+
MountObserver.define('builtIns.HTMLInclude', HTMLIncludeHandler);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EvtRt } from '../EvtRt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol for tracking hoisted template references.
|
|
4
|
+
* Uses a compact GUID to ensure uniqueness - this is essentially private.
|
|
5
|
+
*/
|
|
6
|
+
const remoteTemplElSym = Symbol.for('du3y+tfsAUGFHMG/iHZiMQ');
|
|
7
|
+
/**
|
|
8
|
+
* Handler that hoists template elements from shadow roots to document.head for performance.
|
|
9
|
+
*
|
|
10
|
+
* When templates with IDs are repeated across multiple custom elements, moving them to
|
|
11
|
+
* a centralized location reduces memory usage and improves cloning performance.
|
|
12
|
+
*
|
|
13
|
+
* The handler:
|
|
14
|
+
* 1. Moves template content to a new template in document.head
|
|
15
|
+
* 2. Updates the original template to reference the hoisted version via src="#id"
|
|
16
|
+
* 3. Defines a remoteContent getter that returns the hoisted template's content
|
|
17
|
+
*/
|
|
18
|
+
export class HoistTemplateHandler extends EvtRt {
|
|
19
|
+
static matching = 'template[id]:not([src])';
|
|
20
|
+
static whereInstanceOf = HTMLTemplateElement;
|
|
21
|
+
static shouldMount(el) {
|
|
22
|
+
const template = el;
|
|
23
|
+
// Don't hoist if empty
|
|
24
|
+
if (template.content.childNodes.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
// Case 1: Not connected (being cloned)
|
|
27
|
+
// MountObserver checks conditions even before fragment becomes connected
|
|
28
|
+
if (!template.isConnected)
|
|
29
|
+
return true;
|
|
30
|
+
// Case 2: Connected but in a shadow root
|
|
31
|
+
const root = template.getRootNode();
|
|
32
|
+
return root instanceof ShadowRoot;
|
|
33
|
+
}
|
|
34
|
+
mount(mountedElement, mountConfig, context) {
|
|
35
|
+
hoistTemplate(mountedElement);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hoists a template element to document.head and sets up the remoteContent getter.
|
|
40
|
+
*
|
|
41
|
+
* @param templ - The template element to hoist
|
|
42
|
+
*/
|
|
43
|
+
function hoistTemplate(templ) {
|
|
44
|
+
// Skip if already has remoteContent property
|
|
45
|
+
if (templ.hasOwnProperty('remoteContent'))
|
|
46
|
+
return;
|
|
47
|
+
const { head } = document;
|
|
48
|
+
// Initialize counter on globalThis
|
|
49
|
+
if (globalThis[remoteTemplElSym] === undefined) {
|
|
50
|
+
globalThis[remoteTemplElSym] = 0;
|
|
51
|
+
}
|
|
52
|
+
// Create hoisted template in head with unique ID
|
|
53
|
+
const id = `mount-observer-${globalThis[remoteTemplElSym]++}`;
|
|
54
|
+
const sourceTempl = document.createElement('template');
|
|
55
|
+
sourceTempl.id = id;
|
|
56
|
+
sourceTempl.setAttribute('data-mount-observer-hoisted', 'true');
|
|
57
|
+
sourceTempl.content.appendChild(templ.content);
|
|
58
|
+
head.append(sourceTempl);
|
|
59
|
+
// Update original template to reference the hoisted version
|
|
60
|
+
templ.innerHTML = '';
|
|
61
|
+
templ.setAttribute('src', `#${id}`);
|
|
62
|
+
templ.setAttribute('rel', 'preload');
|
|
63
|
+
templ[remoteTemplElSym] = new WeakRef(sourceTempl);
|
|
64
|
+
// Define remoteContent getter
|
|
65
|
+
Object.defineProperty(templ, 'remoteContent', {
|
|
66
|
+
get() {
|
|
67
|
+
const test = this[remoteTemplElSym]?.deref();
|
|
68
|
+
if (test !== undefined)
|
|
69
|
+
return test.content;
|
|
70
|
+
throw new Error('Hoisted template not found or was garbage collected');
|
|
71
|
+
},
|
|
72
|
+
configurable: true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Register the handler
|
|
76
|
+
import { MountObserver } from '../MountObserver.js';
|
|
77
|
+
MountObserver.define('builtIns.hoistTemplate', HoistTemplateHandler);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { EvtRt } from '../EvtRt.js';
|
|
2
|
+
import type { MountContext, MountConfig } from '../types/mount-observer/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Symbol for tracking hoisted template references.
|
|
6
|
+
* Uses a compact GUID to ensure uniqueness - this is essentially private.
|
|
7
|
+
*/
|
|
8
|
+
const remoteTemplElSym = Symbol.for('du3y+tfsAUGFHMG/iHZiMQ');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handler that hoists template elements from shadow roots to document.head for performance.
|
|
12
|
+
*
|
|
13
|
+
* When templates with IDs are repeated across multiple custom elements, moving them to
|
|
14
|
+
* a centralized location reduces memory usage and improves cloning performance.
|
|
15
|
+
*
|
|
16
|
+
* The handler:
|
|
17
|
+
* 1. Moves template content to a new template in document.head
|
|
18
|
+
* 2. Updates the original template to reference the hoisted version via src="#id"
|
|
19
|
+
* 3. Defines a remoteContent getter that returns the hoisted template's content
|
|
20
|
+
*/
|
|
21
|
+
export class HoistTemplateHandler extends EvtRt {
|
|
22
|
+
static matching = 'template[id]:not([src])';
|
|
23
|
+
static whereInstanceOf = HTMLTemplateElement;
|
|
24
|
+
|
|
25
|
+
static shouldMount(el: Element): boolean {
|
|
26
|
+
const template = el as HTMLTemplateElement;
|
|
27
|
+
|
|
28
|
+
// Don't hoist if empty
|
|
29
|
+
if (template.content.childNodes.length === 0) return false;
|
|
30
|
+
|
|
31
|
+
// Case 1: Not connected (being cloned)
|
|
32
|
+
// MountObserver checks conditions even before fragment becomes connected
|
|
33
|
+
if (!template.isConnected) return true;
|
|
34
|
+
|
|
35
|
+
// Case 2: Connected but in a shadow root
|
|
36
|
+
const root = template.getRootNode();
|
|
37
|
+
return root instanceof ShadowRoot;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mount(mountedElement: Element, mountConfig: MountConfig, context: MountContext): void {
|
|
41
|
+
hoistTemplate(mountedElement as HTMLTemplateElement);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hoists a template element to document.head and sets up the remoteContent getter.
|
|
47
|
+
*
|
|
48
|
+
* @param templ - The template element to hoist
|
|
49
|
+
*/
|
|
50
|
+
function hoistTemplate(templ: HTMLTemplateElement): void {
|
|
51
|
+
// Skip if already has remoteContent property
|
|
52
|
+
if (templ.hasOwnProperty('remoteContent')) return;
|
|
53
|
+
|
|
54
|
+
const { head } = document;
|
|
55
|
+
|
|
56
|
+
// Initialize counter on globalThis
|
|
57
|
+
if ((globalThis as any)[remoteTemplElSym] === undefined) {
|
|
58
|
+
(globalThis as any)[remoteTemplElSym] = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create hoisted template in head with unique ID
|
|
62
|
+
const id = `mount-observer-${(globalThis as any)[remoteTemplElSym]++}`;
|
|
63
|
+
const sourceTempl = document.createElement('template');
|
|
64
|
+
sourceTempl.id = id;
|
|
65
|
+
sourceTempl.setAttribute('data-mount-observer-hoisted', 'true');
|
|
66
|
+
sourceTempl.content.appendChild(templ.content);
|
|
67
|
+
head.append(sourceTempl);
|
|
68
|
+
|
|
69
|
+
// Update original template to reference the hoisted version
|
|
70
|
+
templ.innerHTML = '';
|
|
71
|
+
templ.setAttribute('src', `#${id}`);
|
|
72
|
+
templ.setAttribute('rel', 'preload');
|
|
73
|
+
(templ as any)[remoteTemplElSym] = new WeakRef(sourceTempl);
|
|
74
|
+
|
|
75
|
+
// Define remoteContent getter
|
|
76
|
+
Object.defineProperty(templ, 'remoteContent', {
|
|
77
|
+
get(): DocumentFragment {
|
|
78
|
+
const test = (this as any)[remoteTemplElSym]?.deref();
|
|
79
|
+
if (test !== undefined) return test.content;
|
|
80
|
+
throw new Error('Hoisted template not found or was garbage collected');
|
|
81
|
+
},
|
|
82
|
+
configurable: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Register the handler
|
|
87
|
+
import { MountObserver } from '../MountObserver.js';
|
|
88
|
+
|
|
89
|
+
MountObserver.define('builtIns.hoistTemplate', HoistTemplateHandler);
|