104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
export class Diff {
|
|
// Reuse container for diffs to avoid memory allocation
|
|
static container: HTMLElement
|
|
|
|
// innerHTML will diff the element with the given HTML string and apply DOM mutations.
|
|
static innerHTML(aRoot: HTMLElement, html: string) {
|
|
if(!Diff.container) {
|
|
Diff.container = document.createElement("main")
|
|
}
|
|
|
|
Diff.container.innerHTML = html
|
|
Diff.childNodes(aRoot, Diff.container)
|
|
}
|
|
|
|
// childNodes diffs the child nodes of 2 given elements and applies DOM mutations.
|
|
static childNodes(aRoot: Node, bRoot: Node) {
|
|
let aChild = [...aRoot.childNodes]
|
|
let bChild = [...bRoot.childNodes]
|
|
let numNodes = Math.max(aChild.length, bChild.length)
|
|
|
|
for(let i = 0; i < numNodes; i++) {
|
|
let a = aChild[i]
|
|
|
|
if(i >= bChild.length) {
|
|
aRoot.removeChild(a)
|
|
continue
|
|
}
|
|
|
|
let b = bChild[i]
|
|
|
|
if(i >= aChild.length) {
|
|
aRoot.appendChild(b)
|
|
continue
|
|
}
|
|
|
|
if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) {
|
|
aRoot.replaceChild(b, a)
|
|
continue
|
|
}
|
|
|
|
if(a.nodeType === Node.TEXT_NODE) {
|
|
a.textContent = b.textContent
|
|
continue
|
|
}
|
|
|
|
if(a.nodeType === Node.ELEMENT_NODE) {
|
|
let elemA = a as HTMLElement
|
|
let elemB = b as HTMLElement
|
|
|
|
// Ignore lazy images if they have the same source
|
|
if(elemA.classList.contains("lazy")) {
|
|
if(elemA.dataset.src !== elemB.dataset.src) {
|
|
elemA.dataset.src = elemB.dataset.src
|
|
elemA.title = elemB.title
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Skip iframes
|
|
// This part needs to be executed AFTER lazy images check
|
|
// to allow lazily loaded iframes to update their data src.
|
|
if(elemA.tagName === "IFRAME") {
|
|
continue
|
|
}
|
|
|
|
let removeAttributes: Attr[] = []
|
|
|
|
for(let x = 0; x < elemA.attributes.length; x++) {
|
|
let attrib = elemA.attributes[x]
|
|
|
|
if(attrib.specified) {
|
|
if(!elemB.hasAttribute(attrib.name)) {
|
|
removeAttributes.push(attrib)
|
|
}
|
|
}
|
|
}
|
|
|
|
for(let attr of removeAttributes) {
|
|
elemA.removeAttributeNode(attr)
|
|
}
|
|
|
|
for(let x = 0; x < elemB.attributes.length; x++) {
|
|
let attrib = elemB.attributes[x]
|
|
|
|
if(attrib.specified) {
|
|
// Skip mountables
|
|
if(attrib.name == "class" && elemA.classList.contains("mounted")) {
|
|
continue
|
|
}
|
|
|
|
elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name))
|
|
}
|
|
}
|
|
|
|
// Special case: Apply state of input elements
|
|
if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
|
|
elemA.value = elemB.value
|
|
}
|
|
}
|
|
|
|
Diff.childNodes(a, b)
|
|
}
|
|
}
|
|
} |