@mml-io/networked-dom-web-client 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
@@ -586,51 +586,91 @@ function encodeClientMessage(message, writer, protocolSubversion) {
586
586
  }
587
587
 
588
588
  // ../networked-dom-web/build/index.js
589
- var DOMSanitizer = class _DOMSanitizer {
590
- static sanitise(node, options = {}) {
591
- if (node.getAttributeNames) {
592
- for (const attr of node.getAttributeNames()) {
593
- if (!_DOMSanitizer.IsValidAttributeName(attr)) {
594
- node.removeAttribute(attr);
595
- }
596
- }
589
+ var VIRTUAL_ELEMENT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-element");
590
+ var VIRTUAL_TEXT_BRAND = /* @__PURE__ */ Symbol.for("mml-virtual-text");
591
+ function isElementLike(node) {
592
+ return typeof node.setAttribute === "function";
593
+ }
594
+ function isPortalElement(element) {
595
+ return typeof element.getPortalElement === "function";
596
+ }
597
+ var _DOMSanitizer = class _DOMSanitizer2 {
598
+ /**
599
+ * Returns true if a tag with the given name should be stripped of all
600
+ * content and attributes during sanitisation.
601
+ */
602
+ static isBlockedTag(tagName) {
603
+ return _DOMSanitizer2.BLOCKED_TAGS.has(tagName.toLowerCase());
604
+ }
605
+ /**
606
+ * Given a tag name and sanitisation options, returns the sanitised tag name.
607
+ * Non-prefixed tags are renamed (e.g. "div" → "x-div"). Returns null for
608
+ * blocked tags that should be skipped entirely.
609
+ */
610
+ static sanitiseTagName(tagName, options) {
611
+ const tag = tagName.toLowerCase();
612
+ if (_DOMSanitizer2.isBlockedTag(tag)) {
613
+ return null;
597
614
  }
598
- if (node instanceof HTMLElement) {
599
- if (options.tagPrefix) {
600
- const tag = node.nodeName.toLowerCase();
601
- if (!tag.startsWith(options.tagPrefix.toLowerCase())) {
602
- node = _DOMSanitizer.replaceNodeTagName(
603
- node,
604
- options.replacementTagPrefix ? options.replacementTagPrefix + tag : `x-${tag}`
605
- );
606
- }
607
- }
615
+ if (options.tagPrefix && !tag.startsWith(options.tagPrefix.toLowerCase())) {
616
+ return (options.replacementTagPrefix ?? "x-") + tag;
608
617
  }
609
- if (node.nodeName === "SCRIPT" || node.nodeName === "OBJECT" || node.nodeName === "IFRAME") {
610
- node.innerHTML = "";
611
- _DOMSanitizer.stripAllAttributes(node);
618
+ return tag;
619
+ }
620
+ /**
621
+ * Sanitises a DOM node in-place. When tag replacement occurs (via tagPrefix option),
622
+ * the returned node may be a different object than the input node.
623
+ */
624
+ static sanitise(node, options = {}, doc) {
625
+ if (_DOMSanitizer2.isBlockedTag(node.nodeName)) {
626
+ if (isElementLike(node)) {
627
+ node.innerHTML = "";
628
+ _DOMSanitizer2.stripAllAttributes(node);
629
+ }
612
630
  } else {
613
- if (node.getAttributeNames) {
614
- for (const attr of node.getAttributeNames()) {
615
- if (!_DOMSanitizer.shouldAcceptAttribute(attr)) {
616
- node.removeAttribute(attr);
631
+ if (isElementLike(node)) {
632
+ let element = node;
633
+ for (const attr of element.getAttributeNames()) {
634
+ if (!_DOMSanitizer2.IsValidAttributeName(attr)) {
635
+ element.removeAttribute(attr);
636
+ }
637
+ }
638
+ if (options.tagPrefix) {
639
+ const tag = element.nodeName.toLowerCase();
640
+ if (!tag.startsWith(options.tagPrefix.toLowerCase())) {
641
+ element = _DOMSanitizer2.replaceNodeTagName(
642
+ element,
643
+ (options.replacementTagPrefix ?? "x-") + tag,
644
+ doc
645
+ );
646
+ node = element;
647
+ }
648
+ }
649
+ for (const attr of element.getAttributeNames()) {
650
+ if (!_DOMSanitizer2.shouldAcceptAttribute(attr)) {
651
+ element.removeAttribute(attr);
617
652
  }
618
653
  }
619
654
  }
620
655
  for (let i = 0; i < node.childNodes.length; i++) {
621
- _DOMSanitizer.sanitise(node.childNodes[i], options);
656
+ _DOMSanitizer2.sanitise(node.childNodes[i], options, doc);
622
657
  }
623
658
  }
624
659
  return node;
625
660
  }
626
- static replaceNodeTagName(node, newTagName) {
661
+ static replaceNodeTagName(node, newTagName, doc) {
627
662
  var _a;
628
- const replacementNode = document.createElement(newTagName);
629
- let index;
663
+ if (!doc && typeof document === "undefined") {
664
+ throw new Error(
665
+ "DOMSanitizer.replaceNodeTagName requires a document factory (IDocumentFactory) in non-browser environments"
666
+ );
667
+ }
668
+ const docFactory = doc ?? document;
669
+ const replacementNode = docFactory.createElement(newTagName);
630
670
  while (node.firstChild) {
631
671
  replacementNode.appendChild(node.firstChild);
632
672
  }
633
- for (index = node.attributes.length - 1; index >= 0; --index) {
673
+ for (let index = node.attributes.length - 1; index >= 0; --index) {
634
674
  replacementNode.setAttribute(node.attributes[index].name, node.attributes[index].value);
635
675
  }
636
676
  (_a = node.parentNode) == null ? void 0 : _a.replaceChild(replacementNode, node);
@@ -651,25 +691,27 @@ var DOMSanitizer = class _DOMSanitizer {
651
691
  }
652
692
  static IsValidAttributeName(characters) {
653
693
  const c = characters[0];
654
- if (!(_DOMSanitizer.IsASCIIAlpha(c) || c === ":" || c === "_")) {
694
+ if (!(_DOMSanitizer2.IsASCIIAlpha(c) || c === ":" || c === "_")) {
655
695
  return false;
656
696
  }
657
697
  for (let i = 1; i < characters.length; i++) {
658
698
  const c2 = characters[i];
659
- if (!(_DOMSanitizer.IsASCIIDigit(c2) || _DOMSanitizer.IsASCIIAlpha(c2) || c2 === ":" || c2 === "_" || c2 === "-" || c2 === ".")) {
699
+ if (!(_DOMSanitizer2.IsASCIIDigit(c2) || _DOMSanitizer2.IsASCIIAlpha(c2) || c2 === ":" || c2 === "_" || c2 === "-" || c2 === ".")) {
660
700
  return false;
661
701
  }
662
702
  }
663
703
  return true;
664
704
  }
665
705
  static shouldAcceptAttribute(attribute) {
666
- if (!_DOMSanitizer.IsValidAttributeName(attribute)) {
706
+ if (!_DOMSanitizer2.IsValidAttributeName(attribute)) {
667
707
  console.warn("Invalid attribute name", attribute);
668
708
  return false;
669
709
  }
670
710
  return !attribute.startsWith("on");
671
711
  }
672
712
  };
713
+ _DOMSanitizer.BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "object", "iframe"]);
714
+ var DOMSanitizer = _DOMSanitizer;
673
715
  var ALWAYS_DISALLOWED_TAGS = /* @__PURE__ */ new Set(["foreignobject", "iframe", "script"]);
674
716
  var SVG_TAG_NAMES_ADJUSTMENT_MAP = new Map(
675
717
  [
@@ -793,7 +835,13 @@ function remapAttributeName(attrName) {
793
835
  }
794
836
  return attrName;
795
837
  }
796
- function createElementWithSVGSupport(tag, options = {}) {
838
+ function createElementWithSVGSupport(tag, options = {}, doc) {
839
+ if (!doc && typeof document === "undefined") {
840
+ throw new Error(
841
+ "createElementWithSVGSupport requires a document factory (IDocumentFactory) in non-browser environments"
842
+ );
843
+ }
844
+ const docFactory = doc ?? document;
797
845
  let filteredTag = tag.toLowerCase();
798
846
  if (ALWAYS_DISALLOWED_TAGS.has(filteredTag.toLowerCase())) {
799
847
  console.error("Disallowing tag", filteredTag);
@@ -806,14 +854,17 @@ function createElementWithSVGSupport(tag, options = {}) {
806
854
  if (svgTagMapping) {
807
855
  filteredTag = svgTagMapping;
808
856
  const xmlns = "http://www.w3.org/2000/svg";
809
- return document.createElementNS(xmlns, filteredTag);
857
+ if (docFactory.createElementNS) {
858
+ return docFactory.createElementNS(xmlns, filteredTag);
859
+ }
860
+ return docFactory.createElement(filteredTag);
810
861
  } else {
811
862
  if (options.tagPrefix) {
812
863
  if (!tag.toLowerCase().startsWith(options.tagPrefix.toLowerCase())) {
813
864
  filteredTag = options.replacementTagPrefix ? options.replacementTagPrefix + tag : `x-${tag}`;
814
865
  }
815
866
  }
816
- return document.createElement(filteredTag);
867
+ return docFactory.createElement(filteredTag);
817
868
  }
818
869
  }
819
870
  function setElementAttribute(element, key, value) {
@@ -823,21 +874,58 @@ function setElementAttribute(element, key, value) {
823
874
  }
824
875
  }
825
876
  function getChildrenTarget(parent) {
826
- let targetForChildren = parent;
827
- if (parent.getPortalElement) {
828
- targetForChildren = parent.getPortalElement();
877
+ if (isPortalElement(parent)) {
878
+ return parent.getPortalElement();
829
879
  }
830
- return targetForChildren;
880
+ return parent;
831
881
  }
832
882
  function getRemovalTarget(parent) {
833
- let targetForRemoval = parent;
834
- if (parent.getPortalElement) {
835
- targetForRemoval = parent.getPortalElement();
883
+ if (isPortalElement(parent)) {
884
+ return parent.getPortalElement();
836
885
  }
837
- return targetForRemoval;
886
+ return parent;
838
887
  }
839
- var NetworkedDOMWebsocketV01Adapter = class {
840
- constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}) {
888
+ function resolveChildFactory(parentNode, elementFactoryOverride) {
889
+ var _a;
890
+ if (isElementLike(parentNode) && isPortalElement(parentNode)) {
891
+ const portalFactory = (_a = parentNode.getPortalDocumentFactory) == null ? void 0 : _a.call(parentNode);
892
+ if (portalFactory) {
893
+ return portalFactory;
894
+ }
895
+ }
896
+ return elementFactoryOverride.get(parentNode);
897
+ }
898
+ function resolvePortalChildFactory(element, currentFactory) {
899
+ var _a;
900
+ if (isPortalElement(element)) {
901
+ const portalFactory = (_a = element.getPortalDocumentFactory) == null ? void 0 : _a.call(element);
902
+ if (portalFactory) {
903
+ return { childFactory: portalFactory, usingPortalFactory: true };
904
+ }
905
+ }
906
+ return { childFactory: currentFactory, usingPortalFactory: false };
907
+ }
908
+ function recordFactoryOverride(element, factory, defaultFactory, elementFactoryOverride) {
909
+ if (factory !== defaultFactory) {
910
+ elementFactoryOverride.set(element, factory);
911
+ }
912
+ }
913
+ function flushPendingPortalChildren(pendingPortalChildren) {
914
+ for (const [portalParent, children] of pendingPortalChildren) {
915
+ const target = getChildrenTarget(portalParent);
916
+ for (const child of children) {
917
+ target.appendChild(child);
918
+ }
919
+ }
920
+ pendingPortalChildren.clear();
921
+ }
922
+ function bufferPortalChild(pendingPortalChildren, portalParent, child) {
923
+ const pending = pendingPortalChildren.get(portalParent) ?? [];
924
+ pending.push(child);
925
+ pendingPortalChildren.set(portalParent, pending);
926
+ }
927
+ var NetworkedDOMWebsocketAdapterBase = class {
928
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
841
929
  this.websocket = websocket;
842
930
  this.parentElement = parentElement;
843
931
  this.connectedCallback = connectedCallback;
@@ -846,7 +934,117 @@ var NetworkedDOMWebsocketV01Adapter = class {
846
934
  this.idToElement = /* @__PURE__ */ new Map();
847
935
  this.elementToId = /* @__PURE__ */ new Map();
848
936
  this.currentRoot = null;
937
+ this.pendingPortalChildren = /* @__PURE__ */ new Map();
938
+ this.elementFactoryOverride = /* @__PURE__ */ new Map();
849
939
  this.websocket.binaryType = "arraybuffer";
940
+ if (!doc && typeof document === "undefined") {
941
+ throw new Error(
942
+ "NetworkedDOMWebsocketAdapter requires a document factory (IDocumentFactory) in non-browser environments"
943
+ );
944
+ }
945
+ this.docFactory = doc ?? document;
946
+ }
947
+ clearContents() {
948
+ this.idToElement.clear();
949
+ this.elementToId.clear();
950
+ this.elementFactoryOverride.clear();
951
+ this.pendingPortalChildren.clear();
952
+ if (this.currentRoot) {
953
+ this.currentRoot.remove();
954
+ this.currentRoot = null;
955
+ return true;
956
+ }
957
+ return false;
958
+ }
959
+ /**
960
+ * Creates a text node, registers it in the id maps, and returns it.
961
+ */
962
+ createTextNode(nodeId, text, factory) {
963
+ const textNode = factory.createTextNode("");
964
+ textNode.textContent = text;
965
+ this.idToElement.set(nodeId, textNode);
966
+ this.elementToId.set(textNode, nodeId);
967
+ return textNode;
968
+ }
969
+ /**
970
+ * Inserts elements into the correct position within a parent, using
971
+ * previousElement/nextElement for positioning. Creates a DocumentFragment
972
+ * when inserting before a reference node.
973
+ */
974
+ insertElements(targetForChildren, elementsToAdd, previousElement, nextElement, factory) {
975
+ if (elementsToAdd.length === 0) return;
976
+ if (previousElement) {
977
+ if (nextElement) {
978
+ const docFrag = factory.createDocumentFragment();
979
+ docFrag.append(...elementsToAdd);
980
+ targetForChildren.insertBefore(docFrag, nextElement);
981
+ } else {
982
+ targetForChildren.append(...elementsToAdd);
983
+ }
984
+ } else {
985
+ targetForChildren.prepend(...elementsToAdd);
986
+ }
987
+ }
988
+ /**
989
+ * Recursively removes element-to-id mappings for all descendants of a parent.
990
+ * V02 overrides this to also handle hidden placeholder elements.
991
+ */
992
+ removeChildElementIds(parent) {
993
+ if (isElementLike(parent)) {
994
+ const portal = getChildrenTarget(parent);
995
+ if (portal !== parent) {
996
+ this.removeChildElementIds(portal);
997
+ }
998
+ }
999
+ for (let i = 0; i < parent.childNodes.length; i++) {
1000
+ const child = parent.childNodes[i];
1001
+ const childId = this.elementToId.get(child);
1002
+ if (!childId) {
1003
+ this.handleUnregisteredChild(child);
1004
+ } else {
1005
+ this.elementToId.delete(child);
1006
+ this.idToElement.delete(childId);
1007
+ this.elementFactoryOverride.delete(child);
1008
+ }
1009
+ this.removeChildElementIds(child);
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Called during removeChildElementIds when a child has no registered id.
1014
+ * V01 logs an error. V02 overrides to check for placeholder elements.
1015
+ */
1016
+ handleUnregisteredChild(child) {
1017
+ console.error("Inner child of removed element had no id", child);
1018
+ }
1019
+ /**
1020
+ * Resets state and applies a snapshot element to the parent.
1021
+ * Appending to the tree triggers MElement connectedCallbacks (which set up portals),
1022
+ * then pending portal children are flushed.
1023
+ */
1024
+ resetAndApplySnapshot(element) {
1025
+ if (this.currentRoot) {
1026
+ this.removeChildElementIds(this.currentRoot);
1027
+ const rootId = this.elementToId.get(this.currentRoot);
1028
+ if (rootId !== void 0) {
1029
+ this.elementToId.delete(this.currentRoot);
1030
+ this.idToElement.delete(rootId);
1031
+ this.elementFactoryOverride.delete(this.currentRoot);
1032
+ }
1033
+ this.currentRoot.remove();
1034
+ this.currentRoot = null;
1035
+ this.pendingPortalChildren.clear();
1036
+ }
1037
+ if (!isHTMLElement(element, this.parentElement)) {
1038
+ throw new Error("Snapshot element is not an HTMLElement");
1039
+ }
1040
+ this.currentRoot = element;
1041
+ this.parentElement.append(element);
1042
+ flushPendingPortalChildren(this.pendingPortalChildren);
1043
+ }
1044
+ };
1045
+ var NetworkedDOMWebsocketV01Adapter = class extends NetworkedDOMWebsocketAdapterBase {
1046
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
1047
+ super(websocket, parentElement, connectedCallback, timeCallback, options, doc);
850
1048
  }
851
1049
  handleEvent(element, event) {
852
1050
  const nodeId = this.elementToId.get(element);
@@ -869,16 +1067,6 @@ var NetworkedDOMWebsocketV01Adapter = class {
869
1067
  send(fromClientMessage) {
870
1068
  this.websocket.send(JSON.stringify(fromClientMessage));
871
1069
  }
872
- clearContents() {
873
- this.idToElement.clear();
874
- this.elementToId.clear();
875
- if (this.currentRoot) {
876
- this.currentRoot.remove();
877
- this.currentRoot = null;
878
- return true;
879
- }
880
- return false;
881
- }
882
1070
  receiveMessage(event) {
883
1071
  try {
884
1072
  const messages = JSON.parse(event.data);
@@ -950,14 +1138,15 @@ var NetworkedDOMWebsocketV01Adapter = class {
950
1138
  console.warn("No nodeId in childrenChanged message");
951
1139
  return;
952
1140
  }
953
- const parent = this.idToElement.get(nodeId);
954
- if (!parent) {
1141
+ const parentNode = this.idToElement.get(nodeId);
1142
+ if (!parentNode) {
955
1143
  throw new Error("No parent found for childrenChanged message");
956
1144
  }
957
- if (!isHTMLElement(parent, this.parentElement)) {
1145
+ if (!isHTMLElement(parentNode, this.parentElement)) {
958
1146
  throw new Error("Parent is not an HTMLElement (that supports children)");
959
1147
  }
960
- const targetForChildren = getChildrenTarget(parent);
1148
+ const childFactory = resolveChildFactory(parentNode, this.elementFactoryOverride);
1149
+ const targetForChildren = getChildrenTarget(parentNode);
961
1150
  let nextElement = null;
962
1151
  let previousElement = null;
963
1152
  if (previousNodeId) {
@@ -969,23 +1158,20 @@ var NetworkedDOMWebsocketV01Adapter = class {
969
1158
  }
970
1159
  const elementsToAdd = [];
971
1160
  for (const addedNode of addedNodes) {
972
- const childElement = this.handleNewElement(addedNode);
1161
+ const childElement = this.handleNewElement(addedNode, childFactory);
973
1162
  if (childElement) {
974
1163
  elementsToAdd.push(childElement);
975
1164
  }
976
1165
  }
977
- if (elementsToAdd.length) {
978
- if (previousElement) {
979
- if (nextElement) {
980
- const docFrag = new DocumentFragment();
981
- docFrag.append(...elementsToAdd);
982
- targetForChildren.insertBefore(docFrag, nextElement);
983
- } else {
984
- targetForChildren.append(...elementsToAdd);
985
- }
986
- } else {
987
- targetForChildren.prepend(...elementsToAdd);
988
- }
1166
+ this.insertElements(
1167
+ targetForChildren,
1168
+ elementsToAdd,
1169
+ previousElement,
1170
+ nextElement,
1171
+ childFactory ?? this.docFactory
1172
+ );
1173
+ if (this.pendingPortalChildren.size > 0) {
1174
+ flushPendingPortalChildren(this.pendingPortalChildren);
989
1175
  }
990
1176
  for (const removedNode of removedNodes) {
991
1177
  const childElement = this.idToElement.get(removedNode);
@@ -994,46 +1180,19 @@ var NetworkedDOMWebsocketV01Adapter = class {
994
1180
  }
995
1181
  this.elementToId.delete(childElement);
996
1182
  this.idToElement.delete(removedNode);
997
- const targetForRemoval = getRemovalTarget(parent);
1183
+ const targetForRemoval = getRemovalTarget(parentNode);
998
1184
  targetForRemoval.removeChild(childElement);
999
1185
  if (isHTMLElement(childElement, this.parentElement)) {
1000
1186
  this.removeChildElementIds(childElement);
1001
1187
  }
1002
1188
  }
1003
1189
  }
1004
- removeChildElementIds(parent) {
1005
- const portal = getChildrenTarget(parent);
1006
- if (portal !== parent) {
1007
- this.removeChildElementIds(portal);
1008
- }
1009
- for (let i = 0; i < parent.childNodes.length; i++) {
1010
- const child = parent.childNodes[i];
1011
- const childId = this.elementToId.get(child);
1012
- if (!childId) {
1013
- console.error("Inner child of removed element had no id", child);
1014
- } else {
1015
- this.elementToId.delete(child);
1016
- this.idToElement.delete(childId);
1017
- }
1018
- this.removeChildElementIds(child);
1019
- }
1020
- }
1021
1190
  handleSnapshot(message) {
1022
- if (this.currentRoot) {
1023
- this.currentRoot.remove();
1024
- this.currentRoot = null;
1025
- this.elementToId.clear();
1026
- this.idToElement.clear();
1027
- }
1028
1191
  const element = this.handleNewElement(message.snapshot);
1029
1192
  if (!element) {
1030
1193
  throw new Error("Snapshot element not created");
1031
1194
  }
1032
- if (!isHTMLElement(element, this.parentElement)) {
1033
- throw new Error("Snapshot element is not an HTMLElement");
1034
- }
1035
- this.currentRoot = element;
1036
- this.parentElement.append(element);
1195
+ this.resetAndApplySnapshot(element);
1037
1196
  }
1038
1197
  handleAttributeChange(message) {
1039
1198
  const { nodeId, attribute, newValue } = message;
@@ -1041,29 +1200,25 @@ var NetworkedDOMWebsocketV01Adapter = class {
1041
1200
  console.warn("No nodeId in attributeChange message");
1042
1201
  return;
1043
1202
  }
1044
- const element = this.idToElement.get(nodeId);
1045
- if (element) {
1046
- if (isHTMLElement(element, this.parentElement)) {
1203
+ const node = this.idToElement.get(nodeId);
1204
+ if (node) {
1205
+ if (isHTMLElement(node, this.parentElement)) {
1047
1206
  if (newValue === null) {
1048
- element.removeAttribute(attribute);
1207
+ node.removeAttribute(attribute);
1049
1208
  } else {
1050
- setElementAttribute(element, attribute, newValue);
1209
+ setElementAttribute(node, attribute, newValue);
1051
1210
  }
1052
1211
  } else {
1053
- console.error("Element is not an HTMLElement and cannot support attributes", element);
1212
+ console.error("Element is not an HTMLElement and cannot support attributes", node);
1054
1213
  }
1055
1214
  } else {
1056
1215
  console.error("No element found for attributeChange message");
1057
1216
  }
1058
1217
  }
1059
- handleNewElement(message) {
1218
+ handleNewElement(message, factoryOverride) {
1219
+ const factory = factoryOverride ?? this.docFactory;
1060
1220
  if (message.type === "text") {
1061
- const { nodeId: nodeId2, text: text2 } = message;
1062
- const textNode = document.createTextNode("");
1063
- textNode.textContent = text2;
1064
- this.idToElement.set(nodeId2, textNode);
1065
- this.elementToId.set(textNode, nodeId2);
1066
- return textNode;
1221
+ return this.createTextNode(message.nodeId, message.text, factory);
1067
1222
  }
1068
1223
  const { tag, nodeId, attributes, children, text } = message;
1069
1224
  if (nodeId === void 0 || nodeId === null) {
@@ -1078,18 +1233,14 @@ var NetworkedDOMWebsocketV01Adapter = class {
1078
1233
  );
1079
1234
  }
1080
1235
  if (tag === "#text") {
1081
- const textNode = document.createTextNode("");
1082
- textNode.textContent = text || null;
1083
- this.idToElement.set(nodeId, textNode);
1084
- this.elementToId.set(textNode, nodeId);
1085
- return textNode;
1236
+ return this.createTextNode(nodeId, text || "", factory);
1086
1237
  }
1087
1238
  let element;
1088
1239
  try {
1089
- element = createElementWithSVGSupport(tag, this.options);
1240
+ element = createElementWithSVGSupport(tag, this.options, factory);
1090
1241
  } catch (e) {
1091
1242
  console.error(`Error creating element: (${tag})`, e);
1092
- element = document.createElement("x-div");
1243
+ element = factory.createElement("x-div");
1093
1244
  }
1094
1245
  this.idToElement.set(nodeId, element);
1095
1246
  this.elementToId.set(element, nodeId);
@@ -1097,11 +1248,17 @@ var NetworkedDOMWebsocketV01Adapter = class {
1097
1248
  const value = attributes[key];
1098
1249
  setElementAttribute(element, key, value);
1099
1250
  }
1251
+ recordFactoryOverride(element, factory, this.docFactory, this.elementFactoryOverride);
1252
+ const { childFactory, usingPortalFactory } = resolvePortalChildFactory(element, factory);
1100
1253
  if (children) {
1101
1254
  for (const child of children) {
1102
- const childElement = this.handleNewElement(child);
1255
+ const childElement = this.handleNewElement(child, childFactory);
1103
1256
  if (childElement) {
1104
- element.append(childElement);
1257
+ if (usingPortalFactory) {
1258
+ bufferPortalChild(this.pendingPortalChildren, element, childElement);
1259
+ } else {
1260
+ element.append(childElement);
1261
+ }
1105
1262
  }
1106
1263
  }
1107
1264
  }
@@ -1110,21 +1267,13 @@ var NetworkedDOMWebsocketV01Adapter = class {
1110
1267
  };
1111
1268
  var connectionId = 1;
1112
1269
  var hiddenTag = "x-hidden";
1113
- var NetworkedDOMWebsocketV02Adapter = class {
1114
- constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}) {
1115
- this.websocket = websocket;
1116
- this.parentElement = parentElement;
1117
- this.connectedCallback = connectedCallback;
1118
- this.timeCallback = timeCallback;
1119
- this.options = options;
1120
- this.idToElement = /* @__PURE__ */ new Map();
1121
- this.elementToId = /* @__PURE__ */ new Map();
1270
+ var NetworkedDOMWebsocketV02Adapter = class extends NetworkedDOMWebsocketAdapterBase {
1271
+ constructor(websocket, parentElement, connectedCallback, timeCallback, options = {}, doc) {
1272
+ super(websocket, parentElement, connectedCallback, timeCallback, options, doc);
1122
1273
  this.placeholderToId = /* @__PURE__ */ new Map();
1123
1274
  this.hiddenPlaceholderElements = /* @__PURE__ */ new Map();
1124
- this.currentRoot = null;
1125
1275
  this.batchMode = false;
1126
1276
  this.batchMessages = [];
1127
- this.websocket.binaryType = "arraybuffer";
1128
1277
  this.protocolSubversion = getNetworkedDOMProtocolSubProtocol_v0_2SubversionOrThrow(
1129
1278
  websocket.protocol
1130
1279
  );
@@ -1160,14 +1309,11 @@ var NetworkedDOMWebsocketV02Adapter = class {
1160
1309
  this.websocket.send(writer.getBuffer());
1161
1310
  }
1162
1311
  clearContents() {
1163
- this.idToElement.clear();
1164
- this.elementToId.clear();
1165
- if (this.currentRoot) {
1166
- this.currentRoot.remove();
1167
- this.currentRoot = null;
1168
- return true;
1169
- }
1170
- return false;
1312
+ this.placeholderToId.clear();
1313
+ this.hiddenPlaceholderElements.clear();
1314
+ this.batchMessages = [];
1315
+ this.batchMode = false;
1316
+ return super.clearContents();
1171
1317
  }
1172
1318
  receiveMessage(event) {
1173
1319
  try {
@@ -1262,13 +1408,14 @@ var NetworkedDOMWebsocketV02Adapter = class {
1262
1408
  if (!node) {
1263
1409
  throw new Error("No node found for changeHiddenFrom message");
1264
1410
  }
1265
- const parent = node.parentElement;
1411
+ const element = node;
1412
+ const parent = element.parentElement;
1266
1413
  if (!parent) {
1267
1414
  throw new Error("Node has no parent");
1268
1415
  }
1269
- const placeholder = document.createElement(hiddenTag);
1270
- parent.replaceChild(placeholder, node);
1271
- this.hiddenPlaceholderElements.set(nodeId, { placeholder, element: node });
1416
+ const placeholder = this.docFactory.createElement(hiddenTag);
1417
+ parent.replaceChild(placeholder, element);
1418
+ this.hiddenPlaceholderElements.set(nodeId, { placeholder, element });
1272
1419
  this.placeholderToId.set(placeholder, nodeId);
1273
1420
  } else if (removeHiddenFrom.length > 0 && removeHiddenFrom.indexOf(connectionId) !== -1) {
1274
1421
  if (!hiddenElement) {
@@ -1290,18 +1437,19 @@ var NetworkedDOMWebsocketV02Adapter = class {
1290
1437
  console.warn("No nodeId in childrenChanged message");
1291
1438
  return;
1292
1439
  }
1293
- let parent = this.idToElement.get(nodeId);
1294
- if (!parent) {
1440
+ let parentNode = this.idToElement.get(nodeId);
1441
+ if (!parentNode) {
1295
1442
  throw new Error("No parent found for childrenChanged message");
1296
1443
  }
1297
1444
  const hiddenParent = this.hiddenPlaceholderElements.get(nodeId);
1298
1445
  if (hiddenParent) {
1299
- parent = hiddenParent.element;
1446
+ parentNode = hiddenParent.element;
1300
1447
  }
1301
- if (!isHTMLElement(parent, this.parentElement)) {
1448
+ if (!isHTMLElement(parentNode, this.parentElement)) {
1302
1449
  throw new Error("Parent is not an HTMLElement (that supports children)");
1303
1450
  }
1304
- const targetForChildren = getChildrenTarget(parent);
1451
+ const childFactory = resolveChildFactory(parentNode, this.elementFactoryOverride);
1452
+ const targetForChildren = getChildrenTarget(parentNode);
1305
1453
  let nextElement = null;
1306
1454
  let previousElement = null;
1307
1455
  if (previousNodeId) {
@@ -1313,23 +1461,20 @@ var NetworkedDOMWebsocketV02Adapter = class {
1313
1461
  }
1314
1462
  const elementsToAdd = [];
1315
1463
  for (const addedNode of addedNodes) {
1316
- const childElement = this.handleNewElement(addedNode);
1464
+ const childElement = this.handleNewElement(addedNode, childFactory);
1317
1465
  if (childElement) {
1318
1466
  elementsToAdd.push(childElement);
1319
1467
  }
1320
1468
  }
1321
- if (elementsToAdd.length) {
1322
- if (previousElement) {
1323
- if (nextElement) {
1324
- const docFrag = new DocumentFragment();
1325
- docFrag.append(...elementsToAdd);
1326
- targetForChildren.insertBefore(docFrag, nextElement);
1327
- } else {
1328
- targetForChildren.append(...elementsToAdd);
1329
- }
1330
- } else {
1331
- targetForChildren.prepend(...elementsToAdd);
1332
- }
1469
+ this.insertElements(
1470
+ targetForChildren,
1471
+ elementsToAdd,
1472
+ previousElement,
1473
+ nextElement,
1474
+ childFactory ?? this.docFactory
1475
+ );
1476
+ if (this.pendingPortalChildren.size > 0) {
1477
+ flushPendingPortalChildren(this.pendingPortalChildren);
1333
1478
  }
1334
1479
  }
1335
1480
  handleChildrenRemoved(message) {
@@ -1338,11 +1483,11 @@ var NetworkedDOMWebsocketV02Adapter = class {
1338
1483
  console.warn("No nodeId in childrenChanged message");
1339
1484
  return;
1340
1485
  }
1341
- const parent = this.idToElement.get(nodeId);
1342
- if (!parent) {
1486
+ const parentNode = this.idToElement.get(nodeId);
1487
+ if (!parentNode) {
1343
1488
  throw new Error("No parent found for childrenChanged message");
1344
1489
  }
1345
- if (!isHTMLElement(parent, this.parentElement)) {
1490
+ if (!isHTMLElement(parentNode, this.parentElement)) {
1346
1491
  throw new Error("Parent is not an HTMLElement (that supports children)");
1347
1492
  }
1348
1493
  for (const removedNode of removedNodes) {
@@ -1352,7 +1497,7 @@ var NetworkedDOMWebsocketV02Adapter = class {
1352
1497
  }
1353
1498
  this.elementToId.delete(childElement);
1354
1499
  this.idToElement.delete(removedNode);
1355
- const targetForRemoval = getRemovalTarget(parent);
1500
+ const targetForRemoval = getRemovalTarget(parentNode);
1356
1501
  const hiddenElement = this.hiddenPlaceholderElements.get(removedNode);
1357
1502
  if (hiddenElement) {
1358
1503
  const placeholder = hiddenElement.placeholder;
@@ -1378,62 +1523,36 @@ var NetworkedDOMWebsocketV02Adapter = class {
1378
1523
  }
1379
1524
  }
1380
1525
  }
1381
- removeChildElementIds(parent) {
1382
- const portal = getChildrenTarget(parent);
1383
- if (portal !== parent) {
1384
- this.removeChildElementIds(portal);
1385
- }
1386
- const childNodes = parent.childNodes;
1387
- for (let i = 0; i < childNodes.length; i++) {
1388
- const child = childNodes[i];
1389
- const childId = this.elementToId.get(child);
1390
- if (!childId) {
1391
- const placeholderId = this.placeholderToId.get(child);
1392
- if (placeholderId) {
1393
- const childElement = this.idToElement.get(placeholderId);
1394
- if (childElement) {
1395
- this.elementToId.delete(childElement);
1396
- } else {
1397
- console.error(
1398
- "Inner child of removed placeholder element not found by id",
1399
- placeholderId
1400
- );
1401
- }
1402
- this.idToElement.delete(placeholderId);
1403
- this.placeholderToId.delete(child);
1404
- this.hiddenPlaceholderElements.delete(placeholderId);
1405
- this.removeChildElementIds(childElement);
1406
- } else {
1407
- console.error(
1408
- "Inner child of removed element had no id",
1409
- child.outerHTML
1410
- );
1411
- }
1526
+ handleUnregisteredChild(child) {
1527
+ const placeholderId = this.placeholderToId.get(child);
1528
+ if (placeholderId) {
1529
+ const childElement = this.idToElement.get(placeholderId);
1530
+ if (childElement) {
1531
+ this.elementToId.delete(childElement);
1412
1532
  } else {
1413
- this.elementToId.delete(child);
1414
- this.idToElement.delete(childId);
1415
- this.removeChildElementIds(child);
1533
+ console.error("Inner child of removed placeholder element not found by id", placeholderId);
1534
+ }
1535
+ this.idToElement.delete(placeholderId);
1536
+ this.placeholderToId.delete(child);
1537
+ this.hiddenPlaceholderElements.delete(placeholderId);
1538
+ if (childElement) {
1539
+ this.removeChildElementIds(childElement);
1416
1540
  }
1541
+ } else {
1542
+ console.error(
1543
+ "Inner child of removed element had no id",
1544
+ (child == null ? void 0 : child.outerHTML) ?? child
1545
+ );
1417
1546
  }
1418
1547
  }
1419
1548
  handleSnapshot(message) {
1420
1549
  var _a;
1421
- if (this.currentRoot) {
1422
- this.currentRoot.remove();
1423
- this.currentRoot = null;
1424
- this.elementToId.clear();
1425
- this.idToElement.clear();
1426
- }
1427
1550
  (_a = this.timeCallback) == null ? void 0 : _a.call(this, message.documentTime);
1428
1551
  const element = this.handleNewElement(message.snapshot);
1429
1552
  if (!element) {
1430
1553
  throw new Error("Snapshot element not created");
1431
1554
  }
1432
- if (!isHTMLElement(element, this.parentElement)) {
1433
- throw new Error("Snapshot element is not an HTMLElement");
1434
- }
1435
- this.currentRoot = element;
1436
- this.parentElement.append(element);
1555
+ this.resetAndApplySnapshot(element);
1437
1556
  }
1438
1557
  handleDocumentTime(message) {
1439
1558
  var _a;
@@ -1445,35 +1564,31 @@ var NetworkedDOMWebsocketV02Adapter = class {
1445
1564
  console.warn("No nodeId in attributeChange message");
1446
1565
  return;
1447
1566
  }
1448
- let element = this.idToElement.get(nodeId);
1567
+ let node = this.idToElement.get(nodeId);
1449
1568
  const hiddenElement = this.hiddenPlaceholderElements.get(nodeId);
1450
1569
  if (hiddenElement) {
1451
- element = hiddenElement.element;
1570
+ node = hiddenElement.element;
1452
1571
  }
1453
- if (element) {
1454
- if (isHTMLElement(element, this.parentElement)) {
1572
+ if (node) {
1573
+ if (isHTMLElement(node, this.parentElement)) {
1455
1574
  for (const [key, newValue] of attributes) {
1456
1575
  if (newValue === null) {
1457
- element.removeAttribute(key);
1576
+ node.removeAttribute(key);
1458
1577
  } else {
1459
- setElementAttribute(element, key, newValue);
1578
+ setElementAttribute(node, key, newValue);
1460
1579
  }
1461
1580
  }
1462
1581
  } else {
1463
- console.error("Element is not an HTMLElement and cannot support attributes", element);
1582
+ console.error("Element is not an HTMLElement and cannot support attributes", node);
1464
1583
  }
1465
1584
  } else {
1466
1585
  console.error("No element found for attributeChange message");
1467
1586
  }
1468
1587
  }
1469
- handleNewElement(message) {
1588
+ handleNewElement(message, factoryOverride) {
1589
+ const factory = factoryOverride ?? this.docFactory;
1470
1590
  if (message.type === "text") {
1471
- const { nodeId: nodeId2, text: text2 } = message;
1472
- const textNode = document.createTextNode("");
1473
- textNode.textContent = text2;
1474
- this.idToElement.set(nodeId2, textNode);
1475
- this.elementToId.set(textNode, nodeId2);
1476
- return textNode;
1591
+ return this.createTextNode(message.nodeId, message.text, factory);
1477
1592
  }
1478
1593
  const { tag, nodeId, attributes, children, text, hiddenFrom } = message;
1479
1594
  if (this.idToElement.has(nodeId)) {
@@ -1485,34 +1600,36 @@ var NetworkedDOMWebsocketV02Adapter = class {
1485
1600
  throw new Error("Received nodeId to add that is already present: " + nodeId);
1486
1601
  }
1487
1602
  if (tag === "#text") {
1488
- const textNode = document.createTextNode("");
1489
- textNode.textContent = text || null;
1490
- this.idToElement.set(nodeId, textNode);
1491
- this.elementToId.set(textNode, nodeId);
1492
- return textNode;
1603
+ return this.createTextNode(nodeId, text || "", factory);
1493
1604
  }
1494
1605
  let element;
1495
1606
  try {
1496
- element = createElementWithSVGSupport(tag, this.options);
1607
+ element = createElementWithSVGSupport(tag, this.options, factory);
1497
1608
  } catch (e) {
1498
1609
  console.error(`Error creating element: (${tag})`, e);
1499
- element = document.createElement("x-div");
1610
+ element = factory.createElement("x-div");
1500
1611
  }
1501
1612
  for (const [key, value] of attributes) {
1502
1613
  if (value !== null) {
1503
1614
  setElementAttribute(element, key, value);
1504
1615
  }
1505
1616
  }
1617
+ recordFactoryOverride(element, factory, this.docFactory, this.elementFactoryOverride);
1618
+ const { childFactory, usingPortalFactory } = resolvePortalChildFactory(element, factory);
1506
1619
  if (children) {
1507
1620
  for (const child of children) {
1508
- const childElement = this.handleNewElement(child);
1621
+ const childElement = this.handleNewElement(child, childFactory);
1509
1622
  if (childElement) {
1510
- element.append(childElement);
1623
+ if (usingPortalFactory) {
1624
+ bufferPortalChild(this.pendingPortalChildren, element, childElement);
1625
+ } else {
1626
+ element.append(childElement);
1627
+ }
1511
1628
  }
1512
1629
  }
1513
1630
  }
1514
1631
  if (hiddenFrom && hiddenFrom.length > 0 && hiddenFrom.indexOf(connectionId) !== -1) {
1515
- const placeholder = document.createElement(hiddenTag);
1632
+ const placeholder = this.docFactory.createElement(hiddenTag);
1516
1633
  this.hiddenPlaceholderElements.set(nodeId, { placeholder, element });
1517
1634
  this.placeholderToId.set(placeholder, nodeId);
1518
1635
  this.idToElement.set(nodeId, element);
@@ -1537,13 +1654,14 @@ var startingBackoffTimeMilliseconds = 100;
1537
1654
  var maximumBackoffTimeMilliseconds = 1e4;
1538
1655
  var maximumWebsocketConnectionTimeout = 3e4;
1539
1656
  var NetworkedDOMWebsocket = class {
1540
- constructor(url, websocketFactory, parentElement, timeCallback, statusUpdateCallback, options = {}) {
1657
+ constructor(url, websocketFactory, parentElement, timeCallback, statusUpdateCallback, options = {}, doc) {
1541
1658
  this.url = url;
1542
1659
  this.websocketFactory = websocketFactory;
1543
1660
  this.parentElement = parentElement;
1544
1661
  this.timeCallback = timeCallback;
1545
1662
  this.statusUpdateCallback = statusUpdateCallback;
1546
1663
  this.options = options;
1664
+ this.doc = doc;
1547
1665
  this.websocket = null;
1548
1666
  this.websocketAdapter = null;
1549
1667
  this.stopped = false;
@@ -1594,7 +1712,8 @@ var NetworkedDOMWebsocket = class {
1594
1712
  );
1595
1713
  },
1596
1714
  this.timeCallback,
1597
- this.options
1715
+ this.options,
1716
+ this.doc
1598
1717
  );
1599
1718
  } else {
1600
1719
  websocketAdapter = new NetworkedDOMWebsocketV01Adapter(
@@ -1608,7 +1727,8 @@ var NetworkedDOMWebsocket = class {
1608
1727
  );
1609
1728
  },
1610
1729
  this.timeCallback,
1611
- this.options
1730
+ this.options,
1731
+ this.doc
1612
1732
  );
1613
1733
  }
1614
1734
  this.websocketAdapter = websocketAdapter;
@@ -1712,22 +1832,35 @@ var NetworkedDOMWebsocket = class {
1712
1832
  }
1713
1833
  };
1714
1834
  function isHTMLElement(node, rootNode) {
1715
- if (node instanceof HTMLElement || node instanceof Element) {
1716
- return true;
1717
- }
1718
- if (!rootNode.ownerDocument.defaultView) {
1719
- return false;
1720
- }
1721
- return node instanceof rootNode.ownerDocument.defaultView.HTMLElement;
1835
+ if (!node || typeof node !== "object") return false;
1836
+ const nodeLike = node;
1837
+ if (nodeLike[VIRTUAL_ELEMENT_BRAND] === true) return true;
1838
+ if (typeof HTMLElement !== "undefined" && node instanceof HTMLElement) return true;
1839
+ if (typeof Element !== "undefined" && node instanceof Element) return true;
1840
+ const rootNodeRecord = rootNode;
1841
+ if (rootNodeRecord == null ? void 0 : rootNodeRecord.ownerDocument) {
1842
+ const ownerDoc = rootNodeRecord.ownerDocument;
1843
+ const defaultView = ownerDoc.defaultView;
1844
+ if (defaultView == null ? void 0 : defaultView.HTMLElement) {
1845
+ return node instanceof defaultView.HTMLElement;
1846
+ }
1847
+ }
1848
+ return false;
1722
1849
  }
1723
1850
  function isText(node, rootNode) {
1724
- if (node instanceof Text) {
1725
- return true;
1726
- }
1727
- if (!rootNode.ownerDocument.defaultView) {
1728
- return false;
1729
- }
1730
- return node instanceof rootNode.ownerDocument.defaultView.Text;
1851
+ if (!node || typeof node !== "object") return false;
1852
+ const nodeLike = node;
1853
+ if (nodeLike[VIRTUAL_TEXT_BRAND] === true) return true;
1854
+ if (typeof Text !== "undefined" && node instanceof Text) return true;
1855
+ const rootNodeRecord = rootNode;
1856
+ if (rootNodeRecord == null ? void 0 : rootNodeRecord.ownerDocument) {
1857
+ const ownerDoc = rootNodeRecord.ownerDocument;
1858
+ const defaultView = ownerDoc.defaultView;
1859
+ if (defaultView == null ? void 0 : defaultView.Text) {
1860
+ return node instanceof defaultView.Text;
1861
+ }
1862
+ }
1863
+ return false;
1731
1864
  }
1732
1865
 
1733
1866
  // src/index.ts