From 007dbdbe8a6acc5a8f99f16f907d89fee34f2885 Mon Sep 17 00:00:00 2001 From: jquense Date: Fri, 18 Dec 2015 14:58:36 -0500 Subject: [PATCH] [added] sibling combinators and psuedos --- README.md | 17 +++++---- lib/utils.js | 7 +++- src/common.js | 66 ++++++++++++++++++++++++++++++++ src/compiler.js | 2 +- src/element-selector.js | 32 +++++++--------- src/instance-selector.js | 61 +++++++++++------------------- src/node.js | 81 ++++++++++++++++++++++++++++++++++++++++ src/utils.js | 23 ++++++------ test/selection.js | 54 ++++++++++++++++++++++++++- 9 files changed, 262 insertions(+), 81 deletions(-) create mode 100644 src/common.js create mode 100644 src/node.js diff --git a/README.md b/README.md index f2777ef..d9ab201 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,21 @@ matches[0] // { type: List, props } ### Supported -- classNames -- `div[propName="hi"]` or `div[boolProp]` -- `>`: `div > .foo` -- `:has()`: `div:has(a.foo)` +- classes: `.foo` +- attributes: `div[propName="hi"]` or `div[boolProp]` +- `>`: direct descendent `div > .foo` +- `+`: adjacent sibling selector +- `~`: general sibling selector +- `:has()`: parent selector `div:has(a.foo)` +- `:not()`: negation +- `:first-child` +- `:last-child` ### Not supported -- sibling selectors -- pseudo selectors (except for has) +- most pseudo selectors - non string interpolations for anything other than "tag" or prop values - ## API ### `match(selector, elementOrInstance) -> array` diff --git a/lib/utils.js b/lib/utils.js index 0ef36bc..498b5bc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,8 +4,13 @@ exports.__esModule = true; exports.anyParent = anyParent; exports.directParent = directParent; exports.legacySelector = legacySelector; + +var isValidPlainElement = function isValidPlainElement(element) { + return typeof element === 'object' && element != null; +}; + var isTextElement = function isTextElement(element) { - return typeof element === 'string'; + return !isValidPlainElement(element) && element !== false; }; exports.isTextElement = isTextElement; diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..9224297 --- /dev/null +++ b/src/common.js @@ -0,0 +1,66 @@ +import { + isDomElement, isCompositeElement + , isReactInstance, isDOMComponent, isCompositeComponent } from './utils'; + +function anyParent(test, element, node){ + do { + node = node.parentNode + } while(node && !test(node.element, node)) + + return !!node +} + +function directParent(test, _, node) { + node = node.parentNode + return !!(node && test(node.element, node)) +} + +function anySibling(test, _, node) { + do { + node = node.prevSibling + } while(node && !test(node.element, node)) + + return !!node +} + +function directSibling(test, _, node) { + node = node.prevSibling + return !!(node && test(node.element, node)) +} + +export default function(compiler) { + compiler.registerPseudo('dom', ()=> isDomElement) + compiler.registerPseudo('composite', ()=> isCompositeElement) + + compiler.registerPseudo('not', function(compiledSelector) { + return (element, node) => { + let matches = compiledSelector(element, node) + return !matches + } + }) + + compiler.registerPseudo('first-child', () => + (element, node) => { + let parent = node.parentNode; + return parent && parent.children.indexOf(node.instance || element) === 0 + }) + + compiler.registerPseudo('last-child', () => + (element, node) => { + let parent = node.parentNode; + let children = parent && parent.children + return parent && children.indexOf(node.instance || element) === (children.length - 1) + }) + + compiler.registerNesting('any', test => + (element, node) => anyParent(test, element, node)) + + compiler.registerNesting('>', test => + (element, node) => directParent(test, element, node)) + + compiler.registerNesting('~', test => + (element, node) => anySibling(test, element, node)) + + compiler.registerNesting('+', test => + (element, node) => directSibling(test, element, node)) +} diff --git a/src/compiler.js b/src/compiler.js index 5cc1fde..0c3c398 100644 --- a/src/compiler.js +++ b/src/compiler.js @@ -79,7 +79,7 @@ export function create(options = {}) { return rules .map(ruleSet => compileRule(ruleSet, null, values, ast)) .reduce((current, next)=> { - return (root, parent) => current(root, parent) || next(root, parent) + return (...args) => current(...args) || next(...args) }) } diff --git a/src/element-selector.js b/src/element-selector.js index c6ccdd9..006ed87 100644 --- a/src/element-selector.js +++ b/src/element-selector.js @@ -2,10 +2,14 @@ import React from 'react'; import transform from 'lodash/object/transform'; import has from 'lodash/object/has'; import { create as createCompiler, parse } from './compiler'; +import common from './common'; +import { createNode, eachChild } from './node'; import { anyParent, directParent, isDomElement, isCompositeElement } from './utils'; export let compiler = createCompiler() +common(compiler); + compiler.registerPseudo('has', function(compiledSelector) { return root => { let matches = findAll(root, compiledSelector) @@ -13,36 +17,26 @@ compiler.registerPseudo('has', function(compiledSelector) { } }) -compiler.registerPseudo('dom', ()=> isDomElement) -compiler.registerPseudo('composite', ()=> isCompositeElement) - -compiler.registerNesting('any', test => anyParent.bind(null, test)) - -compiler.registerNesting('>', test => directParent.bind(null, test)) - export function match(selector, tree, includeSelf = true){ return findAll(tree, compiler.compile(selector), includeSelf) } -export function findAll(root, test, includeSelf, getParent = ()=> ({ parent: null })) { +export function findAll(element, test, includeSelf, parent) { let found = [], children = []; - if (root == null || root === false) + if (element == null || element === false) return found; - if (React.isValidElement(root)) - children = root.props.children + var node = createNode(element, parent); - if (includeSelf && test(root, getParent)) - found.push(root); + if (includeSelf && test(element, node)) + found.push(element); - if (React.Children.count(children) === 0) - return found - React.Children.forEach(children, child => { - let parent = ()=> ({ parent: root, getParent }); - found = found.concat(findAll(child, test, true, parent)) - }) + if (React.isValidElement(element)) + eachChild(element, child => { + found = found.concat(findAll(child, test, true, node)) + }) return found } diff --git a/src/instance-selector.js b/src/instance-selector.js index 90c9afc..edc4fee 100644 --- a/src/instance-selector.js +++ b/src/instance-selector.js @@ -1,70 +1,50 @@ +import React from 'react'; import ReactInstanceMap from 'react/lib/ReactInstanceMap'; +import isPlainObject from 'lodash/lang/isPlainObject'; import { create as createCompiler, parse } from './compiler'; +import common from './common'; +import { createNode, eachChild } from './node'; import { anyParent, directParent - , isDomElement, isCompositeElement } from './utils'; - - -let isDOMComponent = inst => !!(inst && inst.nodeType === 1 && inst.tagName); - -let isCompositeComponent = inst => !isDOMComponent(inst) || inst === null - || typeof inst.render === 'function' && typeof inst.setState === 'function'; - + , isDomElement, isCompositeElement + , isReactInstance, isDOMComponent, isCompositeComponent } from './utils'; export let compiler = createCompiler() +common(compiler); + compiler.registerPseudo('has', function(compiledSelector) { - return (_, inst) => { - let matches = findAll(inst, compiledSelector) + return (_, node) => { + let matches = findAll(node.instance, compiledSelector) return !!matches.length } }) -compiler.registerPseudo('dom', ()=> isDomElement) -compiler.registerPseudo('composite', ()=> isCompositeElement) - -compiler.registerNesting('any', test => - (element, inst, parent) => anyParent(test, element, parent)) - -compiler.registerNesting('>', test => - (element, inst, parent) => directParent(test, element, parent)) - - -export function findAll(inst, test, includeSelf, getParent = ()=> ({ parent: null })) { +export function findAll(inst, test, includeSelf, parent) { let found = [], publicInst; - if (!inst ) + if (!inst) return found; if (inst.getPublicInstance) publicInst = inst.getPublicInstance() - var element = inst._currentElement - , parent = ()=> ({ parent: element, getParent }); + var node = createNode(inst, parent); // ReactEmptyComponents (return null render