Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use a sensivityMap

+76 -69
+35 -31
dist/morphlex.js
··· 1 1 export function morph(node, reference, options = {}) { 2 2 const readonlyReference = reference; 3 3 const idMap = new WeakMap(); 4 + const sensitivityMap = new WeakMap(); 4 5 if (isParentNode(node) && isParentNode(readonlyReference)) { 5 6 populateIdSets(node, idMap); 6 7 populateIdSets(readonlyReference, idMap); 8 + populateSensivityMap(node, sensitivityMap); 7 9 } 8 - morphNode(node, readonlyReference, { ...options, idMap }); 10 + morphNode(node, readonlyReference, { ...options, idMap, sensitivityMap }); 11 + } 12 + function populateSensivityMap(node, sensivityMap) { 13 + const sensitiveElements = node.querySelectorAll("iframe,video,object,embed,audio,input,textarea,canvas"); 14 + for (const sensitiveElement of sensitiveElements) { 15 + let sensivity = 0; 16 + if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 17 + sensivity += 1; 18 + if (sensitiveElement.value !== sensitiveElement.defaultValue) sensivity += 1; 19 + if (sensitiveElement === document.activeElement) sensivity += 1; 20 + } else { 21 + sensivity += 3; 22 + if (sensitiveElement instanceof HTMLMediaElement && !sensitiveElement.ended) { 23 + if (!sensitiveElement.paused) sensivity += 1; 24 + if (sensitiveElement.currentTime > 0) sensivity += 1; 25 + } 26 + } 27 + let current = sensitiveElement; 28 + while (current) { 29 + sensivityMap.set(current, (sensivityMap.get(current) || 0) + sensivity); 30 + if (current === node) break; 31 + current = current.parentElement; 32 + } 33 + } 9 34 } 10 35 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 11 36 function populateIdSets(node, idMap) { ··· 18 43 while (current) { 19 44 const idSet = idMap.get(current); 20 45 idSet ? idSet.add(id) : idMap.set(current, new Set([id])); 21 - if (current === elementWithId) break; 46 + if (current === node) break; 22 47 current = current.parentElement; 23 48 } 24 49 } ··· 130 155 } 131 156 if (id !== "") { 132 157 if (id === ref.id) { 133 - insertBefore(parent, currentNode, child); 158 + insertBefore(parent, currentNode, child, context); 134 159 return morphNode(currentNode, ref, context); 135 160 } else { 136 161 const currentIdSet = context.idMap.get(currentNode); 137 162 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 138 - insertBefore(parent, currentNode, child); 163 + insertBefore(parent, currentNode, child, context); 139 164 return morphNode(currentNode, ref, context); 140 165 } 141 166 } ··· 144 169 currentNode = currentNode.nextSibling; 145 170 } 146 171 if (nextMatchByTagName) { 147 - insertBefore(parent, nextMatchByTagName, child); 172 + insertBefore(parent, nextMatchByTagName, child, context); 148 173 morphNode(nextMatchByTagName, ref, context); 149 174 } else { 150 - // TODO: this is missing an inserted callback 151 - // TODO: we'll need to clean up the list again after this 152 - insertBefore(parent, ref.cloneNode(true), child); 175 + // TODO: this is missing an added callback 176 + insertBefore(parent, ref.cloneNode(true), child, context); 153 177 } 154 178 } 155 179 function replaceNode(node, newNode, context) { ··· 162 186 context.afterNodeRemoved?.({ oldNode: node }); 163 187 } 164 188 } 165 - function insertBefore(parent, node, insertionPoint) { 189 + function insertBefore(parent, node, insertionPoint, context) { 166 190 if (node === insertionPoint) return; 167 191 if (isElement(node)) { 168 - const sensitivity = nodeSensitivity(node); 192 + const sensitivity = context.sensitivityMap.get(node) ?? 0; 169 193 if (sensitivity > 0) { 170 194 let previousNode = node.previousSibling; 171 195 while (previousNode) { 172 - const previousNodeSensitivity = nodeSensitivity(previousNode); 196 + const previousNodeSensitivity = context.sensitivityMap.get(previousNode) ?? 0; 173 197 if (previousNodeSensitivity < sensitivity) { 174 198 parent.insertBefore(previousNode, node.nextSibling); 175 199 if (previousNode === insertionPoint) return; ··· 181 205 } 182 206 } 183 207 parent.insertBefore(node, insertionPoint); 184 - } 185 - const sensitiveElements = new Set(["iframe", "audio", "video", "embed", "object", "canvas"]); 186 - const inputElements = new Set(["input", "select", "textarea"]); 187 - function nodeSensitivity(node) { 188 - let sensitivity = 0; 189 - if (!isElement(node)) return sensitivity; 190 - const localName = node.localName; 191 - if (inputElements.has(localName) || node.getAttribute("contenteditable")) { 192 - sensitivity += 1; 193 - if (node === document.activeElement) sensitivity += 1; 194 - if (node instanceof HTMLInputElement && node.value !== node.defaultValue) sensitivity += 1; 195 - } 196 - if (sensitiveElements.has(localName)) { 197 - sensitivity += 3; 198 - if (node instanceof HTMLMediaElement && !node.ended) { 199 - if (!node.paused) sensitivity += 1; 200 - if (node.currentTime > 0) sensitivity += 1; 201 - } 202 - } 203 - return sensitivity; 204 208 } 205 209 function appendChild(node, newNode, context) { 206 210 if (context.beforeNodeAdded?.({ newNode, parentNode: node }) ?? true) {
+41 -38
src/morphlex.ts
··· 1 1 type IdSet = Set<string>; 2 2 type IdMap = WeakMap<ReadonlyNode<Node>, IdSet>; 3 + type SensivityMap = WeakMap<ReadonlyNode<Node>, number>; 3 4 type ObjectKey = string | number | symbol; 4 5 5 6 // Maps to a type that can only read properties ··· 82 83 }) => void; 83 84 } 84 85 85 - type Context = Options & { idMap: IdMap }; 86 + type Context = Options & { idMap: IdMap; sensitivityMap: SensivityMap }; 86 87 87 88 export function morph(node: ChildNode, reference: ChildNode, options: Options = {}): void { 88 89 const readonlyReference = reference as ReadonlyNode<ChildNode>; 89 90 const idMap: IdMap = new WeakMap(); 91 + const sensitivityMap: SensivityMap = new WeakMap(); 90 92 91 93 if (isParentNode(node) && isParentNode(readonlyReference)) { 92 94 populateIdSets(node, idMap); 93 95 populateIdSets(readonlyReference, idMap); 96 + populateSensivityMap(node, sensitivityMap); 94 97 } 95 98 96 - morphNode(node, readonlyReference, { ...options, idMap }); 99 + morphNode(node, readonlyReference, { ...options, idMap, sensitivityMap }); 100 + } 101 + 102 + function populateSensivityMap(node: ReadonlyNode<ParentNode>, sensivityMap: SensivityMap): void { 103 + const sensitiveElements = node.querySelectorAll("iframe,video,object,embed,audio,input,textarea,canvas"); 104 + for (const sensitiveElement of sensitiveElements) { 105 + let sensivity = 0; 106 + 107 + if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 108 + sensivity += 1; 109 + 110 + if (sensitiveElement.value !== sensitiveElement.defaultValue) sensivity += 1; 111 + if (sensitiveElement === document.activeElement) sensivity += 1; 112 + } else { 113 + sensivity += 3; 114 + 115 + if (sensitiveElement instanceof HTMLMediaElement && !sensitiveElement.ended) { 116 + if (!sensitiveElement.paused) sensivity += 1; 117 + if (sensitiveElement.currentTime > 0) sensivity += 1; 118 + } 119 + } 120 + 121 + let current: ReadonlyNode<Element> | null = sensitiveElement; 122 + while (current) { 123 + sensivityMap.set(current, (sensivityMap.get(current) || 0) + sensivity); 124 + if (current === node) break; 125 + current = current.parentElement; 126 + } 127 + } 97 128 } 98 129 99 130 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. ··· 111 142 while (current) { 112 143 const idSet: IdSet | undefined = idMap.get(current); 113 144 idSet ? idSet.add(id) : idMap.set(current, new Set([id])); 114 - if (current === elementWithId) break; 145 + if (current === node) break; 115 146 current = current.parentElement; 116 147 } 117 148 } ··· 241 272 242 273 if (id !== "") { 243 274 if (id === ref.id) { 244 - insertBefore(parent, currentNode, child); 275 + insertBefore(parent, currentNode, child, context); 245 276 return morphNode(currentNode, ref, context); 246 277 } else { 247 278 const currentIdSet = context.idMap.get(currentNode); 248 279 249 280 if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 250 - insertBefore(parent, currentNode, child); 281 + insertBefore(parent, currentNode, child, context); 251 282 return morphNode(currentNode, ref, context); 252 283 } 253 284 } ··· 258 289 } 259 290 260 291 if (nextMatchByTagName) { 261 - insertBefore(parent, nextMatchByTagName, child); 292 + insertBefore(parent, nextMatchByTagName, child, context); 262 293 morphNode(nextMatchByTagName, ref, context); 263 294 } else { 264 295 // TODO: this is missing an added callback 265 - insertBefore(parent, ref.cloneNode(true), child); 296 + insertBefore(parent, ref.cloneNode(true), child, context); 266 297 } 267 298 } 268 299 ··· 277 308 } 278 309 } 279 310 280 - function insertBefore(parent: ParentNode, node: Node, insertionPoint: ChildNode): void { 311 + function insertBefore(parent: ParentNode, node: Node, insertionPoint: ChildNode, context: Context): void { 281 312 if (node === insertionPoint) return; 282 313 283 314 if (isElement(node)) { 284 - const sensitivity = nodeSensitivity(node); 315 + const sensitivity = context.sensitivityMap.get(node) ?? 0; 285 316 286 317 if (sensitivity > 0) { 287 318 let previousNode = node.previousSibling; 288 319 289 320 while (previousNode) { 290 - const previousNodeSensitivity = nodeSensitivity(previousNode); 321 + const previousNodeSensitivity = context.sensitivityMap.get(previousNode) ?? 0; 291 322 292 323 if (previousNodeSensitivity < sensitivity) { 293 324 parent.insertBefore(previousNode, node.nextSibling); ··· 302 333 } 303 334 304 335 parent.insertBefore(node, insertionPoint); 305 - } 306 - 307 - const sensitiveElements = new Set(["iframe", "audio", "video", "embed", "object", "canvas"]); 308 - const inputElements = new Set(["input", "select", "textarea"]); 309 - 310 - function nodeSensitivity(node: Node): number { 311 - let sensitivity = 0; 312 - if (!isElement(node)) return sensitivity; 313 - 314 - const localName = node.localName; 315 - 316 - if (inputElements.has(localName) || node.getAttribute("contenteditable")) { 317 - sensitivity += 1; 318 - 319 - if (node === document.activeElement) sensitivity += 1; 320 - if (node instanceof HTMLInputElement && node.value !== node.defaultValue) sensitivity += 1; 321 - } 322 - 323 - if (sensitiveElements.has(localName)) { 324 - sensitivity += 3; 325 - 326 - if (node instanceof HTMLMediaElement && !node.ended) { 327 - if (!node.paused) sensitivity += 1; 328 - if (node.currentTime > 0) sensitivity += 1; 329 - } 330 - } 331 - 332 - return sensitivity; 333 336 } 334 337 335 338 function appendChild(node: ParentNode, newNode: Node, context: Context): void {