/*
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 = ''
return !!this.getElementById(id)
},
QUERY_SELECTOR: function(test){
// this supposedly fixes a webkit bug with matchesSelector / querySelector & nth-child
test.innerHTML = '_'
// checks if the document has querySelectorAll, and it works
test.innerHTML = ''
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 = ''
// 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 = ''
// 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 = ''
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 = ''
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 = ''
// 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:"") for getElementsByTagName('*') for some documents
// test.innerHTML = '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:"") for querySelector('*') for some documents
// test.innerHTML = 'foo'
// return !!(test.querySelectorAll('*').length)
// },
GET_ATTRIBUTE: function(test){
// tests for working getAttribute implementation
var shout = "fus ro dah"
test.innerHTML = ''
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 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