move diff/patch into smartpatch

This commit is contained in:
David Greenspan
2012-04-23 17:20:42 -07:00
parent 365cd81f55
commit ffd02bf1aa
3 changed files with 103 additions and 96 deletions

View File

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

View File

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

View File

@@ -102,7 +102,10 @@ Tinytest.add("smartpatch - basic", function(test) {
x = div(aaa+"<b><i>foo</i><u>bar</u></b>"+zzz);
y = div("<b><u>bar</u><s>baz</s></b>");
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);