Created
January 7, 2016 19:37
-
-
Save ryanashcraft/6c8913014d3a134825bf to your computer and use it in GitHub Desktop.
DeepDiff.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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([], 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(); | |
} | |
}(window, 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 (subject instanceof Date) { | |
return 'date'; | |
} else if (/^\/.*\//.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 && prefilter(currentPath, key, { lhs: lhs, rhs: rhs })) { | |
return; | |
} | |
currentPath.push(key); | |
} | |
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 (lhs instanceof Date && rhs instanceof 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; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment