MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / selector-css2 / selector-css2.js
blob7c2edde91cd4d76976ec266ead64f4546bb2f443
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('selector-css2', function(Y) {
9 /**
10  * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
11  * @module dom
12  * @submodule selector-css2
13  * @for Selector
14  */
17  * Provides helper methods for collecting and filtering DOM elements.
18  */
20 var PARENT_NODE = 'parentNode',
21     TAG_NAME = 'tagName',
22     ATTRIBUTES = 'attributes',
23     COMBINATOR = 'combinator',
24     PSEUDOS = 'pseudos',
26     Selector = Y.Selector,
28     SelectorCSS2 = {
29         _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/,
30         SORT_RESULTS: true,
32         // TODO: better detection, document specific
33         _isXML: (function() {
34             var isXML = (Y.config.doc.createElement('div').tagName !== 'DIV');
35             return isXML;
36         }()),
38         /**
39          * Mapping of shorthand tokens to corresponding attribute selector 
40          * @property shorthand
41          * @type object
42          */
43         shorthand: {
44             '\\#(-?[_a-z0-9]+[-\\w\\uE000]*)': '[id=$1]',
45             '\\.(-?[_a-z]+[-\\w\\uE000]*)': '[className~=$1]'
46         },
48         /**
49          * List of operators and corresponding boolean functions. 
50          * These functions are passed the attribute and the current node's value of the attribute.
51          * @property operators
52          * @type object
53          */
54         operators: {
55             '': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
56             '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
57             '|=': '^{val}-?' // optional hyphen-delimited
58         },
60         pseudos: {
61            'first-child': function(node) { 
62                 return Y.DOM._children(node[PARENT_NODE])[0] === node; 
63             } 
64         },
66         _bruteQuery: function(selector, root, firstOnly) {
67             var ret = [],
68                 nodes = [],
69                 tokens = Selector._tokenize(selector),
70                 token = tokens[tokens.length - 1],
71                 rootDoc = Y.DOM._getDoc(root),
72                 child,
73                 id,
74                 className,
75                 tagName;
77             if (token) {
78                 // prefilter nodes
79                 id = token.id;
80                 className = token.className;
81                 tagName = token.tagName || '*';
83                 if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
84                     // try ID first, unless no root.all && root not in document
85                     // (root.all works off document, but not getElementById)
86                     if (id && (root.all || (root.nodeType === 9 || Y.DOM.inDoc(root)))) {
87                         nodes = Y.DOM.allById(id, root);
88                     // try className
89                     } else if (className) {
90                         nodes = root.getElementsByClassName(className);
91                     } else { // default to tagName
92                         nodes = root.getElementsByTagName(tagName);
93                     }
95                 } else { // brute getElementsByTagName()
96                     child = root.firstChild;
97                     while (child) {
98                         // only collect HTMLElements
99                         // match tag to supplement missing getElementsByTagName
100                         if (child.tagName && (tagName === '*' || child.tagName === tagName)) {
101                             nodes.push(child);
102                         }
103                         child = child.nextSibling || child.firstChild;
104                     }
105                 }
106                 if (nodes.length) {
107                     ret = Selector._filterNodes(nodes, tokens, firstOnly);
108                 }
109             }
111             return ret;
112         },
113         
114         _filterNodes: function(nodes, tokens, firstOnly) {
115             var i = 0,
116                 j,
117                 len = tokens.length,
118                 n = len - 1,
119                 result = [],
120                 node = nodes[0],
121                 tmpNode = node,
122                 getters = Y.Selector.getters,
123                 operator,
124                 combinator,
125                 token,
126                 path,
127                 pass,
128                 value,
129                 tests,
130                 test;
132             for (i = 0; (tmpNode = node = nodes[i++]);) {
133                 n = len - 1;
134                 path = null;
135                 
136                 testLoop:
137                 while (tmpNode && tmpNode.tagName) {
138                     token = tokens[n];
139                     tests = token.tests;
140                     j = tests.length;
141                     if (j && !pass) {
142                         while ((test = tests[--j])) {
143                             operator = test[1];
144                             if (getters[test[0]]) {
145                                 value = getters[test[0]](tmpNode, test[0]);
146                             } else {
147                                 value = tmpNode[test[0]];
148                                 if (test[0] === 'tagName' && !Selector._isXML) {
149                                     value = value.toUpperCase();    
150                                 }
151                                 if (typeof value != 'string' && value !== undefined && value.toString) {
152                                     value = value.toString(); // coerce for comparison
153                                 } else if (value === undefined && tmpNode.getAttribute) {
154                                     // use getAttribute for non-standard attributes
155                                     value = tmpNode.getAttribute(test[0], 2); // 2 === force string for IE
156                                 }
157                             }
159                             if ((operator === '=' && value !== test[2]) ||  // fast path for equality
160                                 (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
161                                 operator.test && !operator.test(value)) ||  // regex test
162                                 (!operator.test && // protect against RegExp as function (webkit)
163                                         typeof operator === 'function' && !operator(tmpNode, test[0], test[2]))) { // function test
165                                 // skip non element nodes or non-matching tags
166                                 if ((tmpNode = tmpNode[path])) {
167                                     while (tmpNode &&
168                                         (!tmpNode.tagName ||
169                                             (token.tagName && token.tagName !== tmpNode.tagName))
170                                     ) {
171                                         tmpNode = tmpNode[path]; 
172                                     }
173                                 }
174                                 continue testLoop;
175                             }
176                         }
177                     }
179                     n--; // move to next token
180                     // now that we've passed the test, move up the tree by combinator
181                     if (!pass && (combinator = token.combinator)) {
182                         path = combinator.axis;
183                         tmpNode = tmpNode[path];
185                         // skip non element nodes
186                         while (tmpNode && !tmpNode.tagName) {
187                             tmpNode = tmpNode[path]; 
188                         }
190                         if (combinator.direct) { // one pass only
191                             path = null; 
192                         }
194                     } else { // success if we made it this far
195                         result.push(node);
196                         if (firstOnly) {
197                             return result;
198                         }
199                         break;
200                     }
201                 }
202             }
203             node = tmpNode = null;
204             return result;
205         },
207         combinators: {
208             ' ': {
209                 axis: 'parentNode'
210             },
212             '>': {
213                 axis: 'parentNode',
214                 direct: true
215             },
218             '+': {
219                 axis: 'previousSibling',
220                 direct: true
221             }
222         },
224         _parsers: [
225             {
226                 name: ATTRIBUTES,
227                 re: /^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,
228                 fn: function(match, token) {
229                     var operator = match[2] || '',
230                         operators = Selector.operators,
231                         escVal = (match[3]) ? match[3].replace(/\\/g, '') : '',
232                         test;
234                     // add prefiltering for ID and CLASS
235                     if ((match[1] === 'id' && operator === '=') ||
236                             (match[1] === 'className' &&
237                             Y.config.doc.documentElement.getElementsByClassName &&
238                             (operator === '~=' || operator === '='))) {
239                         token.prefilter = match[1];
242                         match[3] = escVal; 
244                         // escape all but ID for prefilter, which may run through QSA (via Dom.allById)
245                         token[match[1]] = (match[1] === 'id') ? match[3] : escVal;
247                     }
249                     // add tests
250                     if (operator in operators) {
251                         test = operators[operator];
252                         if (typeof test === 'string') {
253                             match[3] = escVal.replace(Selector._reRegExpTokens, '\\$1');
254                             test = new RegExp(test.replace('{val}', match[3]));
255                         }
256                         match[2] = test;
257                     }
258                     if (!token.last || token.prefilter !== match[1]) {
259                         return match.slice(1);
260                     }
261                 }
262             },
263             {
264                 name: TAG_NAME,
265                 re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
266                 fn: function(match, token) {
267                     var tag = match[1];
269                     if (!Selector._isXML) {
270                         tag = tag.toUpperCase();
271                     }
273                     token.tagName = tag;
275                     if (tag !== '*' && (!token.last || token.prefilter)) {
276                         return [TAG_NAME, '=', tag];
277                     }
278                     if (!token.prefilter) {
279                         token.prefilter = 'tagName';
280                     }
281                 }
282             },
283             {
284                 name: COMBINATOR,
285                 re: /^\s*([>+~]|\s)\s*/,
286                 fn: function(match, token) {
287                 }
288             },
289             {
290                 name: PSEUDOS,
291                 re: /^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,
292                 fn: function(match, token) {
293                     var test = Selector[PSEUDOS][match[1]];
294                     if (test) { // reorder match array and unescape special chars for tests
295                         if (match[2]) {
296                             match[2] = match[2].replace(/\\/g, '');
297                         }
298                         return [match[2], test]; 
299                     } else { // selector token not supported (possibly missing CSS3 module)
300                         return false;
301                     }
302                 }
303             }
304             ],
306         _getToken: function(token) {
307             return {
308                 tagName: null,
309                 id: null,
310                 className: null,
311                 attributes: {},
312                 combinator: null,
313                 tests: []
314             };
315         },
317         /*
318             Break selector into token units per simple selector.
319             Combinator is attached to the previous token.
320          */
321         _tokenize: function(selector) {
322             selector = selector || '';
323             selector = Selector._parseSelector(Y.Lang.trim(selector)); 
324             var token = Selector._getToken(),     // one token per simple selector (left selector holds combinator)
325                 query = selector, // original query for debug report
326                 tokens = [],    // array of tokens
327                 found = false,  // whether or not any matches were found this pass
328                 match,         // the regex match
329                 test,
330                 i, parser;
332             /*
333                 Search for selector patterns, store, and strip them from the selector string
334                 until no patterns match (invalid selector) or we run out of chars.
336                 Multiple attributes and pseudos are allowed, in any order.
337                 for example:
338                     'form:first-child[type=button]:not(button)[lang|=en]'
339             */
340             outer:
341             do {
342                 found = false; // reset after full pass
343                 for (i = 0; (parser = Selector._parsers[i++]);) {
344                     if ( (match = parser.re.exec(selector)) ) { // note assignment
345                         if (parser.name !== COMBINATOR ) {
346                             token.selector = selector;
347                         }
348                         selector = selector.replace(match[0], ''); // strip current match from selector
349                         if (!selector.length) {
350                             token.last = true;
351                         }
353                         if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
354                             match[1] = Selector._attrFilters[match[1]];
355                         }
357                         test = parser.fn(match, token);
358                         if (test === false) { // selector not supported
359                             found = false;
360                             break outer;
361                         } else if (test) {
362                             token.tests.push(test);
363                         }
365                         if (!selector.length || parser.name === COMBINATOR) {
366                             tokens.push(token);
367                             token = Selector._getToken(token);
368                             if (parser.name === COMBINATOR) {
369                                 token.combinator = Y.Selector.combinators[match[1]];
370                             }
371                         }
372                         found = true;
373                     }
374                 }
375             } while (found && selector.length);
377             if (!found || selector.length) { // not fully parsed
378                 tokens = [];
379             }
380             return tokens;
381         },
383         _replaceMarkers: function(selector) {
384             selector = selector.replace(/\[/g, '\uE003');
385             selector = selector.replace(/\]/g, '\uE004');
387             selector = selector.replace(/\(/g, '\uE005');
388             selector = selector.replace(/\)/g, '\uE006');
389             return selector;
390         },
392         _replaceShorthand: function(selector) {
393             var shorthand = Y.Selector.shorthand,
394                 re;
396             for (re in shorthand) {
397                 if (shorthand.hasOwnProperty(re)) {
398                     selector = selector.replace(new RegExp(re, 'gi'), shorthand[re]);
399                 }
400             }
402             return selector;
403         },
405         _parseSelector: function(selector) {
406             var replaced = Y.Selector._replaceSelector(selector),
407                 selector = replaced.selector;
409             // replace shorthand (".foo, #bar") after pseudos and attrs
410             // to avoid replacing unescaped chars
411             selector = Y.Selector._replaceShorthand(selector);
413             selector = Y.Selector._restore('attr', selector, replaced.attrs);
414             selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
416             // replace braces and parens before restoring escaped chars
417             // to avoid replacing ecaped markers
418             selector = Y.Selector._replaceMarkers(selector);
419             selector = Y.Selector._restore('esc', selector, replaced.esc);
421             return selector;
422         },
424         _attrFilters: {
425             'class': 'className',
426             'for': 'htmlFor'
427         },
429         getters: {
430             href: function(node, attr) {
431                 return Y.DOM.getAttribute(node, attr);
432             },
434             id: function(node, attr) {
435                 return Y.DOM.getId(node);
436             }
437         }
438     };
440 Y.mix(Y.Selector, SelectorCSS2, true);
441 Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
443 // IE wants class with native queries
444 if (Y.Selector.useNative && Y.config.doc.querySelector) {
445     Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
450 }, '3.5.1' ,{requires:['selector-native']});