···11export function morph(node, guide) {
22 const idMap = new Map();
33- if (isElement(node) && isElement(guide)) {
33+ if (isParentNode(node) && isParentNode(guide)) {
44 populateIdMapForNode(node, idMap);
55 populateIdMapForNode(guide, idMap);
66 }
77 morphNodes(node, guide, idMap);
88}
99+function populateIdMapForNode(node, idMap) {
1010+ const elementsWithIds = node.querySelectorAll("[id]");
1111+ for (const elementWithId of elementsWithIds) {
1212+ const id = elementWithId.id;
1313+ if (id === "")
1414+ continue;
1515+ let current = elementWithId;
1616+ while (current) {
1717+ const idSet = idMap.get(current);
1818+ idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
1919+ if (current === elementWithId)
2020+ break;
2121+ current = current.parentElement;
2222+ }
2323+ }
2424+}
925function morphNodes(node, guide, idMap, insertBefore, parent) {
1026 if (parent && insertBefore && insertBefore !== node)
1127 parent.insertBefore(guide, insertBefore);
···2642 for (const { name } of elem.attributes)
2743 guide.hasAttribute(name) || elem.removeAttribute(name);
2844 for (const { name, value } of guide.attributes)
2929- elem.getAttribute(name) !== value && elem.setAttribute(name, value);
4545+ elem.getAttribute(name) === value || elem.setAttribute(name, value);
3046 if (isInput(elem) && isInput(guide) && elem.value !== guide.value)
3147 elem.value = guide.value;
3248 else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected)
···4460 morphChildNode(child, guideChild, elem, idMap);
4561 else if (guideChild)
4662 elem.appendChild(guideChild.cloneNode(true));
6363+ else if (child)
6464+ child.remove();
4765 }
4866 while (elem.childNodes.length > guide.childNodes.length)
4967 elem.lastChild?.remove();
···6179 let nextMatchByTagName = null;
6280 while (currentNode) {
6381 if (isElement(currentNode)) {
6464- if (currentNode.id !== "" && currentNode.id === guide.id) {
8282+ if (currentNode.id === guide.id) {
6583 return morphNodes(currentNode, guide, idMap, child, parent);
6684 }
6767- else {
8585+ else if (currentNode.id !== "") {
6886 const currentIdSet = idMap.get(currentNode);
6987 if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
7088 return morphNodes(currentNode, guide, idMap, child, parent);
7189 }
7272- else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
7373- nextMatchByTagName = currentNode;
7474- }
9090+ }
9191+ else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
9292+ nextMatchByTagName = currentNode;
7593 }
7694 }
7795 currentNode = currentNode.nextSibling;
···8199 else
82100 child.replaceWith(guide.cloneNode(true));
83101}
8484-function populateIdMapForNode(node, idMap) {
8585- const elementsWithIds = node.querySelectorAll("[id]");
8686- for (const elementWithId of elementsWithIds) {
8787- const id = elementWithId.id;
8888- if (id === "")
8989- continue;
9090- let current = elementWithId;
9191- while (current) {
9292- const idSet = idMap.get(current);
9393- idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
9494- if (current === elementWithId)
9595- break;
9696- current = current.parentElement;
9797- }
9898- }
9999-}
100102function isText(node) {
101101- return node.nodeType === 3;
103103+ return node.nodeType === Node.TEXT_NODE;
102104}
103105function isElement(node) {
104104- return node.nodeType === 1;
106106+ return node.nodeType === Node.ELEMENT_NODE;
105107}
106108function isInput(element) {
107109 return element.localName === "input";
···111113}
112114function isTextArea(element) {
113115 return element.localName === "textarea";
116116+}
117117+function isParentNode(node) {
118118+ return (node.nodeType === Node.ELEMENT_NODE ||
119119+ node.nodeType === Node.DOCUMENT_NODE ||
120120+ node.nodeType === Node.DOCUMENT_FRAGMENT_NODE);
114121}
115122//# sourceMappingURL=morphlex.js.map
+43-28
src/morphlex.ts
···44export function morph(node: ChildNode, guide: ChildNode): void {
55 const idMap: IdMap = new Map();
6677- if (isElement(node) && isElement(guide)) {
77+ if (isParentNode(node) && isParentNode(guide)) {
88 populateIdMapForNode(node, idMap);
99 populateIdMapForNode(guide, idMap);
1010 }
···1212 morphNodes(node, guide, idMap);
1313}
14141515+function populateIdMapForNode(node: ParentNode, idMap: IdMap): void {
1616+ const elementsWithIds: NodeListOf<Element> = node.querySelectorAll("[id]");
1717+1818+ for (const elementWithId of elementsWithIds) {
1919+ const id = elementWithId.id;
2020+ if (id === "") continue;
2121+ let current: Element | null = elementWithId;
2222+2323+ while (current) {
2424+ const idSet: IdSet | undefined = idMap.get(current);
2525+ idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
2626+ if (current === elementWithId) break;
2727+ current = current.parentElement;
2828+ }
2929+ }
3030+}
3131+3232+// This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`.
1533function morphNodes(node: ChildNode, guide: ChildNode, idMap: IdMap, insertBefore?: Node, parent?: Node): void {
3434+ // TODO: We should extract this into a separate function.
1635 if (parent && insertBefore && insertBefore !== node) parent.insertBefore(guide, insertBefore);
17361837 if (isText(node) && isText(guide)) {
···2443}
25442645function morphAttributes(elem: Element, guide: Element): void {
4646+ // Remove any excess attributes from the element that aren’t present in the guide.
2747 for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name);
2828- for (const { name, value } of guide.attributes) elem.getAttribute(name) !== value && elem.setAttribute(name, value);
29484949+ // Copy attributes from the guide to the element, if they don’t already match.
5050+ for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value);
5151+5252+ // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
3053 if (isInput(elem) && isInput(guide) && elem.value !== guide.value) elem.value = guide.value;
3154 else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected;
3255 else if (isTextArea(elem) && isTextArea(guide) && elem.value !== guide.value) elem.value = guide.value;
3356}
34575858+// Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match.
3559function morphChildNodes(elem: Element, guide: Element, idMap: IdMap): void {
3660 const childNodes = [...elem.childNodes];
3761 const guideChildNodes = [...guide.childNodes];
···42664367 if (child && guideChild) morphChildNode(child, guideChild, elem, idMap);
4468 else if (guideChild) elem.appendChild(guideChild.cloneNode(true));
6969+ else if (child) child.remove();
4570 }
46714747- // This is separate because the loop above might modify the length of the element's child nodes.
7272+ // Remove any excess child nodes from the main element. This is separate because
7373+ // the loop above might modify the length of the main element’s child nodes.
4874 while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove();
4975}
5076···6591 // Try find a match by idSet, while also looking out for the next best match by tagName.
6692 while (currentNode) {
6793 if (isElement(currentNode)) {
6868- if (currentNode.id !== "" && currentNode.id === guide.id) {
6969- // Exact match by id.
9494+ if (currentNode.id === guide.id) {
7095 return morphNodes(currentNode, guide, idMap, child, parent);
7171- } else {
9696+ } else if (currentNode.id !== "") {
7297 const currentIdSet = idMap.get(currentNode);
73987499 if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
7575- // Match by idSet.
76100 return morphNodes(currentNode, guide, idMap, child, parent);
7777- } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
7878- nextMatchByTagName = currentNode;
79101 }
102102+ } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
103103+ nextMatchByTagName = currentNode;
80104 }
81105 }
82106···87111 else child.replaceWith(guide.cloneNode(true));
88112}
891139090-function populateIdMapForNode(node: ParentNode, idMap: IdMap): void {
9191- const elementsWithIds: NodeListOf<Element> = node.querySelectorAll("[id]");
9292-9393- for (const elementWithId of elementsWithIds) {
9494- const id = elementWithId.id;
9595- if (id === "") continue;
9696- let current: Element | null = elementWithId;
9797-9898- while (current) {
9999- const idSet: IdSet | undefined = idMap.get(current);
100100- idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
101101- if (current === elementWithId) break;
102102- current = current.parentElement;
103103- }
104104- }
105105-}
106106-107114// We cannot use `instanceof` when nodes might be from different documents,
108115// so we use type guards instead. This keeps TypeScript happy, while doing
109116// the necessary checks at runtime.
110117111118function isText(node: Node): node is Text {
112112- return node.nodeType === 3;
119119+ return node.nodeType === Node.TEXT_NODE;
113120}
114121115122function isElement(node: Node): node is Element {
116116- return node.nodeType === 1;
123123+ return node.nodeType === Node.ELEMENT_NODE;
117124}
118125119126function isInput(element: Element): element is HTMLInputElement {
···127134function isTextArea(element: Element): element is HTMLTextAreaElement {
128135 return element.localName === "textarea";
129136}
137137+138138+function isParentNode(node: Node): node is ParentNode {
139139+ return (
140140+ node.nodeType === Node.ELEMENT_NODE ||
141141+ node.nodeType === Node.DOCUMENT_NODE ||
142142+ node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
143143+ );
144144+}