diff --git a/packages/liveui/liveui.js b/packages/liveui/liveui.js index 161f25432a..13e19e1b58 100644 --- a/packages/liveui/liveui.js +++ b/packages/liveui/liveui.js @@ -390,89 +390,42 @@ Meteor.ui = Meteor.ui || {}; // Performs a replacement by determining which nodes should // be preserved and invoking Meteor.ui._Patcher as appropriate. - Meteor.ui._intelligent_replace = function(old_range, new_parent) { + Meteor.ui._intelligent_replace = function(tgtRange, srcParent) { - // Table-body fix: if old_range is in a table and new_parent + // Table-body fix: if tgtRange is in a table and srcParent // contains a TR, wrap fragment in a TBODY on all browsers, // so that it will display properly in IE. - if (old_range.containerNode().nodeName === "TABLE" && - _.any(new_parent.childNodes, + if (tgtRange.containerNode().nodeName === "TABLE" && + _.any(srcParent.childNodes, function(n) { return n.nodeName === "TR"; })) { var tbody = document.createElement("TBODY"); - while (new_parent.firstChild) - tbody.appendChild(new_parent.firstChild); - new_parent.appendChild(tbody); + while (srcParent.firstChild) + tbody.appendChild(srcParent.firstChild); + srcParent.appendChild(tbody); } - var each_labeled_node = function(rangeOrParent, func) { - var visit_node = function(is_start, node) { - if (is_start && node.nodeType === 1) { - if (node.id) { - func('#'+node.id, node); - } else if (node.getAttribute("name")) { - func(node.getAttribute("name"), node); - } else { - return true; - } - return false; // skip children of labeled node - } - return true; - }; - - Meteor.ui._LiveRange.visit_children(rangeOrParent, null, null, - visit_node); + var copyFunc = function(t, s) { + $(t).unbind(); // XXX remove jquery events from node + tgtRange.transplant_tag(t, s); }; - var patch = function(targetRangeOrParent, sourceNode) { + //tgtRange.replace_contents(srcParent); - var targetNodes = {}; - var targetNodeOrder = {}; - var targetNodeCounter = 0; - - each_labeled_node(targetRangeOrParent, function(label, node) { - targetNodes[label] = node; - targetNodeOrder[label] = targetNodeCounter++; - }); - - var patcher = new Meteor.ui._Patcher( - targetRangeOrParent, sourceNode); - var lastPos = -1; - var copyFunc = function(t, s) { - $(t).unbind(); // XXX remove jquery events from node - old_range.transplant_tag(t, s); - }; - each_labeled_node(sourceNode, function(label, node) { - var tgt = targetNodes[label]; - var src = node; - if (tgt && targetNodeOrder[label] > lastPos) { - if (patcher.match(tgt, src, copyFunc)) { - // match succeeded - if (tgt.firstChild || src.firstChild) - patch(tgt, src); // recurse - } - lastPos = targetNodeOrder[label]; - } - }); - patcher.finish(); - }; - - //old_range.replace_contents(new_parent); - - old_range.replace_contents(function(start, end) { - var r; - r = new Meteor.ui._LiveUIRange(start, end); - r.destroy(true); - r = { firstNode: function() { return start; }, - lastNode: function() { return end; } }; + tgtRange.replace_contents(function(start, end) { + // clear all LiveRanges on target + (new Meteor.ui._LiveUIRange(start, end)).destroy(true); // remove event handlers on old nodes (which we will be patching) // at top level, where they are attached by $(...).delegate(). - for(var n = old_range.firstNode(); - n && n !== old_range.lastNode().nextSibling; + for(var n = start; + n && n !== end.nextSibling; n = n.nextSibling) $(n).unbind(); - patch(r, new_parent); + var patcher = new Meteor.ui._Patcher( + start.parentNode, srcParent, + start.previousSibling, end.nextSibling); + patcher.diffpatch(copyFunc); }); }; diff --git a/packages/liveui/smartpatch.js b/packages/liveui/smartpatch.js index dfa7f852c0..ee5b6c2192 100644 --- a/packages/liveui/smartpatch.js +++ b/packages/liveui/smartpatch.js @@ -1,25 +1,76 @@ Meteor.ui = Meteor.ui || {}; -// tgtParentOrRange can be anything with firstNode() and lastNode() -// methods; need not be a functioning LiveRange -Meteor.ui._Patcher = function(tgtParentOrRange, srcParent) { - if (typeof tgtParentOrRange.firstNode === "function") { - this.beforeTgt = tgtParentOrRange.firstNode().previousSibling; - this.afterTgt = tgtParentOrRange.lastNode().nextSibling; - this.tgtParent = tgtParentOrRange.firstNode().parentNode; - if (! this.tgtParent) - throw new Error("Can't patch liverange with no parent ndoe"); - } else { - this.tgtParent = tgtParentOrRange; - } +Meteor.ui._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { + this.tgtParent = tgtParent; this.srcParent = srcParent; + this.tgtBefore = tgtBefore; + this.tgtAfter = tgtAfter; + this.lastKeptTgtNode = null; this.lastKeptSrcNode = null; +}; + +Meteor.ui._Patcher.prototype.diffpatch = function(copyCallback) { + var self = this; + + var each_labeled_node = function(parent, before, after, func) { + for(var n = before ? before.nextSibling : parent.firstChild; + n && n !== after; + n = n.nextSibling) { + + if (n.nodeType === 1) { + if (n.id) { + func('#'+n.id, n); + continue; + } else if (n.getAttribute("name")) { + func(n.getAttribute("name"), n); + continue; + } + } + + // not a labeled node; recurse + each_labeled_node(n, null, null, func); + } + }; + + + var targetNodes = {}; + var targetNodeOrder = {}; + var targetNodeCounter = 0; + + each_labeled_node( + self.tgtParent, self.tgtBefore, self.tgtAfter, + function(label, node) { + targetNodes[label] = node; + targetNodeOrder[label] = targetNodeCounter++; + }); + + var lastPos = -1; + each_labeled_node( + self.srcParent, null, null, + function(label, node) { + var tgt = targetNodes[label]; + var src = node; + if (tgt && targetNodeOrder[label] > lastPos) { + if (self.match(tgt, src, copyCallback)) { + // match succeeded + if (tgt.firstChild || src.firstChild) { + // recurse with a new Patcher! + var patcher = new Meteor.ui._Patcher(tgt, src); + patcher.diffpatch(copyCallback); + } + } + lastPos = targetNodeOrder[label]; + } + }); + + self.finish(); }; + // Advances the patching process up to tgtNode in the target tree, // and srcNode in the source tree. tgtNode will be preserved, with // the attributes of srcNode copied over it, in essence identifying @@ -171,41 +222,41 @@ Meteor.ui._Patcher.prototype.finish = function() { return this.match(null, null); }; -// Replaces the siblings between beforeTgt and afterTgt (exclusive on both -// sides) with the siblings between beforeSrc and afterSrc (exclusive on both +// Replaces the siblings between tgtBefore and tgtAfter (exclusive on both +// sides) with the siblings between srcBefore and srcAfter (exclusive on both // sides). Falsy values indicate start or end of siblings as appropriate. // -// Precondition: beforeTgt and afterTgt have same parent; either may be falsy, -// but not both, unless optTgtParent is provided. Same with beforeSrc/afterSrc. +// Precondition: tgtBefore and tgtAfter have same parent; either may be falsy, +// but not both, unless optTgtParent is provided. Same with srcBefore/srcAfter. Meteor.ui._Patcher.prototype._replaceNodes = function( - beforeTgt, afterTgt, beforeSrc, afterSrc, optTgtParent, optSrcParent) + tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent) { - var tgtParent = optTgtParent || (beforeTgt || afterTgt).parentNode; - var srcParent = optSrcParent || (beforeSrc || afterSrc).parentNode; + var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode; + var srcParent = optSrcParent || (srcBefore || srcAfter).parentNode; // deal with case where top level is a range if (tgtParent === this.tgtParent) { - beforeTgt = beforeTgt || this.beforeTgt; - afterTgt = afterTgt || this.afterTgt; + tgtBefore = tgtBefore || this.tgtBefore; + tgtAfter = tgtAfter || this.tgtAfter; } if (srcParent === this.srcParent) { - beforeSrc = beforeSrc || this.beforeSrc; - afterSrc = afterSrc || this.afterSrc; + srcBefore = srcBefore || this.srcBefore; + srcAfter = srcAfter || this.srcAfter; } // remove old children var n; - while ((n = beforeTgt ? beforeTgt.nextSibling : tgtParent.firstChild) - && n !== afterTgt) { + while ((n = tgtBefore ? tgtBefore.nextSibling : tgtParent.firstChild) + && n !== tgtAfter) { tgtParent.removeChild(n); } // add new children var m; - while ((m = beforeSrc ? beforeSrc.nextSibling : srcParent.firstChild) - && m !== afterSrc) { - tgtParent.insertBefore(m, afterTgt || null); + while ((m = srcBefore ? srcBefore.nextSibling : srcParent.firstChild) + && m !== srcAfter) { + tgtParent.insertBefore(m, tgtAfter || null); } }; diff --git a/packages/liveui/smartpatch_tests.js b/packages/liveui/smartpatch_tests.js index 19c177d43f..2ff721c493 100644 --- a/packages/liveui/smartpatch_tests.js +++ b/packages/liveui/smartpatch_tests.js @@ -102,7 +102,10 @@ Tinytest.add("smartpatch - basic", function(test) { x = div(aaa+"foobar"+zzz); y = div("barbaz"); var rng = liverange(tag(y, 'u')); - p = new Patcher(liverange(tag(x, 'b')), y); + var tgt = liverange(tag(x, 'b')); + p = new Patcher(tgt.containerNode(), y, + tgt.firstNode().previousSibling, + tgt.lastNode().nextSibling); var copyCallback = _.bind(rng.transplant_tag, rng); ret = p.match(tag(x, 'u'), tag(y, 'u'), copyCallback); test.isTrue(ret);