mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
find matching preserved nodes by selector (in theory)
This commit is contained in:
94
packages/liveui/domutils.js
Normal file
94
packages/liveui/domutils.js
Normal 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);
|
||||
});
|
||||
};
|
||||
@@ -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 || {};
|
||||
|
||||
};
|
||||
|
||||
|
||||
})();
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user