API Docs for: 3.8.0
Show:

File: dom/js/selector-native.js

(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 = {
    _types: {
        esc: {
            token: '\uE000',
            re: /\\[:\[\]\(\)#\.\'\>+~"]/gi
        },

        attr: {
            token: '\uE001',
            re: /(\[[^\]]*\])/g
        },

        pseudo: {
            token: '\uE002',
            re: /(\([^\)]*\))/g
        }
    },

    useNative: true,

    _escapeId: function(id) {
        if (id) {
            id = id.replace(/([:\[\]\(\)#\.'<>+~"])/g,'\\$1');
        }
        return id;
    },

    _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;

    },

    _replaceSelector: function(selector) {
        var esc = Y.Selector._parse('esc', selector), // pull escaped colon, brackets, etc. 
            attrs,
            pseudos;

        // first replace escaped chars, which could be present in attrs or pseudos
        selector = Y.Selector._replace('esc', selector);

        // then replace pseudos before attrs to avoid replacing :not([foo])
        pseudos = Y.Selector._parse('pseudo', selector);
        selector = Selector._replace('pseudo', selector);

        attrs = Y.Selector._parse('attr', selector);
        selector = Y.Selector._replace('attr', selector);

        return {
            esc: esc,
            attrs: attrs,
            pseudos: pseudos,
            selector: selector
        };
    },

    _restoreSelector: function(replaced) {
        var selector = replaced.selector;
        selector = Y.Selector._restore('attr', selector, replaced.attrs);
        selector = Y.Selector._restore('pseudo', selector, replaced.pseudos);
        selector = Y.Selector._restore('esc', selector, replaced.esc);
        return selector;
    },

    _replaceCommas: function(selector) {
        var replaced = Y.Selector._replaceSelector(selector),
            selector = replaced.selector;

        if (selector) {
            selector = selector.replace(/,/g, '\uE007');
            replaced.selector = selector;
            selector = Y.Selector._restoreSelector(replaced);
        }
        return selector;
    },

    // allows element scoped queries to begin with combinator
    // e.g. query('> p', document.body) === query('body > p')
    _splitQueries: function(selector, node) {
        if (selector.indexOf(',') > -1) {
            selector = Y.Selector._replaceCommas(selector);
        }

        var groups = selector.split('\uE007'), // split on replaced comma token
            queries = [],
            prefix = '',
            id,
            i,
            len;

        if (node) {
            // enforce for element scoping
            if (node.nodeType === 1) { // Elements only
                id = Y.Selector._escapeId(Y.DOM.getId(node));

                if (!id) {
                    id = Y.guid();
                    Y.DOM.setId(node, id);
                }
            
                prefix = '[id="' + 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 pick up "selected"  with "checked"
            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,
            useFrag = false,
            groups,
            parent,
            item,
            items,
            frag,
            id,
            i, j, group;

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

            if (typeof selector == 'function') { // test with function
                ret = selector.call(node, node);
            } else { // test with query
                // we need a root if off-doc
                groups = selector.split(',');
                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];

                id = Y.Selector._escapeId(Y.DOM.getId(node));
                if (!id) {
                    id = Y.guid();
                    Y.DOM.setId(node, id);
                }

                for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
                    group += '[id="' + 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);
    },

    _parse: function(name, selector) {
        return selector.match(Y.Selector._types[name].re);
    },

    _replace: function(name, selector) {
        var o = Y.Selector._types[name];
        return selector.replace(o.re, o.token);
    },

    _restore: function(name, selector, items) {
        if (items) {
            var token = Y.Selector._types[name].token,
                i, len;
            for (i = 0, len = items.length; i < len; ++i) {
                selector = selector.replace(token, items[i]);
            }
        }
        return selector;
    }
};

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

})(Y);