1 | define(["../_base/kernel", "../has", "../dom", "../_base/sniff", "../_base/array", "../_base/lang", "../_base/window"], function(dojo, has, dom){ |
---|
2 | // module: |
---|
3 | // dojo/selector/acme |
---|
4 | // summary: |
---|
5 | // This module defines the Acme selector engine |
---|
6 | |
---|
7 | /* |
---|
8 | acme architectural overview: |
---|
9 | |
---|
10 | acme is a relatively full-featured CSS3 query library. It is |
---|
11 | designed to take any valid CSS3 selector and return the nodes matching |
---|
12 | the selector. To do this quickly, it processes queries in several |
---|
13 | steps, applying caching where profitable. |
---|
14 | |
---|
15 | The steps (roughly in reverse order of the way they appear in the code): |
---|
16 | 1.) check to see if we already have a "query dispatcher" |
---|
17 | - if so, use that with the given parameterization. Skip to step 4. |
---|
18 | 2.) attempt to determine which branch to dispatch the query to: |
---|
19 | - JS (optimized DOM iteration) |
---|
20 | - native (FF3.1+, Safari 3.1+, IE 8+) |
---|
21 | 3.) tokenize and convert to executable "query dispatcher" |
---|
22 | - this is where the lion's share of the complexity in the |
---|
23 | system lies. In the DOM version, the query dispatcher is |
---|
24 | assembled as a chain of "yes/no" test functions pertaining to |
---|
25 | a section of a simple query statement (".blah:nth-child(odd)" |
---|
26 | but not "div div", which is 2 simple statements). Individual |
---|
27 | statement dispatchers are cached (to prevent re-definition) |
---|
28 | as are entire dispatch chains (to make re-execution of the |
---|
29 | same query fast) |
---|
30 | 4.) the resulting query dispatcher is called in the passed scope |
---|
31 | (by default the top-level document) |
---|
32 | - for DOM queries, this results in a recursive, top-down |
---|
33 | evaluation of nodes based on each simple query section |
---|
34 | - for native implementations, this may mean working around spec |
---|
35 | bugs. So be it. |
---|
36 | 5.) matched nodes are pruned to ensure they are unique (if necessary) |
---|
37 | */ |
---|
38 | |
---|
39 | |
---|
40 | //////////////////////////////////////////////////////////////////////// |
---|
41 | // Toolkit aliases |
---|
42 | //////////////////////////////////////////////////////////////////////// |
---|
43 | |
---|
44 | // if you are extracting acme for use in your own system, you will |
---|
45 | // need to provide these methods and properties. No other porting should be |
---|
46 | // necessary, save for configuring the system to use a class other than |
---|
47 | // dojo.NodeList as the return instance instantiator |
---|
48 | var trim = dojo.trim; |
---|
49 | var each = dojo.forEach; |
---|
50 | // d.isIE; // float |
---|
51 | // d.isSafari; // float |
---|
52 | // d.isOpera; // float |
---|
53 | // d.isWebKit; // float |
---|
54 | // d.doc ; // document element |
---|
55 | |
---|
56 | var getDoc = function(){ return dojo.doc; }; |
---|
57 | // NOTE(alex): the spec is idiotic. CSS queries should ALWAYS be case-sensitive, but nooooooo |
---|
58 | var cssCaseBug = ((dojo.isWebKit||dojo.isMozilla) && ((getDoc().compatMode) == "BackCompat")); |
---|
59 | |
---|
60 | //////////////////////////////////////////////////////////////////////// |
---|
61 | // Global utilities |
---|
62 | //////////////////////////////////////////////////////////////////////// |
---|
63 | |
---|
64 | |
---|
65 | var specials = ">~+"; |
---|
66 | |
---|
67 | // global thunk to determine whether we should treat the current query as |
---|
68 | // case sensitive or not. This switch is flipped by the query evaluator |
---|
69 | // based on the document passed as the context to search. |
---|
70 | var caseSensitive = false; |
---|
71 | |
---|
72 | // how high? |
---|
73 | var yesman = function(){ return true; }; |
---|
74 | |
---|
75 | //////////////////////////////////////////////////////////////////////// |
---|
76 | // Tokenizer |
---|
77 | //////////////////////////////////////////////////////////////////////// |
---|
78 | |
---|
79 | var getQueryParts = function(query){ |
---|
80 | // summary: |
---|
81 | // state machine for query tokenization |
---|
82 | // description: |
---|
83 | // instead of using a brittle and slow regex-based CSS parser, |
---|
84 | // acme implements an AST-style query representation. This |
---|
85 | // representation is only generated once per query. For example, |
---|
86 | // the same query run multiple times or under different root nodes |
---|
87 | // does not re-parse the selector expression but instead uses the |
---|
88 | // cached data structure. The state machine implemented here |
---|
89 | // terminates on the last " " (space) character and returns an |
---|
90 | // ordered array of query component structures (or "parts"). Each |
---|
91 | // part represents an operator or a simple CSS filtering |
---|
92 | // expression. The structure for parts is documented in the code |
---|
93 | // below. |
---|
94 | |
---|
95 | |
---|
96 | // NOTE: |
---|
97 | // this code is designed to run fast and compress well. Sacrifices |
---|
98 | // to readability and maintainability have been made. Your best |
---|
99 | // bet when hacking the tokenizer is to put The Donnas on *really* |
---|
100 | // loud (may we recommend their "Spend The Night" release?) and |
---|
101 | // just assume you're gonna make mistakes. Keep the unit tests |
---|
102 | // open and run them frequently. Knowing is half the battle ;-) |
---|
103 | if(specials.indexOf(query.slice(-1)) >= 0){ |
---|
104 | // if we end with a ">", "+", or "~", that means we're implicitly |
---|
105 | // searching all children, so make it explicit |
---|
106 | query += " * " |
---|
107 | }else{ |
---|
108 | // if you have not provided a terminator, one will be provided for |
---|
109 | // you... |
---|
110 | query += " "; |
---|
111 | } |
---|
112 | |
---|
113 | var ts = function(/*Integer*/ s, /*Integer*/ e){ |
---|
114 | // trim and slice. |
---|
115 | |
---|
116 | // take an index to start a string slice from and an end position |
---|
117 | // and return a trimmed copy of that sub-string |
---|
118 | return trim(query.slice(s, e)); |
---|
119 | }; |
---|
120 | |
---|
121 | // the overall data graph of the full query, as represented by queryPart objects |
---|
122 | var queryParts = []; |
---|
123 | |
---|
124 | |
---|
125 | // state keeping vars |
---|
126 | var inBrackets = -1, inParens = -1, inMatchFor = -1, |
---|
127 | inPseudo = -1, inClass = -1, inId = -1, inTag = -1, |
---|
128 | lc = "", cc = "", pStart; |
---|
129 | |
---|
130 | // iteration vars |
---|
131 | var x = 0, // index in the query |
---|
132 | ql = query.length, |
---|
133 | currentPart = null, // data structure representing the entire clause |
---|
134 | _cp = null; // the current pseudo or attr matcher |
---|
135 | |
---|
136 | // several temporary variables are assigned to this structure during a |
---|
137 | // potential sub-expression match: |
---|
138 | // attr: |
---|
139 | // a string representing the current full attribute match in a |
---|
140 | // bracket expression |
---|
141 | // type: |
---|
142 | // if there's an operator in a bracket expression, this is |
---|
143 | // used to keep track of it |
---|
144 | // value: |
---|
145 | // the internals of parenthetical expression for a pseudo. for |
---|
146 | // :nth-child(2n+1), value might be "2n+1" |
---|
147 | |
---|
148 | var endTag = function(){ |
---|
149 | // called when the tokenizer hits the end of a particular tag name. |
---|
150 | // Re-sets state variables for tag matching and sets up the matcher |
---|
151 | // to handle the next type of token (tag or operator). |
---|
152 | if(inTag >= 0){ |
---|
153 | var tv = (inTag == x) ? null : ts(inTag, x); // .toLowerCase(); |
---|
154 | currentPart[ (specials.indexOf(tv) < 0) ? "tag" : "oper" ] = tv; |
---|
155 | inTag = -1; |
---|
156 | } |
---|
157 | }; |
---|
158 | |
---|
159 | var endId = function(){ |
---|
160 | // called when the tokenizer might be at the end of an ID portion of a match |
---|
161 | if(inId >= 0){ |
---|
162 | currentPart.id = ts(inId, x).replace(/\\/g, ""); |
---|
163 | inId = -1; |
---|
164 | } |
---|
165 | }; |
---|
166 | |
---|
167 | var endClass = function(){ |
---|
168 | // called when the tokenizer might be at the end of a class name |
---|
169 | // match. CSS allows for multiple classes, so we augment the |
---|
170 | // current item with another class in its list |
---|
171 | if(inClass >= 0){ |
---|
172 | currentPart.classes.push(ts(inClass + 1, x).replace(/\\/g, "")); |
---|
173 | inClass = -1; |
---|
174 | } |
---|
175 | }; |
---|
176 | |
---|
177 | var endAll = function(){ |
---|
178 | // at the end of a simple fragment, so wall off the matches |
---|
179 | endId(); |
---|
180 | endTag(); |
---|
181 | endClass(); |
---|
182 | }; |
---|
183 | |
---|
184 | var endPart = function(){ |
---|
185 | endAll(); |
---|
186 | if(inPseudo >= 0){ |
---|
187 | currentPart.pseudos.push({ name: ts(inPseudo + 1, x) }); |
---|
188 | } |
---|
189 | // hint to the selector engine to tell it whether or not it |
---|
190 | // needs to do any iteration. Many simple selectors don't, and |
---|
191 | // we can avoid significant construction-time work by advising |
---|
192 | // the system to skip them |
---|
193 | currentPart.loops = ( |
---|
194 | currentPart.pseudos.length || |
---|
195 | currentPart.attrs.length || |
---|
196 | currentPart.classes.length ); |
---|
197 | |
---|
198 | currentPart.oquery = currentPart.query = ts(pStart, x); // save the full expression as a string |
---|
199 | |
---|
200 | |
---|
201 | // otag/tag are hints to suggest to the system whether or not |
---|
202 | // it's an operator or a tag. We save a copy of otag since the |
---|
203 | // tag name is cast to upper-case in regular HTML matches. The |
---|
204 | // system has a global switch to figure out if the current |
---|
205 | // expression needs to be case sensitive or not and it will use |
---|
206 | // otag or tag accordingly |
---|
207 | currentPart.otag = currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*"); |
---|
208 | |
---|
209 | if(currentPart.tag){ |
---|
210 | // if we're in a case-insensitive HTML doc, we likely want |
---|
211 | // the toUpperCase when matching on element.tagName. If we |
---|
212 | // do it here, we can skip the string op per node |
---|
213 | // comparison |
---|
214 | currentPart.tag = currentPart.tag.toUpperCase(); |
---|
215 | } |
---|
216 | |
---|
217 | // add the part to the list |
---|
218 | if(queryParts.length && (queryParts[queryParts.length-1].oper)){ |
---|
219 | // operators are always infix, so we remove them from the |
---|
220 | // list and attach them to the next match. The evaluator is |
---|
221 | // responsible for sorting out how to handle them. |
---|
222 | currentPart.infixOper = queryParts.pop(); |
---|
223 | currentPart.query = currentPart.infixOper.query + " " + currentPart.query; |
---|
224 | /* |
---|
225 | console.debug( "swapping out the infix", |
---|
226 | currentPart.infixOper, |
---|
227 | "and attaching it to", |
---|
228 | currentPart); |
---|
229 | */ |
---|
230 | } |
---|
231 | queryParts.push(currentPart); |
---|
232 | |
---|
233 | currentPart = null; |
---|
234 | }; |
---|
235 | |
---|
236 | // iterate over the query, character by character, building up a |
---|
237 | // list of query part objects |
---|
238 | for(; lc=cc, cc=query.charAt(x), x < ql; x++){ |
---|
239 | // cc: the current character in the match |
---|
240 | // lc: the last character (if any) |
---|
241 | |
---|
242 | // someone is trying to escape something, so don't try to match any |
---|
243 | // fragments. We assume we're inside a literal. |
---|
244 | if(lc == "\\"){ continue; } |
---|
245 | if(!currentPart){ // a part was just ended or none has yet been created |
---|
246 | // NOTE: I hate all this alloc, but it's shorter than writing tons of if's |
---|
247 | pStart = x; |
---|
248 | // rules describe full CSS sub-expressions, like: |
---|
249 | // #someId |
---|
250 | // .className:first-child |
---|
251 | // but not: |
---|
252 | // thinger > div.howdy[type=thinger] |
---|
253 | // the indidual components of the previous query would be |
---|
254 | // split into 3 parts that would be represented a structure |
---|
255 | // like: |
---|
256 | // [ |
---|
257 | // { |
---|
258 | // query: "thinger", |
---|
259 | // tag: "thinger", |
---|
260 | // }, |
---|
261 | // { |
---|
262 | // query: "div.howdy[type=thinger]", |
---|
263 | // classes: ["howdy"], |
---|
264 | // infixOper: { |
---|
265 | // query: ">", |
---|
266 | // oper: ">", |
---|
267 | // } |
---|
268 | // }, |
---|
269 | // ] |
---|
270 | currentPart = { |
---|
271 | query: null, // the full text of the part's rule |
---|
272 | pseudos: [], // CSS supports multiple pseud-class matches in a single rule |
---|
273 | attrs: [], // CSS supports multi-attribute match, so we need an array |
---|
274 | classes: [], // class matches may be additive, e.g.: .thinger.blah.howdy |
---|
275 | tag: null, // only one tag... |
---|
276 | oper: null, // ...or operator per component. Note that these wind up being exclusive. |
---|
277 | id: null, // the id component of a rule |
---|
278 | getTag: function(){ |
---|
279 | return (caseSensitive) ? this.otag : this.tag; |
---|
280 | } |
---|
281 | }; |
---|
282 | |
---|
283 | // if we don't have a part, we assume we're going to start at |
---|
284 | // the beginning of a match, which should be a tag name. This |
---|
285 | // might fault a little later on, but we detect that and this |
---|
286 | // iteration will still be fine. |
---|
287 | inTag = x; |
---|
288 | } |
---|
289 | |
---|
290 | if(inBrackets >= 0){ |
---|
291 | // look for a the close first |
---|
292 | if(cc == "]"){ // if we're in a [...] clause and we end, do assignment |
---|
293 | if(!_cp.attr){ |
---|
294 | // no attribute match was previously begun, so we |
---|
295 | // assume this is an attribute existence match in the |
---|
296 | // form of [someAttributeName] |
---|
297 | _cp.attr = ts(inBrackets+1, x); |
---|
298 | }else{ |
---|
299 | // we had an attribute already, so we know that we're |
---|
300 | // matching some sort of value, as in [attrName=howdy] |
---|
301 | _cp.matchFor = ts((inMatchFor||inBrackets+1), x); |
---|
302 | } |
---|
303 | var cmf = _cp.matchFor; |
---|
304 | if(cmf){ |
---|
305 | // try to strip quotes from the matchFor value. We want |
---|
306 | // [attrName=howdy] to match the same |
---|
307 | // as [attrName = 'howdy' ] |
---|
308 | if( (cmf.charAt(0) == '"') || (cmf.charAt(0) == "'") ){ |
---|
309 | _cp.matchFor = cmf.slice(1, -1); |
---|
310 | } |
---|
311 | } |
---|
312 | // end the attribute by adding it to the list of attributes. |
---|
313 | currentPart.attrs.push(_cp); |
---|
314 | _cp = null; // necessary? |
---|
315 | inBrackets = inMatchFor = -1; |
---|
316 | }else if(cc == "="){ |
---|
317 | // if the last char was an operator prefix, make sure we |
---|
318 | // record it along with the "=" operator. |
---|
319 | var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : ""; |
---|
320 | _cp.type = addToCc+cc; |
---|
321 | _cp.attr = ts(inBrackets+1, x-addToCc.length); |
---|
322 | inMatchFor = x+1; |
---|
323 | } |
---|
324 | // now look for other clause parts |
---|
325 | }else if(inParens >= 0){ |
---|
326 | // if we're in a parenthetical expression, we need to figure |
---|
327 | // out if it's attached to a pseudo-selector rule like |
---|
328 | // :nth-child(1) |
---|
329 | if(cc == ")"){ |
---|
330 | if(inPseudo >= 0){ |
---|
331 | _cp.value = ts(inParens+1, x); |
---|
332 | } |
---|
333 | inPseudo = inParens = -1; |
---|
334 | } |
---|
335 | }else if(cc == "#"){ |
---|
336 | // start of an ID match |
---|
337 | endAll(); |
---|
338 | inId = x+1; |
---|
339 | }else if(cc == "."){ |
---|
340 | // start of a class match |
---|
341 | endAll(); |
---|
342 | inClass = x; |
---|
343 | }else if(cc == ":"){ |
---|
344 | // start of a pseudo-selector match |
---|
345 | endAll(); |
---|
346 | inPseudo = x; |
---|
347 | }else if(cc == "["){ |
---|
348 | // start of an attribute match. |
---|
349 | endAll(); |
---|
350 | inBrackets = x; |
---|
351 | // provide a new structure for the attribute match to fill-in |
---|
352 | _cp = { |
---|
353 | /*===== |
---|
354 | attr: null, type: null, matchFor: null |
---|
355 | =====*/ |
---|
356 | }; |
---|
357 | }else if(cc == "("){ |
---|
358 | // we really only care if we've entered a parenthetical |
---|
359 | // expression if we're already inside a pseudo-selector match |
---|
360 | if(inPseudo >= 0){ |
---|
361 | // provide a new structure for the pseudo match to fill-in |
---|
362 | _cp = { |
---|
363 | name: ts(inPseudo+1, x), |
---|
364 | value: null |
---|
365 | }; |
---|
366 | currentPart.pseudos.push(_cp); |
---|
367 | } |
---|
368 | inParens = x; |
---|
369 | }else if( |
---|
370 | (cc == " ") && |
---|
371 | // if it's a space char and the last char is too, consume the |
---|
372 | // current one without doing more work |
---|
373 | (lc != cc) |
---|
374 | ){ |
---|
375 | endPart(); |
---|
376 | } |
---|
377 | } |
---|
378 | return queryParts; |
---|
379 | }; |
---|
380 | |
---|
381 | |
---|
382 | //////////////////////////////////////////////////////////////////////// |
---|
383 | // DOM query infrastructure |
---|
384 | //////////////////////////////////////////////////////////////////////// |
---|
385 | |
---|
386 | var agree = function(first, second){ |
---|
387 | // the basic building block of the yes/no chaining system. agree(f1, |
---|
388 | // f2) generates a new function which returns the boolean results of |
---|
389 | // both of the passed functions to a single logical-anded result. If |
---|
390 | // either are not passed, the other is used exclusively. |
---|
391 | if(!first){ return second; } |
---|
392 | if(!second){ return first; } |
---|
393 | |
---|
394 | return function(){ |
---|
395 | return first.apply(window, arguments) && second.apply(window, arguments); |
---|
396 | } |
---|
397 | }; |
---|
398 | |
---|
399 | var getArr = function(i, arr){ |
---|
400 | // helps us avoid array alloc when we don't need it |
---|
401 | var r = arr||[]; // FIXME: should this be 'new d._NodeListCtor()' ? |
---|
402 | if(i){ r.push(i); } |
---|
403 | return r; |
---|
404 | }; |
---|
405 | |
---|
406 | var _isElement = function(n){ return (1 == n.nodeType); }; |
---|
407 | |
---|
408 | // FIXME: need to coalesce _getAttr with defaultGetter |
---|
409 | var blank = ""; |
---|
410 | var _getAttr = function(elem, attr){ |
---|
411 | if(!elem){ return blank; } |
---|
412 | if(attr == "class"){ |
---|
413 | return elem.className || blank; |
---|
414 | } |
---|
415 | if(attr == "for"){ |
---|
416 | return elem.htmlFor || blank; |
---|
417 | } |
---|
418 | if(attr == "style"){ |
---|
419 | return elem.style.cssText || blank; |
---|
420 | } |
---|
421 | return (caseSensitive ? elem.getAttribute(attr) : elem.getAttribute(attr, 2)) || blank; |
---|
422 | }; |
---|
423 | |
---|
424 | var attrs = { |
---|
425 | "*=": function(attr, value){ |
---|
426 | return function(elem){ |
---|
427 | // E[foo*="bar"] |
---|
428 | // an E element whose "foo" attribute value contains |
---|
429 | // the substring "bar" |
---|
430 | return (_getAttr(elem, attr).indexOf(value)>=0); |
---|
431 | } |
---|
432 | }, |
---|
433 | "^=": function(attr, value){ |
---|
434 | // E[foo^="bar"] |
---|
435 | // an E element whose "foo" attribute value begins exactly |
---|
436 | // with the string "bar" |
---|
437 | return function(elem){ |
---|
438 | return (_getAttr(elem, attr).indexOf(value)==0); |
---|
439 | } |
---|
440 | }, |
---|
441 | "$=": function(attr, value){ |
---|
442 | // E[foo$="bar"] |
---|
443 | // an E element whose "foo" attribute value ends exactly |
---|
444 | // with the string "bar" |
---|
445 | return function(elem){ |
---|
446 | var ea = " "+_getAttr(elem, attr); |
---|
447 | return (ea.lastIndexOf(value)==(ea.length-value.length)); |
---|
448 | } |
---|
449 | }, |
---|
450 | "~=": function(attr, value){ |
---|
451 | // E[foo~="bar"] |
---|
452 | // an E element whose "foo" attribute value is a list of |
---|
453 | // space-separated values, one of which is exactly equal |
---|
454 | // to "bar" |
---|
455 | |
---|
456 | // return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]"; |
---|
457 | var tval = " "+value+" "; |
---|
458 | return function(elem){ |
---|
459 | var ea = " "+_getAttr(elem, attr)+" "; |
---|
460 | return (ea.indexOf(tval)>=0); |
---|
461 | } |
---|
462 | }, |
---|
463 | "|=": function(attr, value){ |
---|
464 | // E[hreflang|="en"] |
---|
465 | // an E element whose "hreflang" attribute has a |
---|
466 | // hyphen-separated list of values beginning (from the |
---|
467 | // left) with "en" |
---|
468 | var valueDash = value+"-"; |
---|
469 | return function(elem){ |
---|
470 | var ea = _getAttr(elem, attr); |
---|
471 | return ( |
---|
472 | (ea == value) || |
---|
473 | (ea.indexOf(valueDash)==0) |
---|
474 | ); |
---|
475 | } |
---|
476 | }, |
---|
477 | "=": function(attr, value){ |
---|
478 | return function(elem){ |
---|
479 | return (_getAttr(elem, attr) == value); |
---|
480 | } |
---|
481 | } |
---|
482 | }; |
---|
483 | |
---|
484 | // avoid testing for node type if we can. Defining this in the negative |
---|
485 | // here to avoid negation in the fast path. |
---|
486 | var _noNES = (typeof getDoc().firstChild.nextElementSibling == "undefined"); |
---|
487 | var _ns = !_noNES ? "nextElementSibling" : "nextSibling"; |
---|
488 | var _ps = !_noNES ? "previousElementSibling" : "previousSibling"; |
---|
489 | var _simpleNodeTest = (_noNES ? _isElement : yesman); |
---|
490 | |
---|
491 | var _lookLeft = function(node){ |
---|
492 | // look left |
---|
493 | while(node = node[_ps]){ |
---|
494 | if(_simpleNodeTest(node)){ return false; } |
---|
495 | } |
---|
496 | return true; |
---|
497 | }; |
---|
498 | |
---|
499 | var _lookRight = function(node){ |
---|
500 | // look right |
---|
501 | while(node = node[_ns]){ |
---|
502 | if(_simpleNodeTest(node)){ return false; } |
---|
503 | } |
---|
504 | return true; |
---|
505 | }; |
---|
506 | |
---|
507 | var getNodeIndex = function(node){ |
---|
508 | var root = node.parentNode; |
---|
509 | var i = 0, |
---|
510 | tret = root.children || root.childNodes, |
---|
511 | ci = (node["_i"]||-1), |
---|
512 | cl = (root["_l"]||-1); |
---|
513 | |
---|
514 | if(!tret){ return -1; } |
---|
515 | var l = tret.length; |
---|
516 | |
---|
517 | // we calculate the parent length as a cheap way to invalidate the |
---|
518 | // cache. It's not 100% accurate, but it's much more honest than what |
---|
519 | // other libraries do |
---|
520 | if( cl == l && ci >= 0 && cl >= 0 ){ |
---|
521 | // if it's legit, tag and release |
---|
522 | return ci; |
---|
523 | } |
---|
524 | |
---|
525 | // else re-key things |
---|
526 | root["_l"] = l; |
---|
527 | ci = -1; |
---|
528 | for(var te = root["firstElementChild"]||root["firstChild"]; te; te = te[_ns]){ |
---|
529 | if(_simpleNodeTest(te)){ |
---|
530 | te["_i"] = ++i; |
---|
531 | if(node === te){ |
---|
532 | // NOTE: |
---|
533 | // shortcutting the return at this step in indexing works |
---|
534 | // very well for benchmarking but we avoid it here since |
---|
535 | // it leads to potential O(n^2) behavior in sequential |
---|
536 | // getNodexIndex operations on a previously un-indexed |
---|
537 | // parent. We may revisit this at a later time, but for |
---|
538 | // now we just want to get the right answer more often |
---|
539 | // than not. |
---|
540 | ci = i; |
---|
541 | } |
---|
542 | } |
---|
543 | } |
---|
544 | return ci; |
---|
545 | }; |
---|
546 | |
---|
547 | var isEven = function(elem){ |
---|
548 | return !((getNodeIndex(elem)) % 2); |
---|
549 | }; |
---|
550 | |
---|
551 | var isOdd = function(elem){ |
---|
552 | return ((getNodeIndex(elem)) % 2); |
---|
553 | }; |
---|
554 | |
---|
555 | var pseudos = { |
---|
556 | "checked": function(name, condition){ |
---|
557 | return function(elem){ |
---|
558 | return !!("checked" in elem ? elem.checked : elem.selected); |
---|
559 | } |
---|
560 | }, |
---|
561 | "first-child": function(){ return _lookLeft; }, |
---|
562 | "last-child": function(){ return _lookRight; }, |
---|
563 | "only-child": function(name, condition){ |
---|
564 | return function(node){ |
---|
565 | return _lookLeft(node) && _lookRight(node); |
---|
566 | }; |
---|
567 | }, |
---|
568 | "empty": function(name, condition){ |
---|
569 | return function(elem){ |
---|
570 | // DomQuery and jQuery get this wrong, oddly enough. |
---|
571 | // The CSS 3 selectors spec is pretty explicit about it, too. |
---|
572 | var cn = elem.childNodes; |
---|
573 | var cnl = elem.childNodes.length; |
---|
574 | // if(!cnl){ return true; } |
---|
575 | for(var x=cnl-1; x >= 0; x--){ |
---|
576 | var nt = cn[x].nodeType; |
---|
577 | if((nt === 1)||(nt == 3)){ return false; } |
---|
578 | } |
---|
579 | return true; |
---|
580 | } |
---|
581 | }, |
---|
582 | "contains": function(name, condition){ |
---|
583 | var cz = condition.charAt(0); |
---|
584 | if( cz == '"' || cz == "'" ){ //remove quote |
---|
585 | condition = condition.slice(1, -1); |
---|
586 | } |
---|
587 | return function(elem){ |
---|
588 | return (elem.innerHTML.indexOf(condition) >= 0); |
---|
589 | } |
---|
590 | }, |
---|
591 | "not": function(name, condition){ |
---|
592 | var p = getQueryParts(condition)[0]; |
---|
593 | var ignores = { el: 1 }; |
---|
594 | if(p.tag != "*"){ |
---|
595 | ignores.tag = 1; |
---|
596 | } |
---|
597 | if(!p.classes.length){ |
---|
598 | ignores.classes = 1; |
---|
599 | } |
---|
600 | var ntf = getSimpleFilterFunc(p, ignores); |
---|
601 | return function(elem){ |
---|
602 | return (!ntf(elem)); |
---|
603 | } |
---|
604 | }, |
---|
605 | "nth-child": function(name, condition){ |
---|
606 | var pi = parseInt; |
---|
607 | // avoid re-defining function objects if we can |
---|
608 | if(condition == "odd"){ |
---|
609 | return isOdd; |
---|
610 | }else if(condition == "even"){ |
---|
611 | return isEven; |
---|
612 | } |
---|
613 | // FIXME: can we shorten this? |
---|
614 | if(condition.indexOf("n") != -1){ |
---|
615 | var tparts = condition.split("n", 2); |
---|
616 | var pred = tparts[0] ? ((tparts[0] == '-') ? -1 : pi(tparts[0])) : 1; |
---|
617 | var idx = tparts[1] ? pi(tparts[1]) : 0; |
---|
618 | var lb = 0, ub = -1; |
---|
619 | if(pred > 0){ |
---|
620 | if(idx < 0){ |
---|
621 | idx = (idx % pred) && (pred + (idx % pred)); |
---|
622 | }else if(idx>0){ |
---|
623 | if(idx >= pred){ |
---|
624 | lb = idx - idx % pred; |
---|
625 | } |
---|
626 | idx = idx % pred; |
---|
627 | } |
---|
628 | }else if(pred<0){ |
---|
629 | pred *= -1; |
---|
630 | // idx has to be greater than 0 when pred is negative; |
---|
631 | // shall we throw an error here? |
---|
632 | if(idx > 0){ |
---|
633 | ub = idx; |
---|
634 | idx = idx % pred; |
---|
635 | } |
---|
636 | } |
---|
637 | if(pred > 0){ |
---|
638 | return function(elem){ |
---|
639 | var i = getNodeIndex(elem); |
---|
640 | return (i>=lb) && (ub<0 || i<=ub) && ((i % pred) == idx); |
---|
641 | } |
---|
642 | }else{ |
---|
643 | condition = idx; |
---|
644 | } |
---|
645 | } |
---|
646 | var ncount = pi(condition); |
---|
647 | return function(elem){ |
---|
648 | return (getNodeIndex(elem) == ncount); |
---|
649 | } |
---|
650 | } |
---|
651 | }; |
---|
652 | |
---|
653 | var defaultGetter = (dojo.isIE && (dojo.isIE < 9 || dojo.isQuirks)) ? function(cond){ |
---|
654 | var clc = cond.toLowerCase(); |
---|
655 | if(clc == "class"){ cond = "className"; } |
---|
656 | return function(elem){ |
---|
657 | return (caseSensitive ? elem.getAttribute(cond) : elem[cond]||elem[clc]); |
---|
658 | } |
---|
659 | } : function(cond){ |
---|
660 | return function(elem){ |
---|
661 | return (elem && elem.getAttribute && elem.hasAttribute(cond)); |
---|
662 | } |
---|
663 | }; |
---|
664 | |
---|
665 | var getSimpleFilterFunc = function(query, ignores){ |
---|
666 | // generates a node tester function based on the passed query part. The |
---|
667 | // query part is one of the structures generated by the query parser |
---|
668 | // when it creates the query AST. The "ignores" object specifies which |
---|
669 | // (if any) tests to skip, allowing the system to avoid duplicating |
---|
670 | // work where it may have already been taken into account by other |
---|
671 | // factors such as how the nodes to test were fetched in the first |
---|
672 | // place |
---|
673 | if(!query){ return yesman; } |
---|
674 | ignores = ignores||{}; |
---|
675 | |
---|
676 | var ff = null; |
---|
677 | |
---|
678 | if(!("el" in ignores)){ |
---|
679 | ff = agree(ff, _isElement); |
---|
680 | } |
---|
681 | |
---|
682 | if(!("tag" in ignores)){ |
---|
683 | if(query.tag != "*"){ |
---|
684 | ff = agree(ff, function(elem){ |
---|
685 | return (elem && (elem.tagName == query.getTag())); |
---|
686 | }); |
---|
687 | } |
---|
688 | } |
---|
689 | |
---|
690 | if(!("classes" in ignores)){ |
---|
691 | each(query.classes, function(cname, idx, arr){ |
---|
692 | // get the class name |
---|
693 | /* |
---|
694 | var isWildcard = cname.charAt(cname.length-1) == "*"; |
---|
695 | if(isWildcard){ |
---|
696 | cname = cname.substr(0, cname.length-1); |
---|
697 | } |
---|
698 | // I dislike the regex thing, even if memoized in a cache, but it's VERY short |
---|
699 | var re = new RegExp("(?:^|\\s)" + cname + (isWildcard ? ".*" : "") + "(?:\\s|$)"); |
---|
700 | */ |
---|
701 | var re = new RegExp("(?:^|\\s)" + cname + "(?:\\s|$)"); |
---|
702 | ff = agree(ff, function(elem){ |
---|
703 | return re.test(elem.className); |
---|
704 | }); |
---|
705 | ff.count = idx; |
---|
706 | }); |
---|
707 | } |
---|
708 | |
---|
709 | if(!("pseudos" in ignores)){ |
---|
710 | each(query.pseudos, function(pseudo){ |
---|
711 | var pn = pseudo.name; |
---|
712 | if(pseudos[pn]){ |
---|
713 | ff = agree(ff, pseudos[pn](pn, pseudo.value)); |
---|
714 | } |
---|
715 | }); |
---|
716 | } |
---|
717 | |
---|
718 | if(!("attrs" in ignores)){ |
---|
719 | each(query.attrs, function(attr){ |
---|
720 | var matcher; |
---|
721 | var a = attr.attr; |
---|
722 | // type, attr, matchFor |
---|
723 | if(attr.type && attrs[attr.type]){ |
---|
724 | matcher = attrs[attr.type](a, attr.matchFor); |
---|
725 | }else if(a.length){ |
---|
726 | matcher = defaultGetter(a); |
---|
727 | } |
---|
728 | if(matcher){ |
---|
729 | ff = agree(ff, matcher); |
---|
730 | } |
---|
731 | }); |
---|
732 | } |
---|
733 | |
---|
734 | if(!("id" in ignores)){ |
---|
735 | if(query.id){ |
---|
736 | ff = agree(ff, function(elem){ |
---|
737 | return (!!elem && (elem.id == query.id)); |
---|
738 | }); |
---|
739 | } |
---|
740 | } |
---|
741 | |
---|
742 | if(!ff){ |
---|
743 | if(!("default" in ignores)){ |
---|
744 | ff = yesman; |
---|
745 | } |
---|
746 | } |
---|
747 | return ff; |
---|
748 | }; |
---|
749 | |
---|
750 | var _nextSibling = function(filterFunc){ |
---|
751 | return function(node, ret, bag){ |
---|
752 | while(node = node[_ns]){ |
---|
753 | if(_noNES && (!_isElement(node))){ continue; } |
---|
754 | if( |
---|
755 | (!bag || _isUnique(node, bag)) && |
---|
756 | filterFunc(node) |
---|
757 | ){ |
---|
758 | ret.push(node); |
---|
759 | } |
---|
760 | break; |
---|
761 | } |
---|
762 | return ret; |
---|
763 | } |
---|
764 | }; |
---|
765 | |
---|
766 | var _nextSiblings = function(filterFunc){ |
---|
767 | return function(root, ret, bag){ |
---|
768 | var te = root[_ns]; |
---|
769 | while(te){ |
---|
770 | if(_simpleNodeTest(te)){ |
---|
771 | if(bag && !_isUnique(te, bag)){ |
---|
772 | break; |
---|
773 | } |
---|
774 | if(filterFunc(te)){ |
---|
775 | ret.push(te); |
---|
776 | } |
---|
777 | } |
---|
778 | te = te[_ns]; |
---|
779 | } |
---|
780 | return ret; |
---|
781 | } |
---|
782 | }; |
---|
783 | |
---|
784 | // get an array of child *elements*, skipping text and comment nodes |
---|
785 | var _childElements = function(filterFunc){ |
---|
786 | filterFunc = filterFunc||yesman; |
---|
787 | return function(root, ret, bag){ |
---|
788 | // get an array of child elements, skipping text and comment nodes |
---|
789 | var te, x = 0, tret = root.children || root.childNodes; |
---|
790 | while(te = tret[x++]){ |
---|
791 | if( |
---|
792 | _simpleNodeTest(te) && |
---|
793 | (!bag || _isUnique(te, bag)) && |
---|
794 | (filterFunc(te, x)) |
---|
795 | ){ |
---|
796 | ret.push(te); |
---|
797 | } |
---|
798 | } |
---|
799 | return ret; |
---|
800 | }; |
---|
801 | }; |
---|
802 | |
---|
803 | /* |
---|
804 | // thanks, Dean! |
---|
805 | var itemIsAfterRoot = d.isIE ? function(item, root){ |
---|
806 | return (item.sourceIndex > root.sourceIndex); |
---|
807 | } : function(item, root){ |
---|
808 | return (item.compareDocumentPosition(root) == 2); |
---|
809 | }; |
---|
810 | */ |
---|
811 | |
---|
812 | // test to see if node is below root |
---|
813 | var _isDescendant = function(node, root){ |
---|
814 | var pn = node.parentNode; |
---|
815 | while(pn){ |
---|
816 | if(pn == root){ |
---|
817 | break; |
---|
818 | } |
---|
819 | pn = pn.parentNode; |
---|
820 | } |
---|
821 | return !!pn; |
---|
822 | }; |
---|
823 | |
---|
824 | var _getElementsFuncCache = {}; |
---|
825 | |
---|
826 | var getElementsFunc = function(query){ |
---|
827 | var retFunc = _getElementsFuncCache[query.query]; |
---|
828 | // if we've got a cached dispatcher, just use that |
---|
829 | if(retFunc){ return retFunc; } |
---|
830 | // else, generate a new on |
---|
831 | |
---|
832 | // NOTE: |
---|
833 | // this function returns a function that searches for nodes and |
---|
834 | // filters them. The search may be specialized by infix operators |
---|
835 | // (">", "~", or "+") else it will default to searching all |
---|
836 | // descendants (the " " selector). Once a group of children is |
---|
837 | // found, a test function is applied to weed out the ones we |
---|
838 | // don't want. Many common cases can be fast-pathed. We spend a |
---|
839 | // lot of cycles to create a dispatcher that doesn't do more work |
---|
840 | // than necessary at any point since, unlike this function, the |
---|
841 | // dispatchers will be called every time. The logic of generating |
---|
842 | // efficient dispatchers looks like this in pseudo code: |
---|
843 | // |
---|
844 | // # if it's a purely descendant query (no ">", "+", or "~" modifiers) |
---|
845 | // if infixOperator == " ": |
---|
846 | // if only(id): |
---|
847 | // return def(root): |
---|
848 | // return d.byId(id, root); |
---|
849 | // |
---|
850 | // elif id: |
---|
851 | // return def(root): |
---|
852 | // return filter(d.byId(id, root)); |
---|
853 | // |
---|
854 | // elif cssClass && getElementsByClassName: |
---|
855 | // return def(root): |
---|
856 | // return filter(root.getElementsByClassName(cssClass)); |
---|
857 | // |
---|
858 | // elif only(tag): |
---|
859 | // return def(root): |
---|
860 | // return root.getElementsByTagName(tagName); |
---|
861 | // |
---|
862 | // else: |
---|
863 | // # search by tag name, then filter |
---|
864 | // return def(root): |
---|
865 | // return filter(root.getElementsByTagName(tagName||"*")); |
---|
866 | // |
---|
867 | // elif infixOperator == ">": |
---|
868 | // # search direct children |
---|
869 | // return def(root): |
---|
870 | // return filter(root.children); |
---|
871 | // |
---|
872 | // elif infixOperator == "+": |
---|
873 | // # search next sibling |
---|
874 | // return def(root): |
---|
875 | // return filter(root.nextElementSibling); |
---|
876 | // |
---|
877 | // elif infixOperator == "~": |
---|
878 | // # search rightward siblings |
---|
879 | // return def(root): |
---|
880 | // return filter(nextSiblings(root)); |
---|
881 | |
---|
882 | var io = query.infixOper; |
---|
883 | var oper = (io ? io.oper : ""); |
---|
884 | // the default filter func which tests for all conditions in the query |
---|
885 | // part. This is potentially inefficient, so some optimized paths may |
---|
886 | // re-define it to test fewer things. |
---|
887 | var filterFunc = getSimpleFilterFunc(query, { el: 1 }); |
---|
888 | var qt = query.tag; |
---|
889 | var wildcardTag = ("*" == qt); |
---|
890 | var ecs = getDoc()["getElementsByClassName"]; |
---|
891 | |
---|
892 | if(!oper){ |
---|
893 | // if there's no infix operator, then it's a descendant query. ID |
---|
894 | // and "elements by class name" variants can be accelerated so we |
---|
895 | // call them out explicitly: |
---|
896 | if(query.id){ |
---|
897 | // testing shows that the overhead of yesman() is acceptable |
---|
898 | // and can save us some bytes vs. re-defining the function |
---|
899 | // everywhere. |
---|
900 | filterFunc = (!query.loops && wildcardTag) ? |
---|
901 | yesman : |
---|
902 | getSimpleFilterFunc(query, { el: 1, id: 1 }); |
---|
903 | |
---|
904 | retFunc = function(root, arr){ |
---|
905 | var te = dom.byId(query.id, (root.ownerDocument||root)); |
---|
906 | if(!te || !filterFunc(te)){ return; } |
---|
907 | if(9 == root.nodeType){ // if root's a doc, we just return directly |
---|
908 | return getArr(te, arr); |
---|
909 | }else{ // otherwise check ancestry |
---|
910 | if(_isDescendant(te, root)){ |
---|
911 | return getArr(te, arr); |
---|
912 | } |
---|
913 | } |
---|
914 | } |
---|
915 | }else if( |
---|
916 | ecs && |
---|
917 | // isAlien check. Workaround for Prototype.js being totally evil/dumb. |
---|
918 | /\{\s*\[native code\]\s*\}/.test(String(ecs)) && |
---|
919 | query.classes.length && |
---|
920 | !cssCaseBug |
---|
921 | ){ |
---|
922 | // it's a class-based query and we've got a fast way to run it. |
---|
923 | |
---|
924 | // ignore class and ID filters since we will have handled both |
---|
925 | filterFunc = getSimpleFilterFunc(query, { el: 1, classes: 1, id: 1 }); |
---|
926 | var classesString = query.classes.join(" "); |
---|
927 | retFunc = function(root, arr, bag){ |
---|
928 | var ret = getArr(0, arr), te, x=0; |
---|
929 | var tret = root.getElementsByClassName(classesString); |
---|
930 | while((te = tret[x++])){ |
---|
931 | if(filterFunc(te, root) && _isUnique(te, bag)){ |
---|
932 | ret.push(te); |
---|
933 | } |
---|
934 | } |
---|
935 | return ret; |
---|
936 | }; |
---|
937 | |
---|
938 | }else if(!wildcardTag && !query.loops){ |
---|
939 | // it's tag only. Fast-path it. |
---|
940 | retFunc = function(root, arr, bag){ |
---|
941 | var ret = getArr(0, arr), te, x=0; |
---|
942 | var tret = root.getElementsByTagName(query.getTag()); |
---|
943 | while((te = tret[x++])){ |
---|
944 | if(_isUnique(te, bag)){ |
---|
945 | ret.push(te); |
---|
946 | } |
---|
947 | } |
---|
948 | return ret; |
---|
949 | }; |
---|
950 | }else{ |
---|
951 | // the common case: |
---|
952 | // a descendant selector without a fast path. By now it's got |
---|
953 | // to have a tag selector, even if it's just "*" so we query |
---|
954 | // by that and filter |
---|
955 | filterFunc = getSimpleFilterFunc(query, { el: 1, tag: 1, id: 1 }); |
---|
956 | retFunc = function(root, arr, bag){ |
---|
957 | var ret = getArr(0, arr), te, x=0; |
---|
958 | // we use getTag() to avoid case sensitivity issues |
---|
959 | var tret = root.getElementsByTagName(query.getTag()); |
---|
960 | while((te = tret[x++])){ |
---|
961 | if(filterFunc(te, root) && _isUnique(te, bag)){ |
---|
962 | ret.push(te); |
---|
963 | } |
---|
964 | } |
---|
965 | return ret; |
---|
966 | }; |
---|
967 | } |
---|
968 | }else{ |
---|
969 | // the query is scoped in some way. Instead of querying by tag we |
---|
970 | // use some other collection to find candidate nodes |
---|
971 | var skipFilters = { el: 1 }; |
---|
972 | if(wildcardTag){ |
---|
973 | skipFilters.tag = 1; |
---|
974 | } |
---|
975 | filterFunc = getSimpleFilterFunc(query, skipFilters); |
---|
976 | if("+" == oper){ |
---|
977 | retFunc = _nextSibling(filterFunc); |
---|
978 | }else if("~" == oper){ |
---|
979 | retFunc = _nextSiblings(filterFunc); |
---|
980 | }else if(">" == oper){ |
---|
981 | retFunc = _childElements(filterFunc); |
---|
982 | } |
---|
983 | } |
---|
984 | // cache it and return |
---|
985 | return _getElementsFuncCache[query.query] = retFunc; |
---|
986 | }; |
---|
987 | |
---|
988 | var filterDown = function(root, queryParts){ |
---|
989 | // NOTE: |
---|
990 | // this is the guts of the DOM query system. It takes a list of |
---|
991 | // parsed query parts and a root and finds children which match |
---|
992 | // the selector represented by the parts |
---|
993 | var candidates = getArr(root), qp, x, te, qpl = queryParts.length, bag, ret; |
---|
994 | |
---|
995 | for(var i = 0; i < qpl; i++){ |
---|
996 | ret = []; |
---|
997 | qp = queryParts[i]; |
---|
998 | x = candidates.length - 1; |
---|
999 | if(x > 0){ |
---|
1000 | // if we have more than one root at this level, provide a new |
---|
1001 | // hash to use for checking group membership but tell the |
---|
1002 | // system not to post-filter us since we will already have been |
---|
1003 | // gauranteed to be unique |
---|
1004 | bag = {}; |
---|
1005 | ret.nozip = true; |
---|
1006 | } |
---|
1007 | var gef = getElementsFunc(qp); |
---|
1008 | for(var j = 0; (te = candidates[j]); j++){ |
---|
1009 | // for every root, get the elements that match the descendant |
---|
1010 | // selector, adding them to the "ret" array and filtering them |
---|
1011 | // via membership in this level's bag. If there are more query |
---|
1012 | // parts, then this level's return will be used as the next |
---|
1013 | // level's candidates |
---|
1014 | gef(te, ret, bag); |
---|
1015 | } |
---|
1016 | if(!ret.length){ break; } |
---|
1017 | candidates = ret; |
---|
1018 | } |
---|
1019 | return ret; |
---|
1020 | }; |
---|
1021 | |
---|
1022 | //////////////////////////////////////////////////////////////////////// |
---|
1023 | // the query runner |
---|
1024 | //////////////////////////////////////////////////////////////////////// |
---|
1025 | |
---|
1026 | // these are the primary caches for full-query results. The query |
---|
1027 | // dispatcher functions are generated then stored here for hash lookup in |
---|
1028 | // the future |
---|
1029 | var _queryFuncCacheDOM = {}, |
---|
1030 | _queryFuncCacheQSA = {}; |
---|
1031 | |
---|
1032 | // this is the second level of spliting, from full-length queries (e.g., |
---|
1033 | // "div.foo .bar") into simple query expressions (e.g., ["div.foo", |
---|
1034 | // ".bar"]) |
---|
1035 | var getStepQueryFunc = function(query){ |
---|
1036 | var qparts = getQueryParts(trim(query)); |
---|
1037 | |
---|
1038 | // if it's trivial, avoid iteration and zipping costs |
---|
1039 | if(qparts.length == 1){ |
---|
1040 | // we optimize this case here to prevent dispatch further down the |
---|
1041 | // chain, potentially slowing things down. We could more elegantly |
---|
1042 | // handle this in filterDown(), but it's slower for simple things |
---|
1043 | // that need to be fast (e.g., "#someId"). |
---|
1044 | var tef = getElementsFunc(qparts[0]); |
---|
1045 | return function(root){ |
---|
1046 | var r = tef(root, []); |
---|
1047 | if(r){ r.nozip = true; } |
---|
1048 | return r; |
---|
1049 | } |
---|
1050 | } |
---|
1051 | |
---|
1052 | // otherwise, break it up and return a runner that iterates over the parts recursively |
---|
1053 | return function(root){ |
---|
1054 | return filterDown(root, qparts); |
---|
1055 | } |
---|
1056 | }; |
---|
1057 | |
---|
1058 | // NOTES: |
---|
1059 | // * we can't trust QSA for anything but document-rooted queries, so |
---|
1060 | // caching is split into DOM query evaluators and QSA query evaluators |
---|
1061 | // * caching query results is dirty and leak-prone (or, at a minimum, |
---|
1062 | // prone to unbounded growth). Other toolkits may go this route, but |
---|
1063 | // they totally destroy their own ability to manage their memory |
---|
1064 | // footprint. If we implement it, it should only ever be with a fixed |
---|
1065 | // total element reference # limit and an LRU-style algorithm since JS |
---|
1066 | // has no weakref support. Caching compiled query evaluators is also |
---|
1067 | // potentially problematic, but even on large documents the size of the |
---|
1068 | // query evaluators is often < 100 function objects per evaluator (and |
---|
1069 | // LRU can be applied if it's ever shown to be an issue). |
---|
1070 | // * since IE's QSA support is currently only for HTML documents and even |
---|
1071 | // then only in IE 8's "standards mode", we have to detect our dispatch |
---|
1072 | // route at query time and keep 2 separate caches. Ugg. |
---|
1073 | |
---|
1074 | // we need to determine if we think we can run a given query via |
---|
1075 | // querySelectorAll or if we'll need to fall back on DOM queries to get |
---|
1076 | // there. We need a lot of information about the environment and the query |
---|
1077 | // to make the determiniation (e.g. does it support QSA, does the query in |
---|
1078 | // question work in the native QSA impl, etc.). |
---|
1079 | var nua = navigator.userAgent; |
---|
1080 | // some versions of Safari provided QSA, but it was buggy and crash-prone. |
---|
1081 | // We need te detect the right "internal" webkit version to make this work. |
---|
1082 | var wk = "WebKit/"; |
---|
1083 | var is525 = ( |
---|
1084 | dojo.isWebKit && |
---|
1085 | (nua.indexOf(wk) > 0) && |
---|
1086 | (parseFloat(nua.split(wk)[1]) > 528) |
---|
1087 | ); |
---|
1088 | |
---|
1089 | // IE QSA queries may incorrectly include comment nodes, so we throw the |
---|
1090 | // zipping function into "remove" comments mode instead of the normal "skip |
---|
1091 | // it" which every other QSA-clued browser enjoys |
---|
1092 | var noZip = dojo.isIE ? "commentStrip" : "nozip"; |
---|
1093 | |
---|
1094 | var qsa = "querySelectorAll"; |
---|
1095 | var qsaAvail = ( |
---|
1096 | !!getDoc()[qsa] && |
---|
1097 | // see #5832 |
---|
1098 | (!dojo.isSafari || (dojo.isSafari > 3.1) || is525 ) |
---|
1099 | ); |
---|
1100 | |
---|
1101 | //Don't bother with n+3 type of matches, IE complains if we modify those. |
---|
1102 | var infixSpaceRe = /n\+\d|([^ ])?([>~+])([^ =])?/g; |
---|
1103 | var infixSpaceFunc = function(match, pre, ch, post){ |
---|
1104 | return ch ? (pre ? pre + " " : "") + ch + (post ? " " + post : "") : /*n+3*/ match; |
---|
1105 | }; |
---|
1106 | |
---|
1107 | var getQueryFunc = function(query, forceDOM){ |
---|
1108 | //Normalize query. The CSS3 selectors spec allows for omitting spaces around |
---|
1109 | //infix operators, >, ~ and + |
---|
1110 | //Do the work here since detection for spaces is used as a simple "not use QSA" |
---|
1111 | //test below. |
---|
1112 | query = query.replace(infixSpaceRe, infixSpaceFunc); |
---|
1113 | |
---|
1114 | if(qsaAvail){ |
---|
1115 | // if we've got a cached variant and we think we can do it, run it! |
---|
1116 | var qsaCached = _queryFuncCacheQSA[query]; |
---|
1117 | if(qsaCached && !forceDOM){ return qsaCached; } |
---|
1118 | } |
---|
1119 | |
---|
1120 | // else if we've got a DOM cached variant, assume that we already know |
---|
1121 | // all we need to and use it |
---|
1122 | var domCached = _queryFuncCacheDOM[query]; |
---|
1123 | if(domCached){ return domCached; } |
---|
1124 | |
---|
1125 | // TODO: |
---|
1126 | // today we're caching DOM and QSA branches separately so we |
---|
1127 | // recalc useQSA every time. If we had a way to tag root+query |
---|
1128 | // efficiently, we'd be in good shape to do a global cache. |
---|
1129 | |
---|
1130 | var qcz = query.charAt(0); |
---|
1131 | var nospace = (-1 == query.indexOf(" ")); |
---|
1132 | |
---|
1133 | // byId searches are wicked fast compared to QSA, even when filtering |
---|
1134 | // is required |
---|
1135 | if( (query.indexOf("#") >= 0) && (nospace) ){ |
---|
1136 | forceDOM = true; |
---|
1137 | } |
---|
1138 | |
---|
1139 | var useQSA = ( |
---|
1140 | qsaAvail && (!forceDOM) && |
---|
1141 | // as per CSS 3, we can't currently start w/ combinator: |
---|
1142 | // http://www.w3.org/TR/css3-selectors/#w3cselgrammar |
---|
1143 | (specials.indexOf(qcz) == -1) && |
---|
1144 | // IE's QSA impl sucks on pseudos |
---|
1145 | (!dojo.isIE || (query.indexOf(":") == -1)) && |
---|
1146 | |
---|
1147 | (!(cssCaseBug && (query.indexOf(".") >= 0))) && |
---|
1148 | |
---|
1149 | // FIXME: |
---|
1150 | // need to tighten up browser rules on ":contains" and "|=" to |
---|
1151 | // figure out which aren't good |
---|
1152 | // Latest webkit (around 531.21.8) does not seem to do well with :checked on option |
---|
1153 | // elements, even though according to spec, selected options should |
---|
1154 | // match :checked. So go nonQSA for it: |
---|
1155 | // http://bugs.dojotoolkit.org/ticket/5179 |
---|
1156 | (query.indexOf(":contains") == -1) && (query.indexOf(":checked") == -1) && |
---|
1157 | (query.indexOf("|=") == -1) // some browsers don't grok it |
---|
1158 | ); |
---|
1159 | |
---|
1160 | // TODO: |
---|
1161 | // if we've got a descendant query (e.g., "> .thinger" instead of |
---|
1162 | // just ".thinger") in a QSA-able doc, but are passed a child as a |
---|
1163 | // root, it should be possible to give the item a synthetic ID and |
---|
1164 | // trivially rewrite the query to the form "#synid > .thinger" to |
---|
1165 | // use the QSA branch |
---|
1166 | |
---|
1167 | |
---|
1168 | if(useQSA){ |
---|
1169 | var tq = (specials.indexOf(query.charAt(query.length-1)) >= 0) ? |
---|
1170 | (query + " *") : query; |
---|
1171 | return _queryFuncCacheQSA[query] = function(root){ |
---|
1172 | try{ |
---|
1173 | // the QSA system contains an egregious spec bug which |
---|
1174 | // limits us, effectively, to only running QSA queries over |
---|
1175 | // entire documents. See: |
---|
1176 | // http://ejohn.org/blog/thoughts-on-queryselectorall/ |
---|
1177 | // despite this, we can also handle QSA runs on simple |
---|
1178 | // selectors, but we don't want detection to be expensive |
---|
1179 | // so we're just checking for the presence of a space char |
---|
1180 | // right now. Not elegant, but it's cheaper than running |
---|
1181 | // the query parser when we might not need to |
---|
1182 | if(!((9 == root.nodeType) || nospace)){ throw ""; } |
---|
1183 | var r = root[qsa](tq); |
---|
1184 | // skip expensive duplication checks and just wrap in a NodeList |
---|
1185 | r[noZip] = true; |
---|
1186 | return r; |
---|
1187 | }catch(e){ |
---|
1188 | // else run the DOM branch on this query, ensuring that we |
---|
1189 | // default that way in the future |
---|
1190 | return getQueryFunc(query, true)(root); |
---|
1191 | } |
---|
1192 | } |
---|
1193 | }else{ |
---|
1194 | // DOM branch |
---|
1195 | var parts = query.split(/\s*,\s*/); |
---|
1196 | return _queryFuncCacheDOM[query] = ((parts.length < 2) ? |
---|
1197 | // if not a compound query (e.g., ".foo, .bar"), cache and return a dispatcher |
---|
1198 | getStepQueryFunc(query) : |
---|
1199 | // if it *is* a complex query, break it up into its |
---|
1200 | // constituent parts and return a dispatcher that will |
---|
1201 | // merge the parts when run |
---|
1202 | function(root){ |
---|
1203 | var pindex = 0, // avoid array alloc for every invocation |
---|
1204 | ret = [], |
---|
1205 | tp; |
---|
1206 | while((tp = parts[pindex++])){ |
---|
1207 | ret = ret.concat(getStepQueryFunc(tp)(root)); |
---|
1208 | } |
---|
1209 | return ret; |
---|
1210 | } |
---|
1211 | ); |
---|
1212 | } |
---|
1213 | }; |
---|
1214 | |
---|
1215 | var _zipIdx = 0; |
---|
1216 | |
---|
1217 | // NOTE: |
---|
1218 | // this function is Moo inspired, but our own impl to deal correctly |
---|
1219 | // with XML in IE |
---|
1220 | var _nodeUID = dojo.isIE ? function(node){ |
---|
1221 | if(caseSensitive){ |
---|
1222 | // XML docs don't have uniqueID on their nodes |
---|
1223 | return (node.getAttribute("_uid") || node.setAttribute("_uid", ++_zipIdx) || _zipIdx); |
---|
1224 | |
---|
1225 | }else{ |
---|
1226 | return node.uniqueID; |
---|
1227 | } |
---|
1228 | } : |
---|
1229 | function(node){ |
---|
1230 | return (node._uid || (node._uid = ++_zipIdx)); |
---|
1231 | }; |
---|
1232 | |
---|
1233 | // determine if a node in is unique in a "bag". In this case we don't want |
---|
1234 | // to flatten a list of unique items, but rather just tell if the item in |
---|
1235 | // question is already in the bag. Normally we'd just use hash lookup to do |
---|
1236 | // this for us but IE's DOM is busted so we can't really count on that. On |
---|
1237 | // the upside, it gives us a built in unique ID function. |
---|
1238 | var _isUnique = function(node, bag){ |
---|
1239 | if(!bag){ return 1; } |
---|
1240 | var id = _nodeUID(node); |
---|
1241 | if(!bag[id]){ return bag[id] = 1; } |
---|
1242 | return 0; |
---|
1243 | }; |
---|
1244 | |
---|
1245 | // attempt to efficiently determine if an item in a list is a dupe, |
---|
1246 | // returning a list of "uniques", hopefully in doucment order |
---|
1247 | var _zipIdxName = "_zipIdx"; |
---|
1248 | var _zip = function(arr){ |
---|
1249 | if(arr && arr.nozip){ |
---|
1250 | return arr; |
---|
1251 | } |
---|
1252 | var ret = []; |
---|
1253 | if(!arr || !arr.length){ return ret; } |
---|
1254 | if(arr[0]){ |
---|
1255 | ret.push(arr[0]); |
---|
1256 | } |
---|
1257 | if(arr.length < 2){ return ret; } |
---|
1258 | |
---|
1259 | _zipIdx++; |
---|
1260 | |
---|
1261 | // we have to fork here for IE and XML docs because we can't set |
---|
1262 | // expandos on their nodes (apparently). *sigh* |
---|
1263 | if(dojo.isIE && caseSensitive){ |
---|
1264 | var szidx = _zipIdx+""; |
---|
1265 | arr[0].setAttribute(_zipIdxName, szidx); |
---|
1266 | for(var x = 1, te; te = arr[x]; x++){ |
---|
1267 | if(arr[x].getAttribute(_zipIdxName) != szidx){ |
---|
1268 | ret.push(te); |
---|
1269 | } |
---|
1270 | te.setAttribute(_zipIdxName, szidx); |
---|
1271 | } |
---|
1272 | }else if(dojo.isIE && arr.commentStrip){ |
---|
1273 | try{ |
---|
1274 | for(var x = 1, te; te = arr[x]; x++){ |
---|
1275 | if(_isElement(te)){ |
---|
1276 | ret.push(te); |
---|
1277 | } |
---|
1278 | } |
---|
1279 | }catch(e){ /* squelch */ } |
---|
1280 | }else{ |
---|
1281 | if(arr[0]){ arr[0][_zipIdxName] = _zipIdx; } |
---|
1282 | for(var x = 1, te; te = arr[x]; x++){ |
---|
1283 | if(arr[x][_zipIdxName] != _zipIdx){ |
---|
1284 | ret.push(te); |
---|
1285 | } |
---|
1286 | te[_zipIdxName] = _zipIdx; |
---|
1287 | } |
---|
1288 | } |
---|
1289 | return ret; |
---|
1290 | }; |
---|
1291 | |
---|
1292 | // the main executor |
---|
1293 | var query = function(/*String*/ query, /*String|DOMNode?*/ root){ |
---|
1294 | // summary: |
---|
1295 | // Returns nodes which match the given CSS3 selector, searching the |
---|
1296 | // entire document by default but optionally taking a node to scope |
---|
1297 | // the search by. Returns an array. |
---|
1298 | // description: |
---|
1299 | // dojo.query() is the swiss army knife of DOM node manipulation in |
---|
1300 | // Dojo. Much like Prototype's "$$" (bling-bling) function or JQuery's |
---|
1301 | // "$" function, dojo.query provides robust, high-performance |
---|
1302 | // CSS-based node selector support with the option of scoping searches |
---|
1303 | // to a particular sub-tree of a document. |
---|
1304 | // |
---|
1305 | // Supported Selectors: |
---|
1306 | // -------------------- |
---|
1307 | // |
---|
1308 | // acme supports a rich set of CSS3 selectors, including: |
---|
1309 | // |
---|
1310 | // * class selectors (e.g., `.foo`) |
---|
1311 | // * node type selectors like `span` |
---|
1312 | // * ` ` descendant selectors |
---|
1313 | // * `>` child element selectors |
---|
1314 | // * `#foo` style ID selectors |
---|
1315 | // * `*` universal selector |
---|
1316 | // * `~`, the preceded-by sibling selector |
---|
1317 | // * `+`, the immediately preceded-by sibling selector |
---|
1318 | // * attribute queries: |
---|
1319 | // | * `[foo]` attribute presence selector |
---|
1320 | // | * `[foo='bar']` attribute value exact match |
---|
1321 | // | * `[foo~='bar']` attribute value list item match |
---|
1322 | // | * `[foo^='bar']` attribute start match |
---|
1323 | // | * `[foo$='bar']` attribute end match |
---|
1324 | // | * `[foo*='bar']` attribute substring match |
---|
1325 | // * `:first-child`, `:last-child`, and `:only-child` positional selectors |
---|
1326 | // * `:empty` content emtpy selector |
---|
1327 | // * `:checked` pseudo selector |
---|
1328 | // * `:nth-child(n)`, `:nth-child(2n+1)` style positional calculations |
---|
1329 | // * `:nth-child(even)`, `:nth-child(odd)` positional selectors |
---|
1330 | // * `:not(...)` negation pseudo selectors |
---|
1331 | // |
---|
1332 | // Any legal combination of these selectors will work with |
---|
1333 | // `dojo.query()`, including compound selectors ("," delimited). |
---|
1334 | // Very complex and useful searches can be constructed with this |
---|
1335 | // palette of selectors and when combined with functions for |
---|
1336 | // manipulation presented by dojo.NodeList, many types of DOM |
---|
1337 | // manipulation operations become very straightforward. |
---|
1338 | // |
---|
1339 | // Unsupported Selectors: |
---|
1340 | // ---------------------- |
---|
1341 | // |
---|
1342 | // While dojo.query handles many CSS3 selectors, some fall outside of |
---|
1343 | // what's reasonable for a programmatic node querying engine to |
---|
1344 | // handle. Currently unsupported selectors include: |
---|
1345 | // |
---|
1346 | // * namespace-differentiated selectors of any form |
---|
1347 | // * all `::` pseduo-element selectors |
---|
1348 | // * certain pseduo-selectors which don't get a lot of day-to-day use: |
---|
1349 | // | * `:root`, `:lang()`, `:target`, `:focus` |
---|
1350 | // * all visual and state selectors: |
---|
1351 | // | * `:root`, `:active`, `:hover`, `:visisted`, `:link`, |
---|
1352 | // `:enabled`, `:disabled` |
---|
1353 | // * `:*-of-type` pseudo selectors |
---|
1354 | // |
---|
1355 | // dojo.query and XML Documents: |
---|
1356 | // ----------------------------- |
---|
1357 | // |
---|
1358 | // `dojo.query` (as of dojo 1.2) supports searching XML documents |
---|
1359 | // in a case-sensitive manner. If an HTML document is served with |
---|
1360 | // a doctype that forces case-sensitivity (e.g., XHTML 1.1 |
---|
1361 | // Strict), dojo.query() will detect this and "do the right |
---|
1362 | // thing". Case sensitivity is dependent upon the document being |
---|
1363 | // searched and not the query used. It is therefore possible to |
---|
1364 | // use case-sensitive queries on strict sub-documents (iframes, |
---|
1365 | // etc.) or XML documents while still assuming case-insensitivity |
---|
1366 | // for a host/root document. |
---|
1367 | // |
---|
1368 | // Non-selector Queries: |
---|
1369 | // --------------------- |
---|
1370 | // |
---|
1371 | // If something other than a String is passed for the query, |
---|
1372 | // `dojo.query` will return a new `dojo.NodeList` instance |
---|
1373 | // constructed from that parameter alone and all further |
---|
1374 | // processing will stop. This means that if you have a reference |
---|
1375 | // to a node or NodeList, you can quickly construct a new NodeList |
---|
1376 | // from the original by calling `dojo.query(node)` or |
---|
1377 | // `dojo.query(list)`. |
---|
1378 | // |
---|
1379 | // query: |
---|
1380 | // The CSS3 expression to match against. For details on the syntax of |
---|
1381 | // CSS3 selectors, see <http://www.w3.org/TR/css3-selectors/#selectors> |
---|
1382 | // root: |
---|
1383 | // A DOMNode (or node id) to scope the search from. Optional. |
---|
1384 | // returns: Array |
---|
1385 | // example: |
---|
1386 | // search the entire document for elements with the class "foo": |
---|
1387 | // | dojo.query(".foo"); |
---|
1388 | // these elements will match: |
---|
1389 | // | <span class="foo"></span> |
---|
1390 | // | <span class="foo bar"></span> |
---|
1391 | // | <p class="thud foo"></p> |
---|
1392 | // example: |
---|
1393 | // search the entire document for elements with the classes "foo" *and* "bar": |
---|
1394 | // | dojo.query(".foo.bar"); |
---|
1395 | // these elements will match: |
---|
1396 | // | <span class="foo bar"></span> |
---|
1397 | // while these will not: |
---|
1398 | // | <span class="foo"></span> |
---|
1399 | // | <p class="thud foo"></p> |
---|
1400 | // example: |
---|
1401 | // find `<span>` elements which are descendants of paragraphs and |
---|
1402 | // which have a "highlighted" class: |
---|
1403 | // | dojo.query("p span.highlighted"); |
---|
1404 | // the innermost span in this fragment matches: |
---|
1405 | // | <p class="foo"> |
---|
1406 | // | <span>... |
---|
1407 | // | <span class="highlighted foo bar">...</span> |
---|
1408 | // | </span> |
---|
1409 | // | </p> |
---|
1410 | // example: |
---|
1411 | // set an "odd" class on all odd table rows inside of the table |
---|
1412 | // `#tabular_data`, using the `>` (direct child) selector to avoid |
---|
1413 | // affecting any nested tables: |
---|
1414 | // | dojo.query("#tabular_data > tbody > tr:nth-child(odd)").addClass("odd"); |
---|
1415 | // example: |
---|
1416 | // remove all elements with the class "error" from the document |
---|
1417 | // and store them in a list: |
---|
1418 | // | var errors = dojo.query(".error").orphan(); |
---|
1419 | // example: |
---|
1420 | // add an onclick handler to every submit button in the document |
---|
1421 | // which causes the form to be sent via Ajax instead: |
---|
1422 | // | dojo.query("input[type='submit']").onclick(function(e){ |
---|
1423 | // | dojo.stopEvent(e); // prevent sending the form |
---|
1424 | // | var btn = e.target; |
---|
1425 | // | dojo.xhrPost({ |
---|
1426 | // | form: btn.form, |
---|
1427 | // | load: function(data){ |
---|
1428 | // | // replace the form with the response |
---|
1429 | // | var div = dojo.doc.createElement("div"); |
---|
1430 | // | dojo.place(div, btn.form, "after"); |
---|
1431 | // | div.innerHTML = data; |
---|
1432 | // | dojo.style(btn.form, "display", "none"); |
---|
1433 | // | } |
---|
1434 | // | }); |
---|
1435 | // | }); |
---|
1436 | |
---|
1437 | root = root||getDoc(); |
---|
1438 | var od = root.ownerDocument||root.documentElement; |
---|
1439 | |
---|
1440 | // throw the big case sensitivity switch |
---|
1441 | |
---|
1442 | // NOTE: |
---|
1443 | // Opera in XHTML mode doesn't detect case-sensitivity correctly |
---|
1444 | // and it's not clear that there's any way to test for it |
---|
1445 | caseSensitive = (root.contentType && root.contentType=="application/xml") || |
---|
1446 | (dojo.isOpera && (root.doctype || od.toString() == "[object XMLDocument]")) || |
---|
1447 | (!!od) && |
---|
1448 | (dojo.isIE ? od.xml : (root.xmlVersion || od.xmlVersion)); |
---|
1449 | |
---|
1450 | // NOTE: |
---|
1451 | // adding "true" as the 2nd argument to getQueryFunc is useful for |
---|
1452 | // testing the DOM branch without worrying about the |
---|
1453 | // behavior/performance of the QSA branch. |
---|
1454 | var r = getQueryFunc(query)(root); |
---|
1455 | |
---|
1456 | // FIXME: |
---|
1457 | // need to investigate this branch WRT #8074 and #8075 |
---|
1458 | if(r && r.nozip){ |
---|
1459 | return r; |
---|
1460 | } |
---|
1461 | return _zip(r); // dojo.NodeList |
---|
1462 | }; |
---|
1463 | query.filter = function(/*Node[]*/ nodeList, /*String*/ filter, /*String|DOMNode?*/ root){ |
---|
1464 | // summary: |
---|
1465 | // function for filtering a NodeList based on a selector, optimized for simple selectors |
---|
1466 | var tmpNodeList = [], |
---|
1467 | parts = getQueryParts(filter), |
---|
1468 | filterFunc = |
---|
1469 | (parts.length == 1 && !/[^\w#\.]/.test(filter)) ? |
---|
1470 | getSimpleFilterFunc(parts[0]) : |
---|
1471 | function(node){ |
---|
1472 | return dojo.query(filter, root).indexOf(node) != -1; |
---|
1473 | }; |
---|
1474 | for(var x = 0, te; te = nodeList[x]; x++){ |
---|
1475 | if(filterFunc(te)){ tmpNodeList.push(te); } |
---|
1476 | } |
---|
1477 | return tmpNodeList; |
---|
1478 | }; |
---|
1479 | return query; |
---|
1480 | });//end defineQuery |
---|