@qooxdoo/framework
Version:
The JS Framework for Coders
928 lines (805 loc) • 28.8 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2011-2012 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Martin Wittemann (wittemann)
* Daniel Wagner (danielwagner)
************************************************************************ */
/**
* DOM traversal module
*
* @require(qx.dom.Hierarchy#getSiblings)
* @require(qx.dom.Hierarchy#getNextSiblings)
* @require(qx.dom.Hierarchy#getPreviousSiblings)
* @require(qx.dom.Hierarchy#contains)
*
* @group (Core)
*/
qx.Bootstrap.define("qx.module.Traversing", {
statics :
{
/**
* String attributes used to determine if two DOM nodes are equal
* as defined in <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode">
* DOM Level 3</a>
*/
EQUALITY_ATTRIBUTES : [
"nodeType",
"nodeName",
"localName",
"namespaceURI",
"prefix",
"nodeValue"
],
/**
* Internal helper for getAncestors and getAncestorsUntil
*
* @attach {qxWeb}
* @param selector {String} Selector that indicates where to stop including
* ancestor elements
* @param filter {String?null} Optional selector to match
* @return {qxWeb} Collection containing the ancestor elements
* @internal
*/
__getAncestors : function(selector, filter) {
var ancestors = [];
for (var i=0; i < this.length; i++) {
var parent = qx.dom.Element.getParentElement(this[i]);
while (parent) {
var found = [parent];
if (selector && qx.bom.Selector.matches(selector, found).length > 0) {
break;
}
if (filter) {
found = qx.bom.Selector.matches(filter, found);
}
ancestors = ancestors.concat(found);
parent = qx.dom.Element.getParentElement(parent);
}
}
return qxWeb.$init(ancestors, qxWeb);
},
/**
* Helper which returns the element from the given argument. If it's a collection,
* it returns it's first child. If it's a string, it tries to use the string
* as selector and returns the first child of the new collection.
* @param arg {Node|String|qxWeb} The element.
* @return {Node|var} If a node can be extracted, the node element will be return.
* If not, at given argument will be returned.
*/
__getElementFromArgument : function(arg) {
if (arg instanceof qxWeb) {
return arg[0];
} else if (qx.Bootstrap.isString(arg)) {
return qxWeb(arg)[0];
}
return arg;
},
/**
* Helper that attempts to convert the given argument into a DOM node
* @param arg {var} object to convert
* @return {Node|null} DOM node or null if the conversion failed
*/
__getNodeFromArgument : function(arg) {
if (typeof arg == "string") {
arg = qxWeb(arg);
}
if (arg instanceof Array || arg instanceof qxWeb) {
arg = arg[0];
}
return qxWeb.isNode(arg) ? arg : null;
},
/**
* Returns a map containing the given DOM node's attribute names
* and values
*
* @param node {Node} DOM node
* @return {Map} Map of attribute names/values
*/
__getAttributes : function(node) {
var attributes = {};
for (var attr in node.attributes) {
if (attr == "length") {
continue;
}
var name = node.attributes[attr].name;
var value = node.attributes[attr].value;
attributes[name] = value;
}
return attributes;
},
/**
* Helper function that iterates over a set of items and applies the given
* qx.dom.Hierarchy method to each entry, storing the results in a new Array.
* Duplicates are removed and the items are filtered if a selector is
* provided.
*
* @attach{qxWeb}
* @param collection {Array} Collection to iterate over (any Array-like object)
* @param method {String} Name of the qx.dom.Hierarchy method to apply
* @param selector {String?} Optional selector that elements to be included
* must match
* @return {Array} Result array
* @internal
*/
__hierarchyHelper : function(collection, method, selector)
{
// Iterate ourself, as we want to directly combine the result
var all = [];
var Hierarchy = qx.dom.Hierarchy;
for (var i=0, l=collection.length; i<l; i++) {
all.push.apply(all, Hierarchy[method](collection[i]));
}
// Remove duplicates
var ret = qx.lang.Array.unique(all);
// Post reduce result by selector
if (selector) {
ret = qx.bom.Selector.matches(selector, ret);
}
return ret;
},
/**
* Checks if the given object is a DOM element
*
* @attachStatic{qxWeb}
* @param selector {Object|String|qxWeb} Object to check
* @return {Boolean} <code>true</code> if the object is a DOM element
*/
isElement : function(selector) {
return qx.dom.Node.isElement(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Checks if the given object is a DOM node
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} Object to check
* @return {Boolean} <code>true</code> if the object is a DOM node
*/
isNode : function(selector) {
return qx.dom.Node.isNode(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Whether the node has the given node name
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} the node to check
* @param nodeName {String} the node name to check for
* @return {Boolean} <code>true</code> if the node has the given name
*/
isNodeName : function(selector, nodeName) {
return qx.dom.Node.isNodeName(qx.module.Traversing.__getElementFromArgument(selector), nodeName);
},
/**
* Checks if the given object is a DOM document object
*
* @attachStatic{qxWeb}
* @param node {Object|qxWeb} Object to check. If the value is a qxWeb
* collection, isDocument will check the first item.
* @return {Boolean} <code>true</code> if the object is a DOM document
*/
isDocument : function(node) {
if (node instanceof qxWeb) {
node = node[0];
}
return qx.dom.Node.isDocument(node);
},
/**
* Checks if the given object is a DOM document fragment object
*
* @attachStatic{qxWeb}
* @param node {Object|qxWeb} Object to check. If the value is a qxWeb
* collection, isDocumentFragment will check the first item.
* @return {Boolean} <code>true</code> if the object is a DOM document fragment
*/
isDocumentFragment : function(node) {
if (node instanceof qxWeb) {
node = node[0];
}
return qx.dom.Node.isDocumentFragment(node);
},
/**
* Returns the DOM2 <code>defaultView</code> (window) for the given node.
*
* @attachStatic{qxWeb}
* @param selector {Node|Document|Window|String|qxWeb} Node to inspect
* @return {Window} the <code>defaultView</code> for the given node
*/
getWindow : function(selector) {
return qx.dom.Node.getWindow(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Checks whether the given object is a DOM text node
*
* @attachStatic{qxWeb}
* @param obj {Object} the object to be tested
* @return {Boolean} <code>true</code> if the object is a textNode
*/
isTextNode : function(obj) {
return qx.dom.Node.isText(obj);
},
/**
* Check whether the given object is a browser window object.
*
* @attachStatic{qxWeb}
* @param obj {Object|qxWeb} the object to be tested. If the value
* is a qxWeb collection, isDocument will check the first item.
* @return {Boolean} <code>true</code> if the object is a window object
*/
isWindow : function(obj) {
if (obj instanceof qxWeb) {
obj = obj[0];
}
return qx.dom.Node.isWindow(obj);
},
/**
* Returns the owner document of the given node
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} Node to get the document for
* @return {Document|null} The document of the given DOM node
*/
getDocument : function(selector) {
return qx.dom.Node.getDocument(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Get the DOM node's name as a lowercase string
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} DOM Node
* @return {String} node name
*/
getNodeName : function(selector) {
return qx.dom.Node.getName(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Returns the text content of a node where the node type may be one of
* NODE_ELEMENT, NODE_ATTRIBUTE, NODE_TEXT, NODE_CDATA
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} the node from where the search should start. If the
* node has subnodes the text contents are recursively retrieved and joined
* @return {String} the joined text content of the given node or null if not
* appropriate.
*/
getNodeText : function(selector) {
return qx.dom.Node.getText(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Checks if the given node is a block node
*
* @attachStatic{qxWeb}
* @param selector {Node|String|qxWeb} the node to check
* @return {Boolean} <code>true</code> if the node is a block node
*/
isBlockNode : function(selector) {
return qx.dom.Node.isBlockNode(qx.module.Traversing.__getElementFromArgument(selector));
},
/**
* Determines if two DOM nodes are equal as defined in the
* <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode">DOM Level 3 isEqualNode spec</a>.
* Also works in legacy browsers without native <em>isEqualNode</em> support.
*
* @attachStatic{qxWeb}
* @param node1 {String|Element|Element[]|qxWeb} first object to compare
* @param node2 {String|Element|Element[]|qxWeb} second object to compare
* @return {Boolean} <code>true</code> if the nodes are equal
*/
equalNodes : function(node1, node2) {
node1 = qx.module.Traversing.__getNodeFromArgument(node1);
node2 = qx.module.Traversing.__getNodeFromArgument(node2);
if (!node1 || !node2) {
return false;
}
if (qx.core.Environment.get("html.node.isequalnode")) {
return node1.isEqualNode(node2);
} else {
if (node1 === node2) {
return true;
}
// quick attributes length check
var hasAttributes = node1.attributes && node2.attributes;
if (hasAttributes &&
node1.attributes.length !== node2.attributes.length) {
return false;
}
var hasChildNodes = node1.childNodes && node2.childNodes;
// quick childNodes length check
if (hasChildNodes &&
node1.childNodes.length !== node2.childNodes.length) {
return false;
}
// string attribute check
var domAttributes = qx.module.Traversing.EQUALITY_ATTRIBUTES;
for (var i=0, l=domAttributes.length; i<l; i++) {
var domAttrib = domAttributes[i];
if (node1[domAttrib] !== node2[domAttrib]) {
return false;
}
}
// attribute values
if (hasAttributes) {
var node1Attributes = qx.module.Traversing.__getAttributes(node1);
var node2Attributes = qx.module.Traversing.__getAttributes(node2);
for (var attr in node1Attributes) {
if (node1Attributes[attr] !== node2Attributes[attr]) {
return false;
}
}
}
// child nodes
if (hasChildNodes) {
for (var j=0, m=node1.childNodes.length; j<m; j++) {
var child1 = node1.childNodes[j];
var child2 = node2.childNodes[j];
if (!qx.module.Traversing.equalNodes(child1, child2)) {
return false;
}
}
}
return true;
}
}
},
members :
{
__getAncestors : null,
/**
* Adds an element to the collection
*
* @attach {qxWeb}
* @param el {Element|qxWeb} DOM element to add to the collection.
* If a collection is given, only the first element will be added
* @return {qxWeb} The collection for chaining
*/
add : function(el) {
if (el instanceof qxWeb) {
el = el[0];
}
if (qx.module.Traversing.isElement(el) ||
qx.module.Traversing.isDocument(el) ||
qx.module.Traversing.isWindow(el) ||
qx.module.Traversing.isDocumentFragment(el))
{
this.push(el);
}
return this;
},
/**
* Gets a set of elements containing all of the unique immediate children of
* each of the matched set of elements.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?null} Optional selector to match
* @return {qxWeb} Collection containing the child elements
*/
getChildren : function(selector) {
var children = [];
for (var i=0; i < this.length; i++) {
var found = qx.dom.Hierarchy.getChildElements(this[i]);
if (selector) {
found = qx.bom.Selector.matches(selector, found);
}
children = children.concat(found);
};
return qxWeb.$init(children, qxWeb);
},
/**
* Executes the provided callback function once for each item in the
* collection.
*
* @attach {qxWeb}
* @param fn {Function} Callback function which is called with two parameters
* <ul>
* <li>current item - DOM node</li>
* <li>current index - Number</li>
* </ul>
* @param ctx {Object} Context object
* @return {qxWeb} The collection for chaining
*/
forEach : function(fn, ctx) {
for (var i=0; i < this.length; i++) {
fn.call(ctx, this[i], i, this);
};
return this;
},
/**
* Gets a set of elements containing the parent of each element in the
* collection.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?null} Optional selector to match
* @return {qxWeb} Collection containing the parent elements
*/
getParents : function(selector) {
var parents = [];
for (var i=0; i < this.length; i++) {
var found = qx.dom.Element.getParentElement(this[i]);
if (selector) {
found = qx.bom.Selector.matches(selector, [found]);
}
parents = parents.concat(found);
};
return qxWeb.$init(parents, qxWeb);
},
/**
* Checks if any element of the current collection is child of any element of a given
* parent collection.
*
* @attach{qxWeb}
* @param parent {qxWeb | String} Collection or selector of the parent collection to check.
* @return {Boolean} Returns true if at least one element of the current collection is child of the parent collection
*
*/
isChildOf : function(parent){
if(this.length == 0){
return false;
}
var ancestors = null, parentCollection = qxWeb(parent), isChildOf = false;
for(var i = 0, l = this.length; i < l && !isChildOf; i++){
ancestors = qxWeb(this[i]).getAncestors();
for(var j = 0, len = parentCollection.length; j < len; j++){
if(ancestors.indexOf(parentCollection[j]) != -1){
isChildOf = true;
break;
}
};
}
return isChildOf;
},
/**
* Gets a set of elements containing all ancestors of each element in the
* collection.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param filter {String?null} Optional selector to match
* @return {qxWeb} Collection containing the ancestor elements
*/
getAncestors : function(filter) {
return this.__getAncestors(null, filter);
},
/**
* Gets a set of elements containing all ancestors of each element in the
* collection, up to (but not including) the element matched by the provided
* selector.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String} Selector that indicates where to stop including
* ancestor elements
* @param filter {String?null} Optional selector to match
* @return {qxWeb} Collection containing the ancestor elements
*/
getAncestorsUntil : function(selector, filter) {
return this.__getAncestors(selector, filter);
},
/**
* Gets a set containing the closest matching ancestor for each item in
* the collection.
* If the item itself matches, it is added to the new set. Otherwise, the
* item's parent chain will be traversed until a match is found.
*
* @attach {qxWeb}
* @param selector {String} Selector expression to match
* @return {qxWeb} New collection containing the closest matching ancestors
*/
getClosest : function(selector) {
var closest = [];
var findClosest = function(current) {
var found = qx.bom.Selector.matches(selector, current);
if (found.length) {
closest.push(found[0]);
} else {
current = current.getParents(); // One up
if(current[0] && current[0].parentNode) {
findClosest(current);
}
}
};
for (var i=0; i < this.length; i++) {
findClosest(qxWeb(this[i]));
};
return qxWeb.$init(closest, qxWeb);
},
/**
* Searches the child elements of each item in the collection and returns
* a new collection containing the children that match the provided selector
*
* @attach {qxWeb}
* @param selector {String} Selector expression to match the child elements
* against
* @return {qxWeb} New collection containing the matching child elements
*/
find : function(selector) {
var found = [];
for (var i=0; i < this.length; i++) {
found = found.concat(qx.bom.Selector.query(selector, this[i]));
};
return qxWeb.$init(found, qxWeb);
},
/**
* Gets a new set of elements containing the child nodes of each item in the
* current set.
*
* @attach {qxWeb}
* @return {qxWeb} New collection containing the child nodes
*/
getContents : function() {
var found = [];
this._forEachElement(function(item) {
found = found.concat(qx.lang.Array.fromCollection(item.childNodes));
});
return qxWeb.$init(found, qxWeb);
},
/**
* Checks if at least one element in the collection passes the provided
* filter. This can be either a selector expression or a filter
* function
*
* @attach {qxWeb}
* @param selector {String|Function} Selector expression or filter function
* @return {Boolean} <code>true</code> if at least one element matches
*/
is : function(selector) {
if (qx.lang.Type.isFunction(selector)) {
return this.filter(selector).length > 0;
}
return !!selector && qx.bom.Selector.matches(selector, this).length > 0;
},
/**
* Reduce the set of matched elements to a single element.
*
* @attach {qxWeb}
* @param index {Number} The position of the element in the collection
* @return {qxWeb} A new collection containing one element
*/
eq : function(index) {
return this.slice(index, +index + 1);
},
/**
* Reduces the collection to the first element.
*
* @attach {qxWeb}
* @return {qxWeb} A new collection containing one element
*/
getFirst : function() {
return this.slice(0, 1);
},
/**
* Reduces the collection to the last element.
*
* @attach {qxWeb}
* @return {qxWeb} A new collection containing one element
*/
getLast : function() {
return this.slice(this.length - 1);
},
/**
* Gets a collection containing only the elements that have descendants
* matching the given selector
*
* @attach {qxWeb}
* @param selector {String} Selector expression
* @return {qxWeb} a new collection containing only elements with matching descendants
*/
has : function(selector) {
var found = [];
this._forEachElement(function(item, index) {
var descendants = qx.bom.Selector.matches(selector, this.eq(index).getContents());
if (descendants.length > 0) {
found.push(item);
}
});
return qxWeb.$init(found, this.constructor);
},
/**
* Returns a new collection containing only those nodes that
* contain the given element. Also accepts a qxWeb
* collection or an Array of elements. In those cases, the first element
* in the list is used.
*
* @attach {qxWeb}
* @param element {Element|Window|Element[]|qxWeb} element to check for.
* @return {qxWeb} Collection with matching items
*/
contains : function(element) {
// qxWeb does not inherit from Array in IE
if (element instanceof Array || element instanceof qxWeb) {
element = element[0];
}
if (!element) {
return qxWeb();
}
if (qx.dom.Node.isWindow(element)) {
element = element.document;
}
return this.filter(function(el) {
if (qx.dom.Node.isWindow(el)) {
el = el.document;
}
return qx.dom.Hierarchy.contains(el, element);
});
},
/**
* Gets a collection containing the next sibling element of each item in
* the current set.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing next siblings
*/
getNext : function(selector) {
var found = this.map(qx.dom.Hierarchy.getNextElementSibling, qx.dom.Hierarchy);
if (selector) {
found = qxWeb.$init(qx.bom.Selector.matches(selector, found), qxWeb);
}
return found;
},
/**
* Gets a collection containing all following sibling elements of each
* item in the current set.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing following siblings
*/
getNextAll : function(selector) {
var ret = qx.module.Traversing.__hierarchyHelper(this, "getNextSiblings", selector);
return qxWeb.$init(ret, qxWeb);
},
/**
* Gets a collection containing the following sibling elements of each
* item in the current set up to but not including any element that matches
* the given selector.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing following siblings
*/
getNextUntil : function(selector) {
var found = [];
this.forEach(function(item, index) {
var nextSiblings = qx.dom.Hierarchy.getNextSiblings(item);
for (var i=0, l=nextSiblings.length; i<l; i++) {
if (qx.bom.Selector.matches(selector, [nextSiblings[i]]).length > 0) {
break;
}
found.push(nextSiblings[i]);
}
});
return qxWeb.$init(found, qxWeb);
},
/**
* Gets a collection containing the previous sibling element of each item in
* the current set.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing previous siblings
*/
getPrev : function(selector) {
var found = this.map(qx.dom.Hierarchy.getPreviousElementSibling, qx.dom.Hierarchy);
if (selector) {
found = qxWeb.$init(qx.bom.Selector.matches(selector, found), qxWeb);
}
return found;
},
/**
* Gets a collection containing all preceding sibling elements of each
* item in the current set.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing preceding siblings
*/
getPrevAll : function(selector) {
var ret = qx.module.Traversing.__hierarchyHelper(this, "getPreviousSiblings", selector);
return qxWeb.$init(ret, qxWeb);
},
/**
* Gets a collection containing the preceding sibling elements of each
* item in the current set up to but not including any element that matches
* the given selector.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing preceding siblings
*/
getPrevUntil : function(selector) {
var found = [];
this.forEach(function(item, index) {
var previousSiblings = qx.dom.Hierarchy.getPreviousSiblings(item);
for (var i=0, l=previousSiblings.length; i<l; i++) {
if (qx.bom.Selector.matches(selector, [previousSiblings[i]]).length > 0) {
break;
}
found.push(previousSiblings[i]);
}
});
return qxWeb.$init(found, qxWeb);
},
/**
* Gets a collection containing all sibling elements of the items in the
* current set.
* This set can be filtered with an optional expression that will cause only
* elements matching the selector to be collected.
*
* @attach {qxWeb}
* @param selector {String?} Optional selector expression
* @return {qxWeb} New set containing sibling elements
*/
getSiblings : function(selector) {
var ret = qx.module.Traversing.__hierarchyHelper(this, "getSiblings", selector);
return qxWeb.$init(ret, qxWeb);
},
/**
* Remove elements from the collection that do not pass the given filter.
* This can be either a selector expression or a filter function
*
* @attach {qxWeb}
* @param selector {String|Function} Selector or filter function
* @return {qxWeb} Reduced collection
*/
not : function(selector) {
if (qx.lang.Type.isFunction(selector)) {
return this.filter(function(item, index, obj) {
return !selector(item, index, obj);
});
}
var res = qx.bom.Selector.matches(selector, this);
return this.filter(function(value) {
return res.indexOf(value) === -1;
});
},
/**
* Gets a new collection containing the offset parent of each item in the
* current set.
*
* @attach {qxWeb}
* @return {qxWeb} New collection containing offset parents
*/
getOffsetParent : function() {
return this.map(qx.bom.element.Location.getOffsetParent);
},
/**
* Whether the first element in the collection is inserted into
* the document for which it was created.
*
* @attach {qxWeb}
* @return {Boolean} <code>true</code> when the element is inserted
* into the document.
*/
isRendered : function() {
if (!this[0]) {
return false;
}
return qx.dom.Hierarchy.isRendered(this[0]);
}
},
defer : function(statics) {
qxWeb.$attachAll(this);
// manually attach private method which is ignored by attachAll
qxWeb.$attach({
"__getAncestors" : statics.__getAncestors
});
}
});