2
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
8
* The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements.
10
* @title Selector Utility
11
* @namespace YAHOO.util
12
* @requires yahoo, dom
17
* Provides helper methods for collecting and filtering DOM elements.
18
* @namespace YAHOO.util
22
var Selector = function() {};
26
var reNth = /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/;
28
Selector.prototype = {
30
* Default document for use queries
33
* @default window.document
35
document: window.document,
37
* Mapping of attributes to aliases, normally to work around HTMLAttributes
38
* that conflict with JS reserved words.
39
* @property attrAliases
46
* Mapping of shorthand tokens to corresponding attribute selector
51
//'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]',
52
'\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
53
'\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
57
* List of operators and corresponding boolean functions.
58
* These functions are passed the attribute and the current node's value of the attribute.
63
'=': function(attr, val) { return attr === val; }, // Equality
64
'!=': function(attr, val) { return attr !== val; }, // Inequality
65
'~=': function(attr, val) { // Match one of space seperated words
67
return (s + attr + s).indexOf((s + val + s)) > -1;
69
'|=': function(attr, val) { return getRegExp('^' + val + '[-]?').test(attr); }, // Match start with value followed by optional hyphen
70
'^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
71
'$=': function(attr, val) { return attr.lastIndexOf(val) === attr.length - val.length; }, // Match ends with value
72
'*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring
73
'': function(attr, val) { return attr; } // Just test for existence of attribute
77
* List of pseudo-classes and corresponding boolean functions.
78
* These functions are called with the current node, and any value that was parsed with the pseudo regex.
83
'root': function(node) {
84
return node === node.ownerDocument.documentElement;
87
'nth-child': function(node, val) {
88
return getNth(node, val);
91
'nth-last-child': function(node, val) {
92
return getNth(node, val, null, true);
95
'nth-of-type': function(node, val) {
96
return getNth(node, val, node.tagName);
99
'nth-last-of-type': function(node, val) {
100
return getNth(node, val, node.tagName, true);
103
'first-child': function(node) {
104
return getChildren(node.parentNode)[0] === node;
107
'last-child': function(node) {
108
var children = getChildren(node.parentNode);
109
return children[children.length - 1] === node;
112
'first-of-type': function(node, val) {
113
return getChildren(node.parentNode, node.tagName.toLowerCase())[0];
116
'last-of-type': function(node, val) {
117
var children = getChildren(node.parentNode, node.tagName.toLowerCase());
118
return children[children.length - 1];
121
'only-child': function(node) {
122
var children = getChildren(node.parentNode);
123
return children.length === 1 && children[0] === node;
126
'only-of-type': function(node) {
127
return getChildren(node.parentNode, node.tagName.toLowerCase()).length === 1;
130
'empty': function(node) {
131
return node.childNodes.length === 0;
134
'not': function(node, simple) {
135
return !Selector.test(node, simple);
138
'contains': function(node, str) {
139
var text = node.innerText || node.textContent || '';
140
return text.indexOf(str) > -1;
142
'checked': function(node) {
143
return node.checked === true;
148
* Test if the supplied node matches the supplied selector.
151
* @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
152
* @param {string} selector The CSS Selector to test the node against.
153
* @return{boolean} Whether or not the node matches the selector.
157
test: function(node, selector) {
158
node = Selector.document.getElementById(node) || node;
164
var groups = selector ? selector.split(',') : [];
166
for (var i = 0, len = groups.length; i < len; ++i) {
167
if ( rTestNode(node, groups[i]) ) { // passes if ANY group matches
173
return rTestNode(node, selector);
177
* Filters a set of nodes based on a given CSS selector.
180
* @param {array} nodes A set of nodes/ids to filter.
181
* @param {string} selector The selector used to test each node.
182
* @return{array} An array of nodes from the supplied array that match the given selector.
185
filter: function(nodes, selector) {
190
tokens = tokenize(selector);
192
if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes
193
for (var i = 0, len = nodes.length; i < len; ++i) {
194
if (!nodes[i].tagName) { // tagName limits to HTMLElements
195
node = Selector.document.getElementById(nodes[i]);
196
if (node) { // skip IDs that return null
203
result = rFilter(nodes, tokenize(selector)[0]);
209
* Retrieves a set of nodes based on a given CSS selector.
212
* @param {string} selector The CSS Selector to test the node against.
213
* @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
214
* @param {Boolean} firstOnly optional Whether or not to return only the first match.
215
* @return {Array} An array of nodes that match the given selector.
218
query: function(selector, root, firstOnly) {
219
var result = query(selector, root, firstOnly);
224
var query = function(selector, root, firstOnly, deDupe) {
225
var result = (firstOnly) ? null : [];
230
var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
232
if (groups.length > 1) {
234
for (var i = 0, len = groups.length; i < len; ++i) {
235
found = arguments.callee(groups[i], root, firstOnly, true);
236
result = firstOnly ? found : result.concat(found);
242
if (root && !root.nodeName) { // assume ID
243
root = Selector.document.getElementById(root);
249
root = root || Selector.document;
250
var tokens = tokenize(selector);
251
var idToken = tokens[getIdTokenIndex(tokens)],
255
token = tokens.pop() || {};
258
id = getId(idToken.attributes);
261
// use id shortcut when possible
263
node = Selector.document.getElementById(id);
265
if (node && (root.nodeName == '#document' || contains(node, root))) {
266
if ( rTestNode(node, null, idToken) ) {
267
if (idToken === token) {
268
nodes = [node]; // simple selector
270
root = node; // start from here
278
if (root && !nodes.length) {
279
nodes = root.getElementsByTagName(token.tag);
283
result = rFilter(nodes, token, firstOnly, deDupe);
290
var contains = function() {
291
if (document.documentElement.contains && !YAHOO.env.ua.webkit < 422) { // IE & Opera, Safari < 3 contains is broken
292
return function(needle, haystack) {
293
return haystack.contains(needle);
295
} else if ( document.documentElement.compareDocumentPosition ) { // gecko
296
return function(needle, haystack) {
297
return !!(haystack.compareDocumentPosition(needle) & 16);
299
} else { // Safari < 3
300
return function(needle, haystack) {
301
var parent = needle.parentNode;
303
if (needle === parent) {
306
parent = parent.parentNode;
313
var rFilter = function(nodes, token, firstOnly, deDupe) {
314
var result = firstOnly ? null : [];
316
for (var i = 0, len = nodes.length; i < len; i++) {
317
if (! rTestNode(nodes[i], '', token, deDupe)) {
325
if (nodes[i]._found) {
328
nodes[i]._found = true;
329
foundCache[foundCache.length] = nodes[i];
332
result[result.length] = nodes[i];
338
var rTestNode = function(node, selector, token, deDupe) {
339
token = token || tokenize(selector).pop() || {};
342
(token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) ||
343
(deDupe && node._found) ) {
347
if (token.attributes.length) {
349
for (var i = 0, len = token.attributes.length; i < len; ++i) {
350
attribute = node.getAttribute(token.attributes[i][0], 2);
351
if (attribute === null || attribute === undefined) {
354
if ( Selector.operators[token.attributes[i][1]] &&
355
!Selector.operators[token.attributes[i][1]](attribute, token.attributes[i][2])) {
361
if (token.pseudos.length) {
362
for (var i = 0, len = token.pseudos.length; i < len; ++i) {
363
if (Selector.pseudos[token.pseudos[i][0]] &&
364
!Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) {
370
return (token.previous && token.previous.combinator !== ',') ?
371
combinators[token.previous.combinator](node, token) :
377
var parentCache = [];
380
var clearFoundCache = function() {
381
for (var i = 0, len = foundCache.length; i < len; ++i) {
382
try { // IE no like delete
383
delete foundCache[i]._found;
385
foundCache[i].removeAttribute('_found');
391
var clearParentCache = function() {
392
if (!document.documentElement.children) { // caching children lookups for gecko
394
for (var i = 0, len = parentCache.length; i < len; ++i) {
395
delete parentCache[i]._children;
399
} else return function() {}; // do nothing
402
var getRegExp = function(str, flags) {
404
if (!regexCache[str + flags]) {
405
regexCache[str + flags] = new RegExp(str, flags);
407
return regexCache[str + flags];
411
' ': function(node, token) {
412
while (node = node.parentNode) {
413
if (rTestNode(node, '', token.previous)) {
420
'>': function(node, token) {
421
return rTestNode(node.parentNode, null, token.previous);
424
'+': function(node, token) {
425
var sib = node.previousSibling;
426
while (sib && sib.nodeType !== 1) {
427
sib = sib.previousSibling;
430
if (sib && rTestNode(sib, null, token.previous)) {
436
'~': function(node, token) {
437
var sib = node.previousSibling;
439
if (sib.nodeType === 1 && rTestNode(sib, null, token.previous)) {
442
sib = sib.previousSibling;
449
var getChildren = function() {
450
if (document.documentElement.children) { // document for capability test
451
return function(node, tag) {
452
return (tag) ? node.children.tags(tag) : node.children || [];
455
return function(node, tag) {
456
if (node._children) {
457
return node._children;
460
childNodes = node.childNodes;
462
for (var i = 0, len = childNodes.length; i < len; ++i) {
463
if (childNodes[i].tagName) {
464
if (!tag || childNodes[i].tagName.toLowerCase() === tag) {
465
children[children.length] = childNodes[i];
469
node._children = children;
470
parentCache[parentCache.length] = node;
477
an+b = get every _a_th node starting at the _b_th
478
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
479
1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
480
an+0 = get every _a_th element, "0" may be omitted
482
var getNth = function(node, expr, tag, reverse) {
483
if (tag) tag = tag.toLowerCase();
485
var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
486
n = RegExp.$2, // "n"
487
oddeven = RegExp.$3, // "odd" or "even"
488
b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
491
var siblings = getChildren(node.parentNode, tag);
494
a = 2; // always every other
497
b = (oddeven === 'odd') ? 1 : 0;
498
} else if ( isNaN(a) ) {
499
a = (n) ? 1 : 0; // start from the first or no repeat
502
if (a === 0) { // just the first
504
b = siblings.length - b + 1;
507
if (siblings[b - 1] === node) {
519
for (var i = b - 1, len = siblings.length; i < len; i += a) {
520
if ( i >= 0 && siblings[i] === node ) {
525
for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
526
if ( i < len && siblings[i] === node ) {
534
var getId = function(attr) {
535
for (var i = 0, len = attr.length; i < len; ++i) {
536
if (attr[i][0] == 'id' && attr[i][1] === '=') {
542
var getIdTokenIndex = function(tokens) {
543
for (var i = 0, len = tokens.length; i < len; ++i) {
544
if (getId(tokens[i].attributes)) {
552
tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
553
attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
554
//attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^'"\]]*)['"]?\]*/i,
555
pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
556
combinator: /^\s*([>+~]|\s)\s*/
560
Break selector into token units per simple selector.
561
Combinator is attached to left-hand selector.
563
var tokenize = function(selector) {
564
var token = {}, // one token per simple selector (left selector holds combinator)
565
tokens = [], // array of tokens
566
id, // unique id for the simple selector (if found)
567
found = false, // whether or not any matches were found this pass
568
match; // the regex match
570
selector = replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
573
Search for selector patterns, store, and strip them from the selector string
574
until no patterns match (invalid selector) or we run out of chars.
576
Multiple attributes and pseudos are allowed, in any order.
578
'form:first-child[type=button]:not(button)[lang|=en]'
581
found = false; // reset after full pass
582
for (var re in patterns) {
583
if (!YAHOO.lang.hasOwnProperty(patterns, re)) {
586
if (re != 'tag' && re != 'combinator') { // only one allowed
587
token[re] = token[re] || [];
589
if (match = patterns[re].exec(selector)) { // note assignment
591
if (re != 'tag' && re != 'combinator') { // only one allowed
592
//token[re] = token[re] || [];
594
// capture ID for fast path to element
595
if (re === 'attributes' && match[1] === 'id') {
599
token[re].push(match.slice(1));
600
} else { // single selector (tag, combinator)
601
token[re] = match[1];
603
selector = selector.replace(match[0], ''); // strip current match from selector
604
if (re === 'combinator' || !selector.length) { // next token or done
605
token.attributes = fixAttributes(token.attributes);
606
token.pseudos = token.pseudos || [];
607
token.tag = token.tag ? token.tag.toUpperCase() : '*';
610
token = { // prep next token
621
var fixAttributes = function(attr) {
622
var aliases = Selector.attrAliases;
624
for (var i = 0, len = attr.length; i < len; ++i) {
625
if (aliases[attr[i][0]]) { // convert reserved words, etc
626
attr[i][0] = aliases[attr[i][0]];
628
if (!attr[i][1]) { // use exists operator
635
var replaceShorthand = function(selector) {
636
var shorthand = Selector.shorthand;
637
var attrs = selector.match(patterns.attributes); // pull attributes to avoid false pos on "." and "#"
639
selector = selector.replace(patterns.attributes, 'REPLACED_ATTRIBUTE');
641
for (var re in shorthand) {
642
if (!YAHOO.lang.hasOwnProperty(shorthand, re)) {
645
selector = selector.replace(getRegExp(re, 'gi'), shorthand[re]);
649
for (var i = 0, len = attrs.length; i < len; ++i) {
650
selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
656
Selector = new Selector();
657
Selector.patterns = patterns;
658
Y.Selector = Selector;
660
if (YAHOO.env.ua.ie) { // rewrite class for IE (others use getAttribute('class')
661
Y.Selector.attrAliases['class'] = 'className';
662
Y.Selector.attrAliases['for'] = 'htmlFor';
666
YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"});