@mml-io/networked-dom-web 0.25.0 → 0.26.0

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/build/index.js CHANGED
@@ -1,49 +1,93 @@
1
+ // src/DocumentInterface.ts
2
+ var VIRTUAL_ELEMENT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-element");
3
+ var VIRTUAL_TEXT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-text");
4
+ var VIRTUAL_DOCUMENT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-document");
5
+ var VIRTUAL_FRAGMENT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-fragment");
6
+ function isElementLike(node) {
7
+ return typeof node.setAttribute === "function";
8
+ }
9
+ function isPortalElement(element) {
10
+ return typeof element.getPortalElement === "function";
11
+ }
12
+
1
13
  // src/DOMSanitizer.ts
2
- var DOMSanitizer = class _DOMSanitizer {
3
- static sanitise(node, options = {}) {
4
- if (node.getAttributeNames) {
5
- for (const attr of node.getAttributeNames()) {
6
- if (!_DOMSanitizer.IsValidAttributeName(attr)) {
7
- node.removeAttribute(attr);
8
- }
9
- }
14
+ var _DOMSanitizer = class _DOMSanitizer {
15
+ /**
16
+ * Returns true if a tag with the given name should be stripped of all
17
+ * content and attributes during sanitisation.
18
+ */
19
+ static isBlockedTag(tagName) {
20
+ return _DOMSanitizer.BLOCKED_TAGS.has(tagName.toLowerCase());
21
+ }
22
+ /**
23
+ * Given a tag name and sanitisation options, returns the sanitised tag name.
24
+ * Non-prefixed tags are renamed (e.g. "div" → "x-div"). Returns null for
25
+ * blocked tags that should be skipped entirely.
26
+ */
27
+ static sanitiseTagName(tagName, options) {
28
+ const tag = tagName.toLowerCase();
29
+ if (_DOMSanitizer.isBlockedTag(tag)) {
30
+ return null;
10
31
  }
11
- if (node instanceof HTMLElement) {
12
- if (options.tagPrefix) {
13
- const tag = node.nodeName.toLowerCase();
14
- if (!tag.startsWith(options.tagPrefix.toLowerCase())) {
15
- node = _DOMSanitizer.replaceNodeTagName(
16
- node,
17
- options.replacementTagPrefix ? options.replacementTagPrefix + tag : `x-${tag}`
18
- );
19
- }
20
- }
32
+ if (options.tagPrefix && !tag.startsWith(options.tagPrefix.toLowerCase())) {
33
+ return (options.replacementTagPrefix ?? "x-") + tag;
21
34
  }
22
- if (node.nodeName === "SCRIPT" || node.nodeName === "OBJECT" || node.nodeName === "IFRAME") {
23
- node.innerHTML = "";
24
- _DOMSanitizer.stripAllAttributes(node);
35
+ return tag;
36
+ }
37
+ /**
38
+ * Sanitises a DOM node in-place. When tag replacement occurs (via tagPrefix option),
39
+ * the returned node may be a different object than the input node.
40
+ */
41
+ static sanitise(node, options = {}, doc) {
42
+ if (_DOMSanitizer.isBlockedTag(node.nodeName)) {
43
+ if (isElementLike(node)) {
44
+ node.innerHTML = "";
45
+ _DOMSanitizer.stripAllAttributes(node);
46
+ }
25
47
  } else {
26
- if (node.getAttributeNames) {
27
- for (const attr of node.getAttributeNames()) {
48
+ if (isElementLike(node)) {
49
+ let element = node;
50
+ for (const attr of element.getAttributeNames()) {
51
+ if (!_DOMSanitizer.IsValidAttributeName(attr)) {
52
+ element.removeAttribute(attr);
53
+ }
54
+ }
55
+ if (options.tagPrefix) {
56
+ const tag = element.nodeName.toLowerCase();
57
+ if (!tag.startsWith(options.tagPrefix.toLowerCase())) {
58
+ element = _DOMSanitizer.replaceNodeTagName(
59
+ element,
60
+ (options.replacementTagPrefix ?? "x-") + tag,
61
+ doc
62
+ );
63
+ node = element;
64
+ }
65
+ }
66
+ for (const attr of element.getAttributeNames()) {
28
67
  if (!_DOMSanitizer.shouldAcceptAttribute(attr)) {
29
- node.removeAttribute(attr);
68
+ element.removeAttribute(attr);
30
69
  }
31
70
  }
32
71
  }
33
72
  for (let i = 0; i < node.childNodes.length; i++) {
34
- _DOMSanitizer.sanitise(node.childNodes[i], options);
73
+ _DOMSanitizer.sanitise(node.childNodes[i], options, doc);
35
74
  }
36
75
  }
37
76
  return node;
38
77
  }
39
- static replaceNodeTagName(node, newTagName) {
78
+ static replaceNodeTagName(node, newTagName, doc) {
40
79
  var _a;
41
- const replacementNode = document.createElement(newTagName);
42
- let index;
80
+ if (!doc && typeof document === "undefined") {
81
+ throw new Error(
82
+ "DOMSanitizer.replaceNodeTagName requires a document factory (IDocumentFactory) in non-browser environments"
83
+ );
84
+ }
85
+ const docFactory = doc ?? document;
86
+ const replacementNode = docFactory.createElement(newTagName);
43
87
  while (node.firstChild) {
44
88
  replacementNode.appendChild(node.firstChild);
45
89
  }
46
- for (index = node.attributes.length - 1; index >= 0; --index) {
90
+ for (let index = node.attributes.length - 1; index >= 0; --index) {
47
91
  replacementNode.setAttribute(node.attributes[index].name, node.attributes[index].value);
48
92
  }
49
93
  (_a = node.parentNode) == null ? void 0 : _a.replaceChild(replacementNode, node);
@@ -83,13 +127,9 @@ var DOMSanitizer = class _DOMSanitizer {
83
127
  return !attribute.startsWith("on");
84
128
  }
85
129
  };
86
-
87
- // src/NetworkedDOMWebsocket.ts
88
- import {
89
- isNetworkedDOMProtocolSubProtocol_v0_2,
90
- networkedDOMProtocolSubProtocol_v0_1,
91
- networkedDOMProtocolSubProtocol_v0_2_SubVersionsList
92
- } from "@mml-io/networked-dom-protocol";
130
+ /** Tags whose content and attributes are always stripped. */
131
+ _DOMSanitizer.BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "object", "iframe"]);
132
+ var DOMSanitizer = _DOMSanitizer;
93
133
 
94
134
  // src/ElementUtils.ts
95
135
  var ALWAYS_DISALLOWED_TAGS = /* @__PURE__ */ new Set(["foreignobject", "iframe", "script"]);
@@ -215,7 +255,13 @@ function remapAttributeName(attrName) {
215
255
  }
216
256
  return attrName;
217
257
  }
218
- function createElementWithSVGSupport(tag, options = {}) {
258
+ function createElementWithSVGSupport(tag, options = {}, doc) {
259
+ if (!doc && typeof document === "undefined") {
260
+ throw new Error(
261
+ "createElementWithSVGSupport requires a document factory (IDocumentFactory) in non-browser environments"
262
+ );
263
+ }
264
+ const docFactory = doc ?? document;
219
265
  let filteredTag = tag.toLowerCase();
220
266
  if (ALWAYS_DISALLOWED_TAGS.has(filteredTag.toLowerCase())) {
221
267
  console.error("Disallowing tag", filteredTag);
@@ -228,14 +274,17 @@ function createElementWithSVGSupport(tag, options = {}) {
228
274
  if (svgTagMapping) {
229
275
  filteredTag = svgTagMapping;
230
276
  const xmlns = "http://www.w3.org/2000/svg";
231
- return document.createElementNS(xmlns, filteredTag);
277
+ if (docFactory.createElementNS) {
278
+ return docFactory.createElementNS(xmlns, filteredTag);
279
+ }
280
+ return docFactory.createElement(filteredTag);
232
281
  } else {
233
282
  if (options.tagPrefix) {
234
283
  if (!tag.toLowerCase().startsWith(options.tagPrefix.toLowerCase())) {
235
284
  filteredTag = options.replacementTagPrefix ? options.replacementTagPrefix + tag : `x-${tag}`;
236
285
  }
237
286
  }
238
- return document.createElement(filteredTag);
287
+ return docFactory.createElement(filteredTag);
239
288
  }
240
289
  }
241
290
  function setElementAttribute(element, key, value) {
@@ -245,23 +294,69 @@ function setElementAttribute(element, key, value) {
245
294
  }
246
295
  }
247
296
  function getChildrenTarget(parent) {
248
- let targetForChildren = parent;
249
- if (parent.getPortalElement) {
250
- targetForChildren = parent.getPortalElement();
297
+ if (isPortalElement(parent)) {
298
+ return parent.getPortalElement();
251
299
  }
252
- return targetForChildren;
300
+ return parent;
253
301
  }
254
302
  function getRemovalTarget(parent) {
255
- let targetForRemoval = parent;
256
- if (parent.getPortalElement) {
257
- targetForRemoval = parent.getPortalElement();
303
+ if (isPortalElement(parent)) {
304
+ return parent.getPortalElement();
258
305
  }
259
- return targetForRemoval;
306
+ return parent;
260
307
  }
261
308
 
262
- // src/NetworkedDOMWebsocketV01Adapter.ts
263
- var NetworkedDOMWebsocketV01Adapter = class {
264
- constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}) {
309
+ // src/NetworkedDOMWebsocket.ts
310
+ import {
311
+ isNetworkedDOMProtocolSubProtocol_v0_2,
312
+ networkedDOMProtocolSubProtocol_v0_1,
313
+ networkedDOMProtocolSubProtocol_v0_2_SubVersionsList
314
+ } from "@mml-io/networked-dom-protocol";
315
+
316
+ // src/PortalUtils.ts
317
+ function resolveChildFactory(parentNode, elementFactoryOverride) {
318
+ var _a;
319
+ if (isElementLike(parentNode) && isPortalElement(parentNode)) {
320
+ const portalFactory = (_a = parentNode.getPortalDocumentFactory) == null ? void 0 : _a.call(parentNode);
321
+ if (portalFactory) {
322
+ return portalFactory;
323
+ }
324
+ }
325
+ return elementFactoryOverride.get(parentNode);
326
+ }
327
+ function resolvePortalChildFactory(element, currentFactory) {
328
+ var _a;
329
+ if (isPortalElement(element)) {
330
+ const portalFactory = (_a = element.getPortalDocumentFactory) == null ? void 0 : _a.call(element);
331
+ if (portalFactory) {
332
+ return { childFactory: portalFactory, usingPortalFactory: true };
333
+ }
334
+ }
335
+ return { childFactory: currentFactory, usingPortalFactory: false };
336
+ }
337
+ function recordFactoryOverride(element, factory, defaultFactory, elementFactoryOverride) {
338
+ if (factory !== defaultFactory) {
339
+ elementFactoryOverride.set(element, factory);
340
+ }
341
+ }
342
+ function flushPendingPortalChildren(pendingPortalChildren) {
343
+ for (const [portalParent, children] of pendingPortalChildren) {
344
+ const target = getChildrenTarget(portalParent);
345
+ for (const child of children) {
346
+ target.appendChild(child);
347
+ }
348
+ }
349
+ pendingPortalChildren.clear();
350
+ }
351
+ function bufferPortalChild(pendingPortalChildren, portalParent, child) {
352
+ const pending = pendingPortalChildren.get(portalParent) ?? [];
353
+ pending.push(child);
354
+ pendingPortalChildren.set(portalParent, pending);
355
+ }
356
+
357
+ // src/NetworkedDOMWebsocketAdapterBase.ts
358
+ var NetworkedDOMWebsocketAdapterBase = class {
359
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
265
360
  this.websocket = websocket;
266
361
  this.parentElement = parentElement;
267
362
  this.connectedCallback = connectedCallback;
@@ -270,7 +365,119 @@ var NetworkedDOMWebsocketV01Adapter = class {
270
365
  this.idToElement = /* @__PURE__ */ new Map();
271
366
  this.elementToId = /* @__PURE__ */ new Map();
272
367
  this.currentRoot = null;
368
+ this.pendingPortalChildren = /* @__PURE__ */ new Map();
369
+ this.elementFactoryOverride = /* @__PURE__ */ new Map();
273
370
  this.websocket.binaryType = "arraybuffer";
371
+ if (!doc && typeof document === "undefined") {
372
+ throw new Error(
373
+ "NetworkedDOMWebsocketAdapter requires a document factory (IDocumentFactory) in non-browser environments"
374
+ );
375
+ }
376
+ this.docFactory = doc ?? document;
377
+ }
378
+ clearContents() {
379
+ this.idToElement.clear();
380
+ this.elementToId.clear();
381
+ this.elementFactoryOverride.clear();
382
+ this.pendingPortalChildren.clear();
383
+ if (this.currentRoot) {
384
+ this.currentRoot.remove();
385
+ this.currentRoot = null;
386
+ return true;
387
+ }
388
+ return false;
389
+ }
390
+ /**
391
+ * Creates a text node, registers it in the id maps, and returns it.
392
+ */
393
+ createTextNode(nodeId, text, factory) {
394
+ const textNode = factory.createTextNode("");
395
+ textNode.textContent = text;
396
+ this.idToElement.set(nodeId, textNode);
397
+ this.elementToId.set(textNode, nodeId);
398
+ return textNode;
399
+ }
400
+ /**
401
+ * Inserts elements into the correct position within a parent, using
402
+ * previousElement/nextElement for positioning. Creates a DocumentFragment
403
+ * when inserting before a reference node.
404
+ */
405
+ insertElements(targetForChildren, elementsToAdd, previousElement, nextElement, factory) {
406
+ if (elementsToAdd.length === 0) return;
407
+ if (previousElement) {
408
+ if (nextElement) {
409
+ const docFrag = factory.createDocumentFragment();
410
+ docFrag.append(...elementsToAdd);
411
+ targetForChildren.insertBefore(docFrag, nextElement);
412
+ } else {
413
+ targetForChildren.append(...elementsToAdd);
414
+ }
415
+ } else {
416
+ targetForChildren.prepend(...elementsToAdd);
417
+ }
418
+ }
419
+ /**
420
+ * Recursively removes element-to-id mappings for all descendants of a parent.
421
+ * V02 overrides this to also handle hidden placeholder elements.
422
+ */
423
+ removeChildElementIds(parent) {
424
+ if (isElementLike(parent)) {
425
+ const portal = getChildrenTarget(parent);
426
+ if (portal !== parent) {
427
+ this.removeChildElementIds(portal);
428
+ }
429
+ }
430
+ for (let i = 0; i < parent.childNodes.length; i++) {
431
+ const child = parent.childNodes[i];
432
+ const childId = this.elementToId.get(child);
433
+ if (!childId) {
434
+ this.handleUnregisteredChild(child);
435
+ } else {
436
+ this.elementToId.delete(child);
437
+ this.idToElement.delete(childId);
438
+ this.elementFactoryOverride.delete(child);
439
+ }
440
+ this.removeChildElementIds(child);
441
+ }
442
+ }
443
+ /**
444
+ * Called during removeChildElementIds when a child has no registered id.
445
+ * V01 logs an error. V02 overrides to check for placeholder elements.
446
+ */
447
+ handleUnregisteredChild(child) {
448
+ console.error("Inner child of removed element had no id", child);
449
+ }
450
+ /**
451
+ * Resets state and applies a snapshot element to the parent.
452
+ * Appending to the tree triggers MElement connectedCallbacks (which set up portals),
453
+ * then pending portal children are flushed.
454
+ */
455
+ resetAndApplySnapshot(element) {
456
+ if (this.currentRoot) {
457
+ this.removeChildElementIds(this.currentRoot);
458
+ const rootId = this.elementToId.get(this.currentRoot);
459
+ if (rootId !== void 0) {
460
+ this.elementToId.delete(this.currentRoot);
461
+ this.idToElement.delete(rootId);
462
+ this.elementFactoryOverride.delete(this.currentRoot);
463
+ }
464
+ this.currentRoot.remove();
465
+ this.currentRoot = null;
466
+ this.pendingPortalChildren.clear();
467
+ }
468
+ if (!isHTMLElement(element, this.parentElement)) {
469
+ throw new Error("Snapshot element is not an HTMLElement");
470
+ }
471
+ this.currentRoot = element;
472
+ this.parentElement.append(element);
473
+ flushPendingPortalChildren(this.pendingPortalChildren);
474
+ }
475
+ };
476
+
477
+ // src/NetworkedDOMWebsocketV01Adapter.ts
478
+ var NetworkedDOMWebsocketV01Adapter = class extends NetworkedDOMWebsocketAdapterBase {
479
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
480
+ super(websocket, parentElement, connectedCallback, timeCallback, options, doc);
274
481
  }
275
482
  handleEvent(element, event) {
276
483
  const nodeId = this.elementToId.get(element);
@@ -293,16 +500,6 @@ var NetworkedDOMWebsocketV01Adapter = class {
293
500
  send(fromClientMessage) {
294
501
  this.websocket.send(JSON.stringify(fromClientMessage));
295
502
  }
296
- clearContents() {
297
- this.idToElement.clear();
298
- this.elementToId.clear();
299
- if (this.currentRoot) {
300
- this.currentRoot.remove();
301
- this.currentRoot = null;
302
- return true;
303
- }
304
- return false;
305
- }
306
503
  receiveMessage(event) {
307
504
  try {
308
505
  const messages = JSON.parse(event.data);
@@ -374,14 +571,15 @@ var NetworkedDOMWebsocketV01Adapter = class {
374
571
  console.warn("No nodeId in childrenChanged message");
375
572
  return;
376
573
  }
377
- const parent = this.idToElement.get(nodeId);
378
- if (!parent) {
574
+ const parentNode = this.idToElement.get(nodeId);
575
+ if (!parentNode) {
379
576
  throw new Error("No parent found for childrenChanged message");
380
577
  }
381
- if (!isHTMLElement(parent, this.parentElement)) {
578
+ if (!isHTMLElement(parentNode, this.parentElement)) {
382
579
  throw new Error("Parent is not an HTMLElement (that supports children)");
383
580
  }
384
- const targetForChildren = getChildrenTarget(parent);
581
+ const childFactory = resolveChildFactory(parentNode, this.elementFactoryOverride);
582
+ const targetForChildren = getChildrenTarget(parentNode);
385
583
  let nextElement = null;
386
584
  let previousElement = null;
387
585
  if (previousNodeId) {
@@ -393,23 +591,20 @@ var NetworkedDOMWebsocketV01Adapter = class {
393
591
  }
394
592
  const elementsToAdd = [];
395
593
  for (const addedNode of addedNodes) {
396
- const childElement = this.handleNewElement(addedNode);
594
+ const childElement = this.handleNewElement(addedNode, childFactory);
397
595
  if (childElement) {
398
596
  elementsToAdd.push(childElement);
399
597
  }
400
598
  }
401
- if (elementsToAdd.length) {
402
- if (previousElement) {
403
- if (nextElement) {
404
- const docFrag = new DocumentFragment();
405
- docFrag.append(...elementsToAdd);
406
- targetForChildren.insertBefore(docFrag, nextElement);
407
- } else {
408
- targetForChildren.append(...elementsToAdd);
409
- }
410
- } else {
411
- targetForChildren.prepend(...elementsToAdd);
412
- }
599
+ this.insertElements(
600
+ targetForChildren,
601
+ elementsToAdd,
602
+ previousElement,
603
+ nextElement,
604
+ childFactory ?? this.docFactory
605
+ );
606
+ if (this.pendingPortalChildren.size > 0) {
607
+ flushPendingPortalChildren(this.pendingPortalChildren);
413
608
  }
414
609
  for (const removedNode of removedNodes) {
415
610
  const childElement = this.idToElement.get(removedNode);
@@ -418,46 +613,19 @@ var NetworkedDOMWebsocketV01Adapter = class {
418
613
  }
419
614
  this.elementToId.delete(childElement);
420
615
  this.idToElement.delete(removedNode);
421
- const targetForRemoval = getRemovalTarget(parent);
616
+ const targetForRemoval = getRemovalTarget(parentNode);
422
617
  targetForRemoval.removeChild(childElement);
423
618
  if (isHTMLElement(childElement, this.parentElement)) {
424
619
  this.removeChildElementIds(childElement);
425
620
  }
426
621
  }
427
622
  }
428
- removeChildElementIds(parent) {
429
- const portal = getChildrenTarget(parent);
430
- if (portal !== parent) {
431
- this.removeChildElementIds(portal);
432
- }
433
- for (let i = 0; i < parent.childNodes.length; i++) {
434
- const child = parent.childNodes[i];
435
- const childId = this.elementToId.get(child);
436
- if (!childId) {
437
- console.error("Inner child of removed element had no id", child);
438
- } else {
439
- this.elementToId.delete(child);
440
- this.idToElement.delete(childId);
441
- }
442
- this.removeChildElementIds(child);
443
- }
444
- }
445
623
  handleSnapshot(message) {
446
- if (this.currentRoot) {
447
- this.currentRoot.remove();
448
- this.currentRoot = null;
449
- this.elementToId.clear();
450
- this.idToElement.clear();
451
- }
452
624
  const element = this.handleNewElement(message.snapshot);
453
625
  if (!element) {
454
626
  throw new Error("Snapshot element not created");
455
627
  }
456
- if (!isHTMLElement(element, this.parentElement)) {
457
- throw new Error("Snapshot element is not an HTMLElement");
458
- }
459
- this.currentRoot = element;
460
- this.parentElement.append(element);
628
+ this.resetAndApplySnapshot(element);
461
629
  }
462
630
  handleAttributeChange(message) {
463
631
  const { nodeId, attribute, newValue } = message;
@@ -465,29 +633,25 @@ var NetworkedDOMWebsocketV01Adapter = class {
465
633
  console.warn("No nodeId in attributeChange message");
466
634
  return;
467
635
  }
468
- const element = this.idToElement.get(nodeId);
469
- if (element) {
470
- if (isHTMLElement(element, this.parentElement)) {
636
+ const node = this.idToElement.get(nodeId);
637
+ if (node) {
638
+ if (isHTMLElement(node, this.parentElement)) {
471
639
  if (newValue === null) {
472
- element.removeAttribute(attribute);
640
+ node.removeAttribute(attribute);
473
641
  } else {
474
- setElementAttribute(element, attribute, newValue);
642
+ setElementAttribute(node, attribute, newValue);
475
643
  }
476
644
  } else {
477
- console.error("Element is not an HTMLElement and cannot support attributes", element);
645
+ console.error("Element is not an HTMLElement and cannot support attributes", node);
478
646
  }
479
647
  } else {
480
648
  console.error("No element found for attributeChange message");
481
649
  }
482
650
  }
483
- handleNewElement(message) {
651
+ handleNewElement(message, factoryOverride) {
652
+ const factory = factoryOverride ?? this.docFactory;
484
653
  if (message.type === "text") {
485
- const { nodeId: nodeId2, text: text2 } = message;
486
- const textNode = document.createTextNode("");
487
- textNode.textContent = text2;
488
- this.idToElement.set(nodeId2, textNode);
489
- this.elementToId.set(textNode, nodeId2);
490
- return textNode;
654
+ return this.createTextNode(message.nodeId, message.text, factory);
491
655
  }
492
656
  const { tag, nodeId, attributes, children, text } = message;
493
657
  if (nodeId === void 0 || nodeId === null) {
@@ -502,18 +666,14 @@ var NetworkedDOMWebsocketV01Adapter = class {
502
666
  );
503
667
  }
504
668
  if (tag === "#text") {
505
- const textNode = document.createTextNode("");
506
- textNode.textContent = text || null;
507
- this.idToElement.set(nodeId, textNode);
508
- this.elementToId.set(textNode, nodeId);
509
- return textNode;
669
+ return this.createTextNode(nodeId, text || "", factory);
510
670
  }
511
671
  let element;
512
672
  try {
513
- element = createElementWithSVGSupport(tag, this.options);
673
+ element = createElementWithSVGSupport(tag, this.options, factory);
514
674
  } catch (e) {
515
675
  console.error(`Error creating element: (${tag})`, e);
516
- element = document.createElement("x-div");
676
+ element = factory.createElement("x-div");
517
677
  }
518
678
  this.idToElement.set(nodeId, element);
519
679
  this.elementToId.set(element, nodeId);
@@ -521,11 +681,17 @@ var NetworkedDOMWebsocketV01Adapter = class {
521
681
  const value = attributes[key];
522
682
  setElementAttribute(element, key, value);
523
683
  }
684
+ recordFactoryOverride(element, factory, this.docFactory, this.elementFactoryOverride);
685
+ const { childFactory, usingPortalFactory } = resolvePortalChildFactory(element, factory);
524
686
  if (children) {
525
687
  for (const child of children) {
526
- const childElement = this.handleNewElement(child);
688
+ const childElement = this.handleNewElement(child, childFactory);
527
689
  if (childElement) {
528
- element.append(childElement);
690
+ if (usingPortalFactory) {
691
+ bufferPortalChild(this.pendingPortalChildren, element, childElement);
692
+ } else {
693
+ element.append(childElement);
694
+ }
529
695
  }
530
696
  }
531
697
  }
@@ -543,21 +709,13 @@ import {
543
709
  } from "@mml-io/networked-dom-protocol";
544
710
  var connectionId = 1;
545
711
  var hiddenTag = "x-hidden";
546
- var NetworkedDOMWebsocketV02Adapter = class {
547
- constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}) {
548
- this.websocket = websocket;
549
- this.parentElement = parentElement;
550
- this.connectedCallback = connectedCallback;
551
- this.timeCallback = timeCallback;
552
- this.options = options;
553
- this.idToElement = /* @__PURE__ */ new Map();
554
- this.elementToId = /* @__PURE__ */ new Map();
712
+ var NetworkedDOMWebsocketV02Adapter = class extends NetworkedDOMWebsocketAdapterBase {
713
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
714
+ super(websocket, parentElement, connectedCallback, timeCallback, options, doc);
555
715
  this.placeholderToId = /* @__PURE__ */ new Map();
556
716
  this.hiddenPlaceholderElements = /* @__PURE__ */ new Map();
557
- this.currentRoot = null;
558
717
  this.batchMode = false;
559
718
  this.batchMessages = [];
560
- this.websocket.binaryType = "arraybuffer";
561
719
  this.protocolSubversion = getNetworkedDOMProtocolSubProtocol_v0_2SubversionOrThrow(
562
720
  websocket.protocol
563
721
  );
@@ -593,14 +751,11 @@ var NetworkedDOMWebsocketV02Adapter = class {
593
751
  this.websocket.send(writer.getBuffer());
594
752
  }
595
753
  clearContents() {
596
- this.idToElement.clear();
597
- this.elementToId.clear();
598
- if (this.currentRoot) {
599
- this.currentRoot.remove();
600
- this.currentRoot = null;
601
- return true;
602
- }
603
- return false;
754
+ this.placeholderToId.clear();
755
+ this.hiddenPlaceholderElements.clear();
756
+ this.batchMessages = [];
757
+ this.batchMode = false;
758
+ return super.clearContents();
604
759
  }
605
760
  receiveMessage(event) {
606
761
  try {
@@ -695,13 +850,14 @@ var NetworkedDOMWebsocketV02Adapter = class {
695
850
  if (!node) {
696
851
  throw new Error("No node found for changeHiddenFrom message");
697
852
  }
698
- const parent = node.parentElement;
853
+ const element = node;
854
+ const parent = element.parentElement;
699
855
  if (!parent) {
700
856
  throw new Error("Node has no parent");
701
857
  }
702
- const placeholder = document.createElement(hiddenTag);
703
- parent.replaceChild(placeholder, node);
704
- this.hiddenPlaceholderElements.set(nodeId, { placeholder, element: node });
858
+ const placeholder = this.docFactory.createElement(hiddenTag);
859
+ parent.replaceChild(placeholder, element);
860
+ this.hiddenPlaceholderElements.set(nodeId, { placeholder, element });
705
861
  this.placeholderToId.set(placeholder, nodeId);
706
862
  } else if (removeHiddenFrom.length > 0 && removeHiddenFrom.indexOf(connectionId) !== -1) {
707
863
  if (!hiddenElement) {
@@ -723,18 +879,19 @@ var NetworkedDOMWebsocketV02Adapter = class {
723
879
  console.warn("No nodeId in childrenChanged message");
724
880
  return;
725
881
  }
726
- let parent = this.idToElement.get(nodeId);
727
- if (!parent) {
882
+ let parentNode = this.idToElement.get(nodeId);
883
+ if (!parentNode) {
728
884
  throw new Error("No parent found for childrenChanged message");
729
885
  }
730
886
  const hiddenParent = this.hiddenPlaceholderElements.get(nodeId);
731
887
  if (hiddenParent) {
732
- parent = hiddenParent.element;
888
+ parentNode = hiddenParent.element;
733
889
  }
734
- if (!isHTMLElement(parent, this.parentElement)) {
890
+ if (!isHTMLElement(parentNode, this.parentElement)) {
735
891
  throw new Error("Parent is not an HTMLElement (that supports children)");
736
892
  }
737
- const targetForChildren = getChildrenTarget(parent);
893
+ const childFactory = resolveChildFactory(parentNode, this.elementFactoryOverride);
894
+ const targetForChildren = getChildrenTarget(parentNode);
738
895
  let nextElement = null;
739
896
  let previousElement = null;
740
897
  if (previousNodeId) {
@@ -746,23 +903,20 @@ var NetworkedDOMWebsocketV02Adapter = class {
746
903
  }
747
904
  const elementsToAdd = [];
748
905
  for (const addedNode of addedNodes) {
749
- const childElement = this.handleNewElement(addedNode);
906
+ const childElement = this.handleNewElement(addedNode, childFactory);
750
907
  if (childElement) {
751
908
  elementsToAdd.push(childElement);
752
909
  }
753
910
  }
754
- if (elementsToAdd.length) {
755
- if (previousElement) {
756
- if (nextElement) {
757
- const docFrag = new DocumentFragment();
758
- docFrag.append(...elementsToAdd);
759
- targetForChildren.insertBefore(docFrag, nextElement);
760
- } else {
761
- targetForChildren.append(...elementsToAdd);
762
- }
763
- } else {
764
- targetForChildren.prepend(...elementsToAdd);
765
- }
911
+ this.insertElements(
912
+ targetForChildren,
913
+ elementsToAdd,
914
+ previousElement,
915
+ nextElement,
916
+ childFactory ?? this.docFactory
917
+ );
918
+ if (this.pendingPortalChildren.size > 0) {
919
+ flushPendingPortalChildren(this.pendingPortalChildren);
766
920
  }
767
921
  }
768
922
  handleChildrenRemoved(message) {
@@ -771,11 +925,11 @@ var NetworkedDOMWebsocketV02Adapter = class {
771
925
  console.warn("No nodeId in childrenChanged message");
772
926
  return;
773
927
  }
774
- const parent = this.idToElement.get(nodeId);
775
- if (!parent) {
928
+ const parentNode = this.idToElement.get(nodeId);
929
+ if (!parentNode) {
776
930
  throw new Error("No parent found for childrenChanged message");
777
931
  }
778
- if (!isHTMLElement(parent, this.parentElement)) {
932
+ if (!isHTMLElement(parentNode, this.parentElement)) {
779
933
  throw new Error("Parent is not an HTMLElement (that supports children)");
780
934
  }
781
935
  for (const removedNode of removedNodes) {
@@ -785,7 +939,7 @@ var NetworkedDOMWebsocketV02Adapter = class {
785
939
  }
786
940
  this.elementToId.delete(childElement);
787
941
  this.idToElement.delete(removedNode);
788
- const targetForRemoval = getRemovalTarget(parent);
942
+ const targetForRemoval = getRemovalTarget(parentNode);
789
943
  const hiddenElement = this.hiddenPlaceholderElements.get(removedNode);
790
944
  if (hiddenElement) {
791
945
  const placeholder = hiddenElement.placeholder;
@@ -811,62 +965,36 @@ var NetworkedDOMWebsocketV02Adapter = class {
811
965
  }
812
966
  }
813
967
  }
814
- removeChildElementIds(parent) {
815
- const portal = getChildrenTarget(parent);
816
- if (portal !== parent) {
817
- this.removeChildElementIds(portal);
818
- }
819
- const childNodes = parent.childNodes;
820
- for (let i = 0; i < childNodes.length; i++) {
821
- const child = childNodes[i];
822
- const childId = this.elementToId.get(child);
823
- if (!childId) {
824
- const placeholderId = this.placeholderToId.get(child);
825
- if (placeholderId) {
826
- const childElement = this.idToElement.get(placeholderId);
827
- if (childElement) {
828
- this.elementToId.delete(childElement);
829
- } else {
830
- console.error(
831
- "Inner child of removed placeholder element not found by id",
832
- placeholderId
833
- );
834
- }
835
- this.idToElement.delete(placeholderId);
836
- this.placeholderToId.delete(child);
837
- this.hiddenPlaceholderElements.delete(placeholderId);
838
- this.removeChildElementIds(childElement);
839
- } else {
840
- console.error(
841
- "Inner child of removed element had no id",
842
- child.outerHTML
843
- );
844
- }
968
+ handleUnregisteredChild(child) {
969
+ const placeholderId = this.placeholderToId.get(child);
970
+ if (placeholderId) {
971
+ const childElement = this.idToElement.get(placeholderId);
972
+ if (childElement) {
973
+ this.elementToId.delete(childElement);
845
974
  } else {
846
- this.elementToId.delete(child);
847
- this.idToElement.delete(childId);
848
- this.removeChildElementIds(child);
975
+ console.error("Inner child of removed placeholder element not found by id", placeholderId);
849
976
  }
977
+ this.idToElement.delete(placeholderId);
978
+ this.placeholderToId.delete(child);
979
+ this.hiddenPlaceholderElements.delete(placeholderId);
980
+ if (childElement) {
981
+ this.removeChildElementIds(childElement);
982
+ }
983
+ } else {
984
+ console.error(
985
+ "Inner child of removed element had no id",
986
+ (child == null ? void 0 : child.outerHTML) ?? child
987
+ );
850
988
  }
851
989
  }
852
990
  handleSnapshot(message) {
853
991
  var _a;
854
- if (this.currentRoot) {
855
- this.currentRoot.remove();
856
- this.currentRoot = null;
857
- this.elementToId.clear();
858
- this.idToElement.clear();
859
- }
860
992
  (_a = this.timeCallback) == null ? void 0 : _a.call(this, message.documentTime);
861
993
  const element = this.handleNewElement(message.snapshot);
862
994
  if (!element) {
863
995
  throw new Error("Snapshot element not created");
864
996
  }
865
- if (!isHTMLElement(element, this.parentElement)) {
866
- throw new Error("Snapshot element is not an HTMLElement");
867
- }
868
- this.currentRoot = element;
869
- this.parentElement.append(element);
997
+ this.resetAndApplySnapshot(element);
870
998
  }
871
999
  handleDocumentTime(message) {
872
1000
  var _a;
@@ -878,35 +1006,31 @@ var NetworkedDOMWebsocketV02Adapter = class {
878
1006
  console.warn("No nodeId in attributeChange message");
879
1007
  return;
880
1008
  }
881
- let element = this.idToElement.get(nodeId);
1009
+ let node = this.idToElement.get(nodeId);
882
1010
  const hiddenElement = this.hiddenPlaceholderElements.get(nodeId);
883
1011
  if (hiddenElement) {
884
- element = hiddenElement.element;
1012
+ node = hiddenElement.element;
885
1013
  }
886
- if (element) {
887
- if (isHTMLElement(element, this.parentElement)) {
1014
+ if (node) {
1015
+ if (isHTMLElement(node, this.parentElement)) {
888
1016
  for (const [key, newValue] of attributes) {
889
1017
  if (newValue === null) {
890
- element.removeAttribute(key);
1018
+ node.removeAttribute(key);
891
1019
  } else {
892
- setElementAttribute(element, key, newValue);
1020
+ setElementAttribute(node, key, newValue);
893
1021
  }
894
1022
  }
895
1023
  } else {
896
- console.error("Element is not an HTMLElement and cannot support attributes", element);
1024
+ console.error("Element is not an HTMLElement and cannot support attributes", node);
897
1025
  }
898
1026
  } else {
899
1027
  console.error("No element found for attributeChange message");
900
1028
  }
901
1029
  }
902
- handleNewElement(message) {
1030
+ handleNewElement(message, factoryOverride) {
1031
+ const factory = factoryOverride ?? this.docFactory;
903
1032
  if (message.type === "text") {
904
- const { nodeId: nodeId2, text: text2 } = message;
905
- const textNode = document.createTextNode("");
906
- textNode.textContent = text2;
907
- this.idToElement.set(nodeId2, textNode);
908
- this.elementToId.set(textNode, nodeId2);
909
- return textNode;
1033
+ return this.createTextNode(message.nodeId, message.text, factory);
910
1034
  }
911
1035
  const { tag, nodeId, attributes, children, text, hiddenFrom } = message;
912
1036
  if (this.idToElement.has(nodeId)) {
@@ -918,34 +1042,36 @@ var NetworkedDOMWebsocketV02Adapter = class {
918
1042
  throw new Error("Received nodeId to add that is already present: " + nodeId);
919
1043
  }
920
1044
  if (tag === "#text") {
921
- const textNode = document.createTextNode("");
922
- textNode.textContent = text || null;
923
- this.idToElement.set(nodeId, textNode);
924
- this.elementToId.set(textNode, nodeId);
925
- return textNode;
1045
+ return this.createTextNode(nodeId, text || "", factory);
926
1046
  }
927
1047
  let element;
928
1048
  try {
929
- element = createElementWithSVGSupport(tag, this.options);
1049
+ element = createElementWithSVGSupport(tag, this.options, factory);
930
1050
  } catch (e) {
931
1051
  console.error(`Error creating element: (${tag})`, e);
932
- element = document.createElement("x-div");
1052
+ element = factory.createElement("x-div");
933
1053
  }
934
1054
  for (const [key, value] of attributes) {
935
1055
  if (value !== null) {
936
1056
  setElementAttribute(element, key, value);
937
1057
  }
938
1058
  }
1059
+ recordFactoryOverride(element, factory, this.docFactory, this.elementFactoryOverride);
1060
+ const { childFactory, usingPortalFactory } = resolvePortalChildFactory(element, factory);
939
1061
  if (children) {
940
1062
  for (const child of children) {
941
- const childElement = this.handleNewElement(child);
1063
+ const childElement = this.handleNewElement(child, childFactory);
942
1064
  if (childElement) {
943
- element.append(childElement);
1065
+ if (usingPortalFactory) {
1066
+ bufferPortalChild(this.pendingPortalChildren, element, childElement);
1067
+ } else {
1068
+ element.append(childElement);
1069
+ }
944
1070
  }
945
1071
  }
946
1072
  }
947
1073
  if (hiddenFrom && hiddenFrom.length > 0 && hiddenFrom.indexOf(connectionId) !== -1) {
948
- const placeholder = document.createElement(hiddenTag);
1074
+ const placeholder = this.docFactory.createElement(hiddenTag);
949
1075
  this.hiddenPlaceholderElements.set(nodeId, { placeholder, element });
950
1076
  this.placeholderToId.set(placeholder, nodeId);
951
1077
  this.idToElement.set(nodeId, element);
@@ -996,13 +1122,14 @@ function NetworkedDOMWebsocketStatusToString(status) {
996
1122
  }
997
1123
  }
998
1124
  var NetworkedDOMWebsocket = class {
999
- constructor(url, websocketFactory, parentElement, timeCallback, statusUpdateCallback, options = {}) {
1125
+ constructor(url, websocketFactory, parentElement, timeCallback, statusUpdateCallback, options = {}, doc) {
1000
1126
  this.url = url;
1001
1127
  this.websocketFactory = websocketFactory;
1002
1128
  this.parentElement = parentElement;
1003
1129
  this.timeCallback = timeCallback;
1004
1130
  this.statusUpdateCallback = statusUpdateCallback;
1005
1131
  this.options = options;
1132
+ this.doc = doc;
1006
1133
  this.websocket = null;
1007
1134
  this.websocketAdapter = null;
1008
1135
  this.stopped = false;
@@ -1047,7 +1174,8 @@ var NetworkedDOMWebsocket = class {
1047
1174
  this.setStatus(2 /* Connected */);
1048
1175
  },
1049
1176
  this.timeCallback,
1050
- this.options
1177
+ this.options,
1178
+ this.doc
1051
1179
  );
1052
1180
  } else {
1053
1181
  websocketAdapter = new NetworkedDOMWebsocketV01Adapter(
@@ -1058,7 +1186,8 @@ var NetworkedDOMWebsocket = class {
1058
1186
  this.setStatus(2 /* Connected */);
1059
1187
  },
1060
1188
  this.timeCallback,
1061
- this.options
1189
+ this.options,
1190
+ this.doc
1062
1191
  );
1063
1192
  }
1064
1193
  this.websocketAdapter = websocketAdapter;
@@ -1150,29 +1279,58 @@ var NetworkedDOMWebsocket = class {
1150
1279
  }
1151
1280
  };
1152
1281
  function isHTMLElement(node, rootNode) {
1153
- if (node instanceof HTMLElement || node instanceof Element) {
1154
- return true;
1155
- }
1156
- if (!rootNode.ownerDocument.defaultView) {
1157
- return false;
1282
+ if (!node || typeof node !== "object") return false;
1283
+ const nodeLike = node;
1284
+ if (nodeLike[VIRTUAL_ELEMENT_BRAND] === true) return true;
1285
+ if (typeof HTMLElement !== "undefined" && node instanceof HTMLElement) return true;
1286
+ if (typeof Element !== "undefined" && node instanceof Element) return true;
1287
+ const rootNodeRecord = rootNode;
1288
+ if (rootNodeRecord == null ? void 0 : rootNodeRecord.ownerDocument) {
1289
+ const ownerDoc = rootNodeRecord.ownerDocument;
1290
+ const defaultView = ownerDoc.defaultView;
1291
+ if (defaultView == null ? void 0 : defaultView.HTMLElement) {
1292
+ return node instanceof defaultView.HTMLElement;
1293
+ }
1158
1294
  }
1159
- return node instanceof rootNode.ownerDocument.defaultView.HTMLElement;
1295
+ return false;
1160
1296
  }
1161
1297
  function isText(node, rootNode) {
1162
- if (node instanceof Text) {
1163
- return true;
1164
- }
1165
- if (!rootNode.ownerDocument.defaultView) {
1166
- return false;
1298
+ if (!node || typeof node !== "object") return false;
1299
+ const nodeLike = node;
1300
+ if (nodeLike[VIRTUAL_TEXT_BRAND] === true) return true;
1301
+ if (typeof Text !== "undefined" && node instanceof Text) return true;
1302
+ const rootNodeRecord = rootNode;
1303
+ if (rootNodeRecord == null ? void 0 : rootNodeRecord.ownerDocument) {
1304
+ const ownerDoc = rootNodeRecord.ownerDocument;
1305
+ const defaultView = ownerDoc.defaultView;
1306
+ if (defaultView == null ? void 0 : defaultView.Text) {
1307
+ return node instanceof defaultView.Text;
1308
+ }
1167
1309
  }
1168
- return node instanceof rootNode.ownerDocument.defaultView.Text;
1310
+ return false;
1169
1311
  }
1170
1312
  export {
1171
1313
  DOMSanitizer,
1172
1314
  NetworkedDOMWebsocket,
1173
1315
  NetworkedDOMWebsocketStatus,
1174
1316
  NetworkedDOMWebsocketStatusToString,
1317
+ VIRTUAL_DOCUMENT_BRAND,
1318
+ VIRTUAL_ELEMENT_BRAND,
1319
+ VIRTUAL_FRAGMENT_BRAND,
1320
+ VIRTUAL_TEXT_BRAND,
1321
+ bufferPortalChild,
1322
+ createElementWithSVGSupport,
1323
+ flushPendingPortalChildren,
1324
+ getChildrenTarget,
1325
+ getRemovalTarget,
1326
+ isElementLike,
1175
1327
  isHTMLElement,
1176
- isText
1328
+ isPortalElement,
1329
+ isText,
1330
+ recordFactoryOverride,
1331
+ remapAttributeName,
1332
+ resolveChildFactory,
1333
+ resolvePortalChildFactory,
1334
+ setElementAttribute
1177
1335
  };
1178
1336
  //# sourceMappingURL=index.js.map