2
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.net/yui/license.txt
8
YUI.add('selector-native', function(Y) {
12
* The selector-native module provides support for native querySelector
14
* @submodule selector-native
19
* Provides support for using CSS selectors to query the DOM
25
Y.namespace('Selector'); // allow native module to standalone
27
var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
28
OWNER_DOCUMENT = 'ownerDocument',
29
TMP_PREFIX = 'yui-tmp-',
37
_compare: ('sourceIndex' in document.documentElement) ?
38
function(nodeA, nodeB) {
39
var a = nodeA.sourceIndex,
40
b = nodeB.sourceIndex;
50
} : (document.documentElement[COMPARE_DOCUMENT_POSITION] ?
51
function(nodeA, nodeB) {
52
if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
58
function(nodeA, nodeB) {
59
var rangeA, rangeB, compare;
61
rangeA = nodeA[OWNER_DOCUMENT].createRange();
62
rangeA.setStart(nodeA, 0);
63
rangeB = nodeB[OWNER_DOCUMENT].createRange();
64
rangeB.setStart(nodeB, 0);
65
compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
72
_sort: function(nodes) {
74
nodes = Y.Array(nodes, 0, true);
76
nodes.sort(Selector._compare);
83
_deDupe: function(nodes) {
87
for (i = 0; (node = nodes[i++]);) {
89
ret[ret.length] = node;
94
for (i = 0; (node = ret[i++]);) {
96
node.removeAttribute('_found');
103
* Retrieves a set of nodes based on a given CSS selector.
106
* @param {string} selector The CSS Selector to test the node against.
107
* @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
108
* @param {Boolean} firstOnly optional Whether or not to return only the first match.
109
* @return {Array} An array of nodes that match the given selector.
112
query: function(selector, root, firstOnly, skipNative) {
113
root = root || Y.config.doc;
115
useNative = (Y.Selector.useNative && document.querySelector && !skipNative),
116
queries = [[selector, root]],
120
fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;
122
if (selector && fn) {
123
// split group into seperate queries
124
if (!skipNative && // already done if skipping
125
(!useNative || root.tagName)) { // split native when element scoping is needed
126
queries = Selector._splitQueries(selector, root);
129
for (i = 0; (query = queries[i++]);) {
130
result = fn(query[0], query[1], firstOnly);
131
if (!firstOnly) { // coerce DOM Collection to Array
132
result = Y.Array(result, 0, true);
135
ret = ret.concat(result);
139
if (queries.length > 1) { // remove dupes and sort by doc order
140
ret = Selector._sort(Selector._deDupe(ret));
144
return (firstOnly) ? (ret[0] || null) : ret;
148
// allows element scoped queries to begin with combinator
149
// e.g. query('> p', document.body) === query('body > p')
150
_splitQueries: function(selector, node) {
151
var groups = selector.split(','),
157
// enforce for element scoping
159
node.id = node.id || Y.guid();
160
prefix = '#' + node.id + ' ';
163
for (i = 0, len = groups.length; i < len; ++i) {
164
selector = prefix + groups[i];
165
queries.push([selector, node]);
172
_nativeQuery: function(selector, root, one) {
174
return root['querySelector' + (one ? '' : 'All')](selector);
175
} catch(e) { // fallback to brute if available
176
return Y.Selector.query(selector, root, one, true); // redo with skipNative true
180
filter: function(nodes, selector) {
184
if (nodes && selector) {
185
for (i = 0; (node = nodes[i++]);) {
186
if (Y.Selector.test(node, selector)) {
187
ret[ret.length] = node;
196
test: function(node, selector, root) {
198
groups = selector.split(','),
202
if (node && node.tagName) { // only test HTMLElements
203
root = root || node.ownerDocument;
206
node.id = TMP_PREFIX + g_counter++;
208
for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
209
group += '#' + node.id; // add ID for uniqueness
210
item = Y.Selector.query(group, root, true);
211
ret = (item === node);
222
Y.mix(Y.Selector, Selector, true);
227
}, '3.0.0' ,{requires:['dom-base']});
228
YUI.add('selector-css2', function(Y) {
231
* The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
233
* @submodule selector-css2
238
* Provides helper methods for collecting and filtering DOM elements.
241
var PARENT_NODE = 'parentNode',
242
TAG_NAME = 'tagName',
243
ATTRIBUTES = 'attributes',
244
COMBINATOR = 'combinator',
247
Selector = Y.Selector,
251
_children: function(node, tag) {
252
var ret = node.children,
258
if (node.children && tag && node.children.tags) {
259
children = node.children.tags(tag);
260
} else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
261
childNodes = ret || node.childNodes;
263
for (i = 0; (child = childNodes[i++]);) {
265
if (!tag || tag === child.tagName) {
279
pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\)))*/i
283
* Mapping of shorthand tokens to corresponding attribute selector
284
* @property shorthand
288
'\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
289
'\\.(-?[_a-z]+[-\\w]*)': '[className~=$1]'
293
* List of operators and corresponding boolean functions.
294
* These functions are passed the attribute and the current node's value of the attribute.
295
* @property operators
299
'': function(node, attr) { return Y.DOM.getAttribute(node, attr) !== ''; }, // Just test for existence of attribute
301
//'=': '^{val}$', // equality
302
'~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
303
'|=': '^{val}-?' // optional hyphen-delimited
307
'first-child': function(node) {
308
return Y.Selector._children(node[PARENT_NODE])[0] === node;
312
_bruteQuery: function(selector, root, firstOnly) {
315
tokens = Selector._tokenize(selector),
316
token = tokens[tokens.length - 1],
317
rootDoc = Y.DOM._getDoc(root),
323
// if we have an initial ID, set to root when in document
324
if (tokens[0] && rootDoc === root &&
325
(id = tokens[0].id) &&
326
rootDoc.getElementById(id)) {
327
root = rootDoc.getElementById(id);
333
className = token.className;
334
tagName = token.tagName || '*';
338
if (rootDoc.getElementById(id)) { // if in document
339
nodes = [rootDoc.getElementById(id)]; // TODO: DOM.byId?
341
// try className if supported
342
} else if (className) {
343
nodes = root.getElementsByClassName(className);
344
} else if (tagName) { // default to tagName
345
nodes = root.getElementsByTagName(tagName || '*');
349
ret = Selector._filterNodes(nodes, tokens, firstOnly);
356
_filterNodes: function(nodes, tokens, firstOnly) {
364
getters = Y.Selector.getters,
370
//FUNCTION = 'function',
376
for (i = 0; (tmpNode = node = nodes[i++]);) {
381
while (tmpNode && tmpNode.tagName) {
386
while ((test = tests[--j])) {
388
if (getters[test[0]]) {
389
value = getters[test[0]](tmpNode, test[0]);
391
value = tmpNode[test[0]];
392
// use getAttribute for non-standard attributes
393
if (value === undefined && tmpNode.getAttribute) {
394
value = tmpNode.getAttribute(test[0]);
398
if ((operator === '=' && value !== test[2]) || // fast path for equality
399
(operator.test && !operator.test(value)) || // regex test
400
(operator.call && !operator(tmpNode, test[0]))) { // function test
402
// skip non element nodes or non-matching tags
403
if ((tmpNode = tmpNode[path])) {
406
(token.tagName && token.tagName !== tmpNode.tagName))
408
tmpNode = tmpNode[path];
416
n--; // move to next token
417
// now that we've passed the test, move up the tree by combinator
418
if (!pass && (combinator = token.combinator)) {
419
path = combinator.axis;
420
tmpNode = tmpNode[path];
422
// skip non element nodes
423
while (tmpNode && !tmpNode.tagName) {
424
tmpNode = tmpNode[path];
427
if (combinator.direct) { // one pass only
431
} else { // success if we made it this far
439
}// while (tmpNode = node = nodes[++i]);
440
node = tmpNode = null;
444
_getRegExp: function(str, flags) {
445
var regexCache = Selector._regexCache;
447
if (!regexCache[str + flags]) {
448
regexCache[str + flags] = new RegExp(str, flags);
450
return regexCache[str + flags];
465
axis: 'previousSibling',
473
re: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
474
fn: function(match, token) {
475
var operator = match[2] || '',
476
operators = Y.Selector.operators,
479
// add prefiltering for ID and CLASS
480
if ((match[1] === 'id' && operator === '=') ||
481
(match[1] === 'className' &&
482
document.getElementsByClassName &&
483
(operator === '~=' || operator === '='))) {
484
token.prefilter = match[1];
485
token[match[1]] = match[3];
489
if (operator in operators) {
490
test = operators[operator];
491
if (typeof test === 'string') {
492
test = Y.Selector._getRegExp(test.replace('{val}', match[3]));
496
if (!token.last || token.prefilter !== match[1]) {
497
return match.slice(1);
504
re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
505
fn: function(match, token) {
506
var tag = match[1].toUpperCase();
509
if (tag !== '*' && (!token.last || token.prefilter)) {
510
return [TAG_NAME, '=', tag];
512
if (!token.prefilter) {
513
token.prefilter = 'tagName';
519
re: /^\s*([>+~]|\s)\s*/,
520
fn: function(match, token) {
525
re: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
526
fn: function(match, token) {
527
var test = Selector[PSEUDOS][match[1]];
528
if (test) { // reorder match array
529
return [match[2], test];
530
} else { // selector token not supported (possibly missing CSS3 module)
537
_getToken: function(token) {
549
Break selector into token units per simple selector.
550
Combinator is attached to the previous token.
552
_tokenize: function(selector) {
553
selector = selector || '';
554
selector = Selector._replaceShorthand(Y.Lang.trim(selector));
555
var token = Selector._getToken(), // one token per simple selector (left selector holds combinator)
556
query = selector, // original query for debug report
557
tokens = [], // array of tokens
558
found = false, // whether or not any matches were found this pass
559
match, // the regex match
564
Search for selector patterns, store, and strip them from the selector string
565
until no patterns match (invalid selector) or we run out of chars.
567
Multiple attributes and pseudos are allowed, in any order.
569
'form:first-child[type=button]:not(button)[lang|=en]'
573
found = false; // reset after full pass
574
for (i = 0; (parser = Selector._parsers[i++]);) {
575
if ( (match = parser.re.exec(selector)) ) { // note assignment
576
if (parser !== COMBINATOR ) {
577
token.selector = selector;
579
selector = selector.replace(match[0], ''); // strip current match from selector
580
if (!selector.length) {
584
if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
585
match[1] = Selector._attrFilters[match[1]];
588
test = parser.fn(match, token);
589
if (test === false) { // selector not supported
593
token.tests.push(test);
596
if (!selector.length || parser.name === COMBINATOR) {
598
token = Selector._getToken(token);
599
if (parser.name === COMBINATOR) {
600
token.combinator = Y.Selector.combinators[match[1]];
606
} while (found && selector.length);
608
if (!found || selector.length) { // not fully parsed
614
_replaceShorthand: function(selector) {
615
var shorthand = Selector.shorthand,
616
attrs = selector.match(Selector._re.attr), // pull attributes to avoid false pos on "." and "#"
617
pseudos = selector.match(Selector._re.pseudos), // pull attributes to avoid false pos on "." and "#"
621
selector = selector.replace(Selector._re.pseudos, '!!REPLACED_PSEUDO!!');
625
selector = selector.replace(Selector._re.attr, '!!REPLACED_ATTRIBUTE!!');
628
for (re in shorthand) {
629
if (shorthand.hasOwnProperty(re)) {
630
selector = selector.replace(Selector._getRegExp(re, 'gi'), shorthand[re]);
635
for (i = 0, len = attrs.length; i < len; ++i) {
636
selector = selector.replace('!!REPLACED_ATTRIBUTE!!', attrs[i]);
640
for (i = 0, len = pseudos.length; i < len; ++i) {
641
selector = selector.replace('!!REPLACED_PSEUDO!!', pseudos[i]);
648
'class': 'className',
653
href: function(node, attr) {
654
return Y.DOM.getAttribute(node, attr);
659
Y.mix(Y.Selector, SelectorCSS2, true);
660
Y.Selector.getters.src = Y.Selector.getters.rel = Y.Selector.getters.href;
662
// IE wants class with native queries
663
if (Y.Selector.useNative && document.querySelector) {
664
Y.Selector.shorthand['\\.(-?[_a-z]+[-\\w]*)'] = '[class~=$1]';
669
}, '3.0.0' ,{requires:['selector-native']});
672
YUI.add('selector', function(Y){}, '3.0.0' ,{use:['selector-native', 'selector-css2']});