Edits fixed the following bugs:
- Calling
Array.prototype.indexOf
on $oldkids
would not work as we were comparing an element to a clone data structure. Added a comparitor function to handle the case
- Started ids at 1 as 0 is falsy
- Fixed a bug in resolve conflicts when there is more than 1 conflict (solved by making counter the ceil of dividing len of conflicts by 2 instead of subbing 1)
- Added a special case for when
idx=0
so we can get out of the hacky resolve conflict case and return to the expected case
Usage
Actual Test cases
JSBin (todo)
/*subtree and childlist helpers*/
//indexOf for collection using a comparitor
var has = Object.hasOwnProperty;
var map = Array.prototype.map;
var indexOf = Array.prototype.indexOf;
var findIndex = function(set, comparitor, from) {
for(var i = ~~from, l=set.length; i<l; i++) {
if(comparitor(set[i])) return i;
}
return -1;
};
//using a non id (eg outerHTML or nodeValue) is extremely naive and will run into issues with nodes that may appear the same like <li></li>
var counter = 0;1;//don't use 0 as id (falsy)
var getId = function($ele) {
return $ele.id || ($ele._id$ele[expando] = $ele._id$ele[expando] || counter++++counter);
};
//clone an html node into a custom datastructure
// see https://gist.github.com/megawac/8201012
var clone = function (par, deep) {
var copy = function(par, top) {
return {
node: par,
kids: top || deep ? map.call(par.childNodes, function(node) {
return copy(node);
}) : null
};
};
return copy(par, true);
};
//findChildMutations: array of mutations so far, element, element clone, bool => array of mutations
// dfs comparision search of two nodes
// this has to be as quick as possible
var findChildMutations = function(target, oldstate, deep) {
var mutations = [];
var add = function(node) {
mutations.push(new MutationRecord({
type: "childList"_childList,
target: node.parentElement,
addedNodes: [node]
}));
};
var rem = function(node, tar) {//have to pass tar because node.parentElement will be null when removed
mutations.push(new MutationRecord({
type: "childList"_childList,
target: tar,
removedNodes: [node]
}));
};
var findMut = function(node, old) {
var $kids = node.childNodes;
var $oldkids = old.kids;
var klen = $kids.length;
var olen = $oldkids.length;
if(!olen && !klen) return;//both empty; clearly no changes
//id to i and j search hash to prevent double checking an element
var map = {};
var id;
var idx;//index of a moved or inserted element
var map = {};
//array of potention conflict hashes
var conflicts = [];
//offsets since last resolve. Can also solve the problem with a continue but we exect this method to be faster as i and j should eventually correlate
//var offset_add = 0;//nodes added since last resolve //we dont have to check added as these are handled before remove
var offset_rem = 0;//nodes removed since last resolve
/*
* There is no gaurentee that the same node will be returned for both added and removed nodes
* if the positionpositions hashave been shuffled.
*/
var resolver = function() {
var countersize = 0;//prevents same conflict beingconflicts.length resolved- twice1;
var conflict;
counter = -~ (size / 2);//prevents same conflict being resolved twice consider forwhen (vartwo inodes =switch 0,places. lonly =one conflicts.length-1;should ibe <=given l;a i++)mutation {
event (note -~ is math.ceil shorthand)
conflicts.forEach(function(conflict =) conflicts[i];{
//attempt to determine if there was node rearrangement... won't gaurentee all matches
//also handles case where added/removed nodes cause nodes to be identified as conflicts
if(counter < l && Math.abs(conflict.i - (conflict.j + offset_rem)) >= lsize) {
add($kids[conflict.i]);//rearrangment ie removed then readded
rem($kids[conflict.i], old.node);
counter++;counter --;
} else if(deep) {//conflicts resolved - check deep
findMut($kids[conflict.i], $oldkids[conflict.j]);
}
});
offset_rem = conflicts.length = 0;//clear conflicts
};
var $cur, $old;//current and old nodes
var $cur;
var $old;
//iterate over both old and current child nodes at the same time
for(var i = 0, j = 0; i < klen || j < olen; ) {
//current and old nodes at the indexs
$cur = $kids[i];
$old = j < olen && $oldkids[j].node;
if($cur === $old) {//simple expected case - needs to be as fast as possible
if(deep) {//recurse
on next level of children
if(deep) findMut($cur, $oldkids[j]);
}
//resolve conflicts
if(conflicts.length) resolver();
i++;
j++;
} else {//(uncommon case) lookahead until they are the same again or the end of children
if($cur) {
id = getId($cur);
//check id is in the location map otherwise do a indexOf search
if(!has.call(map, id)) {//not already found
/* jshint loopfunc:true */
if((idx = indexOf.callfindIndex($oldkids, $curfunction($el) { return $el.node === $cur; }, j)) === -1) { //custom indexOf using comparitor
add($cur);//$cur is a new node
} else {
map[id] = true;//mark id as found
conflicts.push({//add conflict
i: i,
j: idx
});
}
}
i++;
//continue;
}
if($old) {
id = getId($old);
if(!has.call(map, id)) {
if((idx = indexOf.call($kids, $old, i)) === -1) {//dont need to use a special indexof but need to i-1 due to o-b-1 from previous part
rem($old, old.node);
offset_rem++;
} else if(idx === 0) {//special case: if idx=0 i and j are congurent so we can continue without conflict
continue;
} else {
map[id] = true;
conflicts.push({
i: idx,
j: j
});
}
}
j++;
}
}
}
if(conflicts.length) resolver();
};
findMut(target, oldstate);
return mutations;
};