Precise DOM morphing
morphing typescript dom

Remove dist from git

-366
-17
dist/morphlex.d.ts
··· 1 - interface Options { 2 - ignoreActiveValue?: boolean; 3 - preserveModifiedValues?: boolean; 4 - beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean; 5 - afterNodeMorphed?: (node: Node, referenceNode: Node) => void; 6 - beforeNodeAdded?: (node: Node) => boolean; 7 - afterNodeAdded?: (node: Node) => void; 8 - beforeNodeRemoved?: (node: Node) => boolean; 9 - afterNodeRemoved?: (node: Node) => void; 10 - beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean; 11 - afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void; 12 - beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean; 13 - afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void; 14 - } 15 - export declare function morph(node: ChildNode, reference: ChildNode | string, options?: Options): void; 16 - export declare function morphInner(element: Element, reference: Element | string, options?: Options): void; 17 - export {};
-349
dist/morphlex.js
··· 1 - export function morph(node, reference, options = {}) { 2 - if (typeof reference === "string") 3 - reference = parseChildNodeFromString(reference); 4 - new Morph(options).morph([node, reference]); 5 - } 6 - export function morphInner(element, reference, options = {}) { 7 - if (typeof reference === "string") 8 - reference = parseElementFromString(reference); 9 - new Morph(options).morphInner([element, reference]); 10 - } 11 - function parseElementFromString(string) { 12 - const node = parseChildNodeFromString(string); 13 - if (isElement(node)) 14 - return node; 15 - else 16 - throw new Error("[Morphlex] The string was not a valid HTML element."); 17 - } 18 - function parseChildNodeFromString(string) { 19 - const parser = new DOMParser(); 20 - const doc = parser.parseFromString(string, "text/html"); 21 - if (doc.childNodes.length === 1) 22 - return doc.body.firstChild; 23 - else 24 - throw new Error("[Morphlex] The string was not a valid HTML node."); 25 - } 26 - class Morph { 27 - #idMap; 28 - #ignoreActiveValue; 29 - #preserveModifiedValues; 30 - #beforeNodeMorphed; 31 - #afterNodeMorphed; 32 - #beforeNodeAdded; 33 - #afterNodeAdded; 34 - #beforeNodeRemoved; 35 - #afterNodeRemoved; 36 - #beforeAttributeUpdated; 37 - #afterAttributeUpdated; 38 - #beforePropertyUpdated; 39 - #afterPropertyUpdated; 40 - constructor(options = {}) { 41 - this.#idMap = new WeakMap(); 42 - this.#ignoreActiveValue = options.ignoreActiveValue || false; 43 - this.#preserveModifiedValues = options.preserveModifiedValues || false; 44 - this.#beforeNodeMorphed = options.beforeNodeMorphed; 45 - this.#afterNodeMorphed = options.afterNodeMorphed; 46 - this.#beforeNodeAdded = options.beforeNodeAdded; 47 - this.#afterNodeAdded = options.afterNodeAdded; 48 - this.#beforeNodeRemoved = options.beforeNodeRemoved; 49 - this.#afterNodeRemoved = options.afterNodeRemoved; 50 - this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 51 - this.#afterAttributeUpdated = options.afterAttributeUpdated; 52 - this.#beforePropertyUpdated = options.beforePropertyUpdated; 53 - this.#afterPropertyUpdated = options.afterPropertyUpdated; 54 - } 55 - morph(pair) { 56 - this.#withAriaBusy(pair[0], () => { 57 - if (isParentNodePair(pair)) 58 - this.#buildMaps(pair); 59 - this.#morphNode(pair); 60 - }); 61 - } 62 - morphInner(pair) { 63 - this.#withAriaBusy(pair[0], () => { 64 - if (isMatchingElementPair(pair)) { 65 - this.#buildMaps(pair); 66 - this.#morphMatchingElementContent(pair); 67 - } 68 - else { 69 - throw new Error("[Morphlex] You can only do an inner morph with matching elements."); 70 - } 71 - }); 72 - } 73 - #withAriaBusy(node, block) { 74 - if (isElement(node)) { 75 - const originalAriaBusy = node.ariaBusy; 76 - node.ariaBusy = "true"; 77 - block(); 78 - node.ariaBusy = originalAriaBusy; 79 - } 80 - else 81 - block(); 82 - } 83 - #buildMaps([node, reference]) { 84 - this.#mapIdSets(node); 85 - this.#mapIdSets(reference); 86 - } 87 - // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 88 - #mapIdSets(node) { 89 - const elementsWithIds = node.querySelectorAll("[id]"); 90 - const elementsWithIdsLength = elementsWithIds.length; 91 - for (let i = 0; i < elementsWithIdsLength; i++) { 92 - const elementWithId = elementsWithIds[i]; 93 - const id = elementWithId.id; 94 - // Ignore empty IDs 95 - if (id === "") 96 - continue; 97 - let current = elementWithId; 98 - while (current) { 99 - const idSet = this.#idMap.get(current); 100 - idSet ? idSet.add(id) : this.#idMap.set(current, new Set([id])); 101 - if (current === node) 102 - break; 103 - current = current.parentElement; 104 - } 105 - } 106 - } 107 - // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 108 - #morphNode(pair) { 109 - if (isMatchingElementPair(pair)) 110 - this.#morphMatchingElementNode(pair); 111 - else 112 - this.#morphOtherNode(pair); 113 - } 114 - #morphMatchingElementNode(pair) { 115 - const [node, reference] = pair; 116 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 117 - return; 118 - if (node.hasAttributes() || reference.hasAttributes()) 119 - this.#morphAttributes(pair); 120 - // TODO: Should use a branded pair here. 121 - this.#morphMatchingElementContent(pair); 122 - this.#afterNodeMorphed?.(node, writableNode(reference)); 123 - } 124 - #morphOtherNode([node, reference]) { 125 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 126 - return; 127 - if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 128 - // Handle text nodes, comments, and CDATA sections. 129 - this.#updateProperty(node, "nodeValue", reference.nodeValue); 130 - } 131 - else 132 - this.#replaceNode(node, reference.cloneNode(true)); 133 - this.#afterNodeMorphed?.(node, writableNode(reference)); 134 - } 135 - #morphMatchingElementContent(pair) { 136 - const [node, reference] = pair; 137 - if (isHead(node)) { 138 - // We can pass the reference as a head here becuase we know it's the same as the node. 139 - this.#morphHeadContents(pair); 140 - } 141 - else if (node.hasChildNodes() || reference.hasChildNodes()) 142 - this.#morphChildNodes(pair); 143 - } 144 - #morphHeadContents([node, reference]) { 145 - const refChildNodesMap = new Map(); 146 - // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 147 - const referenceChildrenLength = reference.children.length; 148 - for (let i = 0; i < referenceChildrenLength; i++) { 149 - const child = reference.children[i]; 150 - refChildNodesMap.set(child.outerHTML, child); 151 - } 152 - // Iterate backwards to safely remove children without affecting indices 153 - for (let i = node.children.length - 1; i >= 0; i--) { 154 - const child = node.children[i]; 155 - const key = child.outerHTML; 156 - const refChild = refChildNodesMap.get(key); 157 - // If the child is in the reference map already, we don't need to add it later. 158 - // If it's not in the map, we need to remove it from the node. 159 - refChild ? refChildNodesMap.delete(key) : this.#removeNode(child); 160 - } 161 - // Any remaining nodes in the map should be appended to the head. 162 - for (const refChild of refChildNodesMap.values()) 163 - this.#appendChild(node, refChild.cloneNode(true)); 164 - } 165 - #morphAttributes([element, reference]) { 166 - // Remove any excess attributes from the element that aren’t present in the reference. 167 - for (const { name, value } of element.attributes) { 168 - if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 169 - element.removeAttribute(name); 170 - this.#afterAttributeUpdated?.(element, name, value); 171 - } 172 - } 173 - // Copy attributes from the reference to the element, if they don’t already match. 174 - for (const { name, value } of reference.attributes) { 175 - const previousValue = element.getAttribute(name); 176 - if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 177 - element.setAttribute(name, value); 178 - this.#afterAttributeUpdated?.(element, name, previousValue); 179 - } 180 - } 181 - // For certain types of elements, we need to do some extra work to ensure 182 - // the element’s state matches the reference elements’ state. 183 - if (isInput(element) && isInput(reference)) { 184 - this.#updateProperty(element, "checked", reference.checked); 185 - this.#updateProperty(element, "disabled", reference.disabled); 186 - this.#updateProperty(element, "indeterminate", reference.indeterminate); 187 - if (element.type !== "file" && 188 - !(this.#ignoreActiveValue && document.activeElement === element) && 189 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 190 - this.#updateProperty(element, "value", reference.value); 191 - } 192 - } 193 - else if (isOption(element) && isOption(reference)) { 194 - this.#updateProperty(element, "selected", reference.selected); 195 - } 196 - else if (isTextArea(element) && 197 - isTextArea(reference) && 198 - !(this.#ignoreActiveValue && document.activeElement === element) && 199 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 200 - this.#updateProperty(element, "value", reference.value); 201 - const text = element.firstElementChild; 202 - if (text) 203 - this.#updateProperty(text, "textContent", reference.value); 204 - } 205 - } 206 - // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 207 - #morphChildNodes(pair) { 208 - const [element, reference] = pair; 209 - const childNodes = element.childNodes; 210 - const refChildNodes = reference.childNodes; 211 - for (let i = 0; i < refChildNodes.length; i++) { 212 - const child = childNodes[i]; 213 - const refChild = refChildNodes[i]; 214 - if (child && refChild) { 215 - const pair = [child, refChild]; 216 - if (isMatchingElementPair(pair)) { 217 - if (isHead(pair[0])) { 218 - this.#morphHeadContents(pair); 219 - } 220 - else { 221 - this.#morphChildElement(pair, element); 222 - } 223 - } 224 - else 225 - this.#morphOtherNode(pair); 226 - } 227 - else if (refChild) { 228 - this.#appendChild(element, refChild.cloneNode(true)); 229 - } 230 - else if (child) { 231 - this.#removeNode(child); 232 - } 233 - } 234 - // Clean up any excess nodes that may be left over 235 - while (childNodes.length > refChildNodes.length) { 236 - const child = element.lastChild; 237 - if (child) 238 - this.#removeNode(child); 239 - } 240 - } 241 - #morphChildElement([child, reference], parent) { 242 - if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) 243 - return; 244 - const refIdSet = this.#idMap.get(reference); 245 - // Generate the array in advance of the loop 246 - const refSetArray = refIdSet ? [...refIdSet] : []; 247 - let currentNode = child; 248 - let nextMatchByTagName = null; 249 - // Try find a match by idSet, while also looking out for the next best match by tagName. 250 - while (currentNode) { 251 - if (isElement(currentNode)) { 252 - const id = currentNode.id; 253 - if (!nextMatchByTagName && currentNode.localName === reference.localName) { 254 - nextMatchByTagName = currentNode; 255 - } 256 - if (id !== "") { 257 - if (id === reference.id) { 258 - this.#insertBefore(parent, currentNode, child); 259 - return this.#morphNode([currentNode, reference]); 260 - } 261 - else { 262 - const currentIdSet = this.#idMap.get(currentNode); 263 - if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 264 - this.#insertBefore(parent, currentNode, child); 265 - return this.#morphNode([currentNode, reference]); 266 - } 267 - } 268 - } 269 - } 270 - currentNode = currentNode.nextSibling; 271 - } 272 - if (nextMatchByTagName) { 273 - this.#insertBefore(parent, nextMatchByTagName, child); 274 - this.#morphNode([nextMatchByTagName, reference]); 275 - } 276 - else { 277 - const newNode = reference.cloneNode(true); 278 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 279 - this.#insertBefore(parent, newNode, child); 280 - this.#afterNodeAdded?.(newNode); 281 - } 282 - } 283 - this.#afterNodeMorphed?.(child, writableNode(reference)); 284 - } 285 - #updateProperty(node, propertyName, newValue) { 286 - const previousValue = node[propertyName]; 287 - if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 288 - node[propertyName] = newValue; 289 - this.#afterPropertyUpdated?.(node, propertyName, previousValue); 290 - } 291 - } 292 - #replaceNode(node, newNode) { 293 - if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 294 - node.replaceWith(newNode); 295 - this.#afterNodeAdded?.(newNode); 296 - this.#afterNodeRemoved?.(node); 297 - } 298 - } 299 - #insertBefore(parent, node, insertionPoint) { 300 - if (node === insertionPoint) 301 - return; 302 - parent.insertBefore(node, insertionPoint); 303 - } 304 - #appendChild(node, newNode) { 305 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 306 - node.appendChild(newNode); 307 - this.#afterNodeAdded?.(newNode); 308 - } 309 - } 310 - #removeNode(node) { 311 - if (this.#beforeNodeRemoved?.(node) ?? true) { 312 - node.remove(); 313 - this.#afterNodeRemoved?.(node); 314 - } 315 - } 316 - } 317 - function writableNode(node) { 318 - return node; 319 - } 320 - function isMatchingElementPair(pair) { 321 - const [a, b] = pair; 322 - return isElement(a) && isElement(b) && a.localName === b.localName; 323 - } 324 - function isParentNodePair(pair) { 325 - return isParentNode(pair[0]) && isParentNode(pair[1]); 326 - } 327 - function isElement(node) { 328 - return node.nodeType === 1; 329 - } 330 - function isMedia(element) { 331 - return element.localName === "video" || element.localName === "audio"; 332 - } 333 - function isInput(element) { 334 - return element.localName === "input"; 335 - } 336 - function isOption(element) { 337 - return element.localName === "option"; 338 - } 339 - function isTextArea(element) { 340 - return element.localName === "textarea"; 341 - } 342 - function isHead(element) { 343 - return element.localName === "head"; 344 - } 345 - const parentNodeTypes = new Set([1, 9, 11]); 346 - function isParentNode(node) { 347 - return parentNodeTypes.has(node.nodeType); 348 - } 349 - //# sourceMappingURL=morphlex.js.map