( 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:
/ * ' a d d A t t r i b u t e ' = 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 . oldValue = a ;
this . newValue = b ;
} ;
SubsetMapping . prototype = {
contains : function contains ( subset ) {
if ( subset . length < this . length ) {
return subset . newValue >= this . newValue && subset . newValue < this . newValue + this . length ;
}
return false ;
} ,
toString : function toString ( ) {
return this . length + " element subset, first mapping: old " + this . oldValue + " → new " + this . newValue ;
}
} ;
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 = < 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 > < b r > < c a n v a s > , t 2 = < b > < / b > < b r > < c a n v a s > < i m g >
*
* 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 ;
// give elements from the same subset the same group number
stable . forEach ( function ( subset ) {
var i , endOld = subset . oldValue + subset . length ,
endNew = subset . newValue + subset . length ;
for ( i = subset . oldValue ; i < endOld ; i += 1 ) {
gaps1 [ i ] = group ;
}
for ( i = subset . newValue ; 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 . oldValue + i ] = true ;
marked2 [ subset . newValue + 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 ( ) { } ,
filterOuterDiff : null
} ,
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 . 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.
// 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 , 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 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 : 'modifyTextElement' ,
route : route ,
oldValue : t1 . data ,
newValue : t2 . data
} ) ] ;
} else {
return [ new Diff ( {
action : 'modifyComment' ,
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 !== undefined ) {
objNode . value = node . value ;
}
if ( node . checked !== undefined ) {
objNode . checked = node . checked ;
}
if ( node . selected !== undefined ) {
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 ) {
try {
node . setAttribute ( attribute , objNode . attributes [ attribute ] ) ;
} catch ( e ) { console . debug ( e ) ; }
} ) ;
}
if ( objNode . childNodes ) {
objNode . childNodes . forEach ( function ( childNode ) {
try {
node . appendChild ( dobj . objToNode ( childNode , insideSvg ) ) ;
} catch ( e ) { console . debug ( e ) ; }
} ) ;
}
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 > 0 ) {
/ * O n e o r m o r e g r o u p s h a v e b e e n i d e n t i f i e d a m o n g t h e c h i l d n o d e s o f t 1
* and t2 .
* /
diffs = this . attemptGroupRelocation ( t1 , t2 , subtrees , route ) ;
if ( diffs . length > 0 ) {
return diffs ;
}
}
/ * 0 o r 1 g r o u p s o f s i m i l a r c h i l d n o d e s h a v e b e e n f o u n d
* 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 ) {
/ * t 1 a n d t 2 h a v e d i f f e r e n t a m o u n t s o f c h i l d N o d e s . A d d
* 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 ( i ) ,
value : e2 . data
} ) ) ;
} else {
diffs . push ( new Diff ( {
action : 'addElement' ,
route : route . concat ( i ) ,
element : cloneObj ( e2 )
} ) ) ;
}
}
}
/ * W e a r e n o w g u a r a n t e e d t h a t c h i l d N o d e s e 1 a n d e 2 e x i s t ,
* and that they can be diffed .
* /
/ * D i f f s i n c h i l d n o d e s s h o u l d n o t a f f e c t t h e p a r e n t n o d e ,
* 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 ) {
/ * E i t h e r t 1 . c h i l d N o d e s a n d t 2 . c h i l d N o d e s h a v e t h e s a m e l e n g t h , o r
* 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
} ) ) ;
return diffs ;
}
}
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 . 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 ( {
action : 'relocateGroup' ,
groupLength : group . length ,
from : group . oldValue ,
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 ) {
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 . 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' ) {
node . checked = false ;
} 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 ;
}
this . textDiff ( node , node . data , diff . oldValue , 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 ) ) ;