diff --git a/diff.js b/diff.js index ec7b202c..20142c71 100644 --- a/diff.js +++ b/diff.js @@ -1,6 +1,7 @@ -var VirtualDOMNode = require("./virtual-dom-node") -var VirtualTextNode = require("./virtual-text-node") +var createPatch = require("./lib/patch-op") +var indexTree = require("./lib/vtree-index") +var isArray = require("./lib/is-array") var isVDOMNode = require("./lib/is-virtual-dom") var isVTextNode = require("./lib/is-virtual-text") var isWidget = require("./lib/is-widget") @@ -15,111 +16,34 @@ function diff(a, b) { return patch } -// Index the tree in-order -function indexTree(tree, index, parent, c) { - - if (tree.index === 0 && !parent) { - // The tree has already been indexed once - return - } else if (tree.index >= 0 && parent) { - // This node has been indexed somewhere else in the tree, so clone - if (isVDOMNode(tree)) { - tree = parent[c] = new VirtualDOMNode( - tree.tagName, - tree.properties, - tree.children) - } else if (isVTextNode(tree)) { - tree = parent[c] = new VirtualTextNode(tree.text) - } else if (isWidget(tree)) { - tree = tree.init(tree) // calling init with self should clone - } - } - - index = index || 0 - tree.index = index - - if (tree.children) { - for (var i = 0; i < tree.children.length; i++) { - index = indexTree(tree.children[i], index + 1, tree, i) - } - } - - return tree.index -} -var remove = [{ type: "remove" }] function walk(a, b, patch) { - var apply - if (a === b) { return b } - if (updateWidget(a, b)) { - apply = [{ "type": "update", widget: a, patch: b }] - b = a - } else if (isVDOMNode(a) && isVDOMNode(b) && a.tagName === b.tagName) { - var propsPatch = diffProps(a.properties, b.properties) - if (propsPatch) { - apply = [{ - type: "update", - patch: propsPatch - }] - } + var apply = patch[a.index] - var aChildren = a.children - var bChildren = b.children - var aLen = aChildren.length - var bLen = bChildren.length - var len = aLen < bLen ? aLen : bLen - for (var i = 0; i < len; i++) { - var rightNode = bChildren[i] - var newRight = walk(aChildren[i], rightNode, patch) - - if (rightNode !== newRight) { - b[i] = newRight - } + if (isWidget(a)) { + apply = appendPatch(apply, createPatch(a, b)) + b = a // replaces the widget in b with the stateful a widget + } else if (isWidget(b)) { + apply = appendPatch(apply, createPatch(a, b)) + } else if (isVTextNode(a) && isVTextNode(b)) { + if (a.text !== b.text) { + apply = appendPatch(apply, createPatch(a.text, b.text)) } - - // Excess nodes in a need to be removed - for (; i < aLen; i++) { - var excess = aChildren[i] - if (isWidget(excess)) { - patch[excess.index] = [{ - type: "remove", - widget: excess - }] - } else { - patch[aChildren[i].index] = remove - } + } else if (isVDOMNode(a) && isVDOMNode(b) && a.tagName === b.tagName) { + var propsPatch = diffProps(a.properties, b.properties) + if (propsPatch) { + apply = appendPatch(apply, createPatch(a.properties, b.properties)) } - // Excess nodes in b need to be added - for (; i < bLen; i++) { - apply = apply || [] - var addition = bChildren[i] - if (isWidget(addition)) { - apply.push({ - type: "insert", - widget: addition - }) - } else { - apply.push({ - type: "insert", - b: addition - }) - } - } - } else if (isVTextNode(a) && isVTextNode(b) && a.text !== b.text) { - apply = [{ type: "update", patch: b.text }] + apply = diffChildren(a, b, patch, apply) } else if (a !== b) { - if (isWidget(b)) { - apply = [{ type: "replace", widget: b }] - } else { - apply = [{ type: "replace", b: b }] - } + apply = appendPatch(apply, createPatch(a, b)) } if (apply) { @@ -162,14 +86,47 @@ function diffProps(a, b) { return diff } -function updateWidget(a, b) { - if (isWidget(a) && isWidget(b)) { - if ("type" in a && "type" in b) { - return a.type === b.type - } else { - return a.init === b.init +function diffChildren(a, b, patch, apply) { + var aChildren = a.children + var bChildren = b.children + var aLen = aChildren.length + var bLen = bChildren.length + var len = aLen < bLen ? aLen : bLen + + for (var i = 0; i < len; i++) { + var rightNode = bChildren[i] + var newRight = walk(aChildren[i], rightNode, patch) + + if (rightNode !== newRight) { + bChildren[i] = newRight } } - return false + // Excess nodes in a need to be removed + for (; i < aLen; i++) { + var excess = aChildren[i] + patch[excess.index] = createPatch(excess, null) + } + + // Excess nodes in b need to be added + for (; i < bLen; i++) { + var addition = bChildren[i] + apply = appendPatch(apply, createPatch(null, addition)) + } + + return apply +} + +function appendPatch(apply, patch) { + if (apply) { + if (isArray(apply)) { + apply.push(patch) + } else { + apply = [apply, patch] + } + + return apply + } else { + return patch + } } diff --git a/h.js b/h.js index 3678f22c..af2e342f 100644 --- a/h.js +++ b/h.js @@ -28,7 +28,7 @@ function h(tagName, properties, children) { if (children) { if (isArray(children)) { - for (var i =0; i < children.length; i++) { + for (var i = 0; i < children.length; i++) { addChild(children[i], childNodes) } } else { diff --git a/lib/dom-index.js b/lib/dom-index.js index 2ae3bccc..db936894 100644 --- a/lib/dom-index.js +++ b/lib/dom-index.js @@ -6,41 +6,37 @@ module.exports = domIndex -function domIndex(rootNode, tree, patches, index) { - var indices = [] - - for (var key in patches) { - if (key !== "a") { - indices.push(key) - } - } - - if (indices.length === 0) { +function domIndex(rootNode, tree, indices, index) { + if (!indices || indices.length === 0) { return {} } else { indices.sort(ascending) - return recurse(rootNode, tree, patches, index, indices) + return recurse(rootNode, tree, indices, index, indices) } } -function recurse(rootNode, tree, patches, index, k) { +function recurse(rootNode, tree, indices, index) { index = index || {} if (rootNode) { - if (tree.index in patches) { + if (indexInRange(indices, tree.index, tree.index)) { index[tree.index] = rootNode } if (tree.children) { var childNodes = rootNode.childNodes + var nextChild + var nextIndex for (var i = 0; i < tree.children.length; i++) { - var nextChild = tree.children[i + 1] - var nextIndex = nextChild ? nextChild.index : Infinity + var child = nextChild || tree.children[i] + var cIndex = nextIndex + 1 || child.index + nextChild = tree.children[i + 1] + nextIndex = nextChild ? nextChild.index - 1 : Infinity // skip recursion down the tree if there are no nodes down here - if (indexInRange(k, tree.index, nextIndex)) { - recurse(childNodes[i], tree.children[i], patches, index, k) + if (indexInRange(indices, cIndex, nextIndex)) { + recurse(childNodes[i], child, indices, index) } } } @@ -49,7 +45,7 @@ function recurse(rootNode, tree, patches, index, k) { return index } -// Binary search for an index in the interval [left, right) +// Binary search for an index in the interval [left, right] function indexInRange(indices, left, right) { if (indices.length === 0) { return false @@ -60,14 +56,16 @@ function indexInRange(indices, left, right) { var currentIndex var currentItem - while (minIndex < maxIndex) { - currentIndex = ((maxIndex - minIndex) / 2) >> 0 + while (minIndex <= maxIndex) { + currentIndex = ((maxIndex + minIndex) / 2) >> 0 currentItem = indices[currentIndex] - if (currentItem < left) { - minIndex = currentIndex - } else if (currentItem >= right) { - maxIndex = currentIndex + if (minIndex === maxIndex) { + return currentItem >= left && currentItem <= right + } else if (currentItem < left) { + minIndex = currentIndex + 1 + } else if (currentItem > right) { + maxIndex = currentIndex - 1 } else { return true } @@ -77,5 +75,5 @@ function indexInRange(indices, left, right) { } function ascending(a, b) { - return a < b + return a > b } diff --git a/lib/patch-op.js b/lib/patch-op.js new file mode 100644 index 00000000..81aaaf42 --- /dev/null +++ b/lib/patch-op.js @@ -0,0 +1,143 @@ +var render = require("../render") +var isWidget = require("./is-widget") +var isString = require("./is-string") +var isVNode = require("./is-virtual-dom") +var updateWidget = require("./update-widget") + +module.exports = createPatch + +function createPatch(vNode, patch) { + return new PatchOp(vNode, patch) +} + +function PatchOp(vNode, patch) { + this.vNode = vNode + this.patch = patch +} + +PatchOp.prototype.apply = applyUpdate + +function applyUpdate(domNode) { + var vNode = this.vNode + var patch = this.patch + + if (patch == null) { + return removeNode(domNode, vNode) + } else if (vNode == null) { + return insertNode(domNode, patch) + } else if (isString(patch)) { + return stringPatch(domNode, vNode, patch) + } else if (isWidget(patch)) { + return widgetPatch(domNode, vNode, patch) + } else if (isVNode(patch)) { + return vNodePatch(domNode, vNode, patch) + } else { + return propPatch(domNode, patch) + } +} + +function removeNode(domNode, vNode) { + var parentNode = domNode.parentNode + + if (parentNode) { + parentNode.removeChild(domNode) + } + + destroyWidgets(vNode); + + return null +} + +function insertNode(parentNode, vNode) { + var newNode = render(vNode) + + if (parentNode) { + parentNode.appendChild(newNode) + } + + return parentNode +} + +function stringPatch(domNode, leftVNode, newString) { + if (domNode.nodeType === 3) { + domNode.replaceData(0, domNode.length, newString) + return domNode + } + + var parentNode = domNode.parentNode + var newNode = render(newString) + + if (parentNode) { + parentNode.replaceChild(newNode, domNode) + } + + destroyWidgets(leftVNode) + + return newNode +} + +function widgetPatch(domNode, leftVNode, widgetUpdate) { + if (isWidget(leftVNode)) { + if (updateWidget(leftVNode, widgetUpdate)) { + leftVNode.update(widgetUpdate) + return domNode + } + } + + var parentNode = domNode.parentNode + var newWidget = render(widgetUpdate) + + if (parentNode) { + parentNode.replaceChild(newWidget, domNode) + } + + destroyWidgets(leftVNode) + + return newWidget +} + +function vNodePatch(domNode, leftVNode, rightVNode) { + var parentNode = domNode.parentNode + var newNode = render(rightVNode) + + if (parentNode) { + parentNode.replaceChild(newNode, domNode) + } + + destroyWidgets(leftVNode) + + return newNode +} + +function propPatch(domNode, patch) { + for (var prop in patch) { + if (prop === "style") { + var stylePatch = patch.style + var domStyle = domNode.style + for (var s in stylePatch) { + domStyle[s] = stylePatch[s] + } + } else { + var patchValue = patch[prop] + + if (typeof patchValue === "function") { + patchValue(domNode, prop) + } else { + domNode[prop] = patchValue + } + } + } + + return domNode +} + +function destroyWidgets(w) { + if (isWidget(w)) { + w.destroy() + } else if (isVNode(w)) { + var children = w.children + for (var i = 0; i < children.length; i++) { + destroyWidgets(children[i]) + } + } +} diff --git a/lib/update-widget.js b/lib/update-widget.js new file mode 100644 index 00000000..9771136c --- /dev/null +++ b/lib/update-widget.js @@ -0,0 +1,15 @@ +var isWidget = require("./is-widget") + +module.exports = updateWidget + +function updateWidget(a, b) { + if (isWidget(a) && isWidget(b)) { + if ("type" in a && "type" in b) { + return a.type === b.type + } else { + return a.init === b.init + } + } + + return false +} \ No newline at end of file diff --git a/lib/vtree-index.js b/lib/vtree-index.js new file mode 100644 index 00000000..dcb3e074 --- /dev/null +++ b/lib/vtree-index.js @@ -0,0 +1,35 @@ +var VirtualDOMNode = require("../virtual-dom-node") +var VirtualTextNode = require("../virtual-text-node") +var isVDOMNode = require("./is-virtual-dom.js") +var isVTextNode = require("./is-virtual-text") + +module.exports = indexTree + +// Index the tree in-order +function indexTree(tree, index, parent, c) { + if (tree.index === 0 && !parent) { + // The tree has already been indexed once + return + } else if (tree.index >= 0 && parent) { + // This node has been indexed somewhere else in the tree, so clone + if (isVDOMNode(tree)) { + tree = parent[c] = new VirtualDOMNode( + tree.tagName, + tree.properties, + tree.children) + } else if (isVTextNode(tree)) { + tree = parent[c] = new VirtualTextNode(tree.text) + } + } + + index = index || 0 + tree.index = index + + if (tree.children) { + for (var i = 0; i < tree.children.length; i++) { + index = indexTree(tree.children[i], index + 1, tree, i) + } + } + + return tree.index +} diff --git a/patch.js b/patch.js index 8df0978c..73e87fac 100644 --- a/patch.js +++ b/patch.js @@ -1,19 +1,19 @@ -var render = require("./render") - var domIndex = require("./lib/dom-index") var isArray = require("./lib/is-array") -var isString = require("./lib/is-string") module.exports = patch function patch(rootNode, patches) { - var index = domIndex(rootNode, patches.a, patches) + var indices = patchIndices(patches) - for (var nodeIndex in patches) { - if (nodeIndex === "a") { - continue - } + if (indices.length === 0) { + return rootNode + } + var index = domIndex(rootNode, patches.a, indices) + + for (var i = 0; i < indices.length; i++) { + var nodeIndex = indices[i] rootNode = applyPatch(rootNode, index[nodeIndex], patches[nodeIndex]) } @@ -21,88 +21,39 @@ function patch(rootNode, patches) { } function applyPatch(rootNode, domNode, patchList) { - if (!domNode || !isArray(patchList)) { + if (!domNode) { return rootNode } - for (var i = 0; i < patchList.length; i++) { - var op = patchList[i] + var newNode + + if (isArray(patchList)) { + for (var i = 0; i < patchList.length; i++) { + newNode = patchList[i].apply(domNode) - if (op.type === "remove") { - remove(domNode) - if (op.widget) { - op.widget.destroy() - } - } else if (op.type === "insert") { - if (op.widget) { - insert(domNode, op.widget.init()) - } else { - insert(domNode, render(op.b)) - } - } else if (op.type === "replace") { if (domNode === rootNode) { - rootNode = render(op.b) - } else { - replace(domNode, render(op.b)) + rootNode = newNode } - } else if (op.type === "update") { - update(domNode, op.patch) } - } - - return rootNode -} - -function remove(domNode) { - var parent = domNode.parentNode - if (parent) { - parent.removeChild(domNode) - } -} + } else { + newNode = patchList.apply(domNode) -function insert(domNode, child) { - if (domNode.nodeType === 1) { - domNode.appendChild(child) + if (domNode === rootNode) { + rootNode = newNode + } } -} -function replace(domNode, newNode) { - var parent = domNode.parentNode - if (parent) { - parent.replaceChild(newNode, domNode) - } + return rootNode } -function update(domNode, patch) { - if (isString(patch)) { - if (domNode.nodeType === 3) { - domNode.replaceData(0, patch.length, patch) - } else if (patch.a && patch.bl) { - // update widget - var wNode = patch.a.update(patch.b) - if (domNode !== wNode) { - replace(domNode, wNode) - } - } else { - replace(domNode, render(patch)) - } - } else { - for (var prop in patch) { - if (prop === "style") { - var stylePatch = patch.style - var domStyle = domNode.style - for (var s in stylePatch) { - domStyle[s] = stylePatch[s] - } - } else { - var patchValue = patch[prop] +function patchIndices(patches) { + var indices = [] - if (typeof patchValue === "function") { - patchValue(domNode, prop) - } else { - domNode[prop] = patchValue - } - } + for (var key in patches) { + if (key !== "a") { + indices.push(Number(key)) } } + + return indices } diff --git a/test/index.js b/test/index.js index 84c821f0..c13d670c 100644 --- a/test/index.js +++ b/test/index.js @@ -35,6 +35,9 @@ test("defaults to div node", function (assert) { assert.end() }) + +/* + test("works with basic html tag types", function (assert) { var nodes = [] @@ -61,7 +64,7 @@ test("forces lowercase tag names", function (assert) { } assert.end() -}) +}) */ test("can use class selector", function (assert) { var node = h("div.pretty") @@ -375,7 +378,6 @@ test("injected warning is used", function (assert) { assert.end() }) - // Complete patch tests test("textnode update test", function (assert) { var hello = h("div", "hello") @@ -422,6 +424,7 @@ test("textnode remove", function (assert) { }) test("dom node update test", function (assert) { + debugger var hello = h("div.hello", "hello") var goodbye = h("div.goodbye", "goodbye") var rootNode = render(hello)