find matching preserved nodes by selector (in theory)

This commit is contained in:
David Greenspan
2012-07-06 12:21:05 -07:00
parent e750178458
commit a1e90afd98
4 changed files with 157 additions and 45 deletions

View File

@@ -0,0 +1,94 @@
Meteor.ui = Meteor.ui || {};
// returns true if element a properly contains element b
Meteor.ui._elementContains = function(a, b) {
// Note: Some special-casing would be required to implement this method
// where a and b aren't necessarily elements, e.g. b is a text node,
// because contains() doesn't seem to work reliably on some browsers
// including IE.
if (a.nodeType !== 1 || b.nodeType !== 1) {
return false; // a and b are not both elements
}
if (a.compareDocumentPosition) {
return a.compareDocumentPosition(b) & 0x10;
} else {
// Should be only old IE and maybe other old browsers here.
// Modern Safari has both methods but seems to get contains() wrong.
return a !== b && a.contains(b);
}
};
// Returns an array of element nodes matching `selector`, where
// the selector is interpreted as rooted at `contextNode`.
// This means that all nodes that participate in the selector
// must be descendents on contextNode.
//
// jQuery dependency to eventually replace with querySelectorAll
// backed up by Sizzle in Old IE. Note that querySelectorAll doesn't
// provide the needed semantics for scoping the selector to contextNode;
// for example, myDiv.querySelectorAll("body *") will match all of myDiv's
// descendents, while $(myDiv).find("body *") won't match any. The latter
// behavior is definitely better, and the way to implement it is to temporarily
// assign an ID to contextNode (if it doesn't have one).
Meteor.ui._findElement = function(contextNode, selector) {
return $(contextNode).find(selector);
};
// Requires: `a` and `b` are element nodes in the same document tree.
// Returns 0 if the nodes are the same or either one contains the other;
// otherwise, 1 if (a,b) are in order and -1 if they are in the opposite order.
Meteor.ui._elementOrder = function(a, b) {
// See http://ejohn.org/blog/comparing-document-position/
if (a === b)
return 0;
if (a.compareDocumentPosition) {
var n = a.compareDocumentPosition(b);
return ((n & 0x18) ? 0 : ((n & 0x4) ? 1 : -1));
} else {
// Only old IE is known to not have compareDocumentPosition (though Safari
// originally lacked it). Thankfully, IE gives us a way of comparing elements
// via the "sourceIndex" property.
if (a.contains(b) || b.contains(a))
return 0;
return (a.sourceIndex < b.sourceIndex ? 1 : -1);
}
};
// Like `findElement` but uses a hypothetical LiveRange wrapping start..end
// as the context.
Meteor.ui._findElementInRange = function(start, end, selector) {
end = (end || start);
var container = start.parentNode;
if (! container) {
if (start === end && (start.nodeType === 9 /* Document */ ||
start.nodeType === 11 /* DocumentFragment */))
return Meteor.ui._findElement(start, selector);
throw new Error("Can't find element in range on detached node");
}
if (end.parentNode !== container)
throw new Error("Bad range");
// narrow the range to exclude top-level non-elements (which can't be
// or contain matches) by moving the `start` pointer forward and `end`
// backward.
while (start !== end && start.nodeType !== 1)
start = start.nextSibling;
while (start !== end && end.nodeType !== 1)
end = end.previousSibling;
if (start.nodeType !== 1)
return []; // no top-level elements! start === end and it's not an element
// resultsPlus includes matches that are contained by the range's
// parent, but are outside of start..end, i.e. are descended from
// (or are) a different sibling.
var resultsPlus = Meteor.ui._findElement(container, selector);
// Filter the list of nodes to remove nodes that occur before start
// or after end.
return _.reject(resultsPlus, function(n) {
// reject node if (n,start) are in order or (end,n) are in order
return (Meteor.ui._elementOrder(n, start) > 0) ||
(Meteor.ui._elementOrder(end, n) > 0);
});
};

View File

