2018-06-03 02:11:19 +00:00
|
|
|
import MutationQueue from "./MutationQueue"
|
2018-03-14 02:08:50 +00:00
|
|
|
|
2018-06-03 02:37:32 +00:00
|
|
|
// Diff provides diffing utilities to morph existing DOM elements
|
|
|
|
// into the target HTML string.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
// Diff.innerHTML(body, "<div>This is my new content</div>")
|
|
|
|
//
|
|
|
|
// Whatever contents will be in the body, they will be re-used and morphed
|
|
|
|
// into the new DOM defined by a simple HTML string. This is useful for
|
2018-06-03 02:39:18 +00:00
|
|
|
// Single Page Applications that use server rendered content. The server
|
2018-06-03 02:37:32 +00:00
|
|
|
// responds with the pre-rendered HTML and we can simply morph our current
|
|
|
|
// contents into the next page.
|
2018-04-02 05:34:16 +00:00
|
|
|
export default class Diff {
|
2017-07-19 02:18:56 +00:00
|
|
|
static persistentClasses = new Set<string>()
|
2017-07-19 07:09:55 +00:00
|
|
|
static persistentAttributes = new Set<string>()
|
2018-03-22 14:52:52 +00:00
|
|
|
static mutations: MutationQueue = new MutationQueue()
|
2017-07-05 19:06:38 +00:00
|
|
|
|
|
|
|
// innerHTML will diff the element with the given HTML string and apply DOM mutations.
|
2017-11-10 07:41:45 +00:00
|
|
|
static innerHTML(aRoot: HTMLElement, html: string): Promise<void> {
|
2018-04-07 10:34:41 +00:00
|
|
|
let container = document.createElement("main")
|
|
|
|
container.innerHTML = html
|
2017-11-10 07:41:45 +00:00
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
return new Promise((resolve, _) => {
|
2018-04-07 10:34:41 +00:00
|
|
|
Diff.childNodes(aRoot, container)
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.wait(resolve)
|
2017-11-10 07:41:45 +00:00
|
|
|
})
|
2017-07-05 19:06:38 +00:00
|
|
|
}
|
|
|
|
|
2017-07-19 03:23:06 +00:00
|
|
|
// root will diff the document root element with the given HTML string and apply DOM mutations.
|
|
|
|
static root(aRoot: HTMLElement, html: string) {
|
2019-04-22 09:06:50 +00:00
|
|
|
return new Promise((resolve, _) => {
|
2018-04-07 10:34:41 +00:00
|
|
|
let rootContainer = document.createElement("html")
|
|
|
|
rootContainer.innerHTML = html.replace("<!DOCTYPE html>", "")
|
2017-11-10 07:41:45 +00:00
|
|
|
|
2018-04-07 10:34:41 +00:00
|
|
|
Diff.childNodes(aRoot.getElementsByTagName("body")[0], rootContainer.getElementsByTagName("body")[0])
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.wait(resolve)
|
2017-11-10 07:41:45 +00:00
|
|
|
})
|
2017-07-19 03:23:06 +00:00
|
|
|
}
|
|
|
|
|
2017-07-05 19:06:38 +00:00
|
|
|
// childNodes diffs the child nodes of 2 given elements and applies DOM mutations.
|
2017-06-26 01:57:29 +00:00
|
|
|
static childNodes(aRoot: Node, bRoot: Node) {
|
2017-06-21 16:44:20 +00:00
|
|
|
let aChild = [...aRoot.childNodes]
|
|
|
|
let bChild = [...bRoot.childNodes]
|
|
|
|
let numNodes = Math.max(aChild.length, bChild.length)
|
2017-11-10 07:41:45 +00:00
|
|
|
|
2017-06-21 16:44:20 +00:00
|
|
|
for(let i = 0; i < numNodes; i++) {
|
2017-06-26 01:57:29 +00:00
|
|
|
let a = aChild[i]
|
2017-06-21 16:44:20 +00:00
|
|
|
|
2017-07-19 02:04:19 +00:00
|
|
|
// Remove nodes at the end of a that do not exist in b
|
2017-06-21 16:44:20 +00:00
|
|
|
if(i >= bChild.length) {
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => aRoot.removeChild(a))
|
2017-06-21 16:44:20 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-06-26 01:57:29 +00:00
|
|
|
let b = bChild[i]
|
2017-06-21 16:44:20 +00:00
|
|
|
|
2017-07-19 02:04:19 +00:00
|
|
|
// If a doesn't have that many nodes, simply append at the end of a
|
2017-06-21 16:44:20 +00:00
|
|
|
if(i >= aChild.length) {
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => aRoot.appendChild(b))
|
2017-06-21 16:44:20 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-07-19 02:04:19 +00:00
|
|
|
// If it's a completely different HTML tag or node type, replace it
|
2017-06-21 16:44:20 +00:00
|
|
|
if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) {
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => aRoot.replaceChild(b, a))
|
2017-06-21 16:44:20 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-07-19 02:04:19 +00:00
|
|
|
// Text node:
|
|
|
|
// We don't need to check for b to be a text node as well because
|
|
|
|
// we eliminated different node types in the previous condition.
|
2017-06-26 01:57:29 +00:00
|
|
|
if(a.nodeType === Node.TEXT_NODE) {
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => a.textContent = b.textContent)
|
2017-06-26 01:57:29 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-07-19 02:04:19 +00:00
|
|
|
// HTML element:
|
2017-06-21 16:44:20 +00:00
|
|
|
if(a.nodeType === Node.ELEMENT_NODE) {
|
2017-06-26 01:57:29 +00:00
|
|
|
let elemA = a as HTMLElement
|
|
|
|
let elemB = b as HTMLElement
|
|
|
|
|
2017-06-21 16:44:20 +00:00
|
|
|
let removeAttributes: Attr[] = []
|
2017-11-10 07:41:45 +00:00
|
|
|
|
2017-06-26 01:57:29 +00:00
|
|
|
for(let x = 0; x < elemA.attributes.length; x++) {
|
|
|
|
let attrib = elemA.attributes[x]
|
2017-06-21 16:44:20 +00:00
|
|
|
|
|
|
|
if(attrib.specified) {
|
2017-07-19 07:09:55 +00:00
|
|
|
if(!elemB.hasAttribute(attrib.name) && !Diff.persistentAttributes.has(attrib.name)) {
|
2017-06-21 16:44:20 +00:00
|
|
|
removeAttributes.push(attrib)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => {
|
|
|
|
for(let attr of removeAttributes) {
|
|
|
|
elemA.removeAttributeNode(attr)
|
|
|
|
}
|
|
|
|
})
|
2017-06-21 16:44:20 +00:00
|
|
|
|
2017-06-26 01:57:29 +00:00
|
|
|
for(let x = 0; x < elemB.attributes.length; x++) {
|
|
|
|
let attrib = elemB.attributes[x]
|
2017-06-21 16:44:20 +00:00
|
|
|
|
2017-07-19 02:47:32 +00:00
|
|
|
if(!attrib.specified) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-10-16 09:53:47 +00:00
|
|
|
// If the attribute value is exactly the same, skip this attribute.
|
|
|
|
if(elemA.getAttribute(attrib.name) === attrib.value) {
|
|
|
|
continue
|
|
|
|
}
|
2017-07-19 02:18:56 +00:00
|
|
|
|
2017-10-16 09:53:47 +00:00
|
|
|
if(attrib.name === "class") {
|
2017-07-19 02:47:32 +00:00
|
|
|
let classesA = elemA.classList
|
|
|
|
let classesB = elemB.classList
|
2017-10-08 07:03:55 +00:00
|
|
|
let removeClasses: string[] = []
|
2017-07-19 02:47:32 +00:00
|
|
|
|
|
|
|
for(let className of classesA) {
|
|
|
|
if(!classesB.contains(className) && !Diff.persistentClasses.has(className)) {
|
2017-10-08 07:03:55 +00:00
|
|
|
removeClasses.push(className)
|
2017-07-19 02:18:56 +00:00
|
|
|
}
|
2017-07-19 02:47:32 +00:00
|
|
|
}
|
2017-07-19 02:18:56 +00:00
|
|
|
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => {
|
|
|
|
for(let className of removeClasses) {
|
|
|
|
classesA.remove(className)
|
|
|
|
}
|
2017-10-08 07:03:55 +00:00
|
|
|
|
2018-03-14 02:08:50 +00:00
|
|
|
for(let className of classesB) {
|
|
|
|
if(!classesA.contains(className)) {
|
|
|
|
classesA.add(className)
|
|
|
|
}
|
2017-07-19 02:47:32 +00:00
|
|
|
}
|
2018-03-14 02:08:50 +00:00
|
|
|
})
|
2017-06-29 13:55:04 +00:00
|
|
|
|
2017-07-19 02:47:32 +00:00
|
|
|
continue
|
2017-06-21 16:44:20 +00:00
|
|
|
}
|
2017-11-10 07:41:45 +00:00
|
|
|
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => elemA.setAttribute(attrib.name, attrib.value))
|
2017-06-21 16:44:20 +00:00
|
|
|
}
|
2017-06-24 00:10:04 +00:00
|
|
|
|
|
|
|
// Special case: Apply state of input elements
|
2017-06-26 01:57:29 +00:00
|
|
|
if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
|
2018-03-14 02:08:50 +00:00
|
|
|
this.mutations.queue(() => {
|
|
|
|
(elemA as HTMLInputElement).value = (elemB as HTMLInputElement).value
|
|
|
|
})
|
2017-06-24 00:10:04 +00:00
|
|
|
}
|
2017-06-21 16:44:20 +00:00
|
|
|
}
|
|
|
|
|
2019-08-30 07:04:28 +00:00
|
|
|
// Never diff the contents of web components
|
|
|
|
if(a.nodeName.includes("-")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Child nodes
|
2017-06-21 16:44:20 +00:00
|
|
|
Diff.childNodes(a, b)
|
|
|
|
}
|
|
|
|
}
|
2017-06-20 13:46:49 +00:00
|
|
|
}
|