2
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
7
YUI.add('selector', function(Y) {
10
* Provides helper methods for collecting and filtering DOM elements.
16
* Provides helper methods for collecting and filtering DOM elements.
22
PARENT_NODE = 'parentNode',
23
PREVIOUS_SIBLING = 'previousSibling',
25
NODE_TYPE = 'nodeType',
27
ATTRIBUTES = 'attributes',
29
COMBINATOR = 'combinator';
31
var reNth = /^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/;
34
tag: /^((?:-?[_a-z]+[\w\-]*)|\*)/i,
35
attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
36
pseudos: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
37
combinator: /^\s*([>+~]|\s)\s*/
42
* Default document for use queries
45
* @default window.document
47
document: Y.config.doc,
49
* Mapping of attributes to aliases, normally to work around HTMLAttributes
50
* that conflict with JS reserved words.
51
* @property attrAliases
57
* Mapping of shorthand tokens to corresponding attribute selector
62
'\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
63
'\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
67
* List of operators and corresponding boolean functions.
68
* These functions are passed the attribute and the current node's value of the attribute.
73
'=': function(attr, val) { return attr === val; }, // Equality
74
'!=': function(attr, val) { return attr !== val; }, // Inequality
75
'~=': function(attr, val) { // Match one of space seperated words
77
return (s + attr + s).indexOf((s + val + s)) > -1;
79
'|=': function(attr, val) { return Y.DOM._getRegExp('^' + val + '[-]?').test(attr); }, // Match start with value followed by optional hyphen
80
'^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
81
'$=': function(attr, val) { return attr.lastIndexOf(val) === attr[LENGTH] - val[LENGTH]; }, // Match ends with value
82
'*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring
83
'': function(attr, val) { return attr; } // Just test for existence of attribute
87
* List of pseudo-classes and corresponding boolean functions.
88
* These functions are called with the current node, and any value that was parsed with the pseudo regex.
93
'root': function(node) {
94
return node === node.ownerDocument.documentElement;
97
'nth-child': function(node, val) {
98
return Selector.getNth(node, val);
101
'nth-last-child': function(node, val) {
102
return Selector.getNth(node, val, null, true);
105
'nth-of-type': function(node, val) {
106
return Selector.getNth(node, val, node[TAG_NAME]);
109
'nth-last-of-type': function(node, val) {
110
return Selector.getNth(node, val, node[TAG_NAME], true);
113
'first-child': function(node) {
114
return Y.DOM.firstChild(node[PARENT_NODE]) === node;
117
'last-child': function(node) {
118
return Y.DOM.lastChild(node[PARENT_NODE]) === node;
121
'first-of-type': function(node, val) {
122
return Y.DOM.firstChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
125
'last-of-type': function(node, val) {
126
return Y.DOM.lastChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
129
'only-child': function(node) {
130
var children = Y.DOM.children(node[PARENT_NODE]);
131
return children[LENGTH] === 1 && children[0] === node;
134
'only-of-type': function(node) {
135
return Y.DOM.childrenByTag(node[PARENT_NODE], node[TAG_NAME])[LENGTH] === 1;
138
'empty': function(node) {
139
return node.childNodes[LENGTH] === 0;
142
'not': function(node, simple) {
143
return !Selector.test(node, simple);
146
'contains': function(node, str) {
147
var text = node.innerText || node.textContent || '';
148
return text.indexOf(str) > -1;
150
'checked': function(node) {
151
return node.checked === true;
156
* Test if the supplied node matches the supplied selector.
159
* @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
160
* @param {string} selector The CSS Selector to test the node against.
161
* @return{boolean} Whether or not the node matches the selector.
165
test: function(node, selector) {
170
var groups = selector ? selector.split(',') : [];
171
if (groups[LENGTH]) {
172
for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
173
if ( Selector._testNode(node, groups[i]) ) { // passes if ANY group matches
179
return Selector._testNode(node, selector);
183
* Filters a set of nodes based on a given CSS selector.
186
* @param {array} nodes A set of nodes/ids to filter.
187
* @param {string} selector The selector used to test each node.
188
* @return{array} An array of nodes from the supplied array that match the given selector.
191
filter: function(nodes, selector) {
194
var result = Selector._filter(nodes, Selector._tokenize(selector)[0]);
199
* Retrieves a set of nodes based on a given CSS selector.
202
* @param {string} selector The CSS Selector to test the node against.
203
* @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
204
* @param {Boolean} firstOnly optional Whether or not to return only the first match.
205
* @return {Array} An array of nodes that match the given selector.
208
query: function(selector, root, firstOnly) {
209
var result = Selector._query(selector, root, firstOnly);
213
_query: function(selector, root, firstOnly, deDupe) {
214
var result = (firstOnly) ? null : [];
219
root = root || Selector.document;
220
var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
222
if (groups[LENGTH] > 1) {
224
for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
225
found = arguments.callee(groups[i], root, firstOnly, true);
226
result = firstOnly ? found : result.concat(found);
228
Selector._clearFoundCache();
232
var tokens = Selector._tokenize(selector);
233
var idToken = tokens[Selector._getIdTokenIndex(tokens)],
237
token = tokens.pop() || {};
240
id = Selector._getId(idToken[ATTRIBUTES]);
243
// use id shortcut when possible
245
node = Selector.document.getElementById(id);
247
if (node && (root[NODE_TYPE] === 9 || Y.DOM.contains(root, node))) {
248
if ( Selector._testNode(node, null, idToken) ) {
249
if (idToken === token) {
250
nodes = [node]; // simple selector
252
root = node; // start from here
260
if (root && !nodes[LENGTH]) {
261
nodes = root.getElementsByTagName(token[TAG]);
265
result = Selector._filter(nodes, token, firstOnly, deDupe);
270
_filter: function(nodes, token, firstOnly, deDupe) {
271
var result = firstOnly ? null : [];
273
result = Y.DOM.filterElementsBy(nodes, function(node) {
274
if (! Selector._testNode(node, '', token, deDupe)) {
283
Selector._foundCache[Selector._foundCache[LENGTH]] = node;
291
_testNode: function(node, selector, token, deDupe) {
292
token = token || Selector._tokenize(selector).pop() || {};
293
var ops = Selector.operators,
294
pseudos = Selector.pseudos,
295
prev = token.previous,
298
if (!node[TAG_NAME] ||
299
(token[TAG] !== '*' && node[TAG_NAME].toUpperCase() !== token[TAG]) ||
300
(deDupe && node._found) ) {
304
if (token[ATTRIBUTES][LENGTH]) {
306
for (i = 0, len = token[ATTRIBUTES][LENGTH]; i < len; ++i) {
307
attribute = node.getAttribute(token[ATTRIBUTES][i][0], 2);
308
if (attribute === undefined) {
309
attribute = node[token[ATTRIBUTES][i][0]];
310
if (attribute === undefined) {
314
if ( ops[token[ATTRIBUTES][i][1]] &&
315
!ops[token[ATTRIBUTES][i][1]](attribute, token[ATTRIBUTES][i][2])) {
321
if (token[PSEUDOS][LENGTH]) {
322
for (i = 0, len = token[PSEUDOS][LENGTH]; i < len; ++i) {
323
if (pseudos[token[PSEUDOS][i][0]] &&
324
!pseudos[token[PSEUDOS][i][0]](node, token[PSEUDOS][i][1])) {
329
return (prev && prev[COMBINATOR] !== ',') ?
330
Selector.combinators[prev[COMBINATOR]](node, token) :
338
_clearFoundCache: function() {
339
for (var i = 0, len = Selector._foundCache[LENGTH]; i < len; ++i) {
340
try { // IE no like delete
341
delete Selector._foundCache[i]._found;
343
Selector._foundCache[i].removeAttribute('_found');
346
Selector._foundCache = [];
350
' ': function(node, token) {
351
while ((node = node[PARENT_NODE])) {
352
if (Selector._testNode(node, '', token.previous)) {
359
'>': function(node, token) {
360
return Selector._testNode(node[PARENT_NODE], null, token.previous);
362
'+': function(node, token) {
363
var sib = node[PREVIOUS_SIBLING];
364
while (sib && sib[NODE_TYPE] !== 1) {
365
sib = sib[PREVIOUS_SIBLING];
368
if (sib && Selector._testNode(sib, null, token.previous)) {
374
'~': function(node, token) {
375
var sib = node[PREVIOUS_SIBLING];
377
if (sib[NODE_TYPE] === 1 && Selector._testNode(sib, null, token.previous)) {
380
sib = sib[PREVIOUS_SIBLING];
389
an+b = get every _a_th node starting at the _b_th
390
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
391
1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
392
an+0 = get every _a_th element, "0" may be omitted
394
getNth: function(node, expr, tag, reverse) {
397
var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
398
n = RegExp.$2, // "n"
399
oddeven = RegExp.$3, // "odd" or "even"
400
b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
401
op, i, len, siblings;
404
siblings = Y.DOM.childrenByTag(node[PARENT_NODE], tag);
406
siblings = Y.DOM.children(node[PARENT_NODE]);
410
a = 2; // always every other
413
b = (oddeven === 'odd') ? 1 : 0;
414
} else if ( isNaN(a) ) {
415
a = (n) ? 1 : 0; // start from the first or no repeat
418
if (a === 0) { // just the first
420
b = siblings[LENGTH] - b + 1;
423
if (siblings[b - 1] === node) {
435
for (i = b - 1, len = siblings[LENGTH]; i < len; i += a) {
436
if ( i >= 0 && siblings[i] === node ) {
441
for (i = siblings[LENGTH] - b, len = siblings[LENGTH]; i >= 0; i -= a) {
442
if ( i < len && siblings[i] === node ) {
450
_getId: function(attr) {
451
for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
452
if (attr[i][0] == 'id' && attr[i][1] === '=') {
458
_getIdTokenIndex: function(tokens) {
459
for (var i = 0, len = tokens[LENGTH]; i < len; ++i) {
460
if (Selector._getId(tokens[i][ATTRIBUTES])) {
468
Break selector into token units per simple selector.
469
Combinator is attached to left-hand selector.
471
_tokenize: function(selector) {
472
var token = {}, // one token per simple selector (left selector holds combinator)
473
tokens = [], // array of tokens
474
found = false, // whether or not any matches were found this pass
475
match; // the regex match
477
selector = Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
480
Search for selector patterns, store, and strip them from the selector string
481
until no patterns match (invalid selector) or we run out of chars.
483
Multiple attributes and pseudos are allowed, in any order.
485
'form:first-child[type=button]:not(button)[lang|=en]'
488
found = false; // reset after full pass
489
for (var re in patterns) {
490
if (patterns.hasOwnProperty(re)) {
491
if (re != TAG && re != COMBINATOR) { // only one allowed
492
token[re] = token[re] || [];
494
if ((match = patterns[re].exec(selector))) { // note assignment
496
if (re != TAG && re != COMBINATOR) { // only one allowed
497
//token[re] = token[re] || [];
499
// capture ID for fast path to element
500
if (re === ATTRIBUTES && match[1] === 'id') {
504
token[re].push(match.slice(1));
505
} else { // single selector (tag, combinator)
506
token[re] = match[1];
508
selector = selector.replace(match[0], ''); // strip current match from selector
509
if (re === COMBINATOR || !selector[LENGTH]) { // next token or done
510
token[ATTRIBUTES] = Selector._fixAttributes(token[ATTRIBUTES]);
511
token[PSEUDOS] = token[PSEUDOS] || [];
512
token[TAG] = token[TAG] ? token[TAG].toUpperCase() : '*';
515
token = { // prep next token
527
_fixAttributes: function(attr) {
528
var aliases = Selector.attrAliases;
530
for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
531
if (aliases[attr[i][0]]) { // convert reserved words, etc
532
attr[i][0] = aliases[attr[i][0]];
534
if (!attr[i][1]) { // use exists operator
541
_replaceShorthand: function(selector) {
542
var shorthand = Selector.shorthand;
543
var attrs = selector.match(patterns[ATTRIBUTES]); // pull attributes to avoid false pos on "." and "#"
545
selector = selector.replace(patterns[ATTRIBUTES], 'REPLACED_ATTRIBUTE');
547
for (var re in shorthand) {
548
if (shorthand.hasOwnProperty(re)) {
549
selector = selector.replace(Y.DOM._getRegExp(re, 'gi'), shorthand[re]);
554
for (var i = 0, len = attrs[LENGTH]; i < len; ++i) {
555
selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
563
if (Y.UA.ie) { // rewrite class for IE (others use getAttribute('class')
564
Selector.attrAliases['class'] = 'className';
565
Selector.attrAliases['for'] = 'htmlFor';
568
Y.Selector = Selector;
569
Y.Selector.patterns = patterns;
573
}, '3.0.0pr1' ,{skinnable:false, requires:['dom-base']});