423 lines
11 KiB
JavaScript
423 lines
11 KiB
JavaScript
|
/*!
|
||
|
* deep-diff.
|
||
|
* Licensed under the MIT License.
|
||
|
*/
|
||
|
;(function(root, factory) {
|
||
|
'use strict';
|
||
|
if (typeof define === 'function' && define.amd) {
|
||
|
// AMD. Register as an anonymous module.
|
||
|
define([], function() {
|
||
|
return factory();
|
||
|
});
|
||
|
} else if (typeof exports === 'object') {
|
||
|
// Node. Does not work with strict CommonJS, but
|
||
|
// only CommonJS-like environments that support module.exports,
|
||
|
// like Node.
|
||
|
module.exports = factory();
|
||
|
} else {
|
||
|
// Browser globals (root is window)
|
||
|
root.DeepDiff = factory();
|
||
|
}
|
||
|
}(this, function(undefined) {
|
||
|
'use strict';
|
||
|
|
||
|
var $scope, conflict, conflictResolution = [];
|
||
|
if (typeof global === 'object' && global) {
|
||
|
$scope = global;
|
||
|
} else if (typeof window !== 'undefined') {
|
||
|
$scope = window;
|
||
|
} else {
|
||
|
$scope = {};
|
||
|
}
|
||
|
conflict = $scope.DeepDiff;
|
||
|
if (conflict) {
|
||
|
conflictResolution.push(
|
||
|
function() {
|
||
|
if ('undefined' !== typeof conflict && $scope.DeepDiff === accumulateDiff) {
|
||
|
$scope.DeepDiff = conflict;
|
||
|
conflict = undefined;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// nodejs compatible on server side and in the browser.
|
||
|
function inherits(ctor, superCtor) {
|
||
|
ctor.super_ = superCtor;
|
||
|
ctor.prototype = Object.create(superCtor.prototype, {
|
||
|
constructor: {
|
||
|
value: ctor,
|
||
|
enumerable: false,
|
||
|
writable: true,
|
||
|
configurable: true
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function Diff(kind, path) {
|
||
|
Object.defineProperty(this, 'kind', {
|
||
|
value: kind,
|
||
|
enumerable: true
|
||
|
});
|
||
|
if (path && path.length) {
|
||
|
Object.defineProperty(this, 'path', {
|
||
|
value: path,
|
||
|
enumerable: true
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function DiffEdit(path, origin, value) {
|
||
|
DiffEdit.super_.call(this, 'E', path);
|
||
|
Object.defineProperty(this, 'lhs', {
|
||
|
value: origin,
|
||
|
enumerable: true
|
||
|
});
|
||
|
Object.defineProperty(this, 'rhs', {
|
||
|
value: value,
|
||
|
enumerable: true
|
||
|
});
|
||
|
}
|
||
|
inherits(DiffEdit, Diff);
|
||
|
|
||
|
function DiffNew(path, value) {
|
||
|
DiffNew.super_.call(this, 'N', path);
|
||
|
Object.defineProperty(this, 'rhs', {
|
||
|
value: value,
|
||
|
enumerable: true
|
||
|
});
|
||
|
}
|
||
|
inherits(DiffNew, Diff);
|
||
|
|
||
|
function DiffDeleted(path, value) {
|
||
|
DiffDeleted.super_.call(this, 'D', path);
|
||
|
Object.defineProperty(this, 'lhs', {
|
||
|
value: value,
|
||
|
enumerable: true
|
||
|
});
|
||
|
}
|
||
|
inherits(DiffDeleted, Diff);
|
||
|
|
||
|
function DiffArray(path, index, item) {
|
||
|
DiffArray.super_.call(this, 'A', path);
|
||
|
Object.defineProperty(this, 'index', {
|
||
|
value: index,
|
||
|
enumerable: true
|
||
|
});
|
||
|
Object.defineProperty(this, 'item', {
|
||
|
value: item,
|
||
|
enumerable: true
|
||
|
});
|
||
|
}
|
||
|
inherits(DiffArray, Diff);
|
||
|
|
||
|
function arrayRemove(arr, from, to) {
|
||
|
var rest = arr.slice((to || from) + 1 || arr.length);
|
||
|
arr.length = from < 0 ? arr.length + from : from;
|
||
|
arr.push.apply(arr, rest);
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
function realTypeOf(subject) {
|
||
|
var type = typeof subject;
|
||
|
if (type !== 'object') {
|
||
|
return type;
|
||
|
}
|
||
|
|
||
|
if (subject === Math) {
|
||
|
return 'math';
|
||
|
} else if (subject === null) {
|
||
|
return 'null';
|
||
|
} else if (Array.isArray(subject)) {
|
||
|
return 'array';
|
||
|
} else if (Object.prototype.toString.call(subject) === '[object Date]') {
|
||
|
return 'date';
|
||
|
} else if (typeof subject.toString !== 'undefined' && /^\/.*\//.test(subject.toString())) {
|
||
|
return 'regexp';
|
||
|
}
|
||
|
return 'object';
|
||
|
}
|
||
|
|
||
|
function deepDiff(lhs, rhs, changes, prefilter, path, key, stack) {
|
||
|
path = path || [];
|
||
|
var currentPath = path.slice(0);
|
||
|
if (typeof key !== 'undefined') {
|
||
|
if (prefilter) {
|
||
|
if (typeof(prefilter) === 'function' && prefilter(currentPath, key)) { return; }
|
||
|
else if (typeof(prefilter) === 'object') {
|
||
|
if (prefilter.prefilter && prefilter.prefilter(currentPath, key)) { return; }
|
||
|
if (prefilter.normalize) {
|
||
|
var alt = prefilter.normalize(currentPath, key, lhs, rhs);
|
||
|
if (alt) {
|
||
|
lhs = alt[0];
|
||
|
rhs = alt[1];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
currentPath.push(key);
|
||
|
}
|
||
|
|
||
|
// Use string comparison for regexes
|
||
|
if (realTypeOf(lhs) === 'regexp' && realTypeOf(rhs) === 'regexp') {
|
||
|
lhs = lhs.toString();
|
||
|
rhs = rhs.toString();
|
||
|
}
|
||
|
|
||
|
var ltype = typeof lhs;
|
||
|
var rtype = typeof rhs;
|
||
|
if (ltype === 'undefined') {
|
||
|
if (rtype !== 'undefined') {
|
||
|
changes(new DiffNew(currentPath, rhs));
|
||
|
}
|
||
|
} else if (rtype === 'undefined') {
|
||
|
changes(new DiffDeleted(currentPath, lhs));
|
||
|
} else if (realTypeOf(lhs) !== realTypeOf(rhs)) {
|
||
|
changes(new DiffEdit(currentPath, lhs, rhs));
|
||
|
} else if (Object.prototype.toString.call(lhs) === '[object Date]' && Object.prototype.toString.call(rhs) === '[object Date]' && ((lhs - rhs) !== 0)) {
|
||
|
changes(new DiffEdit(currentPath, lhs, rhs));
|
||
|
} else if (ltype === 'object' && lhs !== null && rhs !== null) {
|
||
|
stack = stack || [];
|
||
|
if (stack.indexOf(lhs) < 0) {
|
||
|
stack.push(lhs);
|
||
|
if (Array.isArray(lhs)) {
|
||
|
var i, len = lhs.length;
|
||
|
for (i = 0; i < lhs.length; i++) {
|
||
|
if (i >= rhs.length) {
|
||
|
changes(new DiffArray(currentPath, i, new DiffDeleted(undefined, lhs[i])));
|
||
|
} else {
|
||
|
deepDiff(lhs[i], rhs[i], changes, prefilter, currentPath, i, stack);
|
||
|
}
|
||
|
}
|
||
|
while (i < rhs.length) {
|
||
|
changes(new DiffArray(currentPath, i, new DiffNew(undefined, rhs[i++])));
|
||
|
}
|
||
|
} else {
|
||
|
var akeys = Object.keys(lhs);
|
||
|
var pkeys = Object.keys(rhs);
|
||
|
akeys.forEach(function(k, i) {
|
||
|
var other = pkeys.indexOf(k);
|
||
|
if (other >= 0) {
|
||
|
deepDiff(lhs[k], rhs[k], changes, prefilter, currentPath, k, stack);
|
||
|
pkeys = arrayRemove(pkeys, other);
|
||
|
} else {
|
||
|
deepDiff(lhs[k], undefined, changes, prefilter, currentPath, k, stack);
|
||
|
}
|
||
|
});
|
||
|
pkeys.forEach(function(k) {
|
||
|
deepDiff(undefined, rhs[k], changes, prefilter, currentPath, k, stack);
|
||
|
});
|
||
|
}
|
||
|
stack.length = stack.length - 1;
|
||
|
}
|
||
|
} else if (lhs !== rhs) {
|
||
|
if (!(ltype === 'number' && isNaN(lhs) && isNaN(rhs))) {
|
||
|
changes(new DiffEdit(currentPath, lhs, rhs));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function accumulateDiff(lhs, rhs, prefilter, accum) {
|
||
|
accum = accum || [];
|
||
|
deepDiff(lhs, rhs,
|
||
|
function(diff) {
|
||
|
if (diff) {
|
||
|
accum.push(diff);
|
||
|
}
|
||
|
},
|
||
|
prefilter);
|
||
|
return (accum.length) ? accum : undefined;
|
||
|
}
|
||
|
|
||
|
function applyArrayChange(arr, index, change) {
|
||
|
if (change.path && change.path.length) {
|
||
|
var it = arr[index],
|
||
|
i, u = change.path.length - 1;
|
||
|
for (i = 0; i < u; i++) {
|
||
|
it = it[change.path[i]];
|
||
|
}
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
applyArrayChange(it[change.path[i]], change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
delete it[change.path[i]];
|
||
|
break;
|
||
|
case 'E':
|
||
|
case 'N':
|
||
|
it[change.path[i]] = change.rhs;
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
applyArrayChange(arr[index], change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
arr = arrayRemove(arr, index);
|
||
|
break;
|
||
|
case 'E':
|
||
|
case 'N':
|
||
|
arr[index] = change.rhs;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
function applyChange(target, source, change) {
|
||
|
if (target && source && change && change.kind) {
|
||
|
var it = target,
|
||
|
i = -1,
|
||
|
last = change.path ? change.path.length - 1 : 0;
|
||
|
while (++i < last) {
|
||
|
if (typeof it[change.path[i]] === 'undefined') {
|
||
|
it[change.path[i]] = (typeof change.path[i] === 'number') ? [] : {};
|
||
|
}
|
||
|
it = it[change.path[i]];
|
||
|
}
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
applyArrayChange(change.path ? it[change.path[i]] : it, change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
delete it[change.path[i]];
|
||
|
break;
|
||
|
case 'E':
|
||
|
case 'N':
|
||
|
it[change.path[i]] = change.rhs;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function revertArrayChange(arr, index, change) {
|
||
|
if (change.path && change.path.length) {
|
||
|
// the structure of the object at the index has changed...
|
||
|
var it = arr[index],
|
||
|
i, u = change.path.length - 1;
|
||
|
for (i = 0; i < u; i++) {
|
||
|
it = it[change.path[i]];
|
||
|
}
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
revertArrayChange(it[change.path[i]], change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
it[change.path[i]] = change.lhs;
|
||
|
break;
|
||
|
case 'E':
|
||
|
it[change.path[i]] = change.lhs;
|
||
|
break;
|
||
|
case 'N':
|
||
|
delete it[change.path[i]];
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
// the array item is different...
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
revertArrayChange(arr[index], change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
arr[index] = change.lhs;
|
||
|
break;
|
||
|
case 'E':
|
||
|
arr[index] = change.lhs;
|
||
|
break;
|
||
|
case 'N':
|
||
|
arr = arrayRemove(arr, index);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
function revertChange(target, source, change) {
|
||
|
if (target && source && change && change.kind) {
|
||
|
var it = target,
|
||
|
i, u;
|
||
|
u = change.path.length - 1;
|
||
|
for (i = 0; i < u; i++) {
|
||
|
if (typeof it[change.path[i]] === 'undefined') {
|
||
|
it[change.path[i]] = {};
|
||
|
}
|
||
|
it = it[change.path[i]];
|
||
|
}
|
||
|
switch (change.kind) {
|
||
|
case 'A':
|
||
|
// Array was modified...
|
||
|
// it will be an array...
|
||
|
revertArrayChange(it[change.path[i]], change.index, change.item);
|
||
|
break;
|
||
|
case 'D':
|
||
|
// Item was deleted...
|
||
|
it[change.path[i]] = change.lhs;
|
||
|
break;
|
||
|
case 'E':
|
||
|
// Item was edited...
|
||
|
it[change.path[i]] = change.lhs;
|
||
|
break;
|
||
|
case 'N':
|
||
|
// Item is new...
|
||
|
delete it[change.path[i]];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function applyDiff(target, source, filter) {
|
||
|
if (target && source) {
|
||
|
var onChange = function(change) {
|
||
|
if (!filter || filter(target, source, change)) {
|
||
|
applyChange(target, source, change);
|
||
|
}
|
||
|
};
|
||
|
deepDiff(target, source, onChange);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Object.defineProperties(accumulateDiff, {
|
||
|
|
||
|
diff: {
|
||
|
value: accumulateDiff,
|
||
|
enumerable: true
|
||
|
},
|
||
|
observableDiff: {
|
||
|
value: deepDiff,
|
||
|
enumerable: true
|
||
|
},
|
||
|
applyDiff: {
|
||
|
value: applyDiff,
|
||
|
enumerable: true
|
||
|
},
|
||
|
applyChange: {
|
||
|
value: applyChange,
|
||
|
enumerable: true
|
||
|
},
|
||
|
revertChange: {
|
||
|
value: revertChange,
|
||
|
enumerable: true
|
||
|
},
|
||
|
isConflict: {
|
||
|
value: function() {
|
||
|
return 'undefined' !== typeof conflict;
|
||
|
},
|
||
|
enumerable: true
|
||
|
},
|
||
|
noConflict: {
|
||
|
value: function() {
|
||
|
if (conflictResolution) {
|
||
|
conflictResolution.forEach(function(it) {
|
||
|
it();
|
||
|
});
|
||
|
conflictResolution = null;
|
||
|
}
|
||
|
return accumulateDiff;
|
||
|
},
|
||
|
enumerable: true
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return accumulateDiff;
|
||
|
}));
|