# Yahoo! UI Library

### dom 3.3.0

Yahoo! UI Library > dom > selector-native.js (source view)
Search:
Filters Show Private Show Protected Show Deprecated

    (function(Y) {
    /**
     * The selector-native module provides support for native querySelector
     * @module dom
     * @submodule selector-native
     * @for Selector
     */

    /**
     * Provides support for using CSS selectors to query the DOM
     * @class Selector
     * @static
     * @for Selector
     */

    Y.namespace('Selector'); // allow native module to standalone

    var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
        OWNER_DOCUMENT = 'ownerDocument';

    var Selector = {
        _foundCache: [],

        useNative: true,

        _compare: ('sourceIndex' in Y.config.doc.documentElement) ?
            function(nodeA, nodeB) {
                var a = nodeA.sourceIndex,
                    b = nodeB.sourceIndex;

                if (a === b) {
                    return 0;
                } else if (a > b) {
                    return 1;
                }

                return -1;

            } : (Y.config.doc.documentElement[COMPARE_DOCUMENT_POSITION] ?
            function(nodeA, nodeB) {
                if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
                    return -1;
                } else {
                    return 1;
                }
            } :
            function(nodeA, nodeB) {
                var rangeA, rangeB, compare;
                if (nodeA && nodeB) {
                    rangeA = nodeA[OWNER_DOCUMENT].createRange();
                    rangeA.setStart(nodeA, 0);
                    rangeB = nodeB[OWNER_DOCUMENT].createRange();
                    rangeB.setStart(nodeB, 0);
                    compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
                }

                return compare;

        }),

        _sort: function(nodes) {
            if (nodes) {
                nodes = Y.Array(nodes, 0, true);
                if (nodes.sort) {
                    nodes.sort(Selector._compare);
                }
            }

            return nodes;
        },

        _deDupe: function(nodes) {
            var ret = [],
                i, node;

            for (i = 0; (node = nodes[i++]);) {
                if (!node._found) {
                    ret[ret.length] = node;
                    node._found = true;
                }
            }

            for (i = 0; (node = ret[i++]);) {
                node._found = null;
                node.removeAttribute('_found');
            }

            return ret;
        },

        /**
         * 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} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
         * @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, skipNative) {
            root = root || Y.config.doc;
            var ret = [],
                useNative = (Y.Selector.useNative && Y.config.doc.querySelector && !skipNative),
                queries = [[selector, root]],
                query,
                result,
                i,
                fn = (useNative) ? Y.Selector._nativeQuery : Y.Selector._bruteQuery;

            if (selector && fn) {
                // split group into seperate queries
                if (!skipNative && // already done if skipping
                        (!useNative || root.tagName)) { // split native when element scoping is needed
                    queries = Selector._splitQueries(selector, root);
                }

                for (i = 0; (query = queries[i++]);) {
                    result = fn(query[0], query[1], firstOnly);
                    if (!firstOnly) { // coerce DOM Collection to Array
                        result = Y.Array(result, 0, true);
                    }
                    if (result) {
                        ret = ret.concat(result);
                    }
                }

                if (queries.length > 1) { // remove dupes and sort by doc order
                    ret = Selector._sort(Selector._deDupe(ret));
                }
            }

            Y.log('query: ' + selector + ' returning: ' + ret.length, 'info', 'Selector');
            return (firstOnly) ? (ret[0] || null) : ret;

        },

        // allows element scoped queries to begin with combinator
        // e.g. query('> p', document.body) === query('body > p')
        _splitQueries: function(selector, node) {
            var groups = selector.split(','),
                queries = [],
                prefix = '',
                i, len;

            if (node) {
                // enforce for element scoping
                if (node.tagName) {
                    node.id = node.id || Y.guid();
                    prefix = '[id="' + node.id + '"] ';
                }

                for (i = 0, len = groups.length; i < len; ++i) {
                    selector =  prefix + groups[i];
                    queries.push([selector, node]);
                }
            }

            return queries;
        },

        _nativeQuery: function(selector, root, one) {
            if (Y.UA.webkit && selector.indexOf(':checked') > -1 &&
                    (Y.Selector.pseudos && Y.Selector.pseudos.checked)) { // webkit (chrome, safari) fails to find "selected"
                return Y.Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
            }
            try {
                //Y.log('trying native query with: ' + selector, 'info', 'selector-native');
                return root['querySelector' + (one ? '' : 'All')](selector);
            } catch(e) { // fallback to brute if available
                //Y.log('native query error; reverting to brute query with: ' + selector, 'info', 'selector-native');
                return Y.Selector.query(selector, root, one, true); // redo with skipNative true
            }
        },

        filter: function(nodes, selector) {
            var ret = [],
                i, node;

            if (nodes && selector) {
                for (i = 0; (node = nodes[i++]);) {
                    if (Y.Selector.test(node, selector)) {
                        ret[ret.length] = node;
                    }
                }
            } else {
                Y.log('invalid filter input (nodes: ' + nodes +
                        ', selector: ' + selector + ')', 'warn', 'Selector');
            }

            return ret;
        },

        test: function(node, selector, root) {
            var ret = false,
                groups = selector.split(','),
                useFrag = false,
                parent,
                item,
                items,
                frag,
                i, j, group;

            if (node && node.tagName) { // only test HTMLElements

                // we need a root if off-doc
                if (!root && !Y.DOM.inDoc(node)) {
                    parent = node.parentNode;
                    if (parent) {
                        root = parent;
                    } else { // only use frag when no parent to query
                        frag = node[OWNER_DOCUMENT].createDocumentFragment();
                        frag.appendChild(node);
                        root = frag;
                        useFrag = true;
                    }
                }
                root = root || node[OWNER_DOCUMENT];

                if (!node.id) {
                    node.id = Y.guid();
                }
                for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
                    group += '[id="' + node.id + '"]';
                    items = Y.Selector.query(group, root);

                    for (j = 0; item = items[j++];) {
                        if (item === node) {
                            ret = true;
                            break;
                        }
                    }
                    if (ret) {
                        break;
                    }
                }

                if (useFrag) { // cleanup
                    frag.removeChild(node);
                }
            }

            return ret;
        },

        /**
         * A convenience function to emulate Y.Node's aNode.ancestor(selector).
         * @param {HTMLElement} element An HTMLElement to start the query from.
         * @param {String} selector The CSS selector to test the node against.
         * @return {HTMLElement} The ancestor node matching the selector, or null.
         * @param {Boolean} testSelf optional Whether or not to include the element in the scan
         * @static
         * @method ancestor
         */
        ancestor: function (element, selector, testSelf) {
            return Y.DOM.ancestor(element, function(n) {
                return Y.Selector.test(n, selector);
            }, testSelf);
        }
    };

    Y.mix(Y.Selector, Selector, true);

    })(Y);

Copyright © 2011 Yahoo! Inc. All rights reserved.