UNPKG

@silexlabs/grapesjs-advanced-selector

Version:

A GrapesJS plugin for managing advanced CSS selectors with a visual interface

129 lines (115 loc) 5.15 kB
import { PSEUDO_CLASSES, PseudoClass } from './PseudoClass' import { SimpleSelector, toString as toStringSimpleSelector, specificity as simpleSelectorSpecificity, getSelectorPriority, SimpleSelectorType, IdSelector, ClassSelector, AttributeSelector, UniversalSelector, TagSelector, AttributeOperatorType } from './SimpleSelector' import { toString as toStringPseudoClass } from './PseudoClass' export type CompoundSelector = { selectors: SimpleSelector[] pseudoClass?: PseudoClass | null } /** * Get the full CSS selector string from a CompoundSelector */ export function toString(cs: CompoundSelector, ignorePseudoClass: boolean = false): string { // The universal selector can only be used alone if (cs.selectors.length > 1 && cs.selectors.find(s => s.type === 'universal')?.active) { console.warn('The universal selector can only be used alone') return `*${ cs.pseudoClass ? toStringPseudoClass(cs.pseudoClass) : '' }` } const selectorStr = cs.selectors .filter(s => s.active) .sort((a, b) => getSelectorPriority(a) - getSelectorPriority(b)) .map(toStringSimpleSelector).join('') // A pseudo-class alone (e.g. bare `:hover`) is not a valid selector if (!selectorStr) return '' const pseudoStr = (!ignorePseudoClass && cs.pseudoClass) ? toStringPseudoClass(cs.pseudoClass) : '' return `${selectorStr}${pseudoStr}` } export function specificity(compound: CompoundSelector) { return compound.selectors .filter(s => s.active) .reduce((total, selector) => { return total + simpleSelectorSpecificity(selector) }, 0) + (compound.pseudoClass ? 10 : 0) } /** * Parses a CSS selector string and converts it into a CompoundSelector model. * @example fromString('div.class') // { selectors: [{ type: 'tag', value: 'div' }, { type: 'class', value: 'class' }] } */ export function fromString(selectorStr: string): CompoundSelector { const regex = /(#[-\w]+)|(\.[-\w]+)|(\*)|([a-zA-Z][-a-zA-Z0-9]*)|(\[([a-zA-Z][-a-zA-Z0-9]*)\s*([~|^$*]?=)?\s*"?([^"\]]*)"?\])|(:[-\w]+(\([^)]*\))?)/g const matches = selectorStr.match(regex) if (!matches) { throw new Error(`Invalid selector: ${selectorStr}`) } const selectors: SimpleSelector[] = [] let pseudoClass: PseudoClass | undefined = undefined matches.forEach(match => { if (match.startsWith('#')) { selectors.push({ type: SimpleSelectorType.ID, value: match.slice(1), active: true } as IdSelector) } else if (match.startsWith('.')) { selectors.push({ type: SimpleSelectorType.CLASS, value: match.slice(1), active: true } as ClassSelector) } else if (match.startsWith('[')) { const attrMatch = match.match(/^\[([a-zA-Z][-a-zA-Z0-9]*)\s*([~|^$*]?=)?\s*"?([^"\]]*)"?\]$/) if (attrMatch) { const attr = { type: SimpleSelectorType.ATTRIBUTE, value: attrMatch[1], // Attribute name attributeValue: attrMatch[3] || '', active: true, } as AttributeSelector if (attrMatch[2]) { attr.operator = attrMatch[2] as AttributeOperatorType } selectors.push(attr) } } else if (match === '*') { selectors.push({ type: SimpleSelectorType.UNIVERSAL, active: true } as UniversalSelector) } else if (match.match(/^[a-zA-Z][-a-zA-Z0-9]*$/)) { selectors.push({ type: SimpleSelectorType.TAG, value: match, active: true } as TagSelector) } else if (match.startsWith(':')) { const pseudoMatch = match.match(/:([-\w]+)(\([^)]*\))?/) if (pseudoMatch) { pseudoClass = { ...PSEUDO_CLASSES.find(pc => pc.type === pseudoMatch[1]), } as PseudoClass if (!pseudoClass) { throw new Error(`Pseudo-class not found: ${pseudoMatch[1]}`) } if (pseudoClass.hasParam && pseudoMatch[2]) { pseudoClass = { ...pseudoClass, param: pseudoMatch[2] .slice(1, -1) // Remove surrounding parentheses } } } } }) if (pseudoClass) return { selectors, pseudoClass } return { selectors } } /** * Merge two CompoundSelectors into one */ export function merge(cs1: CompoundSelector, cs2: CompoundSelector): CompoundSelector { const mergedSelectors = [...cs1.selectors] cs2.selectors.forEach(sel => { if (!mergedSelectors.some(existingSel => existingSel.type === sel.type && (existingSel as any).value === (sel as any).value)) { mergedSelectors.push(sel) } }) return { selectors: mergedSelectors, pseudoClass: cs1.pseudoClass || cs2.pseudoClass || null, } } /** * Activate or deactivate the selectors of `targetSelectors` based on `referenceSelectors` */ export function updateActivation(targetSelectors: SimpleSelector[], referenceSelectors: SimpleSelector[]): SimpleSelector[] { return targetSelectors.map(sel => { // Find the same selector in `referenceSelectors` const matchingRef = referenceSelectors.find(ref => ref.type === sel.type && (ref as any).value === (sel as any).value) // Activate if present in `other`, otherwise deactivate it return { ...sel, active: matchingRef ? matchingRef.active : false } }) }