move liveui/domutils utilities into DomUtils

This commit is contained in:
David Greenspan
2012-07-27 15:32:51 -07:00
parent 328a2af833
commit 3dbccc131c
6 changed files with 192 additions and 16 deletions

View File

@@ -152,4 +152,168 @@ DomUtils = {};
return container;
};
})();
// Returns true if element a properly contains element b.
// Only works on element nodes (e.g. not text nodes).
DomUtils.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 containing the children of contextNode that
// match `selector`. Unlike querySelectorAll, `selector` is
// interpreted as if the document were rooted at `contextNode` --
// the only nodes that can be used to match components of the
// selector are the descendents of `contextNode`. `contextNode`
// itself is not included (it can't be used to match a component of
// the selector, and it can never be included in the returned
// array.)
//
// `contextNode` may be either a node, a document, or a DocumentFragment.
DomUtils.findElement = function(contextNode, selector) {
// Eventually, we will remove the dependency on jQuery ($) and
// implement this in terms of querySelectorAll on modern browsers
// and Sizzle in old IE. We'll use jQuery's trick for scoped
// querySelectorAll which involves temporarily assigning an ID to
// contextNode (if it doesn't have one) and prepending the ID to
// the selector.
if (contextNode.nodeType === 11 /* DocumentFragment */) {
// Sizzle doesn't work on a DocumentFragment, but it does work on
// a descendent of one.
var frag = contextNode;
var container = DomUtils.fragmentToContainer(frag);
var results = $(container).find(selector);
// put nodes back into frag
while (container.firstChild)
frag.appendChild(container.firstChild);
return results;
} else {
return $(contextNode).find(selector);
}
};
// Like `findElement` but searches the nodes from `start` to `end`
// inclusive. `start` and `end` must be siblings, and they participate
// in the search (they can be used to match selector components, and
// they can appear in the returned results). It's as if the parent of
// `start` and `end` serves as contextNode, but matches from children
// that aren't between `start` and `end` (inclusive) are ignored.
//
// If `selector` involves sibling selectors, child index selectors, or
// the like, the results are undefined.
DomUtils.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 DomUtils.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 = DomUtils.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 (DomUtils.elementOrder(n, start) > 0) ||
(DomUtils.elementOrder(end, n) > 0);
});
};
// Returns 0 if the nodes are the same or either one contains the other;
// otherwise, 1 if a comes before b, or else -1 if b comes before a in
// document order.
// Requires: `a` and `b` are element nodes in the same document tree.
DomUtils.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);
}
};
// Wrap `frag` as necessary to prepare it for insertion in
// `container`. For example, if `frag` has TR nodes at top level,
// and `container` is a TABLE, then it's necessary to wrap `frag` in
// a TBODY to avoid IE quirks.
//
// `frag` is a DocumentFragment and will be modified in
// place. `container` is a DOM element.
DomUtils.wrapFragmentForContainer = function(frag, container) {
if (container && container.nodeName === "TABLE" &&
_.any(frag.childNodes,
function(n) { return n.nodeName === "TR"; })) {
// Avoid putting a TR directly in a TABLE without an
// intervening TBODY, because it doesn't work in IE. We do
// the same thing on all browsers for ease of testing
// and debugging.
var tbody = document.createElement("TBODY");
tbody.appendChild(frag);
frag.appendChild(tbody);
}
};
// Return true if `node` is part of the global DOM document. Like
// elementContains(document, node), except (1) it works for any node
// (eg, text nodes), not just elements; (2) it works around browser
// quirks that would otherwise come up when passing 'document' as
// the first argument to elementContains.
//
// Returns true if node === document.
DomUtils.isInDocument = function (node) {
// Deal with all cases where node is not an element
// node descending from the body first...
if (node === document)
return true;
if (node.nodeType !== 1 /* Element */)
node = node.parentNode;
if (! (node && node.nodeType === 1))
return false;
if (node === document.body)
return true;
return DomUtils.elementContains(document.body, node);
};
})();

View File

