You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
453 lines
14 KiB
453 lines
14 KiB
/* |
|
pseudo selectors |
|
|
|
--- |
|
|
|
they are available in two forms: |
|
* filters called when the selector |
|
is compiled and return a function |
|
that needs to return next() |
|
* pseudos get called on execution |
|
they need to return a boolean |
|
*/ |
|
|
|
var getNCheck = require("nth-check"); |
|
var BaseFuncs = require("boolbase"); |
|
var attributes = require("./attributes.js"); |
|
var trueFunc = BaseFuncs.trueFunc; |
|
var falseFunc = BaseFuncs.falseFunc; |
|
|
|
var checkAttrib = attributes.rules.equals; |
|
|
|
function getAttribFunc(name, value) { |
|
var data = { name: name, value: value }; |
|
return function attribFunc(next, rule, options) { |
|
return checkAttrib(next, data, options); |
|
}; |
|
} |
|
|
|
function getChildFunc(next, adapter) { |
|
return function(elem) { |
|
return !!adapter.getParent(elem) && next(elem); |
|
}; |
|
} |
|
|
|
var filters = { |
|
contains: function(next, text, options) { |
|
var adapter = options.adapter; |
|
|
|
return function contains(elem) { |
|
return next(elem) && adapter.getText(elem).indexOf(text) >= 0; |
|
}; |
|
}, |
|
icontains: function(next, text, options) { |
|
var itext = text.toLowerCase(); |
|
var adapter = options.adapter; |
|
|
|
return function icontains(elem) { |
|
return ( |
|
next(elem) && |
|
adapter |
|
.getText(elem) |
|
.toLowerCase() |
|
.indexOf(itext) >= 0 |
|
); |
|
}; |
|
}, |
|
|
|
//location specific methods |
|
"nth-child": function(next, rule, options) { |
|
var func = getNCheck(rule); |
|
var adapter = options.adapter; |
|
|
|
if (func === falseFunc) return func; |
|
if (func === trueFunc) return getChildFunc(next, adapter); |
|
|
|
return function nthChild(elem) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = 0, pos = 0; i < siblings.length; i++) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) break; |
|
else pos++; |
|
} |
|
} |
|
|
|
return func(pos) && next(elem); |
|
}; |
|
}, |
|
"nth-last-child": function(next, rule, options) { |
|
var func = getNCheck(rule); |
|
var adapter = options.adapter; |
|
|
|
if (func === falseFunc) return func; |
|
if (func === trueFunc) return getChildFunc(next, adapter); |
|
|
|
return function nthLastChild(elem) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) break; |
|
else pos++; |
|
} |
|
} |
|
|
|
return func(pos) && next(elem); |
|
}; |
|
}, |
|
"nth-of-type": function(next, rule, options) { |
|
var func = getNCheck(rule); |
|
var adapter = options.adapter; |
|
|
|
if (func === falseFunc) return func; |
|
if (func === trueFunc) return getChildFunc(next, adapter); |
|
|
|
return function nthOfType(elem) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var pos = 0, i = 0; i < siblings.length; i++) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) break; |
|
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++; |
|
} |
|
} |
|
|
|
return func(pos) && next(elem); |
|
}; |
|
}, |
|
"nth-last-of-type": function(next, rule, options) { |
|
var func = getNCheck(rule); |
|
var adapter = options.adapter; |
|
|
|
if (func === falseFunc) return func; |
|
if (func === trueFunc) return getChildFunc(next, adapter); |
|
|
|
return function nthLastOfType(elem) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) break; |
|
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++; |
|
} |
|
} |
|
|
|
return func(pos) && next(elem); |
|
}; |
|
}, |
|
|
|
//TODO determine the actual root element |
|
root: function(next, rule, options) { |
|
var adapter = options.adapter; |
|
|
|
return function(elem) { |
|
return !adapter.getParent(elem) && next(elem); |
|
}; |
|
}, |
|
|
|
scope: function(next, rule, options, context) { |
|
var adapter = options.adapter; |
|
|
|
if (!context || context.length === 0) { |
|
//equivalent to :root |
|
return filters.root(next, rule, options); |
|
} |
|
|
|
function equals(a, b) { |
|
if (typeof adapter.equals === "function") return adapter.equals(a, b); |
|
|
|
return a === b; |
|
} |
|
|
|
if (context.length === 1) { |
|
//NOTE: can't be unpacked, as :has uses this for side-effects |
|
return function(elem) { |
|
return equals(context[0], elem) && next(elem); |
|
}; |
|
} |
|
|
|
return function(elem) { |
|
return context.indexOf(elem) >= 0 && next(elem); |
|
}; |
|
}, |
|
|
|
//jQuery extensions (others follow as pseudos) |
|
checkbox: getAttribFunc("type", "checkbox"), |
|
file: getAttribFunc("type", "file"), |
|
password: getAttribFunc("type", "password"), |
|
radio: getAttribFunc("type", "radio"), |
|
reset: getAttribFunc("type", "reset"), |
|
image: getAttribFunc("type", "image"), |
|
submit: getAttribFunc("type", "submit"), |
|
|
|
//dynamic state pseudos. These depend on optional Adapter methods. |
|
hover: function(next, rule, options) { |
|
var adapter = options.adapter; |
|
|
|
if (typeof adapter.isHovered === 'function') { |
|
return function hover(elem) { |
|
return next(elem) && adapter.isHovered(elem); |
|
}; |
|
} |
|
|
|
return falseFunc; |
|
}, |
|
visited: function(next, rule, options) { |
|
var adapter = options.adapter; |
|
|
|
if (typeof adapter.isVisited === 'function') { |
|
return function visited(elem) { |
|
return next(elem) && adapter.isVisited(elem); |
|
}; |
|
} |
|
|
|
return falseFunc; |
|
}, |
|
active: function(next, rule, options) { |
|
var adapter = options.adapter; |
|
|
|
if (typeof adapter.isActive === 'function') { |
|
return function active(elem) { |
|
return next(elem) && adapter.isActive(elem); |
|
}; |
|
} |
|
|
|
return falseFunc; |
|
} |
|
}; |
|
|
|
//helper methods |
|
function getFirstElement(elems, adapter) { |
|
for (var i = 0; elems && i < elems.length; i++) { |
|
if (adapter.isTag(elems[i])) return elems[i]; |
|
} |
|
} |
|
|
|
//while filters are precompiled, pseudos get called when they are needed |
|
var pseudos = { |
|
empty: function(elem, adapter) { |
|
return !adapter.getChildren(elem).some(function(elem) { |
|
return adapter.isTag(elem) || elem.type === "text"; |
|
}); |
|
}, |
|
|
|
"first-child": function(elem, adapter) { |
|
return getFirstElement(adapter.getSiblings(elem), adapter) === elem; |
|
}, |
|
"last-child": function(elem, adapter) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = siblings.length - 1; i >= 0; i--) { |
|
if (siblings[i] === elem) return true; |
|
if (adapter.isTag(siblings[i])) break; |
|
} |
|
|
|
return false; |
|
}, |
|
"first-of-type": function(elem, adapter) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = 0; i < siblings.length; i++) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) return true; |
|
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break; |
|
} |
|
} |
|
|
|
return false; |
|
}, |
|
"last-of-type": function(elem, adapter) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = siblings.length - 1; i >= 0; i--) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) return true; |
|
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break; |
|
} |
|
} |
|
|
|
return false; |
|
}, |
|
"only-of-type": function(elem, adapter) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = 0, j = siblings.length; i < j; i++) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) continue; |
|
if (adapter.getName(siblings[i]) === adapter.getName(elem)) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
}, |
|
"only-child": function(elem, adapter) { |
|
var siblings = adapter.getSiblings(elem); |
|
|
|
for (var i = 0; i < siblings.length; i++) { |
|
if (adapter.isTag(siblings[i]) && siblings[i] !== elem) return false; |
|
} |
|
|
|
return true; |
|
}, |
|
|
|
//:matches(a, area, link)[href] |
|
link: function(elem, adapter) { |
|
return adapter.hasAttrib(elem, "href"); |
|
}, |
|
//TODO: :any-link once the name is finalized (as an alias of :link) |
|
|
|
//forms |
|
//to consider: :target |
|
|
|
//:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type) |
|
selected: function(elem, adapter) { |
|
if (adapter.hasAttrib(elem, "selected")) return true; |
|
else if (adapter.getName(elem) !== "option") return false; |
|
|
|
//the first <option> in a <select> is also selected |
|
var parent = adapter.getParent(elem); |
|
|
|
if (!parent || adapter.getName(parent) !== "select" || adapter.hasAttrib(parent, "multiple")) { |
|
return false; |
|
} |
|
|
|
var siblings = adapter.getChildren(parent); |
|
var sawElem = false; |
|
|
|
for (var i = 0; i < siblings.length; i++) { |
|
if (adapter.isTag(siblings[i])) { |
|
if (siblings[i] === elem) { |
|
sawElem = true; |
|
} else if (!sawElem) { |
|
return false; |
|
} else if (adapter.hasAttrib(siblings[i], "selected")) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
return sawElem; |
|
}, |
|
//https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements |
|
//:matches( |
|
// :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled], |
|
// optgroup[disabled] > option), |
|
// fieldset[disabled] * //TODO not child of first <legend> |
|
//) |
|
disabled: function(elem, adapter) { |
|
return adapter.hasAttrib(elem, "disabled"); |
|
}, |
|
enabled: function(elem, adapter) { |
|
return !adapter.hasAttrib(elem, "disabled"); |
|
}, |
|
//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem) |
|
checked: function(elem, adapter) { |
|
return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem, adapter); |
|
}, |
|
//:matches(input, select, textarea)[required] |
|
required: function(elem, adapter) { |
|
return adapter.hasAttrib(elem, "required"); |
|
}, |
|
//:matches(input, select, textarea):not([required]) |
|
optional: function(elem, adapter) { |
|
return !adapter.hasAttrib(elem, "required"); |
|
}, |
|
|
|
//jQuery extensions |
|
|
|
//:not(:empty) |
|
parent: function(elem, adapter) { |
|
return !pseudos.empty(elem, adapter); |
|
}, |
|
//:matches(h1, h2, h3, h4, h5, h6) |
|
header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]), |
|
|
|
//:matches(button, input[type=button]) |
|
button: function(elem, adapter) { |
|
var name = adapter.getName(elem); |
|
return ( |
|
name === "button" || (name === "input" && adapter.getAttributeValue(elem, "type") === "button") |
|
); |
|
}, |
|
//:matches(input, textarea, select, button) |
|
input: namePseudo(["input", "textarea", "select", "button"]), |
|
//input:matches(:not([type!='']), [type='text' i]) |
|
text: function(elem, adapter) { |
|
var attr; |
|
return ( |
|
adapter.getName(elem) === "input" && |
|
(!(attr = adapter.getAttributeValue(elem, "type")) || attr.toLowerCase() === "text") |
|
); |
|
} |
|
}; |
|
|
|
function namePseudo(names) { |
|
if (typeof Set !== "undefined") { |
|
// eslint-disable-next-line no-undef |
|
var nameSet = new Set(names); |
|
|
|
return function(elem, adapter) { |
|
return nameSet.has(adapter.getName(elem)); |
|
}; |
|
} |
|
|
|
return function(elem, adapter) { |
|
return names.indexOf(adapter.getName(elem)) >= 0; |
|
}; |
|
} |
|
|
|
function verifyArgs(func, name, subselect) { |
|
if (subselect === null) { |
|
if (func.length > 2 && name !== "scope") { |
|
throw new Error("pseudo-selector :" + name + " requires an argument"); |
|
} |
|
} else { |
|
if (func.length === 2) { |
|
throw new Error("pseudo-selector :" + name + " doesn't have any arguments"); |
|
} |
|
} |
|
} |
|
|
|
//FIXME this feels hacky |
|
var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/; |
|
|
|
module.exports = { |
|
compile: function(next, data, options, context) { |
|
var name = data.name; |
|
var subselect = data.data; |
|
var adapter = options.adapter; |
|
|
|
if (options && options.strict && !re_CSS3.test(name)) { |
|
throw new Error(":" + name + " isn't part of CSS3"); |
|
} |
|
|
|
if (typeof filters[name] === "function") { |
|
return filters[name](next, subselect, options, context); |
|
} else if (typeof pseudos[name] === "function") { |
|
var func = pseudos[name]; |
|
|
|
verifyArgs(func, name, subselect); |
|
|
|
if (func === falseFunc) { |
|
return func; |
|
} |
|
|
|
if (next === trueFunc) { |
|
return function pseudoRoot(elem) { |
|
return func(elem, adapter, subselect); |
|
}; |
|
} |
|
|
|
return function pseudoArgs(elem) { |
|
return func(elem, adapter, subselect) && next(elem); |
|
}; |
|
} else { |
|
throw new Error("unmatched pseudo-class :" + name); |
|
} |
|
}, |
|
filters: filters, |
|
pseudos: pseudos |
|
};
|
|
|