diff options
author | 2008-11-13 09:49:11 +0000 | |
---|---|---|
committer | 2008-11-13 09:49:11 +0000 | |
commit | e44a7e37b6c7b5961adaffc62b9042b8d442938e (patch) | |
tree | 95b67c356e93163467db2451f2b8cce84ed5d582 /includes/js/dojo/_base/query.js | |
parent | a62b9742ee5e28bcec6872d88f50f25b820914f6 (diff) | |
download | scuttle-e44a7e37b6c7b5961adaffc62b9042b8d442938e.tar.gz scuttle-e44a7e37b6c7b5961adaffc62b9042b8d442938e.zip |
New feature: basic Ajax suggestion for tags and implementation of Dojo toolkit
git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@151 b3834d28-1941-0410-a4f8-b48e95affb8f
Diffstat (limited to 'includes/js/dojo/_base/query.js')
-rw-r--r-- | includes/js/dojo/_base/query.js | 1191 |
1 files changed, 1191 insertions, 0 deletions
diff --git a/includes/js/dojo/_base/query.js b/includes/js/dojo/_base/query.js new file mode 100644 index 0000000..8743df8 --- /dev/null +++ b/includes/js/dojo/_base/query.js @@ -0,0 +1,1191 @@ +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 caseSensitive = false; + + var getQueryParts = function(query){ + // summary: state machine for query tokenization + if(">~+".indexOf(query.charAt(query.length-1)) >= 0){ + 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(; lc=cc, cc=query.charAt(x),x<ql; 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; + var postfix = ""; + if(tqp.oper == ">"){ + prefix = "/"; + // prefix = "/child::*"; + tqp = qparts.shift(); + }else if(tqp.oper == "~"){ + prefix = "/following-sibling::"; // get element following siblings + tqp = qparts.shift(); + }else if(tqp.oper == "+"){ + // FIXME: + // fails when selecting subsequent siblings by node type + // because the position() checks the position in the list + // of matching elements and not the localized siblings + prefix = "/following-sibling::"; + postfix = "[position()=1]"; + tqp = qparts.shift(); + }else{ + prefix = "//"; + // prefix = "/descendant::*" + } + + // get the tag name (if any) + + xpath += prefix + tqp.tag + postfix; + + // 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; + // 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. + + 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 _childElements = 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 _nextSiblings = function(root, single){ + var ret = []; + var te = root; + while(te = te.nextSibling){ + if(te.nodeType == 1){ + ret.push(te); + if(single){ break; } + } + } + return ret; + } + + var _filterDown = function(element, queryParts, matchArr, idx){ + // NOTE: + // in the fast path! this function is called recursively and for + // every run of a query. + 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 = (tqp.oper == ">") ? + _childElements(element) : + _nextSiblings(element, (tqp.oper == "+")); + + 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; + } + }, + "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("*"); + }, + "~": _nextSiblings, + "+": function(root){ return _nextSiblings(root, true); }, + ">": _childElements + }; + + 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 == ">"){ // FIXME: what if it's + or ~? + 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)&& + (query.indexOf("+") == -1) // skip direct sibling matches. See line ~344 + ){ + // 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 + var qcz = query.charAt(0); + if(d.doc["querySelectorAll"] && + ( (!d.isSafari) || (d.isSafari > 3.1) ) && // see #5832 + // as per CSS 3, we can't currently start w/ combinator: + // http://www.w3.org/TR/css3-selectors/#w3cselgrammar + (">+~".indexOf(qcz) == -1) + ){ + return function(root){ + var r = root.querySelectorAll(query); + r.nozip = true; // skip expensive duplication checks and just wrap in a NodeList + return r; + }; + } + 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 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 executor + d.query = function(/*String*/ query, /*String|DOMNode?*/ 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 + // * `~`, the immediately preceeded-by sibling selector + // * `+`, the preceeded-by sibling 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 + // * `:empty` content emtpy selector + // * `:empty` content emtpy selector + // * `: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 these selectors 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 + // * 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: + // ----------------------------- + // + // `dojo.query` currently only supports searching XML documents + // whose tags and attributes are 100% lower-case. This is a known + // limitation and will [be addressed soon](http://trac.dojotoolkit.org/ticket/3866) + // Non-selector Queries: + // --------------------- + // + // If something other than a String is passed for the query, + // `dojo.query` will return a new `dojo.NodeList` constructed from + // that parameter alone and all further processing will stop. This + // means that if you have a reference to a node or NodeList, you + // can quickly construct a new NodeList from the original by + // calling `dojo.query(node)` or `dojo.query(list)`. + // + // query: + // The CSS3 expression to match against. For details on the syntax of + // CSS3 selectors, see <http://www.w3.org/TR/css3-selectors/#selectors> + // root: + // A DOMNode (or node id) to scope the search from. Optional. + // returns: dojo.NodeList + // 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. + // example: + // search the entire document for elements with the class "foo": + // | dojo.query(".foo"); + // these elements will match: + // | <span class="foo"></span> + // | <span class="foo bar"></span> + // | <p class="thud foo"></p> + // example: + // search the entire document for elements with the classes "foo" *and* "bar": + // | dojo.query(".foo.bar"); + // these elements will match: + // | <span class="foo bar"></span> + // while these will not: + // | <span class="foo"></span> + // | <p class="thud foo"></p> + // example: + // find `<span>` elements which are descendants of paragraphs and + // which have a "highlighted" class: + // | dojo.query("p span.highlighted"); + // the innermost span in this fragment matches: + // | <p class="foo"> + // | <span>... + // | <span class="highlighted foo bar">...</span> + // | </span> + // | </p> + // example: + // set an "odd" class on all odd table rows inside of the table + // `#tabular_data`, using the `>` (direct child) selector to avoid + // affecting any nested tables: + // | dojo.query("#tabular_data > tbody > tr:nth-child(odd)").addClass("odd"); + // example: + // remove all elements with the class "error" from the document + // and store them in a list: + // | var errors = dojo.query(".error").orphan(); + // example: + // add an onclick handler to every submit button in the document + // which causes the form to be sent via Ajax instead: + // | dojo.query("input[type='submit']").onclick(function(e){ + // | dojo.stopEvent(e); // prevent sending the form + // | var btn = e.target; + // | dojo.xhrPost({ + // | form: btn.form, + // | load: function(data){ + // | // replace the form with the response + // | var div = dojo.doc.createElement("div"); + // | dojo.place(div, btn.form, "after"); + // | div.innerHTML = data; + // | dojo.style(btn.form, "display", "none"); + // | } + // | }); + // | }); + + + // 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); + } + + return _zip(getQueryFunc(query)(root||d.doc)); // dojo.NodeList + } + + /* + // exposing this was a mistake + d.query.attrs = attrs; + */ + // exposing this because new pseudo matches are only executed through the + // DOM query path (never through the xpath optimizing branch) + 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; + } +})(); + +} |