| /* |
| Copyright (c) 2008, Yahoo! Inc. All rights reserved. |
| Code licensed under the BSD License: |
| http://developer.yahoo.net/yui/license.txt |
| version: 2.6.0 |
| */ |
| /** |
| * The selector module provides helper methods allowing CSS3 Selectors to be used with DOM elements. |
| * @module selector |
| * @title Selector Utility |
| * @namespace YAHOO.util |
| * @requires yahoo, dom |
| */ |
| |
| (function() { |
| /** |
| * Provides helper methods for collecting and filtering DOM elements. |
| * @namespace YAHOO.util |
| * @class Selector |
| * @static |
| */ |
| var Selector = function() {}; |
| |
| var Y = YAHOO.util; |
| |
| var reNth = /^(?:([-]?\d*)(n){1}|(odd|even)$)*([-+]?\d*)$/; |
| |
| Selector.prototype = { |
| /** |
| * Default document for use queries |
| * @property document |
| * @type object |
| * @default window.document |
| */ |
| document: window.document, |
| /** |
| * Mapping of attributes to aliases, normally to work around HTMLAttributes |
| * that conflict with JS reserved words. |
| * @property attrAliases |
| * @type object |
| */ |
| attrAliases: { |
| }, |
| |
| /** |
| * Mapping of shorthand tokens to corresponding attribute selector |
| * @property shorthand |
| * @type object |
| */ |
| shorthand: { |
| //'(?:(?:[^\\)\\]\\s*>+~,]+)(?:-?[_a-z]+[-\\w]))+#(-?[_a-z]+[-\\w]*)': '[id=$1]', |
| '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]', |
| '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]' |
| }, |
| |
| /** |
| * List of operators and corresponding boolean functions. |
| * These functions are passed the attribute and the current node's value of the attribute. |
| * @property operators |
| * @type object |
| */ |
| operators: { |
| '=': function(attr, val) { return attr === val; }, // Equality |
| '!=': function(attr, val) { return attr !== val; }, // Inequality |
| '~=': function(attr, val) { // Match one of space seperated words |
| var s = ' '; |
| return (s + attr + s).indexOf((s + val + s)) > -1; |
| }, |
| '|=': function(attr, val) { return getRegExp('^' + val + '[-]?').test(attr); }, // Match start with value followed by optional hyphen |
| '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value |
| '$=': function(attr, val) { return attr.lastIndexOf(val) === attr.length - val.length; }, // Match ends with value |
| '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring |
| '': function(attr, val) { return attr; } // Just test for existence of attribute |
| }, |
| |
| /** |
| * List of pseudo-classes and corresponding boolean functions. |
| * These functions are called with the current node, and any value that was parsed with the pseudo regex. |
| * @property pseudos |
| * @type object |
| */ |
| pseudos: { |
| 'root': function(node) { |
| return node === node.ownerDocument.documentElement; |
| }, |
| |
| 'nth-child': function(node, val) { |
| return getNth(node, val); |
| }, |
| |
| 'nth-last-child': function(node, val) { |
| return getNth(node, val, null, true); |
| }, |
| |
| 'nth-of-type': function(node, val) { |
| return getNth(node, val, node.tagName); |
| }, |
| |
| 'nth-last-of-type': function(node, val) { |
| return getNth(node, val, node.tagName, true); |
| }, |
| |
| 'first-child': function(node) { |
| return getChildren(node.parentNode)[0] === node; |
| }, |
| |
| 'last-child': function(node) { |
| var children = getChildren(node.parentNode); |
| return children[children.length - 1] === node; |
| }, |
| |
| 'first-of-type': function(node, val) { |
| return getChildren(node.parentNode, node.tagName.toLowerCase())[0]; |
| }, |
| |
| 'last-of-type': function(node, val) { |
| var children = getChildren(node.parentNode, node.tagName.toLowerCase()); |
| return children[children.length - 1]; |
| }, |
| |
| 'only-child': function(node) { |
| var children = getChildren(node.parentNode); |
| return children.length === 1 && children[0] === node; |
| }, |
| |
| 'only-of-type': function(node) { |
| return getChildren(node.parentNode, node.tagName.toLowerCase()).length === 1; |
| }, |
| |
| 'empty': function(node) { |
| return node.childNodes.length === 0; |
| }, |
| |
| 'not': function(node, simple) { |
| return !Selector.test(node, simple); |
| }, |
| |
| 'contains': function(node, str) { |
| var text = node.innerText || node.textContent || ''; |
| return text.indexOf(str) > -1; |
| }, |
| 'checked': function(node) { |
| return node.checked === true; |
| } |
| }, |
| |
| /** |
| * Test if the supplied node matches the supplied selector. |
| * @method test |
| * |
| * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested. |
| * @param {string} selector The CSS Selector to test the node against. |
| * @return{boolean} Whether or not the node matches the selector. |
| * @static |
| |
| */ |
| test: function(node, selector) { |
| node = Selector.document.getElementById(node) || node; |
| |
| if (!node) { |
| return false; |
| } |
| |
| var groups = selector ? selector.split(',') : []; |
| if (groups.length) { |
| for (var i = 0, len = groups.length; i < len; ++i) { |
| if ( rTestNode(node, groups[i]) ) { // passes if ANY group matches |
| return true; |
| } |
| } |
| return false; |
| } |
| return rTestNode(node, selector); |
| }, |
| |
| /** |
| * Filters a set of nodes based on a given CSS selector. |
| * @method filter |
| * |
| * @param {array} nodes A set of nodes/ids to filter. |
| * @param {string} selector The selector used to test each node. |
| * @return{array} An array of nodes from the supplied array that match the given selector. |
| * @static |
| */ |
| filter: function(nodes, selector) { |
| nodes = nodes || []; |
| |
| var node, |
| result = [], |
| tokens = tokenize(selector); |
| |
| if (!nodes.item) { // if not HTMLCollection, handle arrays of ids and/or nodes |
| for (var i = 0, len = nodes.length; i < len; ++i) { |
| if (!nodes[i].tagName) { // tagName limits to HTMLElements |
| node = Selector.document.getElementById(nodes[i]); |
| if (node) { // skip IDs that return null |
| nodes[i] = node; |
| } else { |
| } |
| } |
| } |
| } |
| result = rFilter(nodes, tokenize(selector)[0]); |
| clearParentCache(); |
| return result; |
| }, |
| |
| /** |
| * Retrieves a set of nodes based on a given CSS selector. |
| * @method query |
| * |
| * @param {string} selector The CSS Selector to test the node against. |
| * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document. |
| * @param {Boolean} firstOnly optional Whether or not to return only the first match. |
| * @return {Array} An array of nodes that match the given selector. |
| * @static |
| */ |
| query: function(selector, root, firstOnly) { |
| var result = query(selector, root, firstOnly); |
| return result; |
| } |
| }; |
| |
| var query = function(selector, root, firstOnly, deDupe) { |
| var result = (firstOnly) ? null : []; |
| if (!selector) { |
| return result; |
| } |
| |
| var groups = selector.split(','); // TODO: handle comma in attribute/pseudo |
| |
| if (groups.length > 1) { |
| var found; |
| for (var i = 0, len = groups.length; i < len; ++i) { |
| found = arguments.callee(groups[i], root, firstOnly, true); |
| result = firstOnly ? found : result.concat(found); |
| } |
| clearFoundCache(); |
| return result; |
| } |
| |
| if (root && !root.nodeName) { // assume ID |
| root = Selector.document.getElementById(root); |
| if (!root) { |
| return result; |
| } |
| } |
| |
| root = root || Selector.document; |
| var tokens = tokenize(selector); |
| var idToken = tokens[getIdTokenIndex(tokens)], |
| nodes = [], |
| node, |
| id, |
| token = tokens.pop() || {}; |
| |
| if (idToken) { |
| id = getId(idToken.attributes); |
| } |
| |
| // use id shortcut when possible |
| if (id) { |
| node = Selector.document.getElementById(id); |
| |
| if (node && (root.nodeName == '#document' || contains(node, root))) { |
| if ( rTestNode(node, null, idToken) ) { |
| if (idToken === token) { |
| nodes = [node]; // simple selector |
| } else { |
| root = node; // start from here |
| } |
| } |
| } else { |
| return result; |
| } |
| } |
| |
| if (root && !nodes.length) { |
| nodes = root.getElementsByTagName(token.tag); |
| } |
| |
| if (nodes.length) { |
| result = rFilter(nodes, token, firstOnly, deDupe); |
| } |
| |
| clearParentCache(); |
| return result; |
| }; |
| |
| var contains = function() { |
| if (document.documentElement.contains && !YAHOO.env.ua.webkit < 422) { // IE & Opera, Safari < 3 contains is broken |
| return function(needle, haystack) { |
| return haystack.contains(needle); |
| }; |
| } else if ( document.documentElement.compareDocumentPosition ) { // gecko |
| return function(needle, haystack) { |
| return !!(haystack.compareDocumentPosition(needle) & 16); |
| }; |
| } else { // Safari < 3 |
| return function(needle, haystack) { |
| var parent = needle.parentNode; |
| while (parent) { |
| if (needle === parent) { |
| return true; |
| } |
| parent = parent.parentNode; |
| } |
| return false; |
| }; |
| } |
| }(); |
| |
| var rFilter = function(nodes, token, firstOnly, deDupe) { |
| var result = firstOnly ? null : []; |
| |
| for (var i = 0, len = nodes.length; i < len; i++) { |
| if (! rTestNode(nodes[i], '', token, deDupe)) { |
| continue; |
| } |
| |
| if (firstOnly) { |
| return nodes[i]; |
| } |
| if (deDupe) { |
| if (nodes[i]._found) { |
| continue; |
| } |
| nodes[i]._found = true; |
| foundCache[foundCache.length] = nodes[i]; |
| } |
| |
| result[result.length] = nodes[i]; |
| } |
| |
| return result; |
| }; |
| |
| var rTestNode = function(node, selector, token, deDupe) { |
| token = token || tokenize(selector).pop() || {}; |
| |
| if (!node.tagName || |
| (token.tag !== '*' && node.tagName.toUpperCase() !== token.tag) || |
| (deDupe && node._found) ) { |
| return false; |
| } |
| |
| if (token.attributes.length) { |
| var attribute; |
| for (var i = 0, len = token.attributes.length; i < len; ++i) { |
| attribute = node.getAttribute(token.attributes[i][0], 2); |
| if (attribute === null || attribute === undefined) { |
| return false; |
| } |
| if ( Selector.operators[token.attributes[i][1]] && |
| !Selector.operators[token.attributes[i][1]](attribute, token.attributes[i][2])) { |
| return false; |
| } |
| } |
| } |
| |
| if (token.pseudos.length) { |
| for (var i = 0, len = token.pseudos.length; i < len; ++i) { |
| if (Selector.pseudos[token.pseudos[i][0]] && |
| !Selector.pseudos[token.pseudos[i][0]](node, token.pseudos[i][1])) { |
| return false; |
| } |
| } |
| } |
| |
| return (token.previous && token.previous.combinator !== ',') ? |
| combinators[token.previous.combinator](node, token) : |
| true; |
| }; |
| |
| |
| var foundCache = []; |
| var parentCache = []; |
| var regexCache = {}; |
| |
| var clearFoundCache = function() { |
| for (var i = 0, len = foundCache.length; i < len; ++i) { |
| try { // IE no like delete |
| delete foundCache[i]._found; |
| } catch(e) { |
| foundCache[i].removeAttribute('_found'); |
| } |
| } |
| foundCache = []; |
| }; |
| |
| var clearParentCache = function() { |
| if (!document.documentElement.children) { // caching children lookups for gecko |
| return function() { |
| for (var i = 0, len = parentCache.length; i < len; ++i) { |
| delete parentCache[i]._children; |
| } |
| parentCache = []; |
| }; |
| } else return function() {}; // do nothing |
| }(); |
| |
| var getRegExp = function(str, flags) { |
| flags = flags || ''; |
| if (!regexCache[str + flags]) { |
| regexCache[str + flags] = new RegExp(str, flags); |
| } |
| return regexCache[str + flags]; |
| }; |
| |
| var combinators = { |
| ' ': function(node, token) { |
| while (node = node.parentNode) { |
| if (rTestNode(node, '', token.previous)) { |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| '>': function(node, token) { |
| return rTestNode(node.parentNode, null, token.previous); |
| }, |
| |
| '+': function(node, token) { |
| var sib = node.previousSibling; |
| while (sib && sib.nodeType !== 1) { |
| sib = sib.previousSibling; |
| } |
| |
| if (sib && rTestNode(sib, null, token.previous)) { |
| return true; |
| } |
| return false; |
| }, |
| |
| '~': function(node, token) { |
| var sib = node.previousSibling; |
| while (sib) { |
| if (sib.nodeType === 1 && rTestNode(sib, null, token.previous)) { |
| return true; |
| } |
| sib = sib.previousSibling; |
| } |
| |
| return false; |
| } |
| }; |
| |
| var getChildren = function() { |
| if (document.documentElement.children) { // document for capability test |
| return function(node, tag) { |
| return (tag) ? node.children.tags(tag) : node.children || []; |
| }; |
| } else { |
| return function(node, tag) { |
| if (node._children) { |
| return node._children; |
| } |
| var children = [], |
| childNodes = node.childNodes; |
| |
| for (var i = 0, len = childNodes.length; i < len; ++i) { |
| if (childNodes[i].tagName) { |
| if (!tag || childNodes[i].tagName.toLowerCase() === tag) { |
| children[children.length] = childNodes[i]; |
| } |
| } |
| } |
| node._children = children; |
| parentCache[parentCache.length] = node; |
| return children; |
| }; |
| } |
| }(); |
| |
| /* |
| an+b = get every _a_th node starting at the _b_th |
| 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 |
| 1n+b = get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n") |
| an+0 = get every _a_th element, "0" may be omitted |
| */ |
| var getNth = function(node, expr, tag, reverse) { |
| if (tag) tag = tag.toLowerCase(); |
| reNth.test(expr); |
| var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_) |
| n = RegExp.$2, // "n" |
| oddeven = RegExp.$3, // "odd" or "even" |
| b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_ |
| result = []; |
| |
| var siblings = getChildren(node.parentNode, tag); |
| |
| if (oddeven) { |
| a = 2; // always every other |
| op = '+'; |
| n = 'n'; |
| b = (oddeven === 'odd') ? 1 : 0; |
| } else if ( isNaN(a) ) { |
| a = (n) ? 1 : 0; // start from the first or no repeat |
| } |
| |
| if (a === 0) { // just the first |
| if (reverse) { |
| b = siblings.length - b + 1; |
| } |
| |
| if (siblings[b - 1] === node) { |
| return true; |
| } else { |
| return false; |
| } |
| |
| } else if (a < 0) { |
| reverse = !!reverse; |
| a = Math.abs(a); |
| } |
| |
| if (!reverse) { |
| for (var i = b - 1, len = siblings.length; i < len; i += a) { |
| if ( i >= 0 && siblings[i] === node ) { |
| return true; |
| } |
| } |
| } else { |
| for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) { |
| if ( i < len && siblings[i] === node ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| }; |
| |
| var getId = function(attr) { |
| for (var i = 0, len = attr.length; i < len; ++i) { |
| if (attr[i][0] == 'id' && attr[i][1] === '=') { |
| return attr[i][2]; |
| } |
| } |
| }; |
| |
| var getIdTokenIndex = function(tokens) { |
| for (var i = 0, len = tokens.length; i < len; ++i) { |
| if (getId(tokens[i].attributes)) { |
| return i; |
| } |
| } |
| return -1; |
| }; |
| |
| var patterns = { |
| tag: /^((?:-?[_a-z]+[\w-]*)|\*)/i, |
| attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i, |
| //attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^'"\]]*)['"]?\]*/i, |
| pseudos: /^:([-\w]+)(?:\(['"]?(.+)['"]?\))*/i, |
| combinator: /^\s*([>+~]|\s)\s*/ |
| }; |
| |
| /** |
| Break selector into token units per simple selector. |
| Combinator is attached to left-hand selector. |
| */ |
| var tokenize = function(selector) { |
| var token = {}, // one token per simple selector (left selector holds combinator) |
| tokens = [], // array of tokens |
| id, // unique id for the simple selector (if found) |
| found = false, // whether or not any matches were found this pass |
| match; // the regex match |
| |
| selector = replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes |
| |
| /* |
| Search for selector patterns, store, and strip them from the selector string |
| until no patterns match (invalid selector) or we run out of chars. |
| |
| Multiple attributes and pseudos are allowed, in any order. |
| for example: |
| 'form:first-child[type=button]:not(button)[lang|=en]' |
| */ |
| do { |
| found = false; // reset after full pass |
| for (var re in patterns) { |
| if (!YAHOO.lang.hasOwnProperty(patterns, re)) { |
| continue; |
| } |
| if (re != 'tag' && re != 'combinator') { // only one allowed |
| token[re] = token[re] || []; |
| } |
| if (match = patterns[re].exec(selector)) { // note assignment |
| found = true; |
| if (re != 'tag' && re != 'combinator') { // only one allowed |
| //token[re] = token[re] || []; |
| |
| // capture ID for fast path to element |
| if (re === 'attributes' && match[1] === 'id') { |
| token.id = match[3]; |
| } |
| |
| token[re].push(match.slice(1)); |
| } else { // single selector (tag, combinator) |
| token[re] = match[1]; |
| } |
| selector = selector.replace(match[0], ''); // strip current match from selector |
| if (re === 'combinator' || !selector.length) { // next token or done |
| token.attributes = fixAttributes(token.attributes); |
| token.pseudos = token.pseudos || []; |
| token.tag = token.tag ? token.tag.toUpperCase() : '*'; |
| tokens.push(token); |
| |
| token = { // prep next token |
| previous: token |
| }; |
| } |
| } |
| } |
| } while (found); |
| |
| return tokens; |
| }; |
| |
| var fixAttributes = function(attr) { |
| var aliases = Selector.attrAliases; |
| attr = attr || []; |
| for (var i = 0, len = attr.length; i < len; ++i) { |
| if (aliases[attr[i][0]]) { // convert reserved words, etc |
| attr[i][0] = aliases[attr[i][0]]; |
| } |
| if (!attr[i][1]) { // use exists operator |
| attr[i][1] = ''; |
| } |
| } |
| return attr; |
| }; |
| |
| var replaceShorthand = function(selector) { |
| var shorthand = Selector.shorthand; |
| var attrs = selector.match(patterns.attributes); // pull attributes to avoid false pos on "." and "#" |
| if (attrs) { |
| selector = selector.replace(patterns.attributes, 'REPLACED_ATTRIBUTE'); |
| } |
| for (var re in shorthand) { |
| if (!YAHOO.lang.hasOwnProperty(shorthand, re)) { |
| continue; |
| } |
| selector = selector.replace(getRegExp(re, 'gi'), shorthand[re]); |
| } |
| |
| if (attrs) { |
| for (var i = 0, len = attrs.length; i < len; ++i) { |
| selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]); |
| } |
| } |
| return selector; |
| }; |
| |
| Selector = new Selector(); |
| Selector.patterns = patterns; |
| Y.Selector = Selector; |
| |
| if (YAHOO.env.ua.ie) { // rewrite class for IE (others use getAttribute('class') |
| Y.Selector.attrAliases['class'] = 'className'; |
| Y.Selector.attrAliases['for'] = 'htmlFor'; |
| } |
| |
| })(); |
| YAHOO.register("selector", YAHOO.util.Selector, {version: "2.6.0", build: "1321"}); |