Version 3.18.1
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
                    }
                },
            
                /**
                 *  Use the native version of `querySelectorAll`, if it exists.
                 *
                 * @property useNative
                 * @default true
                 * @static
                 */
                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 A CSS selector.
                 * @param {HTMLElement} root optional A node to start the query from. Defaults to `Y.config.doc`.
                 * @param {Boolean} firstOnly optional Whether or not to return only the first match.
                 * @return {HTMLElement[]} The array of nodes that matched 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 || Y.UA.opera) &&          // webkit (chrome, safari) and Opera
                        selector.indexOf(':checked') > -1 &&    // fail to pick up "selected"  with ":checked"
                        (Y.Selector.pseudos && Y.Selector.pseudos.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
                    }
                },
            
                /**
                 * Filters out nodes that do not match the given CSS selector.
                 * @method filter
                 *
                 * @param {HTMLElement[]} nodes An array of nodes.
                 * @param {String} selector A CSS selector to test each node against.
                 * @return {HTMLElement[]} The nodes that matched the given CSS selector.
                 * @static
                 */
                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;
                },
            
                /**
                 * Determines whether or not the given node matches the given CSS selector.
                 * @method test
                 *
                 * @param {HTMLElement} node A node to test.
                 * @param {String} selector A CSS selector to test the node against.
                 * @param {HTMLElement} root optional A node to start the query from. Defaults to the parent document of the node.
                 * @return {Boolean} Whether or not the given node matched the given CSS selector.
                 * @static
                 */
                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 method to emulate Y.Node's aNode.ancestor(selector).
                 * @method ancestor
                 *
                 * @param {HTMLElement} node A node to start the query from.
                 * @param {String} selector A CSS selector to test the node against.
                 * @param {Boolean} testSelf optional Whether or not to include the node in the scan.
                 * @return {HTMLElement} The ancestor node matching the selector, or null.
                 * @static
                 */
                ancestor: function (node, selector, testSelf) {
                    return Y.DOM.ancestor(node, 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);