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.
Files changed (40) hide show
  1. package/ElementMountExtension.js +5 -2
  2. package/ElementMountExtension.ts +7 -2
  3. package/MountObserver.js +3 -0
  4. package/MountObserver.ts +3 -0
  5. package/RegistryMountCoordinator.js +5 -5
  6. package/RegistryMountCoordinator.ts +8 -6
  7. package/{DefineCustomElementHandler.js → handlers/DefineCustomElement.js} +103 -99
  8. package/handlers/DefineCustomElement.ts +123 -0
  9. package/{EnhanceMountedElementHandler.js → handlers/EnhanceMountedElement.js} +109 -96
  10. package/handlers/EnhanceMountedElement.ts +126 -0
  11. package/handlers/Events.js +110 -0
  12. package/handlers/EvtRt.js +59 -0
  13. package/handlers/GenIds.js +37 -0
  14. package/handlers/GenIds.ts +45 -0
  15. package/handlers/HTMLInclude.js +393 -0
  16. package/handlers/HTMLInclude.ts +453 -0
  17. package/handlers/HoistTemplate.js +77 -0
  18. package/handlers/HoistTemplate.ts +89 -0
  19. package/handlers/MountObserver.js +941 -0
  20. package/handlers/MountObserverScript.js +78 -0
  21. package/handlers/MountObserverScript.ts +89 -0
  22. package/handlers/ScriptExport.js +83 -0
  23. package/handlers/ScriptExport.ts +97 -0
  24. package/handlers/SharedMutationObserver.js +78 -0
  25. package/handlers/arr.js +16 -0
  26. package/handlers/connectionMonitor.js +122 -0
  27. package/handlers/elementIntersection.js +73 -0
  28. package/handlers/emitEvents.js +187 -0
  29. package/handlers/getRegistryRoot.js +52 -0
  30. package/handlers/loadImports.js +129 -0
  31. package/handlers/mediaQuery.js +90 -0
  32. package/handlers/rootSizeObserver.js +131 -0
  33. package/handlers/upShadowSearch.js +70 -0
  34. package/handlers/withScopePerimeter.js +22 -0
  35. package/package.json +12 -2
  36. package/types/assign-gingerly/types.d.ts +244 -0
  37. package/types/be-a-beacon/types.d.ts +3 -0
  38. package/types/global.d.ts +29 -0
  39. package/types/id-generation/types.d.ts +26 -0
  40. 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);