New file |
0,0 → 1,1085 |
if(!dojo._hasResource["dojo._base.query"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. |
dojo._hasResource["dojo._base.query"] = true; |
dojo.provide("dojo._base.query"); |
dojo.require("dojo._base.NodeList"); |
|
/* |
dojo.query() architectural overview: |
|
dojo.query is a relatively full-featured CSS3 query library. It is |
designed to take any valid CSS3 selector and return the nodes matching |
the selector. To do this quickly, it processes queries in several |
steps, applying caching where profitable. |
|
The steps (roughly in reverse order of the way they appear in the code): |
1.) check to see if we already have a "query dispatcher" |
- if so, use that with the given parameterization. Skip to step 4. |
2.) attempt to determine which branch to dispatch the query to: |
- JS (optimized DOM iteration) |
- xpath (for browsers that support it and where it's fast) |
- native (not available in any browser yet) |
3.) tokenize and convert to executable "query dispatcher" |
- this is where the lion's share of the complexity in the |
system lies. In the DOM version, the query dispatcher is |
assembled as a chain of "yes/no" test functions pertaining to |
a section of a simple query statement (".blah:nth-child(odd)" |
but not "div div", which is 2 simple statements). Individual |
statement dispatchers are cached (to prevent re-definition) |
as are entire dispatch chains (to make re-execution of the |
same query fast) |
- in the xpath path, tokenization yeilds a concatenation of |
parameterized xpath selectors. As with the DOM version, both |
simple selector blocks and overall evaluators are cached to |
prevent re-defintion |
4.) the resulting query dispatcher is called in the passed scope (by default the top-level document) |
- for DOM queries, this results in a recursive, top-down |
evaluation of nodes based on each simple query section |
- xpath queries can, thankfully, be executed in one shot |
5.) matched nodes are pruned to ensure they are unique |
*/ |
|
;(function(){ |
// define everything in a closure for compressability reasons. "d" is an |
// alias to "dojo" since it's so frequently used. This seems a |
// transformation that the build system could perform on a per-file basis. |
|
//////////////////////////////////////////////////////////////////////// |
// Utility code |
//////////////////////////////////////////////////////////////////////// |
|
var d = dojo; |
var childNodesName = dojo.isIE ? "children" : "childNodes"; |
|
var getQueryParts = function(query){ |
// summary: state machine for query tokenization |
if(query.charAt(query.length-1) == ">"){ |
query += " *" |
} |
query += " "; // ensure that we terminate the state machine |
|
var ts = function(s, e){ |
return d.trim(query.slice(s, e)); |
} |
|
// the overall data graph of the full query, as represented by queryPart objects |
var qparts = []; |
// state keeping vars |
var inBrackets = -1; |
var inParens = -1; |
var inMatchFor = -1; |
var inPseudo = -1; |
var inClass = -1; |
var inId = -1; |
var inTag = -1; |
var lc = ""; // the last character |
var cc = ""; // the current character |
var pStart; |
// iteration vars |
var x = 0; // index in the query |
var ql = query.length; |
var currentPart = null; // data structure representing the entire clause |
var _cp = null; // the current pseudo or attr matcher |
|
var endTag = function(){ |
if(inTag >= 0){ |
var tv = (inTag == x) ? null : ts(inTag, x).toLowerCase(); |
currentPart[ (">~+".indexOf(tv) < 0)? "tag" : "oper" ] = tv; |
inTag = -1; |
} |
} |
|
var endId = function(){ |
if(inId >= 0){ |
currentPart.id = ts(inId, x).replace(/\\/g, ""); |
inId = -1; |
} |
} |
|
var endClass = function(){ |
if(inClass >= 0){ |
currentPart.classes.push(ts(inClass+1, x).replace(/\\/g, "")); |
inClass = -1; |
} |
} |
|
var endAll = function(){ |
endId(); endTag(); endClass(); |
} |
|
for(; x<ql, lc=cc, cc=query.charAt(x); x++){ |
if(lc == "\\"){ continue; } |
if(!currentPart){ |
// NOTE: I hate all this alloc, but it's shorter than writing tons of if's |
pStart = x; |
currentPart = { |
query: null, |
pseudos: [], |
attrs: [], |
classes: [], |
tag: null, |
oper: null, |
id: null |
}; |
inTag = x; |
} |
|
if(inBrackets >= 0){ |
// look for a the close first |
if(cc == "]"){ |
if(!_cp.attr){ |
_cp.attr = ts(inBrackets+1, x); |
}else{ |
_cp.matchFor = ts((inMatchFor||inBrackets+1), x); |
} |
var cmf = _cp.matchFor; |
if(cmf){ |
if( (cmf.charAt(0) == '"') || (cmf.charAt(0) == "'") ){ |
_cp.matchFor = cmf.substring(1, cmf.length-1); |
} |
} |
currentPart.attrs.push(_cp); |
_cp = null; // necessaray? |
inBrackets = inMatchFor = -1; |
}else if(cc == "="){ |
var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : ""; |
_cp.type = addToCc+cc; |
_cp.attr = ts(inBrackets+1, x-addToCc.length); |
inMatchFor = x+1; |
} |
// now look for other clause parts |
}else if(inParens >= 0){ |
if(cc == ")"){ |
if(inPseudo >= 0){ |
_cp.value = ts(inParens+1, x); |
} |
inPseudo = inParens = -1; |
} |
}else if(cc == "#"){ |
endAll(); |
inId = x+1; |
}else if(cc == "."){ |
endAll(); |
inClass = x; |
}else if(cc == ":"){ |
endAll(); |
inPseudo = x; |
}else if(cc == "["){ |
endAll(); |
inBrackets = x; |
_cp = { |
/*===== |
attr: null, type: null, matchFor: null |
=====*/ |
}; |
}else if(cc == "("){ |
if(inPseudo >= 0){ |
_cp = { |
name: ts(inPseudo+1, x), |
value: null |
} |
currentPart.pseudos.push(_cp); |
} |
inParens = x; |
}else if(cc == " " && lc != cc){ |
// note that we expect the string to be " " terminated |
endAll(); |
if(inPseudo >= 0){ |
currentPart.pseudos.push({ name: ts(inPseudo+1, x) }); |
} |
currentPart.hasLoops = ( |
currentPart.pseudos.length || |
currentPart.attrs.length || |
currentPart.classes.length ); |
currentPart.query = ts(pStart, x); |
currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*"); |
qparts.push(currentPart); |
currentPart = null; |
} |
} |
return qparts; |
}; |
|
|
//////////////////////////////////////////////////////////////////////// |
// XPath query code |
//////////////////////////////////////////////////////////////////////// |
|
// this array is a lookup used to generate an attribute matching function. |
// There is a similar lookup/generator list for the DOM branch with similar |
// calling semantics. |
var xPathAttrs = { |
"*=": function(attr, value){ |
return "[contains(@"+attr+", '"+ value +"')]"; |
}, |
"^=": function(attr, value){ |
return "[starts-with(@"+attr+", '"+ value +"')]"; |
}, |
"$=": function(attr, value){ |
return "[substring(@"+attr+", string-length(@"+attr+")-"+(value.length-1)+")='"+value+"']"; |
}, |
"~=": function(attr, value){ |
return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]"; |
}, |
"|=": function(attr, value){ |
return "[contains(concat(' ',@"+attr+",' '), ' "+ value +"-')]"; |
}, |
"=": function(attr, value){ |
return "[@"+attr+"='"+ value +"']"; |
} |
}; |
|
// takes a list of attribute searches, the overall query, a function to |
// generate a default matcher, and a closure-bound method for providing a |
// matching function that generates whatever type of yes/no distinguisher |
// the query method needs. The method is a bit tortured and hard to read |
// because it needs to be used in both the XPath and DOM branches. |
var handleAttrs = function( attrList, |
query, |
getDefault, |
handleMatch){ |
d.forEach(query.attrs, function(attr){ |
var matcher; |
// type, attr, matchFor |
if(attr.type && attrList[attr.type]){ |
matcher = attrList[attr.type](attr.attr, attr.matchFor); |
}else if(attr.attr.length){ |
matcher = getDefault(attr.attr); |
} |
if(matcher){ handleMatch(matcher); } |
}); |
} |
|
var buildPath = function(query){ |
var xpath = "."; |
var qparts = getQueryParts(d.trim(query)); |
while(qparts.length){ |
var tqp = qparts.shift(); |
var prefix; |
// FIXME: need to add support for ~ and + |
if(tqp.oper == ">"){ |
prefix = "/"; |
// prefix = "/child::node()"; |
tqp = qparts.shift(); |
}else{ |
prefix = "//"; |
// prefix = "/descendant::node()" |
} |
|
// get the tag name (if any) |
|
xpath += prefix + tqp.tag; |
|
// check to see if it's got an id. Needs to come first in xpath. |
if(tqp.id){ |
xpath += "[@id='"+tqp.id+"'][1]"; |
} |
|
d.forEach(tqp.classes, function(cn){ |
var cnl = cn.length; |
var padding = " "; |
if(cn.charAt(cnl-1) == "*"){ |
padding = ""; cn = cn.substr(0, cnl-1); |
} |
xpath += |
"[contains(concat(' ',@class,' '), ' "+ |
cn + padding + "')]"; |
}); |
|
handleAttrs(xPathAttrs, tqp, |
function(condition){ |
return "[@"+condition+"]"; |
}, |
function(matcher){ |
xpath += matcher; |
} |
); |
|
// FIXME: need to implement pseudo-class checks!! |
}; |
return xpath; |
}; |
|
var _xpathFuncCache = {}; |
var getXPathFunc = function(path){ |
if(_xpathFuncCache[path]){ |
return _xpathFuncCache[path]; |
} |
|
var doc = d.doc; |
// var parent = d.body(); // FIXME |
// FIXME: don't need to memoize. The closure scope handles it for us. |
var xpath = buildPath(path); |
|
var tf = function(parent){ |
// XPath query strings are memoized. |
var ret = []; |
var xpathResult; |
try{ |
xpathResult = doc.evaluate(xpath, parent, null, |
// XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); |
XPathResult.ANY_TYPE, null); |
}catch(e){ |
console.debug("failure in exprssion:", xpath, "under:", parent); |
console.debug(e); |
} |
var result = xpathResult.iterateNext(); |
while(result){ |
ret.push(result); |
result = xpathResult.iterateNext(); |
} |
return ret; |
} |
return _xpathFuncCache[path] = tf; |
}; |
|
/* |
d.xPathMatch = function(query){ |
// XPath based DOM query system. Handles a small subset of CSS |
// selectors, subset is identical to the non-XPath version of this |
// function. |
|
// FIXME: need to add support for alternate roots |
return getXPathFunc(query)(); |
} |
*/ |
|
//////////////////////////////////////////////////////////////////////// |
// DOM query code |
//////////////////////////////////////////////////////////////////////// |
|
var _filtersCache = {}; |
var _simpleFiltersCache = {}; |
|
// the basic building block of the yes/no chaining system. agree(f1, f2) |
// generates a new function which returns the boolean results of both of |
// the passed functions to a single logical-anded result. |
var agree = function(first, second){ |
if(!first){ return second; } |
if(!second){ return first; } |
|
return function(){ |
return first.apply(window, arguments) && second.apply(window, arguments); |
} |
} |
|
var _filterDown = function(element, queryParts, matchArr, idx){ |
var nidx = idx+1; |
var isFinal = (queryParts.length == nidx); |
var tqp = queryParts[idx]; |
|
// see if we can constrain our next level to direct children |
if(tqp.oper == ">"){ |
var ecn = element[childNodesName]; |
if(!ecn || !ecn.length){ |
return; |
} |
nidx++; |
isFinal = (queryParts.length == nidx); |
// kinda janky, too much array alloc |
var tf = getFilterFunc(queryParts[idx+1]); |
// for(var x=ecn.length-1, te; x>=0, te=ecn[x]; x--){ |
for(var x=0, ecnl=ecn.length, te; x<ecnl, te=ecn[x]; x++){ |
if(tf(te)){ |
if(isFinal){ |
matchArr.push(te); |
}else{ |
_filterDown(te, queryParts, matchArr, nidx); |
} |
} |
/* |
if(x==0){ |
break; |
} |
*/ |
} |
} |
|
// otherwise, keep going down, unless we'er at the end |
var candidates = getElementsFunc(tqp)(element); |
if(isFinal){ |
while(candidates.length){ |
matchArr.push(candidates.shift()); |
} |
/* |
candidates.unshift(0, matchArr.length-1); |
matchArr.splice.apply(matchArr, candidates); |
*/ |
}else{ |
// if we're not yet at the bottom, keep going! |
while(candidates.length){ |
_filterDown(candidates.shift(), queryParts, matchArr, nidx); |
} |
} |
} |
|
var filterDown = function(elements, queryParts){ |
var ret = []; |
|
// for every root, get the elements that match the descendant selector |
// for(var x=elements.length-1, te; x>=0, te=elements[x]; x--){ |
var x = elements.length - 1, te; |
while(te = elements[x--]){ |
_filterDown(te, queryParts, ret, 0); |
} |
return ret; |
} |
|
var getFilterFunc = function(q){ |
// note: query can't have spaces! |
if(_filtersCache[q.query]){ |
return _filtersCache[q.query]; |
} |
var ff = null; |
|
// does it have a tagName component? |
if(q.tag){ |
if(q.tag == "*"){ |
ff = agree(ff, |
function(elem){ |
return (elem.nodeType == 1); |
} |
); |
}else{ |
// tag name match |
ff = agree(ff, |
function(elem){ |
return ( |
(elem.nodeType == 1) && |
(q.tag == elem.tagName.toLowerCase()) |
); |
// return isTn; |
} |
); |
} |
} |
|
// does the node have an ID? |
if(q.id){ |
ff = agree(ff, |
function(elem){ |
return ( |
(elem.nodeType == 1) && |
(elem.id == q.id) |
); |
} |
); |
} |
|
if(q.hasLoops){ |
// if we have other query param parts, make sure we add them to the |
// filter chain |
ff = agree(ff, getSimpleFilterFunc(q)); |
} |
|
return _filtersCache[q.query] = ff; |
} |
|
var getNodeIndex = function(node){ |
// NOTE: |
// we could have a more accurate caching mechanism by invalidating |
// caches after the query has finished, but I think that'd lead to |
// significantly more cache churn than the cache would provide |
// value for in the common case. Generally, we're more |
// conservative (and therefore, more accurate) than jQuery and |
// DomQuery WRT node node indexes, but there may be corner cases |
// in which we fall down. How much we care about them is TBD. |
|
var pn = node.parentNode; |
var pnc = pn.childNodes; |
|
// check to see if we can trust the cache. If not, re-key the whole |
// thing and return our node match from that. |
|
var nidx = -1; |
var child = pn.firstChild; |
if(!child){ |
return nidx; |
} |
|
var ci = node["__cachedIndex"]; |
var cl = pn["__cachedLength"]; |
|
// only handle cache building if we've gone out of sync |
if(((typeof cl == "number")&&(cl != pnc.length))||(typeof ci != "number")){ |
// rip though the whole set, building cache indexes as we go |
pn["__cachedLength"] = pnc.length; |
var idx = 1; |
do{ |
// we only assign indexes for nodes with nodeType == 1, as per: |
// http://www.w3.org/TR/css3-selectors/#nth-child-pseudo |
// only elements are counted in the search order, and they |
// begin at 1 for the first child's index |
|
if(child === node){ |
nidx = idx; |
} |
if(child.nodeType == 1){ |
child["__cachedIndex"] = idx; |
idx++; |
} |
child = child.nextSibling; |
}while(child); |
}else{ |
// NOTE: |
// could be incorrect in some cases (node swaps involving the |
// passed node, etc.), but we ignore those due to the relative |
// unlikelihood of that occuring |
nidx = ci; |
} |
return nidx; |
} |
|
var firedCount = 0; |
|
var blank = ""; |
var _getAttr = function(elem, attr){ |
if(attr == "class"){ |
return elem.className || blank; |
} |
if(attr == "for"){ |
return elem.htmlFor || blank; |
} |
return elem.getAttribute(attr, 2) || blank; |
} |
|
var attrs = { |
"*=": function(attr, value){ |
return function(elem){ |
// E[foo*="bar"] |
// an E element whose "foo" attribute value contains |
// the substring "bar" |
return (_getAttr(elem, attr).indexOf(value)>=0); |
} |
}, |
"^=": function(attr, value){ |
// E[foo^="bar"] |
// an E element whose "foo" attribute value begins exactly |
// with the string "bar" |
return function(elem){ |
return (_getAttr(elem, attr).indexOf(value)==0); |
} |
}, |
"$=": function(attr, value){ |
// E[foo$="bar"] |
// an E element whose "foo" attribute value ends exactly |
// with the string "bar" |
var tval = " "+value; |
return function(elem){ |
var ea = " "+_getAttr(elem, attr); |
return (ea.lastIndexOf(value)==(ea.length-value.length)); |
} |
}, |
"~=": function(attr, value){ |
// E[foo~="bar"] |
// an E element whose "foo" attribute value is a list of |
// space-separated values, one of which is exactly equal |
// to "bar" |
|
// return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]"; |
var tval = " "+value+" "; |
return function(elem){ |
var ea = " "+_getAttr(elem, attr)+" "; |
return (ea.indexOf(tval)>=0); |
} |
}, |
"|=": function(attr, value){ |
// E[hreflang|="en"] |
// an E element whose "hreflang" attribute has a |
// hyphen-separated list of values beginning (from the |
// left) with "en" |
var valueDash = " "+value+"-"; |
return function(elem){ |
var ea = " "+(elem.getAttribute(attr, 2) || ""); |
return ( |
(ea == value) || |
(ea.indexOf(valueDash)==0) |
); |
} |
}, |
"=": function(attr, value){ |
return function(elem){ |
return (_getAttr(elem, attr) == value); |
} |
} |
}; |
|
var pseudos = { |
"first-child": function(name, condition){ |
return function(elem){ |
if(elem.nodeType != 1){ return false; } |
// check to see if any of the previous siblings are elements |
var fc = elem.previousSibling; |
while(fc && (fc.nodeType != 1)){ |
fc = fc.previousSibling; |
} |
return (!fc); |
} |
}, |
"last-child": function(name, condition){ |
return function(elem){ |
if(elem.nodeType != 1){ return false; } |
// check to see if any of the next siblings are elements |
var nc = elem.nextSibling; |
while(nc && (nc.nodeType != 1)){ |
nc = nc.nextSibling; |
} |
return (!nc); |
} |
}, |
"empty": function(name, condition){ |
return function(elem){ |
// DomQuery and jQuery get this wrong, oddly enough. |
// The CSS 3 selectors spec is pretty explicit about |
// it, too. |
var cn = elem.childNodes; |
var cnl = elem.childNodes.length; |
// if(!cnl){ return true; } |
for(var x=cnl-1; x >= 0; x--){ |
var nt = cn[x].nodeType; |
if((nt == 1)||(nt == 3)){ return false; } |
} |
return true; |
} |
}, |
/* non standard! |
"contains": function(name, condition){ |
return function(elem){ |
// FIXME: I dislike this version of "contains", as |
// whimsical attribute could set it off. An inner-text |
// based version might be more accurate, but since |
// jQuery and DomQuery also potentially get this wrong, |
// I'm leaving it for now. |
return (elem.innerHTML.indexOf(condition) >= 0); |
} |
}, |
*/ |
"not": function(name, condition){ |
var ntf = getFilterFunc(getQueryParts(condition)[0]); |
return function(elem){ |
return (!ntf(elem)); |
} |
}, |
"nth-child": function(name, condition){ |
var pi = parseInt; |
if(condition == "odd"){ |
return function(elem){ |
return ( |
((getNodeIndex(elem)) % 2) == 1 |
); |
} |
}else if((condition == "2n")|| |
(condition == "even")){ |
return function(elem){ |
return ((getNodeIndex(elem) % 2) == 0); |
} |
}else if(condition.indexOf("0n+") == 0){ |
var ncount = pi(condition.substr(3)); |
return function(elem){ |
return (elem.parentNode[childNodesName][ncount-1] === elem); |
} |
}else if( (condition.indexOf("n+") > 0) && |
(condition.length > 3) ){ |
var tparts = condition.split("n+", 2); |
var pred = pi(tparts[0]); |
var idx = pi(tparts[1]); |
return function(elem){ |
return ((getNodeIndex(elem) % pred) == idx); |
} |
}else if(condition.indexOf("n") == -1){ |
var ncount = pi(condition); |
return function(elem){ |
return (getNodeIndex(elem) == ncount); |
} |
} |
} |
}; |
|
var defaultGetter = (d.isIE) ? function(cond){ |
var clc = cond.toLowerCase(); |
return function(elem){ |
return elem[cond]||elem[clc]; |
} |
} : function(cond){ |
return function(elem){ |
return (elem && elem.getAttribute && elem.hasAttribute(cond)); |
} |
}; |
|
var getSimpleFilterFunc = function(query){ |
|
var fcHit = (_simpleFiltersCache[query.query]||_filtersCache[query.query]); |
if(fcHit){ return fcHit; } |
|
var ff = null; |
|
// the only case where we'll need the tag name is if we came from an ID query |
if(query.id){ // do we have an ID component? |
if(query.tag != "*"){ |
ff = agree(ff, function(elem){ |
return (elem.tagName.toLowerCase() == query.tag); |
}); |
} |
} |
|
// if there's a class in our query, generate a match function for it |
d.forEach(query.classes, function(cname, idx, arr){ |
// get the class name |
var isWildcard = cname.charAt(cname.length-1) == "*"; |
if(isWildcard){ |
cname = cname.substr(0, cname.length-1); |
} |
// I dislike the regex thing, even if memozied in a cache, but it's VERY short |
var re = new RegExp("(?:^|\\s)" + cname + (isWildcard ? ".*" : "") + "(?:\\s|$)"); |
ff = agree(ff, function(elem){ |
return re.test(elem.className); |
}); |
ff.count = idx; |
}); |
|
d.forEach(query.pseudos, function(pseudo){ |
if(pseudos[pseudo.name]){ |
ff = agree(ff, pseudos[pseudo.name](pseudo.name, pseudo.value)); |
} |
}); |
|
handleAttrs(attrs, query, defaultGetter, |
function(tmatcher){ ff = agree(ff, tmatcher); } |
); |
if(!ff){ |
ff = function(){ return true; }; |
} |
return _simpleFiltersCache[query.query] = ff; |
} |
|
var _getElementsFuncCache = { }; |
|
var getElementsFunc = function(query, root){ |
var fHit = _getElementsFuncCache[query.query]; |
if(fHit){ return fHit; } |
|
// NOTE: this function is in the fast path! not memoized!!! |
|
// the query doesn't contain any spaces, so there's only so many |
// things it could be |
|
if(query.id && !query.hasLoops && !query.tag){ |
// ID-only query. Easy. |
return _getElementsFuncCache[query.query] = function(root){ |
// FIXME: if root != document, check for parenting! |
return [ d.byId(query.id) ]; |
} |
} |
|
var filterFunc = getSimpleFilterFunc(query); |
|
var retFunc; |
if(query.tag && query.id && !query.hasLoops){ |
// we got a filtered ID search (e.g., "h4#thinger") |
retFunc = function(root){ |
var te = d.byId(query.id); |
if(filterFunc(te)){ |
return [ te ]; |
} |
} |
}else{ |
var tret; |
|
if(!query.hasLoops){ |
// it's just a plain-ol elements-by-tag-name query from the root |
retFunc = function(root){ |
var ret = []; |
var te, x=0, tret = root.getElementsByTagName(query.tag); |
while(te=tret[x++]){ |
ret.push(te); |
} |
return ret; |
} |
}else{ |
retFunc = function(root){ |
var ret = []; |
var te, x=0, tret = root.getElementsByTagName(query.tag); |
while(te=tret[x++]){ |
if(filterFunc(te)){ |
ret.push(te); |
} |
} |
return ret; |
} |
} |
} |
return _getElementsFuncCache[query.query] = retFunc; |
} |
|
var _partsCache = {}; |
|
//////////////////////////////////////////////////////////////////////// |
// the query runner |
//////////////////////////////////////////////////////////////////////// |
|
// this is the second level of spliting, from full-length queries (e.g., |
// "div.foo .bar") into simple query expressions (e.g., ["div.foo", |
// ".bar"]) |
var _queryFuncCache = { |
"*": d.isIE ? |
function(root){ |
return root.all; |
} : |
function(root){ |
return root.getElementsByTagName("*"); |
}, |
">": function(root){ |
var ret = []; |
var te, x=0, tret = root[childNodesName]; |
while(te=tret[x++]){ |
if(te.nodeType == 1){ ret.push(te); } |
} |
return ret; |
} |
}; |
|
var getStepQueryFunc = function(query){ |
// if it's trivial, get a fast-path dispatcher |
var qparts = getQueryParts(d.trim(query)); |
// if(query[query.length-1] == ">"){ query += " *"; } |
if(qparts.length == 1){ |
var tt = getElementsFunc(qparts[0]); |
tt.nozip = true; |
return tt; |
} |
|
// otherwise, break it up and return a runner that iterates over the parts recursively |
var sqf = function(root){ |
var localQueryParts = qparts.slice(0); // clone the src arr |
var candidates; |
if(localQueryParts[0].oper == ">"){ |
candidates = [ root ]; |
// root = document; |
}else{ |
candidates = getElementsFunc(localQueryParts.shift())(root); |
} |
return filterDown(candidates, localQueryParts); |
} |
return sqf; |
} |
|
// a specialized method that implements our primoridal "query optimizer". |
// This allows us to dispatch queries to the fastest subsystem we can get. |
var _getQueryFunc = ( |
// NOTE: |
// XPath on the Webkit nighlies is slower than it's DOM iteration |
// for most test cases |
// FIXME: |
// we should try to capture some runtime speed data for each query |
// function to determine on the fly if we should stick w/ the |
// potentially optimized variant or if we should try something |
// new. |
(document["evaluate"] && !d.isSafari) ? |
function(query){ |
// has xpath support that's faster than DOM |
var qparts = query.split(" "); |
// can we handle it? |
if( (document["evaluate"])&& |
(query.indexOf(":") == -1)&& |
( |
(true) // || |
// (query.indexOf("[") == -1) || |
// (query.indexOf("=") == -1) |
) |
){ |
// dojo.debug(query); |
// should we handle it? |
|
// kind of a lame heuristic, but it works |
if( |
// a "div div div" style query |
((qparts.length > 2)&&(query.indexOf(">") == -1))|| |
// or something else with moderate complexity. kinda janky |
(qparts.length > 3)|| |
(query.indexOf("[")>=0)|| |
// or if it's a ".thinger" query |
((1 == qparts.length)&&(0 <= query.indexOf("."))) |
|
){ |
// use get and cache a xpath runner for this selector |
return getXPathFunc(query); |
} |
} |
|
// fallthrough |
return getStepQueryFunc(query); |
} : getStepQueryFunc |
); |
// uncomment to disable XPath for testing and tuning the DOM path |
// _getQueryFunc = getStepQueryFunc; |
|
// FIXME: we've got problems w/ the NodeList query()/filter() functions if we go XPath for everything |
|
// uncomment to disable DOM queries for testing and tuning XPath |
// _getQueryFunc = getXPathFunc; |
|
// this is the primary caching for full-query results. The query dispatcher |
// functions are generated here and then pickled for hash lookup in the |
// future |
var getQueryFunc = function(query){ |
// return a cached version if one is available |
if(_queryFuncCache[query]){ return _queryFuncCache[query]; } |
if(0 > query.indexOf(",")){ |
// if it's not a compound query (e.g., ".foo, .bar"), cache and return a dispatcher |
return _queryFuncCache[query] = _getQueryFunc(query); |
}else{ |
// if it's a complex query, break it up into it's constituent parts |
// and return a dispatcher that will merge the parts when run |
|
// var parts = query.split(", "); |
var parts = query.split(/\s*,\s*/); |
var tf = function(root){ |
var pindex = 0; // avoid array alloc for every invocation |
var ret = []; |
var tp; |
while(tp = parts[pindex++]){ |
ret = ret.concat(_getQueryFunc(tp, tp.indexOf(" "))(root)); |
} |
return ret; |
} |
// ...cache and return |
return _queryFuncCache[query] = tf; |
} |
} |
|
// FIXME: |
// Dean's new Base2 uses a system whereby queries themselves note if |
// they'll need duplicate filtering. We need to get on that plan!! |
|
// attempt to efficiently determine if an item in a list is a dupe, |
// returning a list of "uniques", hopefully in doucment order |
var _zipIdx = 0; |
var _zip = function(arr){ |
if(arr && arr.nozip){ return d.NodeList._wrap(arr); } |
var ret = new d.NodeList(); |
if(!arr){ return ret; } |
if(arr[0]){ |
ret.push(arr[0]); |
} |
if(arr.length < 2){ return ret; } |
_zipIdx++; |
arr[0]["_zipIdx"] = _zipIdx; |
for(var x=1, te; te = arr[x]; x++){ |
if(arr[x]["_zipIdx"] != _zipIdx){ |
ret.push(te); |
} |
te["_zipIdx"] = _zipIdx; |
} |
// FIXME: should we consider stripping these properties? |
return ret; |
} |
|
// the main exectuor |
d.query = function(query, root){ |
// summary: |
// returns nodes which match the given CSS3 selector, searching the |
// entire document by default but optionally taking a node to scope |
// the search by. Returns an instance of dojo.NodeList. |
// description: |
// dojo.query() is the swiss army knife of DOM node manipulation in |
// Dojo. Much like Prototype's "$$" (bling-bling) function or JQuery's |
// "$" function, dojo.query provides robust, high-performance |
// CSS-based node selector support with the option of scoping searches |
// to a particular sub-tree of a document. |
// |
// Supported Selectors: |
// -------------------- |
// |
// dojo.query() supports a rich set of CSS3 selectors, including: |
// |
// * class selectors (e.g., ".foo") |
// * node type selectors like "span" |
// * " " descendant selectors |
// * ">" child element selectors |
// * "#foo" style ID selectors |
// * "*" universal selector |
// * attribute queries: |
// * "[foo]" attribute presence selector |
// * "[foo='bar']" attribute value exact match |
// * "[foo~='bar']" attribute value list item match |
// * "[foo^='bar']" attribute start match |
// * "[foo$='bar']" attribute end match |
// * "[foo*='bar']" attribute substring match |
// * ":first-child", ":last-child" positional selectors |
// * ":nth-child(n)", ":nth-child(2n+1)" style positional calculations |
// * ":nth-child(even)", ":nth-child(odd)" positional selectors |
// * ":not(...)" negation pseudo selectors |
// |
// Any legal combination of those selector types as per the CSS 3 sepc |
// will work with dojo.query(), including compound selectors ("," |
// delimited). Very complex and useful searches can be constructed |
// with this palette of selectors and when combined with functions for |
// maniplation presented by dojo.NodeList, many types of DOM |
// manipulation operations become very straightforward. |
// |
// Unsupported Selectors: |
// -------------------- |
// |
// While dojo.query handles many CSS3 selectors, some fall outside of |
// what's resaonable for a programmatic node querying engine to |
// handle. Currently unsupported selectors include: |
// |
// * namespace-differentiated selectors of any form |
// * "~", the immediately preceeded-by sibling selector |
// * "+", the preceeded-by sibling selector |
// * all "::" pseduo-element selectors |
// * certain pseduo-selectors which don't get a lot of day-to-day use: |
// * :root, :lang(), :target, :focus |
// * all visual and state selectors: |
// * :root, :active, :hover, :visisted, :link, :enabled, :disabled, :checked |
// * :*-of-type pseudo selectors |
// |
// dojo.query and XML Documents: |
// ----------------------------- |
// FIXME |
// |
// query: String |
// The CSS3 expression to match against. For details on the syntax of |
// CSS3 selectors, see: |
// http://www.w3.org/TR/css3-selectors/#selectors |
// root: String|DOMNode? |
// A node (or string ID of a node) to scope the search from. Optional. |
// returns: |
// An instance of dojo.NodeList. Many methods are available on |
// NodeLists for searching, iterating, manipulating, and handling |
// events on the matched nodes in the returned list. |
|
// return is always an array |
// NOTE: elementsById is not currently supported |
// NOTE: ignores xpath-ish queries for now |
if(query.constructor == d.NodeList){ |
return query; |
} |
if(!d.isString(query)){ |
return new d.NodeList(query); // dojo.NodeList |
} |
if(d.isString(root)){ |
root = d.byId(root); |
} |
|
// FIXME: should support more methods on the return than the stock array. |
return _zip(getQueryFunc(query)(root||d.doc)); |
} |
|
/* |
// exposing these was a mistake |
d.query.attrs = attrs; |
d.query.pseudos = pseudos; |
*/ |
|
// one-off function for filtering a NodeList based on a simple selector |
d._filterQueryResult = function(nodeList, simpleFilter){ |
var tnl = new d.NodeList(); |
var ff = (simpleFilter) ? getFilterFunc(getQueryParts(simpleFilter)[0]) : function(){ return true; }; |
for(var x=0, te; te = nodeList[x]; x++){ |
if(ff(te)){ tnl.push(te); } |
} |
return tnl; |
} |
})(); |
|
} |