PageRenderTime 83ms CodeModel.GetById 17ms app.highlight 60ms RepoModel.GetById 2ms app.codeStats 0ms

/hippo/src/main/webapp/yui/selector/selector.js

http://hdbc.googlecode.com/
JavaScript | 648 lines | 459 code | 90 blank | 99 comment | 134 complexity | 47a87f25c723c349dc2b15f6b76f24b7 MD5 | raw file
  1/*
  2Copyright (c) 2009, Yahoo! Inc. All rights reserved.
  3Code licensed under the BSD License:
  4http://developer.yahoo.net/yui/license.txt
  5version: 2.7.0
  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() {
 16var 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
 25Y.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            for (var i = 0, len = nodes.length; i < len; ++i) {
244                if (!nodes[i].tagName) { // tagName limits to HTMLElements 
245                    node = Y.Selector.document.getElementById(nodes[i]);
246                    if (node) { // skip IDs that return null 
247                        nodes[i] = node;
248                    } else {
249                    }
250                }
251            }
252        }
253        result = Y.Selector._filter(nodes, Y.Selector._tokenize(selector)[0]);
254        return result;
255    },
256
257    _filter: function(nodes, token, firstOnly, deDupe) {
258        var result = firstOnly ? null : [],
259            foundCache = Y.Selector._foundCache;
260
261        for (var i = 0, len = nodes.length; i < len; i++) {
262            if (! Y.Selector._test(nodes[i], '', token, deDupe)) {
263                continue;
264            }
265
266            if (firstOnly) {
267                return nodes[i];
268            }
269            if (deDupe) {
270                if (nodes[i]._found) {
271                    continue;
272                }
273                nodes[i]._found = true;
274                foundCache[foundCache.length] = nodes[i];
275            }
276
277            result[result.length] = nodes[i];
278        }
279
280        return result;
281    },
282
283    /**
284     * Retrieves a set of nodes based on a given CSS selector. 
285     * @method query
286     *
287     * @param {string} selector The CSS Selector to test the node against.
288     * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
289     * @param {Boolean} firstOnly optional Whether or not to return only the first match.
290     * @return {Array} An array of nodes that match the given selector.
291     * @static
292     */
293    query: function(selector, root, firstOnly) {
294        var result = Y.Selector._query(selector, root, firstOnly);
295        return result;
296    },
297
298
299    _query: function(selector, root, firstOnly, deDupe) {
300        var result =  (firstOnly) ? null : [],
301            node;
302
303        if (!selector) {
304            return result;
305        }
306
307        var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
308
309        if (groups.length > 1) {
310            var found;
311            for (var i = 0, len = groups.length; i < len; ++i) {
312                found = arguments.callee(groups[i], root, firstOnly, true);
313                result = firstOnly ? found : result.concat(found); 
314            }
315            Y.Selector._clearFoundCache();
316            return result;
317        }
318
319        if (root && !root.nodeName) { // assume ID
320            root = Y.Selector.document.getElementById(root);
321            if (!root) {
322                return result;
323            }
324        }
325
326        root = root || Y.Selector.document;
327
328        if (root.nodeName !== '#document') { // prepend with root selector
329            Y.Dom.generateId(root); // TODO: cleanup after?
330            selector = root.tagName + '#' + root.id + ' ' + selector;
331            node = root;
332            root = root.ownerDocument;
333        }
334
335        var tokens = Y.Selector._tokenize(selector);
336        var idToken = tokens[Y.Selector._getIdTokenIndex(tokens)],
337            nodes = [],
338            id,
339            token = tokens.pop() || {};
340            
341        if (idToken) {
342            id = Y.Selector._getId(idToken.attributes);
343        }
344
345        // use id shortcut when possible
346        if (id) {
347            node = node || Y.Selector.document.getElementById(id);
348
349            if (node && (root.nodeName === '#document' || Y.Dom.isAncestor(root, node))) {
350                if ( Y.Selector._test(node, null, idToken) ) {
351                    if (idToken === token) {
352                        nodes = [node]; // simple selector
353                    } else if (idToken.combinator === ' ' || idToken.combinator === '>') {
354                        root = node; // start from here
355                    }
356                }
357            } else {
358                return result;
359            }
360        }
361
362        if (root && !nodes.length) {
363            nodes = root.getElementsByTagName(token.tag);
364        }
365
366        if (nodes.length) {
367            result = Y.Selector._filter(nodes, token, firstOnly, deDupe); 
368        }
369
370        return result;
371    },
372
373
374    _clearFoundCache: function() {
375        var foundCache = Y.Selector._foundCache;
376        for (var i = 0, len = foundCache.length; i < len; ++i) {
377            try { // IE no like delete
378                delete foundCache[i]._found;
379            } catch(e) {
380                foundCache[i].removeAttribute('_found');
381            }
382        }
383        foundCache = [];
384    },
385
386
387    _getRegExp: function(str, flags) {
388        var regexCache = Y.Selector._regexCache;
389        flags = flags || '';
390        if (!regexCache[str + flags]) {
391            regexCache[str + flags] = new RegExp(str, flags);
392        }
393        return regexCache[str + flags];
394    },
395
396    _getChildren: function() {
397        if (document.documentElement.children) { // document for capability test
398            return function(node, tag) {
399                return (tag) ? node.children.tags(tag) : node.children || [];
400            };
401        } else {
402            return function(node, tag) {
403                if (node._children) {
404                    return node._children;
405                }
406                var children = [],
407                    childNodes = node.childNodes;
408
409                for (var i = 0, len = childNodes.length; i < len; ++i) {
410                    if (childNodes[i].tagName) {
411                        if (!tag || childNodes[i].tagName === tag) {
412                            children[children.length] = childNodes[i];
413                        }
414                    }
415                }
416                node._children = children;
417                return children;
418            };
419        }
420    }(),
421
422    _combinators: {
423        ' ': function(node, token) {
424            while ( (node = node.parentNode) ) {
425                if (Y.Selector._test(node, '', token.previous)) {
426                    return true;
427                }
428            }  
429            return false;
430        },
431
432        '>': function(node, token) {
433            return Y.Selector._test(node.parentNode, null, token.previous);
434        },
435
436        '+': function(node, token) {
437            var sib = node.previousSibling;
438            while (sib && sib.nodeType !== 1) {
439                sib = sib.previousSibling;
440            }
441
442            if (sib && Y.Selector._test(sib, null, token.previous)) {
443                return true; 
444            }
445            return false;
446        },
447
448        '~': function(node, token) {
449            var sib = node.previousSibling;
450            while (sib) {
451                if (sib.nodeType === 1 && Y.Selector._test(sib, null, token.previous)) {
452                    return true;
453                }
454                sib = sib.previousSibling;
455            }
456
457            return false;
458        }
459    },
460
461
462    /*
463        an+b = get every _a_th node starting at the _b_th
464        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
465        1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
466        an+0 = get every _a_th element, "0" may be omitted 
467    */
468    _getNth: function(node, expr, tag, reverse) {
469        Y.Selector._re.nth.test(expr);
470        var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
471            n = RegExp.$2, // "n"
472            oddeven = RegExp.$3, // "odd" or "even"
473            b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
474            result = [],
475            op;
476
477        var siblings = Y.Selector._getChildren(node.parentNode, tag);
478
479        if (oddeven) {
480            a = 2; // always every other
481            op = '+';
482            n = 'n';
483            b = (oddeven === 'odd') ? 1 : 0;
484        } else if ( isNaN(a) ) {
485            a = (n) ? 1 : 0; // start from the first or no repeat
486        }
487
488        if (a === 0) { // just the first
489            if (reverse) {
490                b = siblings.length - b + 1; 
491            }
492
493            if (siblings[b - 1] === node) {
494                return true;
495            } else {
496                return false;
497            }
498
499        } else if (a < 0) {
500            reverse = !!reverse;
501            a = Math.abs(a);
502        }
503
504        if (!reverse) {
505            for (var i = b - 1, len = siblings.length; i < len; i += a) {
506                if ( i >= 0 && siblings[i] === node ) {
507                    return true;
508                }
509            }
510        } else {
511            for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
512                if ( i < len && siblings[i] === node ) {
513                    return true;
514                }
515            }
516        }
517        return false;
518    },
519
520    _getId: function(attr) {
521        for (var i = 0, len = attr.length; i < len; ++i) {
522            if (attr[i][0] == 'id' && attr[i][1] === '=') {
523                return attr[i][2];
524            }
525        }
526    },
527
528    _getIdTokenIndex: function(tokens) {
529        for (var i = 0, len = tokens.length; i < len; ++i) {
530            if (Y.Selector._getId(tokens[i].attributes)) {
531                return i;
532            }
533        }
534        return -1;
535    },
536
537    _patterns: {
538        tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
539        attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
540        pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
541        combinator: /^\s*([>+~]|\s)\s*/
542    },
543
544    /**
545        Break selector into token units per simple selector.
546        Combinator is attached to left-hand selector.
547     */
548    _tokenize: function(selector) {
549        var token = {},     // one token per simple selector (left selector holds combinator)
550            tokens = [],    // array of tokens
551            id,             // unique id for the simple selector (if found)
552            found = false,  // whether or not any matches were found this pass
553            patterns = Y.Selector._patterns,
554            match;          // the regex match
555
556        selector = Y.Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
557
558        /*
559            Search for selector patterns, store, and strip them from the selector string
560            until no patterns match (invalid selector) or we run out of chars.
561
562            Multiple attributes and pseudos are allowed, in any order.
563            for example:
564                'form:first-child[type=button]:not(button)[lang|=en]'
565        */
566        do {
567            found = false; // reset after full pass
568            for (var re in patterns) {
569                if (YAHOO.lang.hasOwnProperty(patterns, re)) {
570                    if (re != 'tag' && re != 'combinator') { // only one allowed
571                        token[re] = token[re] || [];
572                    }
573                    if ( (match = patterns[re].exec(selector)) ) { // note assignment
574                        found = true;
575                        if (re != 'tag' && re != 'combinator') { // only one allowed
576                            // capture ID for fast path to element
577                            if (re === 'attributes' && match[1] === 'id') {
578                                token.id = match[3];
579                            }
580
581                            token[re].push(match.slice(1));
582                        } else { // single selector (tag, combinator)
583                            token[re] = match[1];
584                        }
585                        selector = selector.replace(match[0], ''); // strip current match from selector
586                        if (re === 'combinator' || !selector.length) { // next token or done
587                            token.attributes = Y.Selector._fixAttributes(token.attributes);
588                            token.pseudos = token.pseudos || [];
589                            token.tag = token.tag ? token.tag.toUpperCase() : '*';
590                            tokens.push(token);
591
592                            token = { // prep next token
593                                previous: token
594                            };
595                        }
596                    }
597                }
598            }
599        } while (found);
600
601        return tokens;
602    },
603
604
605    _fixAttributes: function(attr) {
606        var aliases = Y.Selector.attrAliases;
607        attr = attr || [];
608        for (var i = 0, len = attr.length; i < len; ++i) {
609            if (aliases[attr[i][0]]) { // convert reserved words, etc
610                attr[i][0] = aliases[attr[i][0]];
611            }
612            if (!attr[i][1]) { // use exists operator
613                attr[i][1] = '';
614            }
615        }
616        return attr;
617    },
618
619    _replaceShorthand: function(selector) {
620        var shorthand = Y.Selector.shorthand;
621
622        //var attrs = selector.match(Y.Selector._patterns.attributes); // pull attributes to avoid false pos on "." and "#"
623        var attrs = selector.match(Y.Selector._re.attr); // pull attributes to avoid false pos on "." and "#"
624        if (attrs) {
625            selector = selector.replace(Y.Selector._re.attr, 'REPLACED_ATTRIBUTE');
626        }
627        for (var re in shorthand) {
628            if (YAHOO.lang.hasOwnProperty(shorthand, re)) {
629                selector = selector.replace(Y.Selector._getRegExp(re, 'gi'), shorthand[re]);
630            }
631        }
632
633        if (attrs) {
634            for (var i = 0, len = attrs.length; i < len; ++i) {
635                selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
636            }
637        }
638        return selector;
639    }
640};
641
642if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 8) { // rewrite class for IE < 8
643    Y.Selector.attrAliases['class'] = 'className';
644    Y.Selector.attrAliases['for'] = 'htmlFor';
645}
646
647})();
648YAHOO.register("selector", YAHOO.util.Selector, {version: "2.7.0", build: "1799"});