Merge commit 'koha-biblibre/master' into bl-acq
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / lib / yui / selector / selector-debug.js
1 /*
2 Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
5 version: 2.8.0r4
6 */
7 /**
8  * The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements.
9  * @module selector
10  * @title Selector Utility
11  * @namespace YAHOO.util
12  * @requires yahoo, dom
13  */
14
15 (function() {
16 var Y = YAHOO.util;
17
18 /**
19  * Provides helper methods for collecting and filtering DOM elements.
20  * @namespace YAHOO.util
21  * @class Selector
22  * @static
23  */
24
25 Y.Selector = {
26     _foundCache: [],
27     _regexCache: {},
28
29     _re: {
30         nth: /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/,
31         attr: /(\[.*\])/g,
32         urls: /^(?:href|src)/
33     },
34
35     /**
36      * Default document for use queries 
37      * @property document
38      * @type object
39      * @default window.document
40      */
41     document: window.document,
42     /**
43      * Mapping of attributes to aliases, normally to work around HTMLAttributes
44      * that conflict with JS reserved words.
45      * @property attrAliases
46      * @type object
47      */
48     attrAliases: {
49     },
50
51     /**
52      * Mapping of shorthand tokens to corresponding attribute selector 
53      * @property shorthand
54      * @type object
55      */
56     shorthand: {
57         //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
58         '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
59         '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
60     },
61
62     /**
63      * List of operators and corresponding boolean functions. 
64      * These functions are passed the attribute and the current node's value of the attribute.
65      * @property operators
66      * @type object
67      */
68     operators: {
69         '=': function(attr, val) { return attr === val; }, // Equality
70         '!=': function(attr, val) { return attr !== val; }, // Inequality
71         '~=': function(attr, val) { // Match one of space seperated words 
72             var s = ' ';
73             return (s + attr + s).indexOf((s + val + s)) > -1;
74         },
75         '|=': function(attr, val) { return attr === val || attr.slice(0, val.length + 1) === val + '-'; }, // Matches value followed by optional hyphen
76         '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
77         '$=': function(attr, val) { return attr.slice(-val.length) === val; }, // Match ends with value
78         '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring 
79         '': function(attr, val) { return attr; } // Just test for existence of attribute
80     },
81
82     /**
83      * List of pseudo-classes and corresponding boolean functions. 
84      * These functions are called with the current node, and any value that was parsed with the pseudo regex.
85      * @property pseudos
86      * @type object
87      */
88     pseudos: {
89         'root': function(node) {
90             return node === node.ownerDocument.documentElement;
91         },
92
93         'nth-child': function(node, val) {
94             return Y.Selector._getNth(node, val);
95         },
96
97         'nth-last-child': function(node, val) {
98             return Y.Selector._getNth(node, val, null, true);
99         },
100
101         'nth-of-type': function(node, val) {
102             return Y.Selector._getNth(node, val, node.tagName);
103         },
104          
105         'nth-last-of-type': function(node, val) {
106             return Y.Selector._getNth(node, val, node.tagName, true);
107         },
108          
109         'first-child': function(node) {
110             return Y.Selector._getChildren(node.parentNode)[0] === node;
111         },
112
113         'last-child': function(node) {
114             var children = Y.Selector._getChildren(node.parentNode);
115             return children[children.length - 1] === node;
116         },
117
118         'first-of-type': function(node, val) {
119             return Y.Selector._getChildren(node.parentNode, node.tagName)[0];
120         },
121          
122         'last-of-type': function(node, val) {
123             var children = Y.Selector._getChildren(node.parentNode, node.tagName);
124             return children[children.length - 1];
125         },
126          
127         'only-child': function(node) {
128             var children = Y.Selector._getChildren(node.parentNode);
129             return children.length === 1 && children[0] === node;
130         },
131
132         'only-of-type': function(node) {
133             return Y.Selector._getChildren(node.parentNode, node.tagName).length === 1;
134         },
135
136         'empty': function(node) {
137             return node.childNodes.length === 0;
138         },
139
140         'not': function(node, simple) {
141             return !Y.Selector.test(node, simple);
142         },
143
144         'contains': function(node, str) {
145             var text = node.innerText || node.textContent || '';
146             return text.indexOf(str) > -1;
147         },
148         'checked': function(node) {
149             return node.checked === true;
150         }
151     },
152
153     /**
154      * Test if the supplied node matches the supplied selector.
155      * @method test
156      *
157      * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
158      * @param {string} selector The CSS Selector to test the node against.
159      * @return{boolean} Whether or not the node matches the selector.
160      * @static
161     
162      */
163     test: function(node, selector) {
164         node = Y.Selector.document.getElementById(node) || node;
165
166         if (!node) {
167             return false;
168         }
169
170         var groups = selector ? selector.split(',') : [];
171         if (groups.length) {
172             for (var i = 0, len = groups.length; i < len; ++i) {
173                 if ( Y.Selector._test(node, groups[i]) ) { // passes if ANY group matches
174                     return true;
175                 }
176             }
177             return false;
178         }
179         return Y.Selector._test(node, selector);
180     },
181
182     _test: function(node, selector, token, deDupe) {
183         token = token || Y.Selector._tokenize(selector).pop() || {};
184
185         if (!node.tagName ||
186             (token.tag !== '*' && node.tagName !== token.tag) ||
187             (deDupe && node._found) ) {
188             return false;
189         }
190
191         if (token.attributes.length) {
192             var val,
193                 ieFlag,
194                 re_urls = Y.Selector._re.urls;
195
196             if (!node.attributes || !node.attributes.length) {
197                 return false;
198             }
199             for (var i = 0, attr; attr = token.attributes[i++];) {
200                 ieFlag = (re_urls.test(attr[0])) ? 2 : 0;
201                 val = node.getAttribute(attr[0], ieFlag);
202                 if (val === null || val === undefined) {
203                     return false;
204                 }
205                 if ( Y.Selector.operators[attr[1]] &&
206                         !Y.Selector.operators[attr[1]](val, attr[2])) {
207                     return false;
208                 }
209             }
210         }
211
212         if (token.pseudos.length) {
213             for (var i = 0, len = token.pseudos.length; i < len; ++i) {
214                 if (Y.Selector.pseudos[token.pseudos[i][0]] &&
215                         !Y.Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
216                     return false;
217                 }
218             }
219         }
220
221         return (token.previous && token.previous.combinator !== ',') ?
222                 Y.Selector._combinators[token.previous.combinator](node, token) :
223                 true;
224     },
225
226     /**
227      * Filters a set of nodes based on a given CSS selector. 
228      * @method filter
229      *
230      * @param {array} nodes A set of nodes/ids to filter. 
231      * @param {string} selector The selector used to test each node.
232      * @return{array} An array of nodes from the supplied array that match the given selector.
233      * @static
234      */
235     filter: function(nodes, selector) {
236         nodes = nodes || [];
237
238         var node,
239             result = [],
240             tokens = Y.Selector._tokenize(selector);
241
242         if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes
243             YAHOO.log('filter: scanning input for HTMLElements/IDs', 'info', 'Selector');
244             for (var i = 0, len = nodes.length; i < len; ++i) {
245                 if (!nodes[i].tagName) { // tagName limits to HTMLElements 
246                     node = Y.Selector.document.getElementById(nodes[i]);
247                     if (node) { // skip IDs that return null 
248                         nodes[i] = node;
249                     } else {
250                         YAHOO.log('filter: skipping invalid node', 'warn', 'Selector');
251                     }
252                 }
253             }
254         }
255         result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
256         YAHOO.log('filter: returning:' + result.length, 'info', 'Selector');
257         return result;
258     },
259
260     _filter: function(nodes, token, firstOnly, deDupe) {
261         var result = firstOnly ? null : [],
262             foundCache = Y.Selector._foundCache;
263
264         for (var i = 0, len = nodes.length; i < len; i++) {
265             if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
266                 continue;
267             }
268
269             if (firstOnly) {
270                 return nodes[i];
271             }
272             if (deDupe) {
273                 if (nodes[i]._found) {
274                     continue;
275                 }
276                 nodes[i]._found = true;
277                 foundCache[foundCache.length] = nodes[i];
278             }
279
280             result[result.length] = nodes[i];
281         }
282
283         return result;
284     },
285
286     /**
287      * Retrieves a set of nodes based on a given CSS selector. 
288      * @method query
289      *
290      * @param {string} selector The CSS Selector to test the node against.
291      * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
292      * @param {Boolean} firstOnly optional Whether or not to return only the first match.
293      * @return {Array} An array of nodes that match the given selector.
294      * @static
295      */
296     query: function(selector, root, firstOnly) {
297         var result = Y.Selector._query(selector, root, firstOnly);
298         YAHOO.log('query: returning ' + result, 'info', 'Selector');
299         return result;
300     },
301
302
303     _query: function(selector, root, firstOnly, deDupe) {
304         var result =  (firstOnly) ? null : [],
305             node;
306
307         if (!selector) {
308             return result;
309         }
310
311         var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
312
313         if (groups.length > 1) {
314             var found;
315             for (var i = 0, len = groups.length; i < len; ++i) {
316                 found = Y.Selector._query(groups[i], root, firstOnly, true);
317                 result = firstOnly ? found : result.concat(found); 
318             }
319             Y.Selector._clearFoundCache();
320             return result;
321         }
322
323         if (root && !root.nodeName) { // assume ID
324             root = Y.Selector.document.getElementById(root);
325             if (!root) {
326                 YAHOO.log('invalid root node provided', 'warn', 'Selector');
327                 return result;
328             }
329         }
330
331         root = root || Y.Selector.document;
332
333         if (root.nodeName !== '#document') { // prepend with root selector
334             Y.Dom.generateId(root); // TODO: cleanup after?
335             selector = root.tagName + '#' + root.id + ' ' + selector;
336             node = root;
337             root = root.ownerDocument;
338         }
339
340         var tokens = Y.Selector._tokenize(selector);
341         var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
342             nodes = [],
343             id,
344             token = tokens.pop() || {};
345             
346         if (idToken) {
347             id = Y.Selector._getId(idToken.attributes);
348         }
349
350         // use id shortcut when possible
351         if (id) {
352             node = node || Y.Selector.document.getElementById(id);
353
354             if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
355                 if ( Y.Selector._test(node, null, idToken) ) {
356                     if (idToken === token) {
357                         nodes = [node]; // simple selector
358                     } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
359                         root = node; // start from here
360                     }
361                 }
362             } else {
363                 return result;
364             }
365         }
366
367         if (root && !nodes.length) {
368             nodes = root.getElementsByTagName(token.tag);
369         }
370
371         if (nodes.length) {
372             result = Y.Selector._filter(nodes, token, firstOnly, deDupe); 
373         }
374
375         return result;
376     },
377
378
379     _clearFoundCache: function() {
380         var foundCache = Y.Selector._foundCache;
381         YAHOO.log('getBySelector: clearing found cache of ' + foundCache.length + ' elements');
382         for (var i = 0, len = foundCache.length; i < len; ++i) {
383             try { // IE no like delete
384                 delete foundCache[i]._found;
385             } catch(e) {
386                 foundCache[i].removeAttribute('_found');
387             }
388         }
389         foundCache = [];
390         YAHOO.log('getBySelector: done clearing foundCache');
391     },
392
393
394     _getRegExp: function(str, flags) {
395         var regexCache = Y.Selector._regexCache;
396         flags = flags || '';
397         if (!regexCache[str + flags]) {
398             regexCache[str + flags] = new RegExp(str, flags);
399         }
400         return regexCache[str + flags];
401     },
402
403     _getChildren: function() {
404         if (document.documentElement.children && document.documentElement.children.tags) { // document for capability test
405             return function(node, tag) {
406                 return (tag) ? node.children.tags(tag) : node.children || [];
407             };
408         } else {
409             return function(node, tag) {
410                 var children = [],
411                     childNodes = node.childNodes;
412
413                 for (var i = 0, len = childNodes.length; i < len; ++i) {
414                     if (childNodes[i].tagName) {
415                         if (!tag || childNodes[i].tagName === tag) {
416                             children.push(childNodes[i]);
417                         }
418                     }
419                 }
420                 return children;
421             };
422         }
423     }(),
424
425     _combinators: {
426         ' ': function(node, token) {
427             while ( (node = node.parentNode) ) {
428                 if (Y.Selector._test(node, '', token.previous)) {
429                     return true;
430                 }
431             }  
432             return false;
433         },
434
435         '>': function(node, token) {
436             return Y.Selector._test(node.parentNode, null, token.previous);
437         },
438
439         '+': function(node, token) {
440             var sib = node.previousSibling;
441             while (sib && sib.nodeType !== 1) {
442                 sib = sib.previousSibling;
443             }
444
445             if (sib && Y.Selector._test(sib, null, token.previous)) {
446                 return true; 
447             }
448             return false;
449         },
450
451         '~': function(node, token) {
452             var sib = node.previousSibling;
453             while (sib) {
454                 if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
455                     return true;
456                 }
457                 sib = sib.previousSibling;
458             }
459
460             return false;
461         }
462     },
463
464
465     /*
466         an+b = get every _a_th node starting at the _b_th
467         0n+b = no repeat ("0" and "n" may both be omitted (together) , e.g. "0n+1" or "1", not "0+1"), return only the _b_th element
468         1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
469         an+0 = get every _a_th element, "0" may be omitted 
470     */
471     _getNth: function(node, expr, tag, reverse) {
472         Y.Selector._re.nth.test(expr);
473         var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
474             n = RegExp.$2, // "n"
475             oddeven = RegExp.$3, // "odd" or "even"
476             b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
477             result = [],
478             op;
479
480         var siblings = Y.Selector._getChildren(node.parentNode, tag);
481
482         if (oddeven) {
483             a = 2; // always every other
484             op = '+';
485             n = 'n';
486             b = (oddeven === 'odd') ? 1 : 0;
487         } else if ( isNaN(a) ) {
488             a = (n) ? 1 : 0; // start from the first or no repeat
489         }
490
491         if (a === 0) { // just the first
492             if (reverse) {
493                 b = siblings.length - b + 1; 
494             }
495
496             if (siblings[b - 1] === node) {
497                 return true;
498             } else {
499                 return false;
500             }
501
502         } else if (a < 0) {
503             reverse = !!reverse;
504             a = Math.abs(a);
505         }
506
507         if (!reverse) {
508             for (var i = b - 1, len = siblings.length; i < len; i += a) {
509                 if ( i >= 0 && siblings[i] === node ) {
510                     return true;
511                 }
512             }
513         } else {
514             for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
515                 if ( i < len && siblings[i] === node ) {
516                     return true;
517                 }
518             }
519         }
520         return false;
521     },
522
523     _getId: function(attr) {
524         for (var i = 0, len = attr.length; i < len; ++i) {
525             if (attr[i][0] == 'id' && attr[i][1] === '=') {
526                 return attr[i][2];
527             }
528         }
529     },
530
531     _getIdTokenIndex: function(tokens) {
532         for (var i = 0, len = tokens.length; i < len; ++i) {
533             if (Y.Selector._getId(tokens[i].attributes)) {
534                 return i;
535             }
536         }
537         return -1;
538     },
539
540     _patterns: {
541         tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
542         attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
543         pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
544         combinator: /^\s*([>+~]|\s)\s*/
545     },
546
547     /**
548         Break selector into token units per simple selector.
549         Combinator is attached to left-hand selector.
550      */
551     _tokenize: function(selector) {
552         var token = {},     // one token per simple selector (left selector holds combinator)
553             tokens = [],    // array of tokens
554             id,             // unique id for the simple selector (if found)
555             found = false,  // whether or not any matches were found this pass
556             patterns = Y.Selector._patterns,
557             match;          // the regex match
558
559         selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
560
561         /*
562             Search for selector patterns, store, and strip them from the selector string
563             until no patterns match (invalid selector) or we run out of chars.
564
565             Multiple attributes and pseudos are allowed, in any order.
566             for example:
567                 'form:first-child[type=button]:not(button)[lang|=en]'
568         */
569         do {
570             found = false; // reset after full pass
571             for (var re in patterns) {
572                 if (YAHOO.lang.hasOwnProperty(patterns, re)) {
573                     if (re != 'tag' && re != 'combinator') { // only one allowed
574                         token[re] = token[re] || [];
575                     }
576                     if ( (match = patterns[re].exec(selector)) ) { // note assignment
577                         found = true;
578                         if (re != 'tag' && re != 'combinator') { // only one allowed
579                             // capture ID for fast path to element
580                             if (re === 'attributes' && match[1] === 'id') {
581                                 token.id = match[3];
582                             }
583
584                             token[re].push(match.slice(1));
585                         } else { // single selector (tag, combinator)
586                             token[re] = match[1];
587                         }
588                         selector = selector.replace(match[0], ''); // strip current match from selector
589                         if (re === 'combinator' || !selector.length) { // next token or done
590                             token.attributes = Y.Selector._fixAttributes(token.attributes);
591                             token.pseudos = token.pseudos || [];
592                             token.tag = token.tag ? token.tag.toUpperCase() : '*';
593                             tokens.push(token);
594
595                             token = { // prep next token
596                                 previous: token
597                             };
598                         }
599                     }
600                 }
601             }
602         } while (found);
603
604         return tokens;
605     },
606
607
608     _fixAttributes: function(attr) {
609         var aliases = Y.Selector.attrAliases;
610         attr = attr || [];
611         for (var i = 0, len = attr.length; i < len; ++i) {
612             if (aliases[attr[i][0]]) { // convert reserved words, etc
613                 attr[i][0] = aliases[attr[i][0]];
614             }
615             if (!attr[i][1]) { // use exists operator
616                 attr[i][1] = '';
617             }
618         }
619         return attr;
620     },
621
622     _replaceShorthand: function(selector) {
623         var shorthand = Y.Selector.shorthand;
624
625         //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
626         var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
627         if (attrs) {
628             selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
629         }
630         for (var re in shorthand) {
631             if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
632                 selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
633             }
634         }
635
636         if (attrs) {
637             for (var i = 0, len = attrs.length; i < len; ++i) {
638                 selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
639             }
640         }
641         return selector;
642     }
643 };
644
645 if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
646     Y.Selector.attrAliases['class'] = 'className';
647     Y.Selector.attrAliases['for'] = 'htmlFor';
648 }
649
650 })();
651 YAHOO.register("selector", YAHOO.util.Selector, {version: "2.8.0r4", build: "2449"});