123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- /*
- Slick Finder
- */"use strict"
-
- // Notable changes from Slick.Finder 1.0.x
-
- // faster bottom -> up expression matching
- // prefers mental sanity over *obsessive compulsive* milliseconds savings
- // uses prototypes instead of objects
- // tries to use matchesSelector smartly, whenever available
- // can populate objects as well as arrays
- // lots of stuff is broken or not implemented
-
- var parse = require("./parser")
-
- // utilities
-
- var index = 0,
- counter = document.__counter = (parseInt(document.__counter || -1, 36) + 1).toString(36),
- key = "uid:" + counter
-
- var uniqueID = function(n, xml){
- if (n === window) return "window"
- if (n === document) return "document"
- if (n === document.documentElement) return "html"
-
- if (xml) {
- var uid = n.getAttribute(key)
- if (!uid) {
- uid = (index++).toString(36)
- n.setAttribute(key, uid)
- }
- return uid
- } else {
- return n[key] || (n[key] = (index++).toString(36))
- }
- }
-
- var uniqueIDXML = function(n) {
- return uniqueID(n, true)
- }
-
- var isArray = Array.isArray || function(object){
- return Object.prototype.toString.call(object) === "[object Array]"
- }
-
- // tests
-
- var uniqueIndex = 0;
-
- var HAS = {
-
- GET_ELEMENT_BY_ID: function(test, id){
- id = "slick_" + (uniqueIndex++);
- // checks if the document has getElementById, and it works
- test.innerHTML = '<a id="' + id + '"></a>'
- return !!this.getElementById(id)
- },
-
- QUERY_SELECTOR: function(test){
- // this supposedly fixes a webkit bug with matchesSelector / querySelector & nth-child
- test.innerHTML = '_<style>:nth-child(2){}</style>'
-
- // checks if the document has querySelectorAll, and it works
- test.innerHTML = '<a class="MiX"></a>'
-
- return test.querySelectorAll('.MiX').length === 1
- },
-
- EXPANDOS: function(test, id){
- id = "slick_" + (uniqueIndex++);
- // checks if the document has elements that support expandos
- test._custom_property_ = id
- return test._custom_property_ === id
- },
-
- // TODO: use this ?
-
- // CHECKED_QUERY_SELECTOR: function(test){
- //
- // // checks if the document supports the checked query selector
- // test.innerHTML = '<select><option selected="selected">a</option></select>'
- // return test.querySelectorAll(':checked').length === 1
- // },
-
- // TODO: use this ?
-
- // EMPTY_ATTRIBUTE_QUERY_SELECTOR: function(test){
- //
- // // checks if the document supports the empty attribute query selector
- // test.innerHTML = '<a class=""></a>'
- // return test.querySelectorAll('[class*=""]').length === 1
- // },
-
- MATCHES_SELECTOR: function(test){
-
- test.className = "MiX"
-
- // checks if the document has matchesSelector, and we can use it.
-
- var matches = test.matchesSelector || test.mozMatchesSelector || test.webkitMatchesSelector
-
- // if matchesSelector trows errors on incorrect syntax we can use it
- if (matches) try {
- matches.call(test, ':slick')
- } catch(e){
- // just as a safety precaution, also test if it works on mixedcase (like querySelectorAll)
- return matches.call(test, ".MiX") ? matches : false
- }
-
- return false
- },
-
- GET_ELEMENTS_BY_CLASS_NAME: function(test){
- test.innerHTML = '<a class="f"></a><a class="b"></a>'
- if (test.getElementsByClassName('b').length !== 1) return false
-
- test.firstChild.className = 'b'
- if (test.getElementsByClassName('b').length !== 2) return false
-
- // Opera 9.6 getElementsByClassName doesnt detects the class if its not the first one
- test.innerHTML = '<a class="a"></a><a class="f b a"></a>'
- if (test.getElementsByClassName('a').length !== 2) return false
-
- // tests passed
- return true
- },
-
- // no need to know
-
- // GET_ELEMENT_BY_ID_NOT_NAME: function(test, id){
- // test.innerHTML = '<a name="'+ id +'"></a><b id="'+ id +'"></b>'
- // return this.getElementById(id) !== test.firstChild
- // },
-
- // this is always checked for and fixed
-
- // STAR_GET_ELEMENTS_BY_TAG_NAME: function(test){
- //
- // // IE returns comment nodes for getElementsByTagName('*') for some documents
- // test.appendChild(this.createComment(''))
- // if (test.getElementsByTagName('*').length > 0) return false
- //
- // // IE returns closed nodes (EG:"</foo>") for getElementsByTagName('*') for some documents
- // test.innerHTML = 'foo</foo>'
- // if (test.getElementsByTagName('*').length) return false
- //
- // // tests passed
- // return true
- // },
-
- // this is always checked for and fixed
-
- // STAR_QUERY_SELECTOR: function(test){
- //
- // // returns closed nodes (EG:"</foo>") for querySelector('*') for some documents
- // test.innerHTML = 'foo</foo>'
- // return !!(test.querySelectorAll('*').length)
- // },
-
- GET_ATTRIBUTE: function(test){
- // tests for working getAttribute implementation
- var shout = "fus ro dah"
- test.innerHTML = '<a class="' + shout + '"></a>'
- return test.firstChild.getAttribute('class') === shout
- }
-
- }
-
- // Finder
-
- var Finder = function Finder(document){
-
- this.document = document
- var root = this.root = document.documentElement
- this.tested = {}
-
- // uniqueID
-
- this.uniqueID = this.has("EXPANDOS") ? uniqueID : uniqueIDXML
-
- // getAttribute
-
- this.getAttribute = (this.has("GET_ATTRIBUTE")) ? function(node, name){
-
- return node.getAttribute(name)
-
- } : function(node, name){
-
- node = node.getAttributeNode(name)
- return (node && node.specified) ? node.value : null
-
- }
-
- // hasAttribute
-
- this.hasAttribute = (root.hasAttribute) ? function(node, attribute){
-
- return node.hasAttribute(attribute)
-
- } : function(node, attribute) {
-
- node = node.getAttributeNode(attribute)
- return !!(node && node.specified)
-
- }
-
- // contains
-
- this.contains = (document.contains && root.contains) ? function(context, node){
-
- return context.contains(node)
-
- } : (root.compareDocumentPosition) ? function(context, node){
-
- return context === node || !!(context.compareDocumentPosition(node) & 16)
-
- } : function(context, node){
-
- do {
- if (node === context) return true
- } while ((node = node.parentNode))
-
- return false
- }
-
- // sort
- // credits to Sizzle (http://sizzlejs.com/)
-
- this.sorter = (root.compareDocumentPosition) ? function(a, b){
-
- if (!a.compareDocumentPosition || !b.compareDocumentPosition) return 0
- return a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1
-
- } : ('sourceIndex' in root) ? function(a, b){
-
- if (!a.sourceIndex || !b.sourceIndex) return 0
- return a.sourceIndex - b.sourceIndex
-
- } : (document.createRange) ? function(a, b){
-
- if (!a.ownerDocument || !b.ownerDocument) return 0
- var aRange = a.ownerDocument.createRange(),
- bRange = b.ownerDocument.createRange()
-
- aRange.setStart(a, 0)
- aRange.setEnd(a, 0)
- bRange.setStart(b, 0)
- bRange.setEnd(b, 0)
- return aRange.compareBoundaryPoints(Range.START_TO_END, bRange)
-
- } : null
-
- this.failed = {}
-
- var nativeMatches = this.has("MATCHES_SELECTOR")
-
- if (nativeMatches) this.matchesSelector = function(node, expression){
-
- if (this.failed[expression]) return null
-
- try {
- return nativeMatches.call(node, expression)
- } catch(e){
- if (slick.debug) console.warn("matchesSelector failed on " + expression)
- this.failed[expression] = true
- return null
- }
-
- }
-
- if (this.has("QUERY_SELECTOR")){
-
- this.querySelectorAll = function(node, expression){
-
- if (this.failed[expression]) return true
-
- var result, _id, _expression, _combinator, _node
-
-
- // non-document rooted QSA
- // credits to Andrew Dupont
-
- if (node !== this.document){
-
- _combinator = expression[0].combinator
-
- _id = node.getAttribute("id")
- _expression = expression
-
- if (!_id){
- _node = node
- _id = "__slick__"
- _node.setAttribute("id", _id)
- }
-
- expression = "#" + _id + " " + _expression
-
-
- // these combinators need a parentNode due to how querySelectorAll works, which is:
- // finding all the elements that match the given selector
- // then filtering by the ones that have the specified element as an ancestor
- if (_combinator.indexOf("~") > -1 || _combinator.indexOf("+") > -1){
-
- node = node.parentNode
- if (!node) result = true
- // if node has no parentNode, we return "true" as if it failed, without polluting the failed cache
-
- }
-
- }
-
- if (!result) try {
- result = node.querySelectorAll(expression.toString())
- } catch(e){
- if (slick.debug) console.warn("querySelectorAll failed on " + (_expression || expression))
- result = this.failed[_expression || expression] = true
- }
-
- if (_node) _node.removeAttribute("id")
-
- return result
-
- }
-
- }
-
- }
-
- Finder.prototype.has = function(FEATURE){
-
- var tested = this.tested,
- testedFEATURE = tested[FEATURE]
-
- if (testedFEATURE != null) return testedFEATURE
-
- var root = this.root,
- document = this.document,
- testNode = document.createElement("div")
-
- testNode.setAttribute("style", "display: none;")
-
- root.appendChild(testNode)
-
- var TEST = HAS[FEATURE], result = false
-
- if (TEST) try {
- result = TEST.call(document, testNode)
- } catch(e){}
-
- if (slick.debug && !result) console.warn("document has no " + FEATURE)
-
- root.removeChild(testNode)
-
- return tested[FEATURE] = result
-
- }
-
- var combinators = {
-
- " ": function(node, part, push){
-
- var item, items
-
- var noId = !part.id, noTag = !part.tag, noClass = !part.classes
-
- if (part.id && node.getElementById && this.has("GET_ELEMENT_BY_ID")){
- item = node.getElementById(part.id)
-
- // return only if id is found, else keep checking
- // might be a tad slower on non-existing ids, but less insane
-
- if (item && item.getAttribute('id') === part.id){
- items = [item]
- noId = true
- // if tag is star, no need to check it in match()
- if (part.tag === "*") noTag = true
- }
- }
-
- if (!items){
-
- if (part.classes && node.getElementsByClassName && this.has("GET_ELEMENTS_BY_CLASS_NAME")){
- items = node.getElementsByClassName(part.classList)
- noClass = true
- // if tag is star, no need to check it in match()
- if (part.tag === "*") noTag = true
- } else {
- items = node.getElementsByTagName(part.tag)
- // if tag is star, need to check it in match because it could select junk, boho
- if (part.tag !== "*") noTag = true
- }
-
- if (!items || !items.length) return false
-
- }
-
- for (var i = 0; item = items[i++];)
- if ((noTag && noId && noClass && !part.attributes && !part.pseudos) || this.match(item, part, noTag, noId, noClass))
- push(item)
-
- return true
-
- },
-
- ">": function(node, part, push){ // direct children
- if ((node = node.firstChild)) do {
- if (node.nodeType == 1 && this.match(node, part)) push(node)
- } while ((node = node.nextSibling))
- },
-
- "+": function(node, part, push){ // next sibling
- while ((node = node.nextSibling)) if (node.nodeType == 1){
- if (this.match(node, part)) push(node)
- break
- }
- },
-
- "^": function(node, part, push){ // first child
- node = node.firstChild
- if (node){
- if (node.nodeType === 1){
- if (this.match(node, part)) push(node)
- } else {
- combinators['+'].call(this, node, part, push)
- }
- }
- },
-
- "~": function(node, part, push){ // next siblings
- while ((node = node.nextSibling)){
- if (node.nodeType === 1 && this.match(node, part)) push(node)
- }
- },
-
- "++": function(node, part, push){ // next sibling and previous sibling
- combinators['+'].call(this, node, part, push)
- combinators['!+'].call(this, node, part, push)
- },
-
- "~~": function(node, part, push){ // next siblings and previous siblings
- combinators['~'].call(this, node, part, push)
- combinators['!~'].call(this, node, part, push)
- },
-
- "!": function(node, part, push){ // all parent nodes up to document
- while ((node = node.parentNode)) if (node !== this.document && this.match(node, part)) push(node)
- },
-
- "!>": function(node, part, push){ // direct parent (one level)
- node = node.parentNode
- if (node !== this.document && this.match(node, part)) push(node)
- },
-
- "!+": function(node, part, push){ // previous sibling
- while ((node = node.previousSibling)) if (node.nodeType == 1){
- if (this.match(node, part)) push(node)
- break
- }
- },
-
- "!^": function(node, part, push){ // last child
- node = node.lastChild
- if (node){
- if (node.nodeType == 1){
- if (this.match(node, part)) push(node)
- } else {
- combinators['!+'].call(this, node, part, push)
- }
- }
- },
-
- "!~": function(node, part, push){ // previous siblings
- while ((node = node.previousSibling)){
- if (node.nodeType === 1 && this.match(node, part)) push(node)
- }
- }
-
- }
-
- Finder.prototype.search = function(context, expression, found){
-
- if (!context) context = this.document
- else if (!context.nodeType && context.document) context = context.document
-
- var expressions = parse(expression)
-
- // no expressions were parsed. todo: is this really necessary?
- if (!expressions || !expressions.length) throw new Error("invalid expression")
-
- if (!found) found = []
-
- var uniques, push = isArray(found) ? function(node){
- found[found.length] = node
- } : function(node){
- found[found.length++] = node
- }
-
- // if there is more than one expression we need to check for duplicates when we push to found
- // this simply saves the old push and wraps it around an uid dupe check.
- if (expressions.length > 1){
- uniques = {}
- var plush = push
- push = function(node){
- var uid = uniqueID(node)
- if (!uniques[uid]){
- uniques[uid] = true
- plush(node)
- }
- }
- }
-
- // walker
-
- var node, nodes, part
-
- main: for (var i = 0; expression = expressions[i++];){
-
- // querySelector
-
- // TODO: more functional tests
-
- // if there is querySelectorAll (and the expression does not fail) use it.
- if (!slick.noQSA && this.querySelectorAll){
-
- nodes = this.querySelectorAll(context, expression)
- if (nodes !== true){
- if (nodes && nodes.length) for (var j = 0; node = nodes[j++];) if (node.nodeName > '@'){
- push(node)
- }
- continue main
- }
- }
-
- // if there is only one part in the expression we don't need to check each part for duplicates.
- // todo: this might be too naive. while solid, there can be expression sequences that do not
- // produce duplicates. "body div" for instance, can never give you each div more than once.
- // "body div a" on the other hand might.
- if (expression.length === 1){
-
- part = expression[0]
- combinators[part.combinator].call(this, context, part, push)
-
- } else {
-
- var cs = [context], c, f, u, p = function(node){
- var uid = uniqueID(node)
- if (!u[uid]){
- u[uid] = true
- f[f.length] = node
- }
- }
-
- // loop the expression parts
- for (var j = 0; part = expression[j++];){
- f = []; u = {}
- // loop the contexts
- for (var k = 0; c = cs[k++];) combinators[part.combinator].call(this, c, part, p)
- // nothing was found, the expression failed, continue to the next expression.
- if (!f.length) continue main
- cs = f // set the contexts for future parts (if any)
- }
-
- if (i === 0) found = f // first expression. directly set found.
- else for (var l = 0; l < f.length; l++) push(f[l]) // any other expression needs to push to found.
- }
-
- }
-
- if (uniques && found && found.length > 1) this.sort(found)
-
- return found
-
- }
-
- Finder.prototype.sort = function(nodes){
- return this.sorter ? Array.prototype.sort.call(nodes, this.sorter) : nodes
- }
-
- // TODO: most of these pseudo selectors include <html> and qsa doesnt. fixme.
-
- var pseudos = {
-
-
- // TODO: returns different results than qsa empty.
-
- 'empty': function(){
- return !(this && this.nodeType === 1) && !(this.innerText || this.textContent || '').length
- },
-
- 'not': function(expression){
- return !slick.matches(this, expression)
- },
-
- 'contains': function(text){
- return (this.innerText || this.textContent || '').indexOf(text) > -1
- },
-
- 'first-child': function(){
- var node = this
- while ((node = node.previousSibling)) if (node.nodeType == 1) return false
- return true
- },
-
- 'last-child': function(){
- var node = this
- while ((node = node.nextSibling)) if (node.nodeType == 1) return false
- return true
- },
-
- 'only-child': function(){
- var prev = this
- while ((prev = prev.previousSibling)) if (prev.nodeType == 1) return false
-
- var next = this
- while ((next = next.nextSibling)) if (next.nodeType == 1) return false
-
- return true
- },
-
- 'first-of-type': function(){
- var node = this, nodeName = node.nodeName
- while ((node = node.previousSibling)) if (node.nodeName == nodeName) return false
- return true
- },
-
- 'last-of-type': function(){
- var node = this, nodeName = node.nodeName
- while ((node = node.nextSibling)) if (node.nodeName == nodeName) return false
- return true
- },
-
- 'only-of-type': function(){
- var prev = this, nodeName = this.nodeName
- while ((prev = prev.previousSibling)) if (prev.nodeName == nodeName) return false
- var next = this
- while ((next = next.nextSibling)) if (next.nodeName == nodeName) return false
- return true
- },
-
- 'enabled': function(){
- return !this.disabled
- },
-
- 'disabled': function(){
- return this.disabled
- },
-
- 'checked': function(){
- return this.checked || this.selected
- },
-
- 'selected': function(){
- return this.selected
- },
-
- 'focus': function(){
- var doc = this.ownerDocument
- return doc.activeElement === this && (this.href || this.type || slick.hasAttribute(this, 'tabindex'))
- },
-
- 'root': function(){
- return (this === this.ownerDocument.documentElement)
- }
-
- }
-
- Finder.prototype.match = function(node, bit, noTag, noId, noClass){
-
- // TODO: more functional tests ?
-
- if (!slick.noQSA && this.matchesSelector){
- var matches = this.matchesSelector(node, bit)
- if (matches !== null) return matches
- }
-
- // normal matching
-
- if (!noTag && bit.tag){
-
- var nodeName = node.nodeName.toLowerCase()
- if (bit.tag === "*"){
- if (nodeName < "@") return false
- } else if (nodeName != bit.tag){
- return false
- }
-
- }
-
- if (!noId && bit.id && node.getAttribute('id') !== bit.id) return false
-
- var i, part
-
- if (!noClass && bit.classes){
-
- var className = this.getAttribute(node, "class")
- if (!className) return false
-
- for (part in bit.classes) if (!RegExp('(^|\\s)' + bit.classes[part] + '(\\s|$)').test(className)) return false
- }
-
- var name, value
-
- if (bit.attributes) for (i = 0; part = bit.attributes[i++];){
-
- var operator = part.operator,
- escaped = part.escapedValue
-
- name = part.name
- value = part.value
-
- if (!operator){
-
- if (!this.hasAttribute(node, name)) return false
-
- } else {
-
- var actual = this.getAttribute(node, name)
- if (actual == null) return false
-
- switch (operator){
- case '^=' : if (!RegExp( '^' + escaped ).test(actual)) return false; break
- case '$=' : if (!RegExp( escaped + '$' ).test(actual)) return false; break
- case '~=' : if (!RegExp('(^|\\s)' + escaped + '(\\s|$)').test(actual)) return false; break
- case '|=' : if (!RegExp( '^' + escaped + '(-|$)' ).test(actual)) return false; break
-
- case '=' : if (actual !== value) return false; break
- case '*=' : if (actual.indexOf(value) === -1) return false; break
- default : return false
- }
-
- }
- }
-
- if (bit.pseudos) for (i = 0; part = bit.pseudos[i++];){
-
- name = part.name
- value = part.value
-
- if (pseudos[name]) return pseudos[name].call(node, value)
-
- if (value != null){
- if (this.getAttribute(node, name) !== value) return false
- } else {
- if (!this.hasAttribute(node, name)) return false
- }
-
- }
-
- return true
-
- }
-
- Finder.prototype.matches = function(node, expression){
-
- var expressions = parse(expression)
-
- if (expressions.length === 1 && expressions[0].length === 1){ // simplest match
- return this.match(node, expressions[0][0])
- }
-
- // TODO: more functional tests ?
-
- if (!slick.noQSA && this.matchesSelector){
- var matches = this.matchesSelector(node, expressions)
- if (matches !== null) return matches
- }
-
- var nodes = this.search(this.document, expression, {length: 0})
-
- for (var i = 0, res; res = nodes[i++];) if (node === res) return true
- return false
-
- }
-
- var finders = {}
-
- var finder = function(context){
- var doc = context || document
- if (doc.ownerDocument) doc = doc.ownerDocument
- else if (doc.document) doc = doc.document
-
- if (doc.nodeType !== 9) throw new TypeError("invalid document")
-
- var uid = uniqueID(doc)
- return finders[uid] || (finders[uid] = new Finder(doc))
- }
-
- // ... API ...
-
- var slick = function(expression, context){
- return slick.search(expression, context)
- }
-
- slick.search = function(expression, context, found){
- return finder(context).search(context, expression, found)
- }
-
- slick.find = function(expression, context){
- return finder(context).search(context, expression)[0] || null
- }
-
- slick.getAttribute = function(node, name){
- return finder(node).getAttribute(node, name)
- }
-
- slick.hasAttribute = function(node, name){
- return finder(node).hasAttribute(node, name)
- }
-
- slick.contains = function(context, node){
- return finder(context).contains(context, node)
- }
-
- slick.matches = function(node, expression){
- return finder(node).matches(node, expression)
- }
-
- slick.sort = function(nodes){
- if (nodes && nodes.length > 1) finder(nodes[0]).sort(nodes)
- return nodes
- }
-
- slick.parse = parse;
-
- // slick.debug = true
- // slick.noQSA = true
-
- module.exports = slick
|