1585 lines
63 KiB
JavaScript
1585 lines
63 KiB
JavaScript
(function(root, factory) {
|
|
if (typeof exports !== 'undefined') {
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
exports = module.exports = factory();
|
|
} else {
|
|
exports.diffDOM = factory();
|
|
}
|
|
} else if (typeof define === 'function') {
|
|
// AMD loader
|
|
define(factory);
|
|
} else {
|
|
// `window` in the browser, or `exports` on the server
|
|
root.diffDOM = factory();
|
|
}
|
|
})(this, function() {
|
|
"use strict";
|
|
|
|
var diffcount, foundAll = false;
|
|
|
|
var Diff = function(options) {
|
|
var diff = this;
|
|
if (options) {
|
|
var keys = Object.keys(options),
|
|
length = keys.length,
|
|
i;
|
|
for (i = 0; i < length; i++) {
|
|
diff[keys[i]] = options[keys[i]];
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
Diff.prototype = {
|
|
toString: function() {
|
|
return JSON.stringify(this);
|
|
},
|
|
setValue: function(aKey, aValue) {
|
|
this[aKey] = aValue;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
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 = {},
|
|
liLength = li.length,
|
|
nodeLength, node, descriptors, descriptor, inUnique, inDupes, i, j;
|
|
|
|
for (i = 0; i < liLength; i++) {
|
|
node = li[i];
|
|
nodeLength = node.length;
|
|
descriptors = elementDescriptors(node);
|
|
for (j = 0; j < nodeLength; j++) {
|
|
descriptor = descriptors[j];
|
|
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 = {},
|
|
keys = Object.keys(l1Unique),
|
|
length = keys.length,
|
|
key,
|
|
i;
|
|
|
|
for (i = 0; i < length; i++) {
|
|
key = keys[i];
|
|
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 true;
|
|
})) {
|
|
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) {
|
|
if (e1.attributes.id !== e2.attributes.id) {
|
|
return false;
|
|
} else {
|
|
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));
|
|
};
|
|
|
|
/**
|
|
* 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 = [],
|
|
c1Length = c1.length,
|
|
c2Length = c2.length,
|
|
matches = Array.apply(null, new Array(c1Length + 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 = c1Length === c2Length,
|
|
origin, ret, c1Index, c2Index, c1Element, c2Element;
|
|
|
|
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
|
|
for (c1Index = 0; c1Index < c1Length; c1Index++) {
|
|
c1Element = c1[c1Index];
|
|
for (c2Index = 0; c2Index < c2Length; c2Index++) {
|
|
c2Element = c2[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;
|
|
}
|
|
|
|
return {
|
|
oldValue: index[0] - lcsSize,
|
|
newValue: index[1] - lcsSize,
|
|
length: lcsSize
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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 = <img><canvas><br>, t2 = <canvas><br><img>.
|
|
* The longest subset is "<canvas><br>" (length 2), so it will group 0.
|
|
* The second longest is "<img>" (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 = <img><p></p><br><canvas>, t2 = <b></b><br><canvas><img>
|
|
*
|
|
* The "<p></p>" and "<b></b>" 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,
|
|
length = stable.length,
|
|
i, j, endOld, endNew, subset;
|
|
|
|
// give elements from the same subset the same group number
|
|
for (i = 0; i < length; i++) {
|
|
subset = stable[i];
|
|
endOld = subset.oldValue + subset.length;
|
|
endNew = subset.newValue + subset.length;
|
|
for (j = subset.oldValue; j < endOld; j += 1) {
|
|
gaps1[j] = group;
|
|
}
|
|
for (j = subset.newValue; j < endNew; j += 1) {
|
|
gaps2[j] = 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.oldValue + i] = true;
|
|
marked2[subset.newValue + i] = true;
|
|
},
|
|
length, subsetArray, i;
|
|
|
|
while (subset) {
|
|
subset = findCommonSubsets(oldChildren, newChildren, marked1, marked2);
|
|
if (subset) {
|
|
subsets.push(subset);
|
|
subsetArray = Array.apply(null, new Array(subset.length)).map(returnIndex);
|
|
length = subsetArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
markBoth(subsetArray[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
oldTree.subsets = subsets;
|
|
oldTree.subsetsAge = 100;
|
|
return subsets;
|
|
};
|
|
|
|
|
|
function swap(obj, p1, p2) {
|
|
var tmp = obj[p1];
|
|
obj[p1] = obj[p2];
|
|
obj[p2] = tmp;
|
|
}
|
|
|
|
|
|
var DiffTracker = function() {
|
|
this.list = [];
|
|
};
|
|
|
|
DiffTracker.prototype = {
|
|
list: false,
|
|
add: function(diffs) {
|
|
this.list.push.apply(this.list, diffs);
|
|
},
|
|
forEach: function(fn) {
|
|
var length = this.list.length,
|
|
i;
|
|
for (i = 0; i < length; i++) {
|
|
fn(this.list[i]);
|
|
}
|
|
}
|
|
};
|
|
|
|
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.
|
|
maxChildCount: 50, // False or a numeral. If set to a numeral, does not try to diff the contents of nodes with more children if there are more than maxChildDiffCount differences among child nodes.
|
|
maxChildDiffCount: 3, // Numeral. See maxChildCount.
|
|
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() {},
|
|
filterOuterDiff: null,
|
|
compress: false // Whether to work with compressed diffs
|
|
},
|
|
varNames, i, j;
|
|
|
|
if (typeof options === "undefined") {
|
|
options = {};
|
|
}
|
|
|
|
for (i in defaults) {
|
|
if (typeof options[i] === "undefined") {
|
|
this[i] = defaults[i];
|
|
} else {
|
|
this[i] = options[i];
|
|
}
|
|
}
|
|
|
|
var varNames = {
|
|
'addAttribute': 'addAttribute',
|
|
'modifyAttribute': 'modifyAttribute',
|
|
'removeAttribute': 'removeAttribute',
|
|
'modifyTextElement': 'modifyTextElement',
|
|
'relocateGroup': 'relocateGroup',
|
|
'removeElement': 'removeElement',
|
|
'addElement': 'addElement',
|
|
'removeTextElement': 'removeTextElement',
|
|
'addTextElement': 'addTextElement',
|
|
'replaceElement': 'replaceElement',
|
|
'modifyValue': 'modifyValue',
|
|
'modifyChecked': 'modifyChecked',
|
|
'modifySelected': 'modifySelected',
|
|
'modifyComment': 'modifyComment',
|
|
'action': 'action',
|
|
'route': 'route',
|
|
'oldValue': 'oldValue',
|
|
'newValue': 'newValue',
|
|
'element': 'element',
|
|
'group': 'group',
|
|
'from': 'from',
|
|
'to': 'to',
|
|
'name': 'name',
|
|
'value': 'value',
|
|
'data': 'data',
|
|
'attributes': 'attributes',
|
|
'nodeName': 'nodeName',
|
|
'childNodes': 'childNodes',
|
|
'checked': 'checked',
|
|
'selected': 'selected'
|
|
};
|
|
|
|
if (this.compress) {
|
|
j = 0;
|
|
this._const = {};
|
|
for (i in varNames) {
|
|
this._const[i] = j;
|
|
j++;
|
|
}
|
|
} else {
|
|
this._const = varNames;
|
|
}
|
|
};
|
|
|
|
diffDOM.Diff = Diff;
|
|
|
|
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.
|
|
// Sometimes a node can be marked as done, but the creation of subsequent diffs means that it has to be changed again.
|
|
if (!isEqual(t1, t2)) {
|
|
if (foundAll) {
|
|
console.error('Could not find remaining diffs!');
|
|
// console.log({t1, t2});
|
|
} else {
|
|
foundAll = true;
|
|
removeDone(t1);
|
|
diffs = this.findNextDiff(t1, t2, []);
|
|
}
|
|
}
|
|
}
|
|
if (diffs.length > 0) {
|
|
foundAll = false
|
|
this.tracker.add(diffs);
|
|
this.applyVirtual(t1, diffs);
|
|
}
|
|
} while (diffs.length > 0);
|
|
return this.tracker.list;
|
|
},
|
|
findNextDiff: function(t1, t2, route) {
|
|
var diffs, fdiffs;
|
|
|
|
if (this.maxDepth && route.length > this.maxDepth) {
|
|
return [];
|
|
}
|
|
// outer differences?
|
|
if (!t1.outerDone) {
|
|
diffs = this.findOuterDiff(t1, t2, route);
|
|
if (this.filterOuterDiff) {
|
|
fdiffs = this.filterOuterDiff(t1, t2, diffs);
|
|
if (fdiffs) diffs = fdiffs;
|
|
}
|
|
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 t = this;
|
|
var diffs = [],
|
|
attr,
|
|
attr1, attr2, attrLength, pos, i;
|
|
|
|
if (t1.nodeName !== t2.nodeName) {
|
|
return [new Diff()
|
|
.setValue(t._const.action, t._const.replaceElement)
|
|
.setValue(t._const.oldValue, cloneObj(t1))
|
|
.setValue(t._const.newValue, cloneObj(t2))
|
|
.setValue(t._const.route, route)
|
|
];
|
|
}
|
|
|
|
if (route.length && this.maxChildCount && t1.childNodes && t2.childNodes && t1.childNodes.length > this.maxChildCount && t2.childNodes.length > this.maxChildCount) {
|
|
var childNodesLength = t1.childNodes.length < t2.childNodes.length ? t1.childNodes.length : t2.childNodes.length, childDiffCount = 0, j = 0;
|
|
while (childDiffCount < this.maxChildDiffCount && j < childNodesLength) {
|
|
if (!isEqual(t1.childNodes[j], t2.childNodes[j])) {
|
|
childDiffCount++;
|
|
}
|
|
j++;
|
|
}
|
|
if (childDiffCount === this.maxChildDiffCount) {
|
|
return [new Diff()
|
|
.setValue(t._const.action, t._const.replaceElement)
|
|
.setValue(t._const.oldValue, cloneObj(t1))
|
|
.setValue(t._const.newValue, cloneObj(t2))
|
|
.setValue(t._const.route, route)
|
|
];
|
|
}
|
|
}
|
|
|
|
if (t1.data !== t2.data) {
|
|
// Comment or text node.
|
|
if (t1.nodeName === '#text') {
|
|
return [new Diff()
|
|
.setValue(t._const.action, t._const.modifyTextElement)
|
|
.setValue(t._const.route, route)
|
|
.setValue(t._const.oldValue, t1.data)
|
|
.setValue(t._const.newValue, t2.data)
|
|
];
|
|
} else {
|
|
return [new Diff()
|
|
.setValue(t._const.action, t._const.modifyComment)
|
|
.setValue(t._const.route, route)
|
|
.setValue(t._const.oldValue, t1.data)
|
|
.setValue(t._const.newValue, t2.data)
|
|
];
|
|
}
|
|
|
|
}
|
|
|
|
|
|
attr1 = t1.attributes ? Object.keys(t1.attributes).sort() : [];
|
|
attr2 = t2.attributes ? Object.keys(t2.attributes).sort() : [];
|
|
|
|
attrLength = attr1.length;
|
|
for (i = 0; i < attrLength; i++) {
|
|
attr = attr1[i];
|
|
pos = attr2.indexOf(attr);
|
|
if (pos === -1) {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.removeAttribute)
|
|
.setValue(t._const.route, route)
|
|
.setValue(t._const.name, attr)
|
|
.setValue(t._const.value, t1.attributes[attr])
|
|
);
|
|
} else {
|
|
attr2.splice(pos, 1);
|
|
if (t1.attributes[attr] !== t2.attributes[attr]) {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.modifyAttribute)
|
|
.setValue(t._const.route, route)
|
|
.setValue(t._const.name, attr)
|
|
.setValue(t._const.oldValue, t1.attributes[attr])
|
|
.setValue(t._const.newValue, t2.attributes[attr])
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
attrLength = attr2.length;
|
|
for (i = 0; i < attrLength; i++) {
|
|
attr = attr2[i];
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.addAttribute)
|
|
.setValue(t._const.route, route)
|
|
.setValue(t._const.name, attr)
|
|
.setValue(t._const.value, t2.attributes[attr])
|
|
);
|
|
}
|
|
|
|
return diffs;
|
|
},
|
|
nodeToObj: function(aNode) {
|
|
var objNode = {},
|
|
dobj = this,
|
|
nodeArray, childNode, length, attribute, i;
|
|
objNode.nodeName = aNode.nodeName;
|
|
if (objNode.nodeName === '#text' || objNode.nodeName === '#comment') {
|
|
objNode.data = aNode.data;
|
|
} else {
|
|
if (aNode.attributes && aNode.attributes.length > 0) {
|
|
objNode.attributes = {};
|
|
nodeArray = Array.prototype.slice.call(aNode.attributes);
|
|
length = nodeArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
attribute = nodeArray[i];
|
|
objNode.attributes[attribute.name] = attribute.value;
|
|
}
|
|
}
|
|
if (objNode.nodeName === 'TEXTAREA') {
|
|
objNode.value = aNode.value;
|
|
} else if (aNode.childNodes && aNode.childNodes.length > 0) {
|
|
objNode.childNodes = [];
|
|
nodeArray = Array.prototype.slice.call(aNode.childNodes);
|
|
length = nodeArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
childNode = nodeArray[i];
|
|
objNode.childNodes.push(dobj.nodeToObj(childNode));
|
|
}
|
|
}
|
|
if (this.valueDiffing) {
|
|
if (aNode.checked !== undefined && aNode.type &&
|
|
['radio','checkbox'].indexOf(aNode.type.toLowerCase()) !== -1
|
|
) {
|
|
objNode.checked = aNode.checked;
|
|
} else if (aNode.value !== undefined) {
|
|
objNode.value = aNode.value;
|
|
}
|
|
if (aNode.selected !== undefined) {
|
|
objNode.selected = aNode.selected;
|
|
}
|
|
}
|
|
}
|
|
return objNode;
|
|
},
|
|
objToNode: function(objNode, insideSvg) {
|
|
var node, dobj = this,
|
|
attribute, attributeArray, childNode, childNodeArray, length, i;
|
|
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) {
|
|
attributeArray = Object.keys(objNode.attributes);
|
|
length = attributeArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
attribute = attributeArray[i];
|
|
node.setAttribute(attribute, objNode.attributes[attribute]);
|
|
}
|
|
}
|
|
if (objNode.childNodes) {
|
|
childNodeArray = objNode.childNodes;
|
|
length = childNodeArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
childNode = childNodeArray[i];
|
|
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 oldSubsets = t1.subsets;
|
|
//var subtrees = (t1.childNodes && t2.childNodes) ? markSubTrees(t1, t2) : [],
|
|
var subtrees = t1.subsets && t1.subsetsAge-- ? t1.subsets : (t1.childNodes && t2.childNodes) ? markSubTrees(t1, t2) : [],
|
|
t1ChildNodes = t1.childNodes ? t1.childNodes : [],
|
|
t2ChildNodes = t2.childNodes ? t2.childNodes : [],
|
|
childNodesLengthDifference, diffs = [],
|
|
index = 0, t = this,
|
|
last, e1, e2, i;
|
|
|
|
if (subtrees.length > 0) {
|
|
/* One or more groups have been identified among the childnodes of t1
|
|
* and t2.
|
|
*/
|
|
diffs = this.attemptGroupRelocation(t1, t2, subtrees, route);
|
|
if (diffs.length > 0) {
|
|
return diffs;
|
|
}
|
|
}
|
|
|
|
/* 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()
|
|
.setValue(t._const.action, t._const.removeTextElement)
|
|
.setValue(t._const.route, route.concat(index))
|
|
.setValue(t._const.value, e1.data)
|
|
);
|
|
index -= 1;
|
|
} else {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.removeElement)
|
|
.setValue(t._const.route, route.concat(index))
|
|
.setValue(t._const.element, cloneObj(e1))
|
|
);
|
|
index -= 1;
|
|
}
|
|
|
|
} else if (e2 && !e1) {
|
|
if (e2.nodeName === '#text') {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.addTextElement)
|
|
.setValue(t._const.route, route.concat(index))
|
|
.setValue(t._const.value, e2.data)
|
|
);
|
|
} else {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.addElement)
|
|
.setValue(t._const.route, route.concat(index))
|
|
.setValue(t._const.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 t = this;
|
|
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()
|
|
.setValue(t._const.action, t._const.modifyTextElement)
|
|
.setValue(t._const.route, route.concat(index2))
|
|
.setValue(t._const.oldValue, node.data)
|
|
.setValue(t._const.newValue, t2.childNodes[index2].data)
|
|
);
|
|
return diffs;
|
|
}
|
|
}
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.removeTextElement)
|
|
.setValue(t._const.route, route.concat(index2))
|
|
.setValue(t._const.value, node.data)
|
|
);
|
|
gaps1.splice(index2, 1);
|
|
shortest = Math.min(gaps1.length, gaps2.length);
|
|
index2 -= 1;
|
|
} else {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.removeElement)
|
|
.setValue(t._const.route, route.concat(index2))
|
|
.setValue(t._const.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()
|
|
.setValue(t._const.action, t._const.addTextElement)
|
|
.setValue(t._const.route, route.concat(index2))
|
|
.setValue(t._const.value, node.data)
|
|
);
|
|
gaps1.splice(index2, 0, true);
|
|
shortest = Math.min(gaps1.length, gaps2.length);
|
|
index1 -= 1;
|
|
} else {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.addElement)
|
|
.setValue(t._const.route, route.concat(index2))
|
|
.setValue(t._const.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.newValue, (t1.childNodes.length - group.length));
|
|
if (toGroup !== group.oldValue) {
|
|
// 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.oldValue + j], [], false, true)) {
|
|
destinationDifferent = true;
|
|
}
|
|
}
|
|
if (destinationDifferent) {
|
|
return [new Diff()
|
|
.setValue(t._const.action, t._const.relocateGroup)
|
|
.setValue('groupLength', group.length)
|
|
.setValue(t._const.from, group.oldValue)
|
|
.setValue(t._const.to, toGroup)
|
|
.setValue(t._const.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 = [];
|
|
var t = this;
|
|
|
|
if (t1.selected !== t2.selected) {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.modifySelected)
|
|
.setValue(t._const.oldValue, t1.selected)
|
|
.setValue(t._const.newValue, t2.selected)
|
|
.setValue(t._const.route, route)
|
|
);
|
|
}
|
|
|
|
if ((t1.value || t2.value) && t1.value !== t2.value && t1.nodeName !== 'OPTION') {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.modifyValue)
|
|
.setValue(t._const.oldValue, t1.value)
|
|
.setValue(t._const.newValue, t2.value)
|
|
.setValue(t._const.route, route)
|
|
);
|
|
}
|
|
if (t1.checked !== t2.checked) {
|
|
diffs.push(new Diff()
|
|
.setValue(t._const.action, t._const.modifyChecked)
|
|
.setValue(t._const.oldValue, t1.checked)
|
|
.setValue(t._const.newValue, t2.checked)
|
|
.setValue(t._const.route, route)
|
|
);
|
|
}
|
|
|
|
return diffs;
|
|
},
|
|
|
|
// ===== Apply a virtual diff =====
|
|
|
|
applyVirtual: function(tree, diffs) {
|
|
var dobj = this,
|
|
length = diffs.length,
|
|
diff, i;
|
|
if (length === 0) {
|
|
return true;
|
|
}
|
|
for (i = 0; i < length; i++) {
|
|
diff = diffs[i];
|
|
dobj.applyVirtualDiff(tree, diff);
|
|
}
|
|
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[this._const.route]),
|
|
node = routeInfo.node,
|
|
parentNode = routeInfo.parentNode,
|
|
nodeIndex = routeInfo.nodeIndex,
|
|
newSubsets = [], splitLength,
|
|
newNode, movedNode, nodeArray, route, length, c, i;
|
|
|
|
var t = this;
|
|
// pre-diff hook
|
|
var info = {
|
|
diff: diff,
|
|
node: node
|
|
};
|
|
|
|
if (this.preVirtualDiffApply(info)) {
|
|
return true;
|
|
}
|
|
|
|
switch (diff[this._const.action]) {
|
|
case this._const.addAttribute:
|
|
if (!node.attributes) {
|
|
node.attributes = {};
|
|
}
|
|
node.attributes[diff[this._const.name]] = diff[this._const.value];
|
|
|
|
if (diff[this._const.name] === 'checked') {
|
|
node.checked = true;
|
|
} else if (diff[this._const.name] === 'selected') {
|
|
node.selected = true;
|
|
} else if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
|
node.value = diff[this._const.value];
|
|
}
|
|
|
|
break;
|
|
case this._const.modifyAttribute:
|
|
node.attributes[diff[this._const.name]] = diff[this._const.newValue];
|
|
if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
|
node.value = diff[this._const.value];
|
|
}
|
|
break;
|
|
case this._const.removeAttribute:
|
|
|
|
delete node.attributes[diff[this._const.name]];
|
|
|
|
if (Object.keys(node.attributes).length === 0) {
|
|
delete node.attributes;
|
|
}
|
|
|
|
if (diff[this._const.name] === 'checked') {
|
|
node.checked = false;
|
|
} else if (diff[this._const.name] === 'selected') {
|
|
delete node.selected;
|
|
} else if (node.nodeName === 'INPUT' && diff[this._const.name] === 'value') {
|
|
delete node.value;
|
|
}
|
|
|
|
break;
|
|
case this._const.modifyTextElement:
|
|
node.data = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifyValue:
|
|
node.value = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifyComment:
|
|
node.data = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifyChecked:
|
|
node.checked = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifySelected:
|
|
node.selected = diff[this._const.newValue];
|
|
break;
|
|
case this._const.replaceElement:
|
|
newNode = cloneObj(diff[this._const.newValue]);
|
|
newNode.outerDone = true;
|
|
newNode.innerDone = true;
|
|
newNode.valueDone = true;
|
|
parentNode.childNodes[nodeIndex] = newNode;
|
|
break;
|
|
case this._const.relocateGroup:
|
|
nodeArray = node.childNodes.splice(diff[this._const.from], diff.groupLength).reverse();
|
|
length = nodeArray.length;
|
|
for (i = 0; i < length; i++) {
|
|
movedNode = nodeArray[i];
|
|
node.childNodes.splice(diff[t._const.to], 0, movedNode);
|
|
}
|
|
if (node.subsets) {
|
|
|
|
node.subsets.forEach(function(map) {
|
|
if (diff[t._const.from] < diff[t._const.to] && map.oldValue <= diff[t._const.to] && map.oldValue > diff[t._const.from]) {
|
|
map.oldValue -= diff.groupLength;
|
|
splitLength = map.oldValue + map.length - diff[t._const.to];
|
|
if (splitLength > 0) {
|
|
// new insertion splits map.
|
|
newSubsets.push({
|
|
oldValue: diff[t._const.to] + diff.groupLength,
|
|
newValue: map.newValue + map.length - splitLength,
|
|
length: splitLength
|
|
})
|
|
map.length -= splitLength;
|
|
}
|
|
} else if (diff[t._const.from] > diff[t._const.to] && map.oldValue > diff[t._const.to] && map.oldValue < diff[t._const.from]) {
|
|
map.oldValue += diff.groupLength;
|
|
splitLength = map.oldValue + map.length - diff[t._const.to];
|
|
if (splitLength > 0) {
|
|
// new insertion splits map.
|
|
newSubsets.push({
|
|
oldValue: diff[t._const.to] + diff.groupLength,
|
|
newValue: map.newValue + map.length - splitLength,
|
|
length: splitLength
|
|
})
|
|
map.length -= splitLength;
|
|
}
|
|
} else if (map.oldValue === diff[t._const.from]) {
|
|
map.oldValue = diff[t._const.to];
|
|
}
|
|
});
|
|
}
|
|
|
|
break;
|
|
case this._const.removeElement:
|
|
parentNode.childNodes.splice(nodeIndex, 1);
|
|
if (parentNode.subsets) {
|
|
parentNode.subsets.forEach(function(map) {
|
|
if (map.oldValue > nodeIndex) {
|
|
map.oldValue -= 1;
|
|
} else if (map.oldValue === nodeIndex) {
|
|
map.delete = true;
|
|
} else if (map.oldValue < nodeIndex && (map.oldValue + map.length) > nodeIndex) {
|
|
if (map.oldValue + map.length - 1 === nodeIndex) {
|
|
map.length--;
|
|
} else {
|
|
newSubsets.push({
|
|
newValue: map.newValue + nodeIndex - map.oldValue,
|
|
oldValue: nodeIndex,
|
|
length: map.length - nodeIndex + map.oldValue - 1
|
|
})
|
|
map.length = nodeIndex - map.oldValue
|
|
}
|
|
}
|
|
});
|
|
}
|
|
node = parentNode;
|
|
break;
|
|
case this._const.addElement:
|
|
route = diff[this._const.route].slice();
|
|
c = route.splice(route.length - 1, 1)[0];
|
|
node = this.getFromVirtualRoute(tree, route).node;
|
|
newNode = cloneObj(diff[this._const.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);
|
|
}
|
|
if (node.subsets) {
|
|
node.subsets.forEach(function(map) {
|
|
if (map.oldValue >= c) {
|
|
map.oldValue += 1;
|
|
} else if (map.oldValue < c && (map.oldValue + map.length) > c) {
|
|
splitLength = map.oldValue + map.length - c
|
|
newSubsets.push({
|
|
newValue: map.newValue + map.length - splitLength,
|
|
oldValue: c + 1,
|
|
length: splitLength
|
|
})
|
|
map.length -= splitLength
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
case this._const.removeTextElement:
|
|
parentNode.childNodes.splice(nodeIndex, 1);
|
|
if (parentNode.nodeName === 'TEXTAREA') {
|
|
delete parentNode.value;
|
|
}
|
|
if (parentNode.subsets) {
|
|
parentNode.subsets.forEach(function(map) {
|
|
if (map.oldValue > nodeIndex) {
|
|
map.oldValue -= 1;
|
|
} else if (map.oldValue === nodeIndex) {
|
|
map.delete = true;
|
|
} else if (map.oldValue < nodeIndex && (map.oldValue + map.length) > nodeIndex) {
|
|
if (map.oldValue + map.length - 1 === nodeIndex) {
|
|
map.length--;
|
|
} else {
|
|
newSubsets.push({
|
|
newValue: map.newValue + nodeIndex - map.oldValue,
|
|
oldValue: nodeIndex,
|
|
length: map.length - nodeIndex + map.oldValue - 1
|
|
})
|
|
map.length = nodeIndex - map.oldValue
|
|
}
|
|
}
|
|
});
|
|
}
|
|
node = parentNode;
|
|
break;
|
|
case this._const.addTextElement:
|
|
route = diff[this._const.route].slice();
|
|
c = route.splice(route.length - 1, 1)[0];
|
|
newNode = {};
|
|
newNode.nodeName = '#text';
|
|
newNode.data = diff[this._const.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[this._const.newValue];
|
|
}
|
|
if (node.subsets) {
|
|
node.subsets.forEach(function(map) {
|
|
if (map.oldValue >= c) {
|
|
map.oldValue += 1;
|
|
} if (map.oldValue < c && (map.oldValue + map.length) > c) {
|
|
splitLength = map.oldValue + map.length - c
|
|
newSubsets.push({
|
|
newValue: map.newValue + map.length - splitLength,
|
|
oldValue: c + 1,
|
|
length: splitLength
|
|
})
|
|
map.length -= splitLength
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
console.log('unknown action');
|
|
}
|
|
|
|
if (node.subsets) {
|
|
node.subsets = node.subsets.filter(function(map) {
|
|
return !map.delete && map.oldValue !== map.newValue;
|
|
});
|
|
if (newSubsets.length) {
|
|
node.subsets = node.subsets.concat(newSubsets);
|
|
}
|
|
}
|
|
|
|
// capture newNode for the callback
|
|
info.newNode = newNode;
|
|
this.postVirtualDiffApply(info);
|
|
|
|
return;
|
|
},
|
|
|
|
|
|
|
|
|
|
// ===== Apply a diff =====
|
|
|
|
apply: function(tree, diffs) {
|
|
var dobj = this,
|
|
length = diffs.length,
|
|
diff, i;
|
|
|
|
if (length === 0) {
|
|
return true;
|
|
}
|
|
for (i = 0; i < length; i++) {
|
|
diff = diffs[i];
|
|
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[this._const.route]),
|
|
newNode, reference, route, nodeArray, length, childNode, index, c;
|
|
|
|
var t = this;
|
|
// pre-diff hook
|
|
var info = {
|
|
diff: diff,
|
|
node: node
|
|
};
|
|
|
|
if (this.preDiffApply(info)) {
|
|
return true;
|
|
}
|
|
switch (diff[this._const.action]) {
|
|
case this._const.addAttribute:
|
|
if (!node || !node.setAttribute) {
|
|
return false;
|
|
}
|
|
|
|
node.setAttribute(diff[this._const.name], diff[this._const.value]);
|
|
|
|
if ( 0 === diff[this._const.name].indexOf( 'data-' ) ) {
|
|
jQuery( '#fb-preview' )[0].contentWindow.jQuery( node ).data( diff[this._const.name].replace( 'data-', '' ), diff[this._const.newValue] );
|
|
}
|
|
break;
|
|
case this._const.modifyAttribute:
|
|
if (!node || !node.setAttribute) {
|
|
return false;
|
|
}
|
|
node.setAttribute(diff[this._const.name], diff[this._const.newValue]);
|
|
|
|
if ( 0 === diff[this._const.name].indexOf( 'data-' ) ) {
|
|
jQuery( '#fb-preview' )[0].contentWindow.jQuery( node ).data( diff[this._const.name].replace( 'data-', '' ), diff[this._const.newValue] );
|
|
}
|
|
|
|
break;
|
|
case this._const.removeAttribute:
|
|
if (!node || !node.removeAttribute) {
|
|
return false;
|
|
}
|
|
node.removeAttribute(diff[this._const.name]);
|
|
|
|
if ( 0 === diff[this._const.name].indexOf( 'data-' ) ) {
|
|
jQuery( '#fb-preview' )[0].contentWindow.jQuery( node ).removeData( diff[this._const.name].replace( 'data-', '' ) );
|
|
}
|
|
|
|
break;
|
|
case this._const.modifyTextElement:
|
|
if (!node || node.nodeType !== 3) {
|
|
return false;
|
|
}
|
|
this.textDiff(node, node.data, diff[this._const.oldValue], diff[this._const.newValue]);
|
|
break;
|
|
case this._const.modifyValue:
|
|
if (!node || typeof node.value === 'undefined') {
|
|
return false;
|
|
}
|
|
node.value = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifyComment:
|
|
if (!node || typeof node.data === 'undefined') {
|
|
return false;
|
|
}
|
|
this.textDiff(node, node.data, diff[this._const.oldValue], diff[this._const.newValue]);
|
|
break;
|
|
case this._const.modifyChecked:
|
|
if (!node || typeof node.checked === 'undefined') {
|
|
return false;
|
|
}
|
|
node.checked = diff[this._const.newValue];
|
|
break;
|
|
case this._const.modifySelected:
|
|
if (!node || typeof node.selected === 'undefined') {
|
|
return false;
|
|
}
|
|
node.selected = diff[this._const.newValue];
|
|
break;
|
|
case this._const.replaceElement:
|
|
node.parentNode.replaceChild(this.objToNode(diff[this._const.newValue], node.namespaceURI === 'http://www.w3.org/2000/svg'), node);
|
|
break;
|
|
case this._const.relocateGroup:
|
|
nodeArray = Array.apply(null, new Array(diff.groupLength)).map(function() {
|
|
return node.removeChild(node.childNodes[diff[t._const.from]]);
|
|
});
|
|
length = nodeArray.length;
|
|
for (index = 0; index < length; index++) {
|
|
childNode = nodeArray[index];
|
|
if (index === 0) {
|
|
reference = node.childNodes[diff[t._const.to]];
|
|
}
|
|
node.insertBefore(childNode, reference || null);
|
|
}
|
|
break;
|
|
case this._const.removeElement:
|
|
jQuery( '#fb-preview' )[0].contentWindow.jQuery( node ).removeData();
|
|
if ( 'undefined' !== typeof node && 'undefined' !== typeof node.parentNode ) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
break;
|
|
case this._const.addElement:
|
|
route = diff[this._const.route].slice();
|
|
c = route.splice(route.length - 1, 1)[0];
|
|
node = this.getFromRoute(tree, route);
|
|
node.insertBefore(this.objToNode(diff[this._const.element], node.namespaceURI === 'http://www.w3.org/2000/svg'), node.childNodes[c] || null);
|
|
break;
|
|
case this._const.removeTextElement:
|
|
if (!node || node.nodeType !== 3) {
|
|
return false;
|
|
}
|
|
node.parentNode.removeChild(node);
|
|
break;
|
|
case this._const.addTextElement:
|
|
route = diff[this._const.route].slice();
|
|
c = route.splice(route.length - 1, 1)[0];
|
|
newNode = document.createTextNode(diff[this._const.value]);
|
|
node = this.getFromRoute(tree, route);
|
|
if (!node || !node.childNodes) {
|
|
return false;
|
|
}
|
|
node.insertBefore(newNode, node.childNodes[c] || null);
|
|
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) {
|
|
var dobj = this, diff, length = diffs.length, i;
|
|
diffs = diffs.slice();
|
|
if (!length) {
|
|
diffs = [diffs];
|
|
}
|
|
diffs.reverse();
|
|
for (i = 0; i < length; i++) {
|
|
diff = diffs[i];
|
|
dobj.undoDiff(tree, diff);
|
|
}
|
|
},
|
|
undoDiff: function(tree, diff) {
|
|
|
|
switch (diff[this._const.action]) {
|
|
case this._const.addAttribute:
|
|
diff[this._const.action] = this._const.removeAttribute;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifyAttribute:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.removeAttribute:
|
|
diff[this._const.action] = this._const.addAttribute;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifyTextElement:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifyValue:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifyComment:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifyChecked:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.modifySelected:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.replaceElement:
|
|
swap(diff, this._const.oldValue, this._const.newValue);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.relocateGroup:
|
|
swap(diff, this._const.from, this._const.to);
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.removeElement:
|
|
diff[this._const.action] = this._const.addElement;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.addElement:
|
|
diff[this._const.action] = this._const.removeElement;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.removeTextElement:
|
|
diff[this._const.action] = this._const.addTextElement;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
case this._const.addTextElement:
|
|
diff[this._const.action] = this._const.removeTextElement;
|
|
this.applyDiff(tree, diff);
|
|
break;
|
|
default:
|
|
console.log('unknown action');
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
return diffDOM;
|
|
});
|