UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

153 lines (152 loc) 7.39 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ESLTraversingQuery_1; import { isElement } from '../../esl-utils/dom/api'; import { isVisible } from '../../esl-utils/dom/visible'; import { ExportNs } from '../../esl-utils/environment/export-ns'; import { tuple, wrap, uniq } from '../../esl-utils/misc/array'; import { unwrapParenthesis } from '../../esl-utils/misc/format'; import { findAll, findChildren, findNext, findParent, findClosest, findPrev } from '../../esl-utils/dom/traversing'; /** * Traversing Query utility to find element via extended selector query * Extended query supports * - plain CSS selectors * - relative selectors (selectors that don't start from a plain selector will use passed base Element as a root) * - ::next and ::prev sibling pseudo-selectors * - ::parent, ::closest and ::child pseudo-selectors * - ::find pseudo-selector * - ::first, ::last and :nth(#) limitation pseudo-selectors * - ::filter, ::not filtration pseudo-selectors * * @example * - `#id .class [attr]` - find by CSS selector in a current document * - ` ` - get current base element * - `::next` - get next sibling element * - `::prev` - get previous sibling element * - `::parent` - get base element parent * - `::parent(#id .class [attr])` - find the closest parent matching passed selector * - `::closest(#id .class [attr])` - find the closest ancestor including base element that matches passed selector * - `::child(#id .class [attr])` - find direct child element(s) that match passed selector * - `::find(#id .class [attr])` - find child element(s) that match passed selector * - `::find(buttons, a)::not([hidden])` - find all buttons and anchors that are not have hidden attribute * - `::find(buttons, a)::filter(:first-child)` - find all buttons and anchors that are first child in container * - `::parent::child(some-tag)` - find direct child element(s) that match tag 'some-tag' in the parent * - `#id .class [attr]::parent` - find parent of element matching selector '#id .class [attr]' in document * - `::find(.row)::last::parent` - find parent of the last element matching selector '.row' from the base element subtree */ let ESLTraversingQuery = ESLTraversingQuery_1 = class ESLTraversingQuery { /** * @returns RegExp that selects all known processors in query string * e.g. /(::parent|::closest|::child|::next|::prev)/ */ static get PROCESSORS_REGEX() { const keys = Object.keys(this.ELEMENT_PROCESSORS).concat(Object.keys(this.COLLECTION_PROCESSORS)); return new RegExp(`(${keys.join('|')})`, 'g'); } static isCollectionProcessor([name]) { return !!name && (name in this.COLLECTION_PROCESSORS); } static processElement(el, [name, selString]) { const sel = unwrapParenthesis(selString || ''); if (!name || !(name in this.ELEMENT_PROCESSORS)) return []; return wrap(this.ELEMENT_PROCESSORS[name](el, sel)); } static processCollection(els, [name, selString]) { const sel = unwrapParenthesis(selString || ''); if (!name || !(name in this.COLLECTION_PROCESSORS)) return []; return wrap(this.COLLECTION_PROCESSORS[name](els, sel)); } static traverseChain(collection, processors, findFirst) { if (!processors.length || !collection.length) return collection; const [processor, ...rest] = processors; if (this.isCollectionProcessor(processor)) { const processedItem = this.processCollection(collection, processor); return this.traverseChain(processedItem, rest, findFirst); } const result = []; for (const target of collection) { const processedItem = this.processElement(target, processor); const resultCollection = this.traverseChain(processedItem, rest, findFirst); if (!resultCollection.length) continue; if (findFirst) return resultCollection.slice(0, 1); result.push(...resultCollection); } return uniq(result); } /** Split multiple queries separated by comma (respects query brackets) */ // This can be solved by RegEx /(?<!\([^\)]*),(?![^\(]*\))/g)/, when the WebKit browser implements this feature static splitQueries(str) { let last = 0; let stack = 0; const result = []; for (let i = 0; i < str.length; i++) { if (str[i] === '(') stack++; if (str[i] === ')') stack = Math.max(0, stack - 1); if (str[i] === ',' && !stack) { result.push(str.substring(last, i).trim()); last = i + 1; } } result.push(str.substring(last).trim()); return result; } static traverse(query, findFirst, base, scope = document) { const found = []; for (const part of ESLTraversingQuery_1.splitQueries(query)) { const els = this.traverseQuery(part, findFirst, base, scope); if (findFirst && els.length) return [els[0]]; found.push(...els); } return uniq(found); } static traverseQuery(query, findFirst, base, scope = document) { const parts = query.split(this.PROCESSORS_REGEX).map((term) => term.trim()); const rootSel = parts.shift(); const baseCollection = base ? [base] : []; const initial = rootSel ? Array.from(scope.querySelectorAll(rootSel)) : baseCollection; return this.traverseChain(initial, tuple(parts), findFirst); } /** @returns first matching element reached via {@link ESLTraversingQuery} rules */ static first(query, base, scope) { return ESLTraversingQuery_1.traverse(query, true, base, scope)[0] || null; } /** @returns Array of all matching elements reached via {@link ESLTraversingQuery} rules */ static all(query, base, scope) { return ESLTraversingQuery_1.traverse(query, false, base, scope); } }; ESLTraversingQuery.ELEMENT_PROCESSORS = { '::find': findAll, '::next': findNext, '::prev': findPrev, '::child': findChildren, '::parent': findParent, '::closest': findClosest }; ESLTraversingQuery.COLLECTION_PROCESSORS = { '::first': (list) => list.slice(0, 1), '::last': (list) => list.slice(-1), '::nth': (list, sel) => { const index = sel ? +sel : NaN; return wrap(list[index - 1]); }, '::not': (list, sel) => list.filter((el) => !el.matches(sel || '')), '::visible': (list) => list.filter((el) => isElement(el) && isVisible(el)), '::filter': (list, sel) => list.filter((el) => el.matches(sel || '')) }; ESLTraversingQuery = ESLTraversingQuery_1 = __decorate([ ExportNs('TraversingQuery') ], ESLTraversingQuery); export { ESLTraversingQuery };