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);