|
|
- /*
- 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 DomUtils = require("domutils"),
- isTag = DomUtils.isTag,
- getText = DomUtils.getText,
- getParent = DomUtils.getParent,
- getChildren = DomUtils.getChildren,
- getSiblings = DomUtils.getSiblings,
- hasAttrib = DomUtils.hasAttrib,
- getName = DomUtils.getName,
- getAttribute= DomUtils.getAttributeValue,
- getNCheck = require("nth-check"),
- checkAttrib = require("./attributes.js").rules.equals,
- BaseFuncs = require("boolbase"),
- trueFunc = BaseFuncs.trueFunc,
- falseFunc = BaseFuncs.falseFunc;
-
- //helper methods
- function getFirstElement(elems){
- for(var i = 0; elems && i < elems.length; i++){
- if(isTag(elems[i])) return elems[i];
- }
- }
-
- function getAttribFunc(name, value){
- var data = {name: name, value: value};
- return function attribFunc(next){
- return checkAttrib(next, data);
- };
- }
-
- function getChildFunc(next){
- return function(elem){
- return !!getParent(elem) && next(elem);
- };
- }
-
- var filters = {
- contains: function(next, text){
- return function contains(elem){
- return next(elem) && getText(elem).indexOf(text) >= 0;
- };
- },
- icontains: function(next, text){
- var itext = text.toLowerCase();
- return function icontains(elem){
- return next(elem) &&
- getText(elem).toLowerCase().indexOf(itext) >= 0;
- };
- },
-
- //location specific methods
- "nth-child": function(next, rule){
- var func = getNCheck(rule);
-
- if(func === falseFunc) return func;
- if(func === trueFunc) return getChildFunc(next);
-
- return function nthChild(elem){
- var siblings = getSiblings(elem);
-
- for(var i = 0, pos = 0; i < siblings.length; i++){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) break;
- else pos++;
- }
- }
-
- return func(pos) && next(elem);
- };
- },
- "nth-last-child": function(next, rule){
- var func = getNCheck(rule);
-
- if(func === falseFunc) return func;
- if(func === trueFunc) return getChildFunc(next);
-
- return function nthLastChild(elem){
- var siblings = getSiblings(elem);
-
- for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) break;
- else pos++;
- }
- }
-
- return func(pos) && next(elem);
- };
- },
- "nth-of-type": function(next, rule){
- var func = getNCheck(rule);
-
- if(func === falseFunc) return func;
- if(func === trueFunc) return getChildFunc(next);
-
- return function nthOfType(elem){
- var siblings = getSiblings(elem);
-
- for(var pos = 0, i = 0; i < siblings.length; i++){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) break;
- if(getName(siblings[i]) === getName(elem)) pos++;
- }
- }
-
- return func(pos) && next(elem);
- };
- },
- "nth-last-of-type": function(next, rule){
- var func = getNCheck(rule);
-
- if(func === falseFunc) return func;
- if(func === trueFunc) return getChildFunc(next);
-
- return function nthLastOfType(elem){
- var siblings = getSiblings(elem);
-
- for(var pos = 0, i = siblings.length - 1; i >= 0; i--){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) break;
- if(getName(siblings[i]) === getName(elem)) pos++;
- }
- }
-
- return func(pos) && next(elem);
- };
- },
-
- //TODO determine the actual root element
- root: function(next){
- return function(elem){
- return !getParent(elem) && next(elem);
- };
- },
-
- scope: function(next, rule, options, context){
- if(!context || context.length === 0){
- //equivalent to :root
- return filters.root(next);
- }
-
- if(context.length === 1){
- //NOTE: can't be unpacked, as :has uses this for side-effects
- return function(elem){
- return 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")
- };
-
- //while filters are precompiled, pseudos get called when they are needed
- var pseudos = {
- empty: function(elem){
- return !getChildren(elem).some(function(elem){
- return isTag(elem) || elem.type === "text";
- });
- },
-
- "first-child": function(elem){
- return getFirstElement(getSiblings(elem)) === elem;
- },
- "last-child": function(elem){
- var siblings = getSiblings(elem);
-
- for(var i = siblings.length - 1; i >= 0; i--){
- if(siblings[i] === elem) return true;
- if(isTag(siblings[i])) break;
- }
-
- return false;
- },
- "first-of-type": function(elem){
- var siblings = getSiblings(elem);
-
- for(var i = 0; i < siblings.length; i++){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) return true;
- if(getName(siblings[i]) === getName(elem)) break;
- }
- }
-
- return false;
- },
- "last-of-type": function(elem){
- var siblings = getSiblings(elem);
-
- for(var i = siblings.length-1; i >= 0; i--){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) return true;
- if(getName(siblings[i]) === getName(elem)) break;
- }
- }
-
- return false;
- },
- "only-of-type": function(elem){
- var siblings = getSiblings(elem);
-
- for(var i = 0, j = siblings.length; i < j; i++){
- if(isTag(siblings[i])){
- if(siblings[i] === elem) continue;
- if(getName(siblings[i]) === getName(elem)) return false;
- }
- }
-
- return true;
- },
- "only-child": function(elem){
- var siblings = getSiblings(elem);
-
- for(var i = 0; i < siblings.length; i++){
- if(isTag(siblings[i]) && siblings[i] !== elem) return false;
- }
-
- return true;
- },
-
- //:matches(a, area, link)[href]
- link: function(elem){
- return hasAttrib(elem, "href");
- },
- visited: falseFunc, //seems to be a valid implementation
- //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){
- if(hasAttrib(elem, "selected")) return true;
- else if(getName(elem) !== "option") return false;
-
- //the first <option> in a <select> is also selected
- var parent = getParent(elem);
-
- if(
- !parent ||
- getName(parent) !== "select" ||
- hasAttrib(parent, "multiple")
- ) return false;
-
- var siblings = getChildren(parent),
- sawElem = false;
-
- for(var i = 0; i < siblings.length; i++){
- if(isTag(siblings[i])){
- if(siblings[i] === elem){
- sawElem = true;
- } else if(!sawElem){
- return false;
- } else if(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){
- return hasAttrib(elem, "disabled");
- },
- enabled: function(elem){
- return !hasAttrib(elem, "disabled");
- },
- //:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
- checked: function(elem){
- return hasAttrib(elem, "checked") || pseudos.selected(elem);
- },
- //:matches(input, select, textarea)[required]
- required: function(elem){
- return hasAttrib(elem, "required");
- },
- //:matches(input, select, textarea):not([required])
- optional: function(elem){
- return !hasAttrib(elem, "required");
- },
-
- //jQuery extensions
-
- //:not(:empty)
- parent: function(elem){
- return !pseudos.empty(elem);
- },
- //:matches(h1, h2, h3, h4, h5, h6)
- header: function(elem){
- var name = getName(elem);
- return name === "h1" ||
- name === "h2" ||
- name === "h3" ||
- name === "h4" ||
- name === "h5" ||
- name === "h6";
- },
-
- //:matches(button, input[type=button])
- button: function(elem){
- var name = getName(elem);
- return name === "button" ||
- name === "input" &&
- getAttribute(elem, "type") === "button";
- },
- //:matches(input, textarea, select, button)
- input: function(elem){
- var name = getName(elem);
- return name === "input" ||
- name === "textarea" ||
- name === "select" ||
- name === "button";
- },
- //input:matches(:not([type!='']), [type='text' i])
- text: function(elem){
- var attr;
- return getName(elem) === "input" && (
- !(attr = getAttribute(elem, "type")) ||
- attr.toLowerCase() === "text"
- );
- }
- };
-
- function verifyArgs(func, name, subselect){
- if(subselect === null){
- if(func.length > 1 && name !== "scope"){
- throw new SyntaxError("pseudo-selector :" + name + " requires an argument");
- }
- } else {
- if(func.length === 1){
- throw new SyntaxError("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,
- subselect = data.data;
-
- if(options && options.strict && !re_CSS3.test(name)){
- throw SyntaxError(":" + name + " isn't part of CSS3");
- }
-
- if(typeof filters[name] === "function"){
- verifyArgs(filters[name], name, subselect);
- return filters[name](next, subselect, options, context);
- } else if(typeof pseudos[name] === "function"){
- var func = pseudos[name];
- verifyArgs(func, name, subselect);
-
- if(next === trueFunc) return func;
-
- return function pseudoArgs(elem){
- return func(elem, subselect) && next(elem);
- };
- } else {
- throw new SyntaxError("unmatched pseudo-class :" + name);
- }
- },
- filters: filters,
- pseudos: pseudos
- };
|