diff --git a/www/common/diffDOM.js b/www/common/diffDOM.js deleted file mode 100644 index 18ac7be12..000000000 --- a/www/common/diffDOM.js +++ /dev/null @@ -1,1349 +0,0 @@ -(function() { - "use strict"; - - var diffcount; - - var Diff = function (options) { - var diff = this; - Object.keys(options).forEach(function(option) { - diff[option] = options[option]; - }); - }; - - Diff.prototype = { - toString: function() { - return JSON.stringify(this); - } - - // TODO: compress diff output by replacing these keys with numbers or alike: - /* 'addAttribute' = 0, - 'modifyAttribute' = 1, - 'removeAttribute' = 2, - 'modifyTextElement' = 3, - 'relocateGroup' = 4, - 'removeElement' = 5, - 'addElement' = 6, - 'removeTextElement' = 7, - 'addTextElement' = 8, - 'replaceElement' = 9, - 'modifyValue' = 10, - 'modifyChecked' = 11, - 'modifySelected' = 12, - 'modifyComment' = 13, - 'action' = 14, - 'route' = 15, - 'oldValue' = 16, - 'newValue' = 17, - 'element' = 18, - 'group' = 19, - 'from' = 20, - 'to' = 21, - 'name' = 22, - 'value' = 23, - 'data' = 24, - 'attributes' = 25, - 'nodeName' = 26, - 'childNodes' = 27, - 'checked' = 28, - 'selected' = 29;*/ - }; - - var SubsetMapping = function SubsetMapping(a, b) { - this.old = a; - this.new = b; - }; - - SubsetMapping.prototype = { - contains: function contains(subset) { - if (subset.length < this.length) { - return subset.new >= this.new && subset.new < this.new + this.length; - } - return false; - }, - toString: function toString() { - return this.length + " element subset, first mapping: old " + this.old + " → new " + this.new; - } - }; - - var elementDescriptors = function(el) { - var output = []; - if (el.nodeName !== '#text' && el.nodeName !== '#comment') { - output.push(el.nodeName); - if (el.attributes) { - if (el.attributes.class) { - output.push(el.nodeName + '.' + el.attributes.class.replace(/ /g, '.')); - } - if (el.attributes.id) { - output.push(el.nodeName + '#' + el.attributes.id); - } - } - - } - return output; - }; - - var findUniqueDescriptors = function(li) { - var uniqueDescriptors = {}, - duplicateDescriptors = {}; - - li.forEach(function(node) { - elementDescriptors(node).forEach(function(descriptor) { - var inUnique = descriptor in uniqueDescriptors, - inDupes = descriptor in duplicateDescriptors; - if (!inUnique && !inDupes) { - uniqueDescriptors[descriptor] = true; - } else if (inUnique) { - delete uniqueDescriptors[descriptor]; - duplicateDescriptors[descriptor] = true; - } - }); - - }); - - return uniqueDescriptors; - }; - - var uniqueInBoth = function(l1, l2) { - var l1Unique = findUniqueDescriptors(l1), - l2Unique = findUniqueDescriptors(l2), - inBoth = {}; - - Object.keys(l1Unique).forEach(function(key) { - if (l2Unique[key]) { - inBoth[key] = true; - } - }); - - return inBoth; - }; - - var removeDone = function(tree) { - delete tree.outerDone; - delete tree.innerDone; - delete tree.valueDone; - if (tree.childNodes) { - return tree.childNodes.every(removeDone); - } else { - return true; - } - }; - - var isEqual = function(e1, e2) { - - var e1Attributes, e2Attributes; - - if (!['nodeName', 'value', 'checked', 'selected', 'data'].every(function(element) { - if (e1[element] !== e2[element]) { - return false; - } - return true; - })) { - return false; - } - - if (Boolean(e1.attributes) !== Boolean(e2.attributes)) { - return false; - } - - if (Boolean(e1.childNodes) !== Boolean(e2.childNodes)) { - return false; - } - - if (e1.attributes) { - e1Attributes = Object.keys(e1.attributes); - e2Attributes = Object.keys(e2.attributes); - - if (e1Attributes.length !== e2Attributes.length) { - return false; - } - if (!e1Attributes.every(function(attribute) { - if (e1.attributes[attribute] !== e2.attributes[attribute]) { - return false; - } - })) { - return false; - } - } - - if (e1.childNodes) { - if (e1.childNodes.length !== e2.childNodes.length) { - return false; - } - if (!e1.childNodes.every(function(childNode, index) { - return isEqual(childNode, e2.childNodes[index]); - })) { - - return false; - } - - } - - return true; - - }; - - - var roughlyEqual = function(e1, e2, uniqueDescriptors, sameSiblings, preventRecursion) { - var childUniqueDescriptors, nodeList1, nodeList2; - - if (!e1 || !e2) { - return false; - } - - if (e1.nodeName !== e2.nodeName) { - return false; - } - - if (e1.nodeName === '#text') { - // Note that we initially don't care what the text content of a node is, - // the mere fact that it's the same tag and "has text" means it's roughly - // equal, and then we can find out the true text difference later. - return preventRecursion ? true : e1.data === e2.data; - } - - - if (e1.nodeName in uniqueDescriptors) { - return true; - } - - if (e1.attributes && e2.attributes) { - - if (e1.attributes.id && e1.attributes.id === e2.attributes.id) { - var idDescriptor = e1.nodeName + '#' + e1.attributes.id; - if (idDescriptor in uniqueDescriptors) { - return true; - } - } - if (e1.attributes.class && e1.attributes.class === e2.attributes.class) { - var classDescriptor = e1.nodeName + '.' + e1.attributes.class.replace(/ /g, '.'); - if (classDescriptor in uniqueDescriptors) { - return true; - } - } - } - - if (sameSiblings) { - return true; - } - - nodeList1 = e1.childNodes ? e1.childNodes.slice().reverse() : []; - nodeList2 = e2.childNodes ? e2.childNodes.slice().reverse() : []; - - if (nodeList1.length !== nodeList2.length) { - return false; - } - - if (preventRecursion) { - return nodeList1.every(function(element, index) { - return element.nodeName === nodeList2[index].nodeName; - }); - } else { - // note: we only allow one level of recursion at any depth. If 'preventRecursion' - // was not set, we must explicitly force it to true for child iterations. - childUniqueDescriptors = uniqueInBoth(nodeList1, nodeList2); - return nodeList1.every(function(element, index) { - return roughlyEqual(element, nodeList2[index], childUniqueDescriptors, true, true); - }); - } - }; - - - var cloneObj = function(obj) { - // TODO: Do we really need to clone here? Is it not enough to just return the original object? - return JSON.parse(JSON.stringify(obj)); - //return obj; - }; - - /** - * based on https://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Longest_common_substring#JavaScript - */ - var findCommonSubsets = function(c1, c2, marked1, marked2) { - var lcsSize = 0, - index = [], - matches = Array.apply(null, new Array(c1.length + 1)).map(function() { - return []; - }), // set up the matching table - uniqueDescriptors = uniqueInBoth(c1, c2), - // If all of the elements are the same tag, id and class, then we can - // consider them roughly the same even if they have a different number of - // children. This will reduce removing and re-adding similar elements. - subsetsSame = c1.length === c2.length, - origin, ret; - - if (subsetsSame) { - - c1.some(function(element, i) { - var c1Desc = elementDescriptors(element), - c2Desc = elementDescriptors(c2[i]); - if (c1Desc.length !== c2Desc.length) { - subsetsSame = false; - return true; - } - c1Desc.some(function(description, i) { - if (description !== c2Desc[i]) { - subsetsSame = false; - return true; - } - }); - if (!subsetsSame) { - return true; - } - - }); - } - - // fill the matches with distance values - c1.forEach(function(c1Element, c1Index) { - c2.forEach(function(c2Element, c2Index) { - if (!marked1[c1Index] && !marked2[c2Index] && roughlyEqual(c1Element, c2Element, uniqueDescriptors, subsetsSame)) { - matches[c1Index + 1][c2Index + 1] = (matches[c1Index][c2Index] ? matches[c1Index][c2Index] + 1 : 1); - if (matches[c1Index + 1][c2Index + 1] >= lcsSize) { - lcsSize = matches[c1Index + 1][c2Index + 1]; - index = [c1Index + 1, c2Index + 1]; - } - } else { - matches[c1Index + 1][c2Index + 1] = 0; - } - }); - }); - if (lcsSize === 0) { - return false; - } - origin = [index[0] - lcsSize, index[1] - lcsSize]; - ret = new SubsetMapping(origin[0], origin[1]); - ret.length = lcsSize; - - return ret; - }; - - /** - * This should really be a predefined function in Array... - */ - var makeArray = function(n, v) { - return Array.apply(null, new Array(n)).map(function() { - return v; - }); - }; - - /** - * Generate arrays that indicate which node belongs to which subset, - * or whether it's actually an orphan node, existing in only one - * of the two trees, rather than somewhere in both. - * - * So if t1 =
, t2 =
. - * The longest subset is "
" (length 2), so it will group 0. - * The second longest is "" (length 1), so it will be group 1. - * gaps1 will therefore be [1,0,0] and gaps2 [0,0,1]. - * - * If an element is not part of any group, it will stay being 'true', which - * is the initial value. For example: - * t1 =


, t2 =
- * - * The "

" and "" do only show up in one of the two and will - * therefore be marked by "true". The remaining parts are parts of the - * groups 0 and 1: - * gaps1 = [1, true, 0, 0], gaps2 = [true, 0, 0, 1] - * - */ - var getGapInformation = function(t1, t2, stable) { - - var gaps1 = t1.childNodes ? makeArray(t1.childNodes.length, true) : [], - gaps2 = t2.childNodes ? makeArray(t2.childNodes.length, true) : [], - group = 0; - - // give elements from the same subset the same group number - stable.forEach(function(subset) { - var i, endOld = subset.old + subset.length, - endNew = subset.new + subset.length; - for (i = subset.old; i < endOld; i += 1) { - gaps1[i] = group; - } - for (i = subset.new; i < endNew; i += 1) { - gaps2[i] = group; - } - group += 1; - }); - - return { - gaps1: gaps1, - gaps2: gaps2 - }; - }; - - /** - * Find all matching subsets, based on immediate child differences only. - */ - var markSubTrees = function(oldTree, newTree) { - // note: the child lists are views, and so update as we update old/newTree - var oldChildren = oldTree.childNodes ? oldTree.childNodes : [], - newChildren = newTree.childNodes ? newTree.childNodes : [], - marked1 = makeArray(oldChildren.length, false), - marked2 = makeArray(newChildren.length, false), - subsets = [], - subset = true, - returnIndex = function() { - return arguments[1]; - }, - markBoth = function(i) { - marked1[subset.old + i] = true; - marked2[subset.new + i] = true; - }; - - while (subset) { - subset = findCommonSubsets(oldChildren, newChildren, marked1, marked2); - if (subset) { - subsets.push(subset); - - Array.apply(null, new Array(subset.length)).map(returnIndex).forEach(markBoth); - - } - } - return subsets; - }; - - - function swap(obj, p1, p2) { - (function(_) { - obj[p1] = obj[p2]; - obj[p2] = _; - }(obj[p1])); - } - - - var DiffTracker = function() { - this.list = []; - }; - - DiffTracker.prototype = { - list: false, - add: function(diffs) { - var list = this.list; - diffs.forEach(function(diff) { - list.push(diff); - }); - }, - forEach: function(fn) { - this.list.forEach(fn); - } - }; - - var diffDOM = function(options) { - - var defaults = { - debug: false, - diffcap: 10, // Limit for how many diffs are accepting when debugging. Inactive when debug is false. - maxDepth: false, // False or a numeral. If set to a numeral, limits the level of depth that the the diff mechanism looks for differences. If false, goes through the entire tree. - valueDiffing: true, // Whether to take into consideration the values of forms that differ from auto assigned values (when a user fills out a form). - // syntax: textDiff: function (node, currentValue, expectedValue, newValue) - textDiff: function() { - arguments[0].data = arguments[3]; - return; - }, - // empty functions were benchmarked as running faster than both - // `f && f()` and `if (f) { f(); }` - preVirtualDiffApply: function () {}, - postVirtualDiffApply: function () {}, - preDiffApply: function () {}, - postDiffApply: function () {} - }, - i; - - if (typeof options === "undefined") { - options = {}; - } - - for (i in defaults) { - if (typeof options[i] === "undefined") { - this[i] = defaults[i]; - } else { - this[i] = options[i]; - } - } - - }; - diffDOM.prototype = { - - // ===== Create a diff ===== - - diff: function(t1Node, t2Node) { - - var t1 = this.nodeToObj(t1Node), - t2 = this.nodeToObj(t2Node); - - diffcount = 0; - - if (this.debug) { - this.t1Orig = this.nodeToObj(t1Node); - this.t2Orig = this.nodeToObj(t2Node); - } - - this.tracker = new DiffTracker(); - return this.findDiffs(t1, t2); - }, - findDiffs: function(t1, t2) { - var diffs; - do { - if (this.debug) { - diffcount += 1; - if (diffcount > this.diffcap) { - window.diffError = [this.t1Orig, this.t2Orig]; - throw new Error("surpassed diffcap:" + JSON.stringify(this.t1Orig) + " -> " + JSON.stringify(this.t2Orig)); - } - } - diffs = this.findNextDiff(t1, t2, []); - if (diffs.length === 0) { - // Last check if the elements really are the same now. - // If not, remove all info about being done and start over. - // Somtimes a node can be marked as done, but the creation of subsequent diffs means that it has to be changed anyway. - if (!isEqual(t1, t2)) { - removeDone(t1); - diffs = this.findNextDiff(t1, t2, []); - } - } - - if (diffs.length > 0) { - this.tracker.add(diffs); - this.applyVirtual(t1, diffs); - } - } while (diffs.length > 0); - return this.tracker.list; - }, - findNextDiff: function(t1, t2, route) { - var diffs; - - if (this.maxDepth && route.length > this.maxDepth) { - return []; - } - // outer differences? - if (!t1.outerDone) { - diffs = this.findOuterDiff(t1, t2, route); - if (diffs.length > 0) { - t1.outerDone = true; - return diffs; - } else { - t1.outerDone = true; - } - } - // inner differences? - if (!t1.innerDone) { - diffs = this.findInnerDiff(t1, t2, route); - if (diffs.length > 0) { - return diffs; - } else { - t1.innerDone = true; - } - } - - if (this.valueDiffing && !t1.valueDone) { - // value differences? - diffs = this.findValueDiff(t1, t2, route); - - if (diffs.length > 0) { - t1.valueDone = true; - return diffs; - } else { - t1.valueDone = true; - } - } - - // no differences - return []; - }, - findOuterDiff: function(t1, t2, route) { - - var diffs = [], - attr1, attr2; - - if (t1.nodeName !== t2.nodeName) { - return [new Diff({ - action: 'replaceElement', - oldValue: cloneObj(t1), - newValue: cloneObj(t2), - route: route - })]; - } - - if (t1.data !== t2.data) { - // Comment or text node. - if (t1.nodeName === '#text') { - return [new Diff({ - action: 'modifyComment', - route: route, - oldValue: t1.data, - newValue: t2.data - })]; - } else { - return [new Diff({ - action: 'modifyTextElement', - route: route, - oldValue: t1.data, - newValue: t2.data - })]; - } - - } - - - attr1 = t1.attributes ? Object.keys(t1.attributes).sort() : []; - attr2 = t2.attributes ? Object.keys(t2.attributes).sort() : []; - - attr1.forEach(function(attr) { - var pos = attr2.indexOf(attr); - if (pos === -1) { - diffs.push(new Diff({ - action: 'removeAttribute', - route: route, - name: attr, - value: t1.attributes[attr] - })); - } else { - attr2.splice(pos, 1); - if (t1.attributes[attr] !== t2.attributes[attr]) { - diffs.push(new Diff({ - action: 'modifyAttribute', - route: route, - name: attr, - oldValue: t1.attributes[attr], - newValue: t2.attributes[attr] - })); - } - } - - }); - - - attr2.forEach(function(attr) { - diffs.push(new Diff({ - action: 'addAttribute', - route: route, - name: attr, - value: t2.attributes[attr] - })); - - }); - - return diffs; - }, - nodeToObj: function(node) { - var objNode = {}, dobj = this; - objNode.nodeName = node.nodeName; - if (objNode.nodeName === '#text' || objNode.nodeName === '#comment') { - objNode.data = node.data; - } else { - if (node.attributes && node.attributes.length > 0) { - objNode.attributes = {}; - Array.prototype.slice.call(node.attributes).forEach( - function(attribute) { - objNode.attributes[attribute.name] = attribute.value; - } - ); - } - if (node.childNodes && node.childNodes.length > 0) { - objNode.childNodes = []; - Array.prototype.slice.call(node.childNodes).forEach( - function(childNode) { - objNode.childNodes.push(dobj.nodeToObj(childNode)); - } - ); - } - if (this.valueDiffing) { - if (node.value) { - objNode.value = node.value; - } - if (node.checked) { - objNode.checked = node.checked; - } - if (node.selected) { - objNode.selected = node.selected; - } - } - } - - return objNode; - }, - objToNode: function(objNode, insideSvg) { - var node, dobj = this; - if (objNode.nodeName === '#text') { - node = document.createTextNode(objNode.data); - - } else if (objNode.nodeName === '#comment') { - node = document.createComment(objNode.data); - } else { - if (objNode.nodeName === 'svg' || insideSvg) { - node = document.createElementNS('http://www.w3.org/2000/svg', objNode.nodeName); - insideSvg = true; - } else { - node = document.createElement(objNode.nodeName); - } - if (objNode.attributes) { - Object.keys(objNode.attributes).forEach(function(attribute) { - node.setAttribute(attribute, objNode.attributes[attribute]); - }); - } - if (objNode.childNodes) { - objNode.childNodes.forEach(function(childNode) { - node.appendChild(dobj.objToNode(childNode, insideSvg)); - }); - } - if (this.valueDiffing) { - if (objNode.value) { - node.value = objNode.value; - } - if (objNode.checked) { - node.checked = objNode.checked; - } - if (objNode.selected) { - node.selected = objNode.selected; - } - } - } - return node; - }, - findInnerDiff: function(t1, t2, route) { - - var subtrees = (t1.childNodes && t2.childNodes) ? markSubTrees(t1, t2) : [], - t1ChildNodes = t1.childNodes ? t1.childNodes : [], - t2ChildNodes = t2.childNodes ? t2.childNodes : [], - childNodesLengthDifference, diffs = [], - index = 0, - last, e1, e2, i; - - if (subtrees.length > 1) { - /* Two or more groups have been identified among the childnodes of t1 - * and t2. - */ - return this.attemptGroupRelocation(t1, t2, subtrees, route); - } - - /* 0 or 1 groups of similar child nodes have been found - * for t1 and t2. 1 If there is 1, it could be a sign that the - * contents are the same. When the number of groups is below 2, - * t1 and t2 are made to have the same length and each of the - * pairs of child nodes are diffed. - */ - - - last = Math.max(t1ChildNodes.length, t2ChildNodes.length); - if (t1ChildNodes.length !== t2ChildNodes.length) { - childNodesLengthDifference = true; - } - - for (i = 0; i < last; i += 1) { - e1 = t1ChildNodes[i]; - e2 = t2ChildNodes[i]; - - if (childNodesLengthDifference) { - /* t1 and t2 have different amounts of childNodes. Add - * and remove as necessary to obtain the same length */ - if (e1 && !e2) { - if (e1.nodeName === '#text') { - diffs.push(new Diff({ - action: 'removeTextElement', - route: route.concat(index), - value: e1.data - })); - index -= 1; - } else { - diffs.push(new Diff({ - action: 'removeElement', - route: route.concat(index), - element: cloneObj(e1) - })); - index -= 1; - } - - } else if (e2 && !e1) { - if (e2.nodeName === '#text') { - diffs.push(new Diff({ - action: 'addTextElement', - route: route.concat(index), - value: e2.data - })); - } else { - diffs.push(new Diff({ - action: 'addElement', - route: route.concat(index), - element: cloneObj(e2) - })); - } - } - } - /* We are now guaranteed that childNodes e1 and e2 exist, - * and that they can be diffed. - */ - /* Diffs in child nodes should not affect the parent node, - * so we let these diffs be submitted together with other - * diffs. - */ - - if (e1 && e2) { - diffs = diffs.concat(this.findNextDiff(e1, e2, route.concat(index))); - } - - index += 1; - - } - t1.innerDone = true; - return diffs; - - }, - - attemptGroupRelocation: function(t1, t2, subtrees, route) { - /* Either t1.childNodes and t2.childNodes have the same length, or - * there are at least two groups of similar elements can be found. - * attempts are made at equalizing t1 with t2. First all initial - * elements with no group affiliation (gaps=true) are removed (if - * only in t1) or added (if only in t2). Then the creation of a group - * relocation diff is attempted. - */ - - var gapInformation = getGapInformation(t1, t2, subtrees), - gaps1 = gapInformation.gaps1, - gaps2 = gapInformation.gaps2, - shortest = Math.min(gaps1.length, gaps2.length), - destinationDifferent, toGroup, - group, node, similarNode, testI, diffs = [], - index1, index2, j; - - - for (index2 = 0, index1 = 0; index2 < shortest; index1 += 1, index2 += 1) { - if (gaps1[index2] === true) { - node = t1.childNodes[index1]; - if (node.nodeName === '#text') { - if (t2.childNodes[index2].nodeName === '#text' && node.data !== t2.childNodes[index2].data) { - testI = index1; - while (t1.childNodes.length > testI + 1 && t1.childNodes[testI + 1].nodeName === '#text') { - testI += 1; - if (t2.childNodes[index2].data === t1.childNodes[testI].data) { - similarNode = true; - break; - } - } - if (!similarNode) { - diffs.push(new Diff({ - action: 'modifyTextElement', - route: route.concat(index2), - oldValue: node.data, - newValue: t2.childNodes[index2].data - })); - } - } - diffs.push(new Diff({ - action: 'removeTextElement', - route: route.concat(index2), - value: node.data - })); - gaps1.splice(index2, 1); - shortest = Math.min(gaps1.length, gaps2.length); - index2 -= 1; - } else { - diffs.push(new Diff({ - action: 'removeElement', - route: route.concat(index2), - element: cloneObj(node) - })); - gaps1.splice(index2, 1); - shortest = Math.min(gaps1.length, gaps2.length); - index2 -= 1; - } - - } else if (gaps2[index2] === true) { - node = t2.childNodes[index2]; - if (node.nodeName === '#text') { - diffs.push(new Diff({ - action: 'addTextElement', - route: route.concat(index2), - value: node.data - })); - gaps1.splice(index2, 0, true); - shortest = Math.min(gaps1.length, gaps2.length); - index1 -= 1; - } else { - diffs.push(new Diff({ - action: 'addElement', - route: route.concat(index2), - element: cloneObj(node) - })); - gaps1.splice(index2, 0, true); - shortest = Math.min(gaps1.length, gaps2.length); - index1 -= 1; - } - - } else if (gaps1[index2] !== gaps2[index2]) { - if (diffs.length > 0) { - return diffs; - } - // group relocation - group = subtrees[gaps1[index2]]; - toGroup = Math.min(group.new, (t1.childNodes.length - group.length)); - if (toGroup !== group.old) { - // Check whether destination nodes are different than originating ones. - destinationDifferent = false; - for (j = 0; j < group.length; j += 1) { - if (!roughlyEqual(t1.childNodes[toGroup + j], t1.childNodes[group.old + j], [], false, true)) { - destinationDifferent = true; - } - } - if (destinationDifferent) { - return [new Diff({ - action: 'relocateGroup', - groupLength: group.length, - from: group.old, - to: toGroup, - route: route - })]; - } - } - } - } - return diffs; - }, - - findValueDiff: function(t1, t2, route) { - // Differences of value. Only useful if the value/selection/checked value - // differs from what is represented in the DOM. For example in the case - // of filled out forms, etc. - var diffs = []; - - if (t1.selected !== t2.selected) { - diffs.push(new Diff({ - action: 'modifySelected', - oldValue: t1.selected, - newValue: t2.selected, - route: route - })); - } - - if ((t1.value || t2.value) && t1.value !== t2.value && t1.nodeName !== 'OPTION') { - diffs.push(new Diff({ - action: 'modifyValue', - oldValue: t1.value, - newValue: t2.value, - route: route - })); - } - if (t1.checked !== t2.checked) { - diffs.push(new Diff({ - action: 'modifyChecked', - oldValue: t1.checked, - newValue: t2.checked, - route: route - })); - } - - return diffs; - }, - - // ===== Apply a virtual diff ===== - - applyVirtual: function(tree, diffs) { - var dobj = this; - if (diffs.length === 0) { - return true; - } - diffs.forEach(function(diff) { - // console.log(JSON.stringify(diff)); - // console.log(JSON.stringify(tree)); - // console.log(this.objToNode(tree).outerHTML); - dobj.applyVirtualDiff(tree, diff); - // console.log(JSON.stringify(tree)); - // console.log(this.objToNode(tree).outerHTML); - }); - return true; - }, - getFromVirtualRoute: function(tree, route) { - var node = tree, - parentNode, nodeIndex; - - route = route.slice(); - while (route.length > 0) { - if (!node.childNodes) { - return false; - } - nodeIndex = route.splice(0, 1)[0]; - parentNode = node; - node = node.childNodes[nodeIndex]; - } - return { - node: node, - parentNode: parentNode, - nodeIndex: nodeIndex - }; - }, - applyVirtualDiff: function(tree, diff) { - var routeInfo = this.getFromVirtualRoute(tree, diff.route), - node = routeInfo.node, - parentNode = routeInfo.parentNode, - nodeIndex = routeInfo.nodeIndex, - newNode, route, c; - - // pre-diff hook - var info = { - diff: diff, - node: node - }; - - if (this.preVirtualDiffApply(info)) { return true; } - - switch (diff.action) { - case 'addAttribute': - if (!node.attributes) { - node.attributes = {}; - } - - node.attributes[diff.name] = diff.value; - - if (diff.name === 'checked') { - node.checked = true; - } else if (diff.name === 'selected') { - node.selected = true; - } else if (node.nodeName === 'INPUT' && diff.name === 'value') { - node.value = diff.value; - } - - break; - case 'modifyAttribute': - node.attributes[diff.name] = diff.newValue; - if (node.nodeName === 'INPUT' && diff.name === 'value') { - node.value = diff.value; - } - break; - case 'removeAttribute': - - delete node.attributes[diff.name]; - - if (Object.keys(node.attributes).length === 0) { - delete node.attributes; - } - - if (diff.name === 'checked') { - delete node.checked; - } else if (diff.name === 'selected') { - delete node.selected; - } else if (node.nodeName === 'INPUT' && diff.name === 'value') { - delete node.value; - } - - break; - case 'modifyTextElement': - node.data = diff.newValue; - - if (parentNode.nodeName === 'TEXTAREA') { - parentNode.value = diff.newValue; - } - break; - case 'modifyValue': - node.value = diff.newValue; - break; - case 'modifyComment': - node.data = diff.newValue; - break; - case 'modifyChecked': - node.checked = diff.newValue; - break; - case 'modifySelected': - node.selected = diff.newValue; - break; - case 'replaceElement': - newNode = cloneObj(diff.newValue); - newNode.outerDone = true; - newNode.innerDone = true; - newNode.valueDone = true; - parentNode.childNodes[nodeIndex] = newNode; - break; - case 'relocateGroup': - node.childNodes.splice(diff.from, diff.groupLength).reverse() - .forEach(function(movedNode) { - node.childNodes.splice(diff.to, 0, movedNode); - }); - break; - case 'removeElement': - parentNode.childNodes.splice(nodeIndex, 1); - break; - case 'addElement': - route = diff.route.slice(); - c = route.splice(route.length - 1, 1)[0]; - node = this.getFromVirtualRoute(tree, route).node; - newNode = cloneObj(diff.element); - newNode.outerDone = true; - newNode.innerDone = true; - newNode.valueDone = true; - - if (!node.childNodes) { - node.childNodes = []; - } - - if (c >= node.childNodes.length) { - node.childNodes.push(newNode); - } else { - node.childNodes.splice(c, 0, newNode); - } - break; - case 'removeTextElement': - parentNode.childNodes.splice(nodeIndex, 1); - if (parentNode.nodeName === 'TEXTAREA') { - delete parentNode.value; - } - break; - case 'addTextElement': - route = diff.route.slice(); - c = route.splice(route.length - 1, 1)[0]; - newNode = {}; - newNode.nodeName = '#text'; - newNode.data = diff.value; - node = this.getFromVirtualRoute(tree, route).node; - if (!node.childNodes) { - node.childNodes = []; - } - - if (c >= node.childNodes.length) { - node.childNodes.push(newNode); - } else { - node.childNodes.splice(c, 0, newNode); - } - if (node.nodeName === 'TEXTAREA') { - node.value = diff.newValue; - } - break; - default: - console.log('unknown action'); - } - - // capture newNode for the callback - info.newNode = newNode; - this.postVirtualDiffApply(info); - - return; - }, - - - - - // ===== Apply a diff ===== - - apply: function(tree, diffs) { - var dobj = this; - - if (diffs.length === 0) { - return true; - } - diffs.forEach(function(diff) { - if (!dobj.applyDiff(tree, diff)) { - return false; - } - }); - return true; - }, - getFromRoute: function(tree, route) { - route = route.slice(); - var c, node = tree; - while (route.length > 0) { - if (!node.childNodes) { - return false; - } - c = route.splice(0, 1)[0]; - node = node.childNodes[c]; - } - return node; - }, - applyDiff: function(tree, diff) { - var node = this.getFromRoute(tree, diff.route), - newNode, reference, route, c; - - // pre-diff hook - var info = { - diff: diff, - node: node - }; - - if (this.preDiffApply(info)) { return true; } - - switch (diff.action) { - case 'addAttribute': - if (!node || !node.setAttribute) { - return false; - } - node.setAttribute(diff.name, diff.value); - break; - case 'modifyAttribute': - if (!node || !node.setAttribute) { - return false; - } - node.setAttribute(diff.name, diff.newValue); - break; - case 'removeAttribute': - if (!node || !node.removeAttribute) { - return false; - } - node.removeAttribute(diff.name); - break; - case 'modifyTextElement': - if (!node || node.nodeType !== 3) { - return false; - } - this.textDiff(node, node.data, diff.oldValue, diff.newValue); - break; - case 'modifyValue': - if (!node || typeof node.value === 'undefined') { - return false; - } - node.value = diff.newValue; - break; - case 'modifyComment': - if (!node || typeof node.data === 'undefined') { - return false; - } - node.data = diff.newValue; - break; - case 'modifyChecked': - if (!node || typeof node.checked === 'undefined') { - return false; - } - node.checked = diff.newValue; - break; - case 'modifySelected': - if (!node || typeof node.selected === 'undefined') { - return false; - } - node.selected = diff.newValue; - break; - case 'replaceElement': - node.parentNode.replaceChild(this.objToNode(diff.newValue, node.namespaceURI === 'http://www.w3.org/2000/svg'), node); - break; - case 'relocateGroup': - Array.apply(null, new Array(diff.groupLength)).map(function() { - return node.removeChild(node.childNodes[diff.from]); - }).forEach(function(childNode, index) { - if (index === 0) { - reference = node.childNodes[diff.to]; - } - node.insertBefore(childNode, reference); - }); - break; - case 'removeElement': - node.parentNode.removeChild(node); - break; - case 'addElement': - route = diff.route.slice(); - c = route.splice(route.length - 1, 1)[0]; - node = this.getFromRoute(tree, route); - node.insertBefore(this.objToNode(diff.element, node.namespaceURI === 'http://www.w3.org/2000/svg'), node.childNodes[c]); - break; - case 'removeTextElement': - if (!node || node.nodeType !== 3) { - return false; - } - node.parentNode.removeChild(node); - break; - case 'addTextElement': - route = diff.route.slice(); - c = route.splice(route.length - 1, 1)[0]; - newNode = document.createTextNode(diff.value); - node = this.getFromRoute(tree, route); - if (!node || !node.childNodes) { - return false; - } - node.insertBefore(newNode, node.childNodes[c]); - break; - default: - console.log('unknown action'); - } - - // if a new node was created, we might be interested in it - // post diff hook - info.newNode = newNode; - this.postDiffApply(info); - - return true; - }, - - // ===== Undo a diff ===== - - undo: function(tree, diffs) { - diffs = diffs.slice(); - var dobj = this; - if (!diffs.length) { - diffs = [diffs]; - } - diffs.reverse(); - diffs.forEach(function(diff) { - dobj.undoDiff(tree, diff); - }); - }, - undoDiff: function(tree, diff) { - - switch (diff.action) { - case 'addAttribute': - diff.action = 'removeAttribute'; - this.applyDiff(tree, diff); - break; - case 'modifyAttribute': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'removeAttribute': - diff.action = 'addAttribute'; - this.applyDiff(tree, diff); - break; - case 'modifyTextElement': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'modifyValue': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'modifyComment': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'modifyChecked': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'modifySelected': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'replaceElement': - swap(diff, 'oldValue', 'newValue'); - this.applyDiff(tree, diff); - break; - case 'relocateGroup': - swap(diff, 'from', 'to'); - this.applyDiff(tree, diff); - break; - case 'removeElement': - diff.action = 'addElement'; - this.applyDiff(tree, diff); - break; - case 'addElement': - diff.action = 'removeElement'; - this.applyDiff(tree, diff); - break; - case 'removeTextElement': - diff.action = 'addTextElement'; - this.applyDiff(tree, diff); - break; - case 'addTextElement': - diff.action = 'removeTextElement'; - this.applyDiff(tree, diff); - break; - default: - console.log('unknown action'); - } - - } - }; - - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = diffDOM; - } - exports.diffDOM = diffDOM; - } else { - // `window` in the browser, or `exports` on the server - this.diffDOM = diffDOM; - } - -}.call(this));