···11export function morph(node, guide) {
22- const idMap = new Map();
33- if (isParentNode(node) && isParentNode(guide)) {
44- populateIdSets(node, idMap);
55- populateIdSets(guide, idMap);
66- }
77- morphNodes(node, guide, idMap);
22+ const idMap = new Map();
33+ if (isParentNode(node) && isParentNode(guide)) {
44+ populateIdSets(node, idMap);
55+ populateIdSets(guide, idMap);
66+ }
77+ morphNodes(node, guide, idMap);
88}
99// For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements.
1010function populateIdSets(node, idMap) {
1111- const elementsWithIds = node.querySelectorAll("[id]");
1212- for (const elementWithId of elementsWithIds) {
1313- const id = elementWithId.id;
1414- // Ignore empty IDs
1515- if (id === "")
1616- continue;
1717- let current = elementWithId;
1818- while (current) {
1919- const idSet = idMap.get(current);
2020- idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
2121- if (current === elementWithId)
2222- break;
2323- current = current.parentElement;
2424- }
2525- }
1111+ const elementsWithIds = node.querySelectorAll("[id]");
1212+ for (const elementWithId of elementsWithIds) {
1313+ const id = elementWithId.id;
1414+ // Ignore empty IDs
1515+ if (id === "") continue;
1616+ let current = elementWithId;
1717+ while (current) {
1818+ const idSet = idMap.get(current);
1919+ idSet ? idSet.add(id) : idMap.set(current, new Set([id]));
2020+ if (current === elementWithId) break;
2121+ current = current.parentElement;
2222+ }
2323+ }
2624}
2725// This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`.
2826function morphNodes(node, guide, idMap, insertBefore, parent) {
2929- // TODO: We should extract this into a separate function.
3030- if (parent && insertBefore && insertBefore !== node)
3131- parent.insertBefore(guide, insertBefore);
3232- if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) {
3333- // We need to check if the element is an input, option, or textarea here, because they have
3434- // special attributes not covered by the isEqualNode check.
3535- if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide))
3636- return;
3737- else {
3838- if (node.hasAttributes() || guide.hasAttributes())
3939- morphAttributes(node, guide);
4040- if (node.hasChildNodes() || guide.hasChildNodes())
4141- morphChildNodes(node, guide, idMap);
4242- }
4343- }
4444- else {
4545- if (node.isEqualNode(guide))
4646- return;
4747- else if (isText(node) && isText(guide)) {
4848- if (node.textContent !== guide.textContent)
4949- node.textContent = guide.textContent;
5050- }
5151- else if (isComment(node) && isComment(guide)) {
5252- if (node.nodeValue !== guide.nodeValue)
5353- node.nodeValue = guide.nodeValue;
5454- }
5555- else
5656- node.replaceWith(guide.cloneNode(true));
5757- }
2727+ // TODO: We should extract this into a separate function.
2828+ if (parent && insertBefore && insertBefore !== node) parent.insertBefore(guide, insertBefore);
2929+ if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) {
3030+ // We need to check if the element is an input, option, or textarea here, because they have
3131+ // special attributes not covered by the isEqualNode check.
3232+ if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return;
3333+ else {
3434+ if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide);
3535+ if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap);
3636+ }
3737+ } else {
3838+ if (node.isEqualNode(guide)) return;
3939+ else if (isText(node) && isText(guide)) {
4040+ if (node.textContent !== guide.textContent) node.textContent = guide.textContent;
4141+ } else if (isComment(node) && isComment(guide)) {
4242+ if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue;
4343+ } else node.replaceWith(guide.cloneNode(true));
4444+ }
5845}
5946function morphAttributes(elem, guide) {
6060- // Remove any excess attributes from the element that aren’t present in the guide.
6161- for (const { name } of elem.attributes)
6262- guide.hasAttribute(name) || elem.removeAttribute(name);
6363- // Copy attributes from the guide to the element, if they don’t already match.
6464- for (const { name, value } of guide.attributes)
6565- elem.getAttribute(name) === value || elem.setAttribute(name, value);
6666- elem.nodeValue;
6767- // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
6868- if (isInput(elem) && isInput(guide)) {
6969- if (elem.checked !== guide.checked)
7070- elem.checked = guide.checked;
7171- if (elem.disabled !== guide.disabled)
7272- elem.disabled = guide.disabled;
7373- if (elem.indeterminate !== guide.indeterminate)
7474- elem.indeterminate = guide.indeterminate;
7575- if (elem.type !== "file" && elem.value !== guide.value)
7676- elem.value = guide.value;
7777- }
7878- else if (isOption(elem) && isOption(guide) && elem.value !== guide.value)
7979- elem.value = guide.value;
8080- else if (isTextArea(elem) && isTextArea(guide)) {
8181- if (elem.value !== guide.value)
8282- elem.value = guide.value;
8383- const text = elem.firstChild;
8484- if (text && isText(text) && text.textContent !== guide.value)
8585- text.textContent = guide.value;
8686- }
4747+ // Remove any excess attributes from the element that aren’t present in the guide.
4848+ for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name);
4949+ // 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+ elem.nodeValue;
5252+ // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
5353+ if (isInput(elem) && isInput(guide)) {
5454+ if (elem.checked !== guide.checked) elem.checked = guide.checked;
5555+ if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled;
5656+ if (elem.indeterminate !== guide.indeterminate) elem.indeterminate = guide.indeterminate;
5757+ if (elem.type !== "file" && elem.value !== guide.value) elem.value = guide.value;
5858+ } else if (isOption(elem) && isOption(guide) && elem.value !== guide.value) elem.value = guide.value;
5959+ else if (isTextArea(elem) && isTextArea(guide)) {
6060+ if (elem.value !== guide.value) elem.value = guide.value;
6161+ const text = elem.firstChild;
6262+ if (text && isText(text) && text.textContent !== guide.value) text.textContent = guide.value;
6363+ }
8764}
8865// Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match.
8966function morphChildNodes(elem, guide, idMap) {
9090- const childNodes = [...elem.childNodes];
9191- const guideChildNodes = [...guide.childNodes];
9292- for (let i = 0; i < guideChildNodes.length; i++) {
9393- const child = childNodes.at(i);
9494- const guideChild = guideChildNodes.at(i);
9595- if (child && guideChild)
9696- morphChildNode(child, guideChild, elem, idMap);
9797- else if (guideChild)
9898- elem.appendChild(guideChild.cloneNode(true));
9999- else if (child)
100100- child.remove();
101101- }
102102- // Remove any excess child nodes from the main element. This is separate because
103103- // the loop above might modify the length of the main element’s child nodes.
104104- while (elem.childNodes.length > guide.childNodes.length)
105105- elem.lastChild?.remove();
6767+ const childNodes = [...elem.childNodes];
6868+ const guideChildNodes = [...guide.childNodes];
6969+ for (let i = 0; i < guideChildNodes.length; i++) {
7070+ const child = childNodes.at(i);
7171+ const guideChild = guideChildNodes.at(i);
7272+ if (child && guideChild) morphChildNode(child, guideChild, elem, idMap);
7373+ else if (guideChild) elem.appendChild(guideChild.cloneNode(true));
7474+ else if (child) child.remove();
7575+ }
7676+ // Remove any excess child nodes from the main element. This is separate because
7777+ // the loop above might modify the length of the main element’s child nodes.
7878+ while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove();
10679}
10780function morphChildNode(child, guide, parent, idMap) {
108108- if (isElement(child) && isElement(guide))
109109- morphChildElement(child, guide, parent, idMap);
110110- else
111111- morphNodes(child, guide, idMap);
8181+ if (isElement(child) && isElement(guide)) morphChildElement(child, guide, parent, idMap);
8282+ else morphNodes(child, guide, idMap);
11283}
11384function morphChildElement(child, guide, parent, idMap) {
114114- const guideIdSet = idMap.get(guide);
115115- // Generate the array in advance of the loop
116116- const guideSetArray = guideIdSet ? [...guideIdSet] : [];
117117- let currentNode = child;
118118- let nextMatchByTagName = null;
119119- // Try find a match by idSet, while also looking out for the next best match by tagName.
120120- while (currentNode) {
121121- if (isElement(currentNode)) {
122122- if (currentNode.id === guide.id) {
123123- return morphNodes(currentNode, guide, idMap, child, parent);
124124- }
125125- else if (currentNode.id !== "") {
126126- const currentIdSet = idMap.get(currentNode);
127127- if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
128128- return morphNodes(currentNode, guide, idMap, child, parent);
129129- }
130130- }
131131- else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
132132- nextMatchByTagName = currentNode;
133133- }
134134- }
135135- currentNode = currentNode.nextSibling;
136136- }
137137- if (nextMatchByTagName)
138138- morphNodes(nextMatchByTagName, guide, idMap, child, parent);
139139- else
140140- child.replaceWith(guide.cloneNode(true));
8585+ const guideIdSet = idMap.get(guide);
8686+ // Generate the array in advance of the loop
8787+ const guideSetArray = guideIdSet ? [...guideIdSet] : [];
8888+ let currentNode = child;
8989+ let nextMatchByTagName = null;
9090+ // Try find a match by idSet, while also looking out for the next best match by tagName.
9191+ while (currentNode) {
9292+ if (isElement(currentNode)) {
9393+ if (currentNode.id === guide.id) {
9494+ return morphNodes(currentNode, guide, idMap, child, parent);
9595+ } else if (currentNode.id !== "") {
9696+ const currentIdSet = idMap.get(currentNode);
9797+ if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
9898+ return morphNodes(currentNode, guide, idMap, child, parent);
9999+ }
100100+ } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
101101+ nextMatchByTagName = currentNode;
102102+ }
103103+ }
104104+ currentNode = currentNode.nextSibling;
105105+ }
106106+ if (nextMatchByTagName) morphNodes(nextMatchByTagName, guide, idMap, child, parent);
107107+ else child.replaceWith(guide.cloneNode(true));
141108}
142109// We cannot use `instanceof` when nodes might be from different documents,
143110// so we use type guards instead. This keeps TypeScript happy, while doing
144111// the necessary checks at runtime.
145112function isText(node) {
146146- return node.nodeType === 3;
113113+ return node.nodeType === 3;
147114}
148115function isComment(node) {
149149- return node.nodeType === 8;
116116+ return node.nodeType === 8;
150117}
151118function isElement(node) {
152152- return node.nodeType === 1;
119119+ return node.nodeType === 1;
153120}
154121function isInput(element) {
155155- return element.localName === "input";
122122+ return element.localName === "input";
156123}
157124function isOption(element) {
158158- return element.localName === "option";
125125+ return element.localName === "option";
159126}
160127function isTextArea(element) {
161161- return element.localName === "textarea";
128128+ return element.localName === "textarea";
162129}
163130function isParentNode(node) {
164164- return node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11;
131131+ return node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11;
165132}
166166-//# sourceMappingURL=morphlex.js.map133133+//# sourceMappingURL=morphlex.js.map