/*
 * This is code is also distributed under MIT license for use
 * with jQuery and prototype JavaScript libraries.
 */
/** * @class Ext.DomQuery Provides high performance selector/xpath processing by compiling queries into reusable functions. New pseudo classes and matchers can be plugged. It works on HTML and XML documents (if a content node is passed in).

DomQuery supports most of the CSS3 selectors spec, along with some custom selectors and basic XPath.

All selectors, attribute filters and pseudos below can be combined infinitely in any order. For example "div.foo:nth-child(odd)[@foo=bar].bar:first" would be a perfectly valid selector. Node filters are processed in the order in which they appear, which allows you to optimize your queries for your document structure.

Element Selectors:

Attribute Selectors:

The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.

Pseudo Classes:

CSS Value Selectors:

* @singleton */ Ext.DomQuery = function(){ var cache = {}, simpleCache = {}, valueCache = {}, nonSpace = /\S/, trimRe = /^\s+|\s+$/g, tplRe = /\{(\d+)\}/g, modeRe = /^(\s?[\/>+~]\s?|\s|$)/, tagTokenRe = /^(#)?([\w-\*]+)/, nthRe = /(\d*)n\+?(\d*)/, nthRe2 = /\D/, // This is for IE MSXML which does not support expandos. // IE runs the same speed using setAttribute, however FF slows way down // and Safari completely fails so they need to continue to use expandos. isIE = window.ActiveXObject ? true : false, key = 30803; // this eval is stop the compressor from // renaming the variable to something shorter eval("var batch = 30803;"); // Retrieve the child node from a particular // parent at the specified index. function child(parent, index){ var i = 0, n = parent.firstChild; while(n){ if(n.nodeType == 1){ if(++i == index){ return n; } } n = n.nextSibling; } return null; } // retrieve the next element node function next(n){ while((n = n.nextSibling) && n.nodeType != 1); return n; } // retrieve the previous element node function prev(n){ while((n = n.previousSibling) && n.nodeType != 1); return n; } // Mark each child node with a nodeIndex skipping and // removing empty text nodes. function children(parent){ var n = parent.firstChild, nodeIndex = -1, nextNode; while(n){ nextNode = n.nextSibling; // clean worthless empty nodes. if(n.nodeType == 3 && !nonSpace.test(n.nodeValue)){ parent.removeChild(n); }else{ // add an expando nodeIndex n.nodeIndex = ++nodeIndex; } n = nextNode; } return this; } // nodeSet - array of nodes // cls - CSS Class function byClassName(nodeSet, cls){ if(!cls){ return nodeSet; } var result = [], ri = -1; for(var i = 0, ci; ci = nodeSet[i]; i++){ if((' '+ci.className+' ').indexOf(cls) != -1){ result[++ri] = ci; } } return result; }; function attrValue(n, attr){ // if its an array, use the first node. if(!n.tagName && typeof n.length != "undefined"){ n = n[0]; } if(!n){ return null; } if(attr == "for"){ return n.htmlFor; } if(attr == "class" || attr == "className"){ return n.className; } return n.getAttribute(attr) || n[attr]; }; // ns - nodes // mode - false, /, >, +, ~ // tagName - defaults to "*" function getNodes(ns, mode, tagName){ var result = [], ri = -1, cs; if(!ns){ return result; } tagName = tagName || "*"; // convert to array if(typeof ns.getElementsByTagName != "undefined"){ ns = [ns]; } // no mode specified, grab all elements by tagName // at any depth if(!mode){ for(var i = 0, ni; ni = ns[i]; i++){ cs = ni.getElementsByTagName(tagName); for(var j = 0, ci; ci = cs[j]; j++){ result[++ri] = ci; } } // Direct Child mode (/ or >) // E > F or E/F all direct children elements of E that have the tag } else if(mode == "/" || mode == ">"){ var utag = tagName.toUpperCase(); for(var i = 0, ni, cn; ni = ns[i]; i++){ cn = ni.childNodes; for(var j = 0, cj; cj = cn[j]; j++){ if(cj.nodeName == utag || cj.nodeName == tagName || tagName == '*'){ result[++ri] = cj; } } } // Immediately Preceding mode (+) // E + F all elements with the tag F that are immediately preceded by an element with the tag E }else if(mode == "+"){ var utag = tagName.toUpperCase(); for(var i = 0, n; n = ns[i]; i++){ while((n = n.nextSibling) && n.nodeType != 1); if(n && (n.nodeName == utag || n.nodeName == tagName || tagName == '*')){ result[++ri] = n; } } // Sibling mode (~) // E ~ F all elements with the tag F that are preceded by a sibling element with the tag E }else if(mode == "~"){ var utag = tagName.toUpperCase(); for(var i = 0, n; n = ns[i]; i++){ while((n = n.nextSibling)){ if (n.nodeName == utag || n.nodeName == tagName || tagName == '*'){ result[++ri] = n; } } } } return result; } function concat(a, b){ if(b.slice){ return a.concat(b); } for(var i = 0, l = b.length; i < l; i++){ a[a.length] = b[i]; } return a; } function byTag(cs, tagName){ if(cs.tagName || cs == document){ cs = [cs]; } if(!tagName){ return cs; } var result = [], ri = -1; tagName = tagName.toLowerCase(); for(var i = 0, ci; ci = cs[i]; i++){ if(ci.nodeType == 1 && ci.tagName.toLowerCase() == tagName){ result[++ri] = ci; } } return result; } function byId(cs, id){ if(cs.tagName || cs == document){ cs = [cs]; } if(!id){ return cs; } var result = [], ri = -1; for(var i = 0, ci; ci = cs[i]; i++){ if(ci && ci.id == id){ result[++ri] = ci; return result; } } return result; } // operators are =, !=, ^=, $=, *=, %=, |= and ~= // custom can be "{" function byAttribute(cs, attr, value, op, custom){ var result = [], ri = -1, useGetStyle = custom == "{", fn = Ext.DomQuery.operators[op], a, innerHTML; for(var i = 0, ci; ci = cs[i]; i++){ // skip non-element nodes. if(ci.nodeType != 1){ continue; } innerHTML = ci.innerHTML; // we only need to change the property names if we're dealing with html nodes, not XML if(innerHTML !== null && innerHTML !== undefined){ if(useGetStyle){ a = Ext.DomQuery.getStyle(ci, attr); } else if (attr == "class" || attr == "className"){ a = ci.className; } else if (attr == "for"){ a = ci.htmlFor; } else if (attr == "href"){ // getAttribute href bug // http://www.glennjones.net/Post/809/getAttributehrefbug.htm a = ci.getAttribute("href", 2); } else{ a = ci.getAttribute(attr); } }else{ a = ci.getAttribute(attr); } if((fn && fn(a, value)) || (!fn && a)){ result[++ri] = ci; } } return result; } function byPseudo(cs, name, value){ return Ext.DomQuery.pseudos[name](cs, value); } function nodupIEXml(cs){ var d = ++key, r; cs[0].setAttribute("_nodup", d); r = [cs[0]]; for(var i = 1, len = cs.length; i < len; i++){ var c = cs[i]; if(!c.getAttribute("_nodup") != d){ c.setAttribute("_nodup", d); r[r.length] = c; } } for(var i = 0, len = cs.length; i < len; i++){ cs[i].removeAttribute("_nodup"); } return r; } function nodup(cs){ if(!cs){ return []; } var len = cs.length, c, i, r = cs, cj, ri = -1; if(!len || typeof cs.nodeType != "undefined" || len == 1){ return cs; } if(isIE && typeof cs[0].selectSingleNode != "undefined"){ return nodupIEXml(cs); } var d = ++key; cs[0]._nodup = d; for(i = 1; c = cs[i]; i++){ if(c._nodup != d){ c._nodup = d; }else{ r = []; for(var j = 0; j < i; j++){ r[++ri] = cs[j]; } for(j = i+1; cj = cs[j]; j++){ if(cj._nodup != d){ cj._nodup = d; r[++ri] = cj; } } return r; } } return r; } function quickDiffIEXml(c1, c2){ var d = ++key, r = []; for(var i = 0, len = c1.length; i < len; i++){ c1[i].setAttribute("_qdiff", d); } for(var i = 0, len = c2.length; i < len; i++){ if(c2[i].getAttribute("_qdiff") != d){ r[r.length] = c2[i]; } } for(var i = 0, len = c1.length; i < len; i++){ c1[i].removeAttribute("_qdiff"); } return r; } function quickDiff(c1, c2){ var len1 = c1.length, d = ++key, r = []; if(!len1){ return c2; } if(isIE && typeof c1[0].selectSingleNode != "undefined"){ return quickDiffIEXml(c1, c2); } for(var i = 0; i < len1; i++){ c1[i]._qdiff = d; } for(var i = 0, len = c2.length; i < len; i++){ if(c2[i]._qdiff != d){ r[r.length] = c2[i]; } } return r; } function quickId(ns, mode, root, id){ if(ns == root){ var d = root.ownerDocument || root; return d.getElementById(id); } ns = getNodes(ns, mode, "*"); return byId(ns, id); } return { getStyle : function(el, name){ return Ext.fly(el).getStyle(name); },
/** * Compiles a selector/xpath query into a reusable function. The returned function * takes one parameter "root" (optional), which is the context node from where the query should start. * @param {String} selector The selector/xpath query * @param {String} type (optional) Either "select" (the default) or "simple" for a simple selector match * @return {Function} */ compile : function(path, type){ type = type || "select"; // setup fn preamble var fn = ["var f = function(root){\n var mode; ++batch; var n = root || document;\n"], mode, lastPath, matchers = Ext.DomQuery.matchers, matchersLn = matchers.length, modeMatch, // accept leading mode switch lmode = path.match(modeRe); if(lmode && lmode[1]){ fn[fn.length] = 'mode="'+lmode[1].replace(trimRe, "")+'";'; path = path.replace(lmode[1], ""); } // strip leading slashes while(path.substr(0, 1)=="/"){ path = path.substr(1); } while(path && lastPath != path){ lastPath = path; var tokenMatch = path.match(tagTokenRe); if(type == "select"){ if(tokenMatch){ // ID Selector if(tokenMatch[1] == "#"){ fn[fn.length] = 'n = quickId(n, mode, root, "'+tokenMatch[2]+'");'; }else{ fn[fn.length] = 'n = getNodes(n, mode, "'+tokenMatch[2]+'");'; } path = path.replace(tokenMatch[0], ""); }else if(path.substr(0, 1) != '@'){ fn[fn.length] = 'n = getNodes(n, mode, "*");'; } // type of "simple" }else{ if(tokenMatch){ if(tokenMatch[1] == "#"){ fn[fn.length] = 'n = byId(n, "'+tokenMatch[2]+'");'; }else{ fn[fn.length] = 'n = byTag(n, "'+tokenMatch[2]+'");'; } path = path.replace(tokenMatch[0], ""); } } while(!(modeMatch = path.match(modeRe))){ var matched = false; for(var j = 0; j < matchersLn; j++){ var t = matchers[j]; var m = path.match(t.re); if(m){ fn[fn.length] = t.select.replace(tplRe, function(x, i){ return m[i]; }); path = path.replace(m[0], ""); matched = true; break; } } // prevent infinite loop on bad selector if(!matched){ throw 'Error parsing selector, parsing failed at "' + path + '"'; } } if(modeMatch[1]){ fn[fn.length] = 'mode="'+modeMatch[1].replace(trimRe, "")+'";'; path = path.replace(modeMatch[1], ""); } } // close fn out fn[fn.length] = "return nodup(n);\n}"; // eval fn and return it eval(fn.join("")); return f; },
/** * Selects a group of elements. * @param {String} selector The selector/xpath query (can be a comma separated list of selectors) * @param {Node/String} root (optional) The start of the query (defaults to document). * @return {Array} An Array of DOM elements which match the selector. If there are * no matches, and empty Array is returned. */ jsSelect: function(path, root, type){ // set root to doc if not specified. root = root || document; if(typeof root == "string"){ root = document.getElementById(root); } var paths = path.split(","), results = []; // loop over each selector for(var i = 0, len = paths.length; i < len; i++){ var subPath = paths[i].replace(trimRe, ""); // compile and place in cache if(!cache[subPath]){ cache[subPath] = Ext.DomQuery.compile(subPath); if(!cache[subPath]){ throw subPath + " is not a valid selector"; } } var result = cache[subPath](root); if(result && result != document){ results = results.concat(result); } } // if there were multiple selectors, make sure dups // are eliminated if(paths.length > 1){ return nodup(results); } return results; }, isXml: function(el) { var docEl = (el ? el.ownerDocument || el : 0).documentElement; return docEl ? docEl.nodeName !== "HTML" : false; }, select : document.querySelectorAll ? function(path, root, type) { root = root || document; if (!Ext.DomQuery.isXml(root)) { try { var cs = root.querySelectorAll(path); return Ext.toArray(cs); } catch (ex) {} } return Ext.DomQuery.jsSelect.call(this, path, root, type); } : function(path, root, type) { return Ext.DomQuery.jsSelect.call(this, path, root, type); },
/** * Selects a single element. * @param {String} selector The selector/xpath query * @param {Node} root (optional) The start of the query (defaults to document). * @return {Element} The DOM element which matched the selector. */ selectNode : function(path, root){ return Ext.DomQuery.select(path, root)[0]; },
/** * Selects the value of a node, optionally replacing null with the defaultValue. * @param {String} selector The selector/xpath query * @param {Node} root (optional) The start of the query (defaults to document). * @param {String} defaultValue * @return {String} */ selectValue : function(path, root, defaultValue){ path = path.replace(trimRe, ""); if(!valueCache[path]){ valueCache[path] = Ext.DomQuery.compile(path, "select"); } var n = valueCache[path](root), v; n = n[0] ? n[0] : n; // overcome a limitation of maximum textnode size // Rumored to potentially crash IE6 but has not been confirmed. // http://reference.sitepoint.com/javascript/Node/normalize // https://developer.mozilla.org/En/DOM/Node.normalize if (typeof n.normalize == 'function') n.normalize(); v = (n && n.firstChild ? n.firstChild.nodeValue : null); return ((v === null||v === undefined||v==='') ? defaultValue : v); },
/** * Selects the value of a node, parsing integers and floats. Returns the defaultValue, or 0 if none is specified. * @param {String} selector The selector/xpath query * @param {Node} root (optional) The start of the query (defaults to document). * @param {Number} defaultValue * @return {Number} */ selectNumber : function(path, root, defaultValue){ var v = Ext.DomQuery.selectValue(path, root, defaultValue || 0); return parseFloat(v); },
/** * Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child) * @param {String/HTMLElement/Array} el An element id, element or array of elements * @param {String} selector The simple selector to test * @return {Boolean} */ is : function(el, ss){ if(typeof el == "string"){ el = document.getElementById(el); } var isArray = Ext.isArray(el), result = Ext.DomQuery.filter(isArray ? el : [el], ss); return isArray ? (result.length == el.length) : (result.length > 0); },
/** * Filters an array of elements to only include matches of a simple selector (e.g. div.some-class or span:first-child) * @param {Array} el An array of elements to filter * @param {String} selector The simple selector to test * @param {Boolean} nonMatches If true, it returns the elements that DON'T match * the selector instead of the ones that match * @return {Array} An Array of DOM elements which match the selector. If there are * no matches, and empty Array is returned. */ filter : function(els, ss, nonMatches){ ss = ss.replace(trimRe, ""); if(!simpleCache[ss]){ simpleCache[ss] = Ext.DomQuery.compile(ss, "simple"); } var result = simpleCache[ss](els); return nonMatches ? quickDiff(result, els) : result; },
/** * Collection of matching regular expressions and code snippets. * Each capture group within () will be replace the {} in the select * statement as specified by their index. */ matchers : [{ re: /^\.([\w-]+)/, select: 'n = byClassName(n, " {1} ");' }, { re: /^\:([\w-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/, select: 'n = byPseudo(n, "{1}", "{2}");' },{ re: /^(?:([\[\{])(?:@)?([\w-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]\}])/, select: 'n = byAttribute(n, "{2}", "{4}", "{3}", "{1}");' }, { re: /^#([\w-]+)/, select: 'n = byId(n, "{1}");' },{ re: /^@([\w-]+)/, select: 'return {firstChild:{nodeValue:attrValue(n, "{1}")}};' } ],
/** * Collection of operator comparison functions. The default operators are =, !=, ^=, $=, *=, %=, |= and ~=. * New operators can be added as long as the match the format c= where c is any character other than space, > <. */ operators : { "=" : function(a, v){ return a == v; }, "!=" : function(a, v){ return a != v; }, "^=" : function(a, v){ return a && a.substr(0, v.length) == v; }, "$=" : function(a, v){ return a && a.substr(a.length-v.length) == v; }, "*=" : function(a, v){ return a && a.indexOf(v) !== -1; }, "%=" : function(a, v){ return (a % v) == 0; }, "|=" : function(a, v){ return a && (a == v || a.substr(0, v.length+1) == v+'-'); }, "~=" : function(a, v){ return a && (' '+a+' ').indexOf(' '+v+' ') != -1; } },
/** *

Object hash of "pseudo class" filter functions which are used when filtering selections. Each function is passed * two parameters:

*

A filter function returns an Array of DOM elements which conform to the pseudo class.

*

In addition to the provided pseudo classes listed above such as first-child and nth-child, * developers may add additional, custom psuedo class filters to select elements according to application-specific requirements.

*

For example, to filter <a> elements to only return links to external resources:

*
Ext.DomQuery.pseudos.external = function(c, v){
    var r = [], ri = -1;
    for(var i = 0, ci; ci = c[i]; i++){
//      Include in result set only if it's a link to an external resource
        if(ci.hostname != location.hostname){
            r[++ri] = ci;
        }
    }
    return r;
};
* Then external links could be gathered with the following statement:
var externalLinks = Ext.select("a:external");
*/ pseudos : { "first-child" : function(c){ var r = [], ri = -1, n; for(var i = 0, ci; ci = n = c[i]; i++){ while((n = n.previousSibling) && n.nodeType != 1); if(!n){ r[++ri] = ci; } } return r; }, "last-child" : function(c){ var r = [], ri = -1, n; for(var i = 0, ci; ci = n = c[i]; i++){ while((n = n.nextSibling) && n.nodeType != 1); if(!n){ r[++ri] = ci; } } return r; }, "nth-child" : function(c, a) { var r = [], ri = -1, m = nthRe.exec(a == "even" && "2n" || a == "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a), f = (m[1] || 1) - 0, l = m[2] - 0; for(var i = 0, n; n = c[i]; i++){ var pn = n.parentNode; if (batch != pn._batch) { var j = 0; for(var cn = pn.firstChild; cn; cn = cn.nextSibling){ if(cn.nodeType == 1){ cn.nodeIndex = ++j; } } pn._batch = batch; } if (f == 1) { if (l == 0 || n.nodeIndex == l){ r[++ri] = n; } } else if ((n.nodeIndex + l) % f == 0){ r[++ri] = n; } } return r; }, "only-child" : function(c){ var r = [], ri = -1;; for(var i = 0, ci; ci = c[i]; i++){ if(!prev(ci) && !next(ci)){ r[++ri] = ci; } } return r; }, "empty" : function(c){ var r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ var cns = ci.childNodes, j = 0, cn, empty = true; while(cn = cns[j]){ ++j; if(cn.nodeType == 1 || cn.nodeType == 3){ empty = false; break; } } if(empty){ r[++ri] = ci; } } return r; }, "contains" : function(c, v){ var r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ if((ci.textContent||ci.innerText||'').indexOf(v) != -1){ r[++ri] = ci; } } return r; }, "nodeValue" : function(c, v){ var r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ if(ci.firstChild && ci.firstChild.nodeValue == v){ r[++ri] = ci; } } return r; }, "checked" : function(c){ var r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ if(ci.checked == true){ r[++ri] = ci; } } return r; }, "not" : function(c, ss){ return Ext.DomQuery.filter(c, ss, true); }, "any" : function(c, selectors){ var ss = selectors.split('|'), r = [], ri = -1, s; for(var i = 0, ci; ci = c[i]; i++){ for(var j = 0; s = ss[j]; j++){ if(Ext.DomQuery.is(ci, s)){ r[++ri] = ci; break; } } } return r; }, "odd" : function(c){ return this["nth-child"](c, "odd"); }, "even" : function(c){ return this["nth-child"](c, "even"); }, "nth" : function(c, a){ return c[a-1] || []; }, "first" : function(c){ return c[0] || []; }, "last" : function(c){ return c[c.length-1] || []; }, "has" : function(c, ss){ var s = Ext.DomQuery.select, r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ if(s(ss, ci).length > 0){ r[++ri] = ci; } } return r; }, "next" : function(c, ss){ var is = Ext.DomQuery.is, r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ var n = next(ci); if(n && is(n, ss)){ r[++ri] = ci; } } return r; }, "prev" : function(c, ss){ var is = Ext.DomQuery.is, r = [], ri = -1; for(var i = 0, ci; ci = c[i]; i++){ var n = prev(ci); if(n && is(n, ss)){ r[++ri] = ci; } } return r; } } }; }();
/** * Selects an array of DOM nodes by CSS/XPath selector. Shorthand of {@link Ext.DomQuery#select} * @param {String} path The selector/xpath query * @param {Node} root (optional) The start of the query (defaults to document). * @return {Array} * @member Ext * @method query */ Ext.query = Ext.DomQuery.select;