@@ -21,7 +21,7 @@ Meteor.ui._elementContains = function(a, 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.
// must be descendents of contextNode.
//
// jQuery dependency to eventually replace with querySelectorAll
// backed up by Sizzle in Old IE. Note that querySelectorAll doesn't
@@ -46,9 +46,10 @@ Meteor.ui._findElement = function(contextNode, 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.
// otherwise, 1 if a comes before b, or else -1 if b comes before a in
// document order.
// Requires: `a` and `b` are element nodes in the same document tree.
Meteor.ui._elementOrder = function(a, b) {
// See http://ejohn.org/blog/comparing-document-position/
if (a === b)
@@ -66,8 +67,15 @@ Meteor.ui._elementOrder = function(a, b) {
}
};
// Like `findElement` but uses a hypothetical LiveRange wrapping start..end
// as the context.
// Like `findElement` but searches the nodes from `start` to `end`
// inclusive. `start` and `end` must be siblings, and they participate
// in the search (they can be used to match selector components, and
// they can appear in the returned results). It's as if the parent of
// `start` and `end` serves as contextNode, but matches from children
// that aren't between `start` and `end` (inclusive) are ignored.
//
// If `selector` involves sibling selectors, child index selectors, or
// the like, the results are undefined.
Meteor.ui._findElementInRange = function(start, end, selector) {
end = (end || start);
@@ -122,6 +130,11 @@ Meteor.ui._isNodeOnscreen = function (node) {
return Meteor.ui._elementContains(document.body, node);
};
// Wraps the contents of `frag`, a DocumentFragment, if necessary
// to insert the fragment into `container`, a DOM element.
// For example, if `frag` has TR nodes as children and container
// is a TABLE, the children of `frag` will be wrapped with a
// TBODY in place to work around IE quirks.
Meteor.ui._wrapFragmentForContainer = function(frag, container) {
if (container && container.nodeName === "TABLE" &&
_.any(frag.childNodes,

View File

@@ -126,7 +126,7 @@ Meteor.ui._doc = Meteor.ui._doc || {};
var next = comment.nextSibling;
Meteor.ui._wrapFragmentForContainer(subFrag, comment.parentNode);
DomUtils.wrapFragmentForContainer(subFrag, comment.parentNode);
comment.parentNode.replaceChild(subFrag, comment);
return next;
@@ -182,7 +182,7 @@ Meteor.ui._doc = Meteor.ui._doc || {};
throw new Error("Double-GCed range: "+range.id);
if (! (node.parentNode &&
(Meteor.ui._isNodeOnscreen(node) ||
(DomUtils.isInDocument(node) ||
Meteor.ui._doc._isNodeHeld(node)))) {
// range is offscreen!
// kill all ranges in this fragment or detached DOM tree,

View File

@@ -128,7 +128,7 @@ Meteor.ui._event._loadW3CImpl = function() {
// relatedTarget is present and a descendent).
(! event.relatedTarget ||
(event.currentTarget !== event.relatedTarget &&
! Meteor.ui._elementContains(
! DomUtils.elementContains(
event.currentTarget, event.relatedTarget)))) {
if (event.type === 'mouseover'){
sendUIEvent('mouseenter', event.currentTarget, false);

View File

@@ -61,7 +61,7 @@ Meteor.ui = Meteor.ui || {};
Meteor.ui._inRender = false;
}
Meteor.ui._wrapFragmentForContainer(frag, range.containerNode());
DomUtils.wrapFragmentForContainer(frag, range.containerNode());
// Perform patching
var nodeMatches = matchChunks(range, frag);
@@ -540,7 +540,7 @@ Meteor.ui = Meteor.ui || {};
var node = range.firstNode();
if (node.parentNode &&
(Meteor.ui._isNodeOnscreen(node) || Sarge.isNodeHeld(node)))
(DomUtils.isInDocument(node) || Sarge.isNodeHeld(node)))
return false;
while (node.parentNode)
@@ -673,7 +673,7 @@ Meteor.ui = Meteor.ui || {};
var selector = h.selector;
if (selector) {
var contextNode = range.containerNode();
var results = Meteor.ui._findElement(contextNode, selector);
var results = DomUtils.findElement(contextNode, selector);
if (! _.contains(results, curNode))
continue;
} else {
@@ -757,7 +757,7 @@ Meteor.ui = Meteor.ui || {};
var collectLabeledNodes = function(range, preserveMap) {
var labeledNodes = {};
_.each(preserveMap, function(labelFunc, sel) {
var matchingNodes = Meteor.ui._findElementInRange(
var matchingNodes = DomUtils.findElementInRange(
range.firstNode(), range.lastNode(), sel);
_.each(matchingNodes, function(n) {
// labelFunc can be a function or a constant,
@@ -857,7 +857,7 @@ Meteor.ui = Meteor.ui || {};
if (pair) {
var tgt = pair[0];
if (! lastTgtMatch ||
Meteor.ui._elementOrder(lastTgtMatch, tgt) > 0) {
DomUtils.elementOrder(lastTgtMatch, tgt) > 0) {
if (pair.rangeMatch) {
// range match! for constant chunk
if (patcher.match(pair[0], pair[1], null, true)) {

View File

@@ -93,7 +93,6 @@ Meteor.ui._Patcher.prototype.match = function(
var starting = ! lastKeptTgt;
var finishing = ! tgt;
var elementContains = Meteor.ui._elementContains;
if (! starting) {
// move lastKeptTgt/lastKeptSrc forward and out,
@@ -101,7 +100,7 @@ Meteor.ui._Patcher.prototype.match = function(
// replacing as we go. If tgt/src is falsy, we make it to the
// top level.
while (lastKeptTgt.parentNode !== this.tgtParent &&
! (tgt && elementContains(lastKeptTgt.parentNode, tgt))) {
! (tgt && DomUtils.elementContains(lastKeptTgt.parentNode, tgt))) {
// Last-kept nodes are inside parents that are not
// parents of the newly matched nodes. Must finish
// replacing their contents and back out.