@@ -2,7 +2,7 @@ Meteor.ui = Meteor.ui || {};
// TODO:
//
// - Match DOM elements in chunks based on "preserve"
// - Perform DOM patching based on "preserve"
// - {constant:true} chunk options
(function() {
@@ -269,7 +269,10 @@ Meteor.ui = Meteor.ui || {};
if (mode === "patch") {
// Rendering top level of the current update, with patching
copyChunkState(range, frag);
var nodeMatches = matchChunks(range, frag);
// XXX use nodeMatches
range.operate(function(start, end) {
Sarge.shuck(start, end);
@@ -491,23 +494,19 @@ Meteor.ui = Meteor.ui || {};
// Check whether a node is contained in the document.
isNodeOnscreen: function (node) {
// http://jsperf.com/is-element-in-the-dom
// Deal with all cases where node is not an element
// node descending from the body first...
if (node === document)
return true;
if (document.compareDocumentPosition)
return document.compareDocumentPosition(node) & 16;
else {
if (node.nodeType !== 1 /* Element */)
/* contains() doesn't work reliably on non-Elements. Fine on
Chrome, not so much on Safari and IE. */
node = node.parentNode;
if (node.nodeType === 11 /* DocumentFragment */ ||
node.nodeType === 9 /* Document */)
/* contains() chokes on DocumentFragments on IE8 */
return node === document;
/* contains() exists on document on Chrome, but only on
document.body on some other browsers. */
return document.body.contains(node);
}
if (node.nodeType !== 1 /* Element */)
node = node.parentNode;
if (! (node && node.nodeType === 1))
return false;
if (node === document.body)
return true;
return Meteor.ui._elementContains(document.body, node);
},
// Internal facility, only used by tests, for holding onto
@@ -632,7 +631,7 @@ Meteor.ui = Meteor.ui || {};
var selector = h.selector;
if (selector) {
var contextNode = range.containerNode();
var results = $(contextNode).find(selector);
var results = Meteor.ui._findElement(contextNode, selector);
if (! _.contains(results, curNode))
continue;
} else {
@@ -677,12 +676,13 @@ Meteor.ui = Meteor.ui || {};
// Match branch keys and copy chunkState from liveranges in the
// interior of oldRange onto matching liveranges in newFrag.
var copyChunkState = function(oldRange, newFrag) {
// Return pairs of matching DOM nodes to preserve.
var matchChunks = function(oldRange, newFrag) {
if (! newFrag.firstChild)
return; // allow empty newFrag
return []; // allow empty newFrag
var oldChunks = {};
var currentPath = [];
var oldChunks = {}; // { path -> range }
var currentPath = []; // list of branch keys (path segments)
// visit the interior of outerRange and call
// `func(r, path)` on every range with a branch key,
@@ -706,25 +706,57 @@ Meteor.ui = Meteor.ui || {};
// collect old chunks keyed by their branch key paths
eachKeyedChunk(oldRange, function(r, path) {
oldChunks[path] = r;
// XXX preserve
});
// Run the selectors from preserveMap over the nodes
// in range and create a map { label -> node }.
var collectLabeledNodes = function(range, preserveMap) {
var labeledNodes = {};
_.each(preserveMap, function(labelFunc, sel) {
var matchingNodes = Meteor.ui._findElementInRange(
range.firstNode(), range.lastNode(), sel);
_.each(matchingNodes, function(n) {
// labelFunc can be a function or a constant,
// the latter for single-match selectors {'.foo': 1}
var pernodeLabel = (
typeof labelFunc === 'function' ? labelFunc(n) : labelFunc);
var fullLabel = sel+'/'+pernodeLabel;
// in case of duplicates, we ignore the second node (this one).
// eventually, the developer might want to get debug info.
if (! labeledNodes[fullLabel])
labeledNodes[fullLabel] = n;
});
});
return labeledNodes;
};
var nodeMatches = []; // [[oldNode, newNode], ...]
// create a temporary range around newFrag in order
// to visit it.
var tempRange = new Meteor.ui._LiveRange(Meteor.ui._tag, newFrag);
// visit new frag
eachKeyedChunk(tempRange, function(r, path) {
var oldR = oldChunks[path];
if (oldR) {
var oldRange = oldChunks[path];
if (oldRange) {
// copy over chunkState
r.chunkState = oldR.chunkState;
oldR.chunkState = null; // don't call offscreen() on old range
r.chunkState = oldRange.chunkState;
oldRange.chunkState = null; // don't call offscreen() on old range
// any second occurrence of `path` is ignored (not matched)
delete oldChunks[path];
var oldLabeledNodes = collectLabeledNodes(oldRange, r.preserve);
var newLabeledNodes = collectLabeledNodes(r, r.preserve);
_.each(newLabeledNodes, function(newNode, label) {
var oldNode = oldLabeledNodes[label];
if (oldNode)
nodeMatches.push([oldNode, newNode]);
});
}
});
tempRange.destroy();
return nodeMatches;
};
var diffPatch = function(tgtParent, srcParent, tgtBefore, tgtAfter) {
@@ -808,5 +840,4 @@ Meteor.ui = Meteor.ui || {};
};
})();

View File

@@ -13,6 +13,7 @@ Package.on_use(function (api) {
// you still want the event object normalization that jquery provides?)
api.use('jquery');
api.add_files(['domutils.js'], 'client');
api.add_files(['liveevents_w3c.js', 'liveevents_now3c.js'], 'client');
api.add_files(['liveevents.js'], 'client');
api.add_files(['liverange.js', 'liveui.js', 'innerhtml.js', 'patcher.js'],

View File

@@ -92,7 +92,7 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
var starting = ! lastKeptTgt;
var finishing = ! tgt;
var elementContains = Meteor.ui._Patcher._elementContains;
var elementContains = Meteor.ui._elementContains;
if (! starting) {
// move lastKeptTgt/lastKeptSrc forward and out,
@@ -377,17 +377,3 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) {
}
};
// returns true if element a properly contains element b
Meteor.ui._Patcher._elementContains = function(a, b) {
if (a.nodeType !== 1 || b.nodeType !== 1) {
return false;
}
if (a.compareDocumentPosition) {
return a.compareDocumentPosition(b) & 0x10;
} else {
// Should be only old IE and maybe other old browsers here.
// Modern Safari has both methods but seems to get contains() wrong.
return a !== b && a.contains(b);
}
};