@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
JavaScript
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 };