Skip to main content
Notice removed Improve details by CommunityBot
Bounty Ended with no winning answer by CommunityBot
added 39 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele["mo_id"] = $ele["mo_id"] || ++counter);//normally mo_id is an expando variable
};

//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",
            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",
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele["mo_id"] = $ele["mo_id"] || ++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",
            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",
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele["mo_id"] = $ele["mo_id"] || ++counter);//normally mo_id is an expando variable
};

//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",
            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",
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
added 2 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele[expando]$ele["mo_id"] = $ele[expando]$ele["mo_id"] || ++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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele[expando] = $ele[expando] || ++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,
            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,
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
/*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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele["mo_id"] = $ele["mo_id"] || ++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",
            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",
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
Fix a bunch of bugs
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26

Edits fixed the following bugs:

  1. 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
  2. Started ids at 1 as 0 is falsy
  3. 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)
  4. 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;
};

Usage
Actual Test cases
JSBin (todo)

/*subtree and childlist helpers*/
var has = Object.hasOwnProperty;
var map = Array.prototype.map;
var indexOf = Array.prototype.indexOf;

//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;
var getId = function($ele) {
    return $ele.id || ($ele._id = $ele._id || 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",
            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",
            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 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 position has been shuffled
        */
        var resolver = function() {
            var counter = 0;//prevents same conflict being resolved twice
            var conflict;
            for (var i = 0, l = conflicts.length-1; i <= l; i++) {
                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)) >= l) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    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

        //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
                     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
                        if((idx = indexOf.call($oldkids, $cur, j)) === -1) {
                            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) {
                            rem($old, old.node);
                            offset_rem++;
                        } else {
                            map[id] = true;
                            conflicts.push({
                                i: idx,
                                j: j
                            });
                        }
                    }
                    j++;
                }
            }
        }
        if(conflicts.length) resolver();
    };
    findMut(target, oldstate);
    return mutations;
};

Edits fixed the following bugs:

  1. 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
  2. Started ids at 1 as 0 is falsy
  3. 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)
  4. 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 = 1;//don't use 0 as id (falsy)
var getId = function($ele) {
    return $ele.id || ($ele[expando] = $ele[expando] || ++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,
            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,
            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

        //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 positions have been shuffled.
        */
        var resolver = function() {
            var size = conflicts.length - 1;
            var counter = -~ (size / 2);//prevents same conflict being resolved twice consider when two nodes switch places. only one should be given a mutation event (note -~ is math.ceil shorthand)

            conflicts.forEach(function(conflict) {
                //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 && Math.abs(conflict.i - (conflict.j + offset_rem)) >= size) {
                    add($kids[conflict.i]);//rearrangment ie removed then readded
                    rem($kids[conflict.i], old.node);
                    counter --;
                } else if(deep) {//conflicts resolved - check deep
                    findMut($kids[conflict.i], $oldkids[conflict.j]);
                }
            });
            offset_rem = conflicts.length = 0;//clear conflicts
        };

        //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
                //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(map, id)) {//not already found
                        /* jshint loopfunc:true */
                        if((idx = findIndex($oldkids, function($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++;
                }

                if($old) {
                    id = getId($old);
                    if(!has(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;
};
Specify bounty target
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Add some comments and some simplifications in imp #2
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 117 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 212 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
edited title and tags
Link
200_success
  • 144.1k
  • 22
  • 188
  • 473
Loading
Improved correctness of urls
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 148 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 10 characters in body; Post Made Community Wiki
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 88 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Formatting and links
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Added a second implementation
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Added a second implementation
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 62 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Notice added Improve details by megawac
Bounty Started worth 100 reputation by megawac
added 249 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Fixed broken links from deleted branch
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
edited body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
added 92 characters in body
Source Link
megawac
  • 2.3k
  • 1
  • 15
  • 26
Loading
Tweeted twitter.com/#!/StackCodeReview/status/417922817431441408