@silexlabs/grapesjs-advanced-selector
Version:
A GrapesJS plugin for managing advanced CSS selectors with a visual interface
233 lines (203 loc) • 7 kB
text/typescript
import { CompoundSelector, toString, specificity, fromString } from './CompoundSelector'
import { SimpleSelectorType, SimpleSelector, TagSelector, IdSelector, ClassSelector, AttributeSelector, UniversalSelector } from './SimpleSelector'
import { PseudoClass } from './PseudoClass'
type AnySelector = TagSelector | IdSelector | ClassSelector | AttributeSelector | UniversalSelector
const tagSelector: TagSelector = { type: SimpleSelectorType.TAG, value: 'div', active: true }
const IdSelector: IdSelector = { type: SimpleSelectorType.ID, value: 'main', active: true }
const classSelector: ClassSelector = { type: SimpleSelectorType.CLASS, value: 'container', active: true }
const attributeSelector: AttributeSelector = { type: SimpleSelectorType.ATTRIBUTE, value: 'data-test', active: true }
const universalSelector: UniversalSelector = { type: SimpleSelectorType.UNIVERSAL, active: true }
const pseudoClass = {
type: 'hover',
hasParam: false,
sentencePre: 'prefix ',
}
// Mock functions from SimpleSelector and PseudoClass
jest.mock('./SimpleSelector', () => ({
...jest.requireActual('./SimpleSelector'),
toString: (selector: AnySelector) => {
switch (selector.type) {
case 'id': return `#${selector.value}`
case 'class': return `.${selector.value}`
case 'tag': return selector.value
case 'attribute': return `[${selector.value}]`
case 'universal': return '*'
default: return ''
}
},
specificity: (selector: SimpleSelector) => {
switch (selector.type) {
case 'id': return 100
case 'class': return 10
case 'tag': return 1
case 'attribute': return 10
case 'universal': return 0
default: return 0
}
}
}))
jest.mock('./PseudoClass', () => ({
toString: (pseudoClass: PseudoClass) => `:${pseudoClass.type}`,
PSEUDO_CLASSES: jest.requireActual('./PseudoClass').PSEUDO_CLASSES,
}))
describe('CompoundSelector', () => {
test('toString - builds correct selector string', () => {
expect(toString({
selectors: [
tagSelector,
classSelector,
IdSelector,
]
} as CompoundSelector)).toBe('div#main.container')
expect(toString({
selectors: [
classSelector,
],
pseudoClass,
} as CompoundSelector)).toBe('.container:hover')
expect(toString({
selectors: [
tagSelector,
{
...IdSelector,
active: false,
},
classSelector,
]
})).toBe('div.container')
expect(toString({
selectors: [
IdSelector,
classSelector,
]
})).toBe('#main.container')
expect(toString({
selectors: [
classSelector,
IdSelector,
]
})).toBe('#main.container')
expect(toString({
selectors: [
classSelector,
tagSelector,
IdSelector,
]
})).toBe('div#main.container')
expect(toString({
selectors: [
universalSelector,
classSelector,
tagSelector,
IdSelector,
]
})).toBe('*')
expect(toString({
selectors: [
attributeSelector,
classSelector,
tagSelector,
IdSelector,
]
})).toBe('div#main.container[data-test]')
})
test('toString - bare pseudo-class without active selectors returns empty string', () => {
expect(toString({
selectors: [],
pseudoClass,
} as CompoundSelector)).toBe('')
expect(toString({
selectors: [
{ ...classSelector, active: false },
],
pseudoClass,
} as CompoundSelector)).toBe('')
})
test('specificity - calculates correct specificity', () => {
expect(specificity({
selectors: [
IdSelector,
classSelector,
tagSelector,
]
})).toBe(100 + 10 + 1) // ID (100) + Class (10) + Tag (1)
expect(specificity({
selectors: [
classSelector,
],
pseudoClass,
} as CompoundSelector)).toBe(10 + 10) // Class (10) + Pseudo-Class (10)
expect(specificity({
selectors: [
{
...classSelector,
active: false,
},
classSelector,
]
})).toBe(10) // Only the active class contributes
})
})
describe('parseSelectorString', () => {
test('parses a tag selector', () => {
const result = fromString('div')
expect(result.selectors).toEqual([{ type: SimpleSelectorType.TAG, value: 'div', active: true }])
})
test('parses an ID selector', () => {
const result = fromString('#main')
expect(result.selectors).toEqual([{ type: SimpleSelectorType.ID, value: 'main', active: true }])
})
test('parses a class selector', () => {
const result = fromString('.container')
expect(result.selectors).toEqual([{ type: SimpleSelectorType.CLASS, value: 'container', active: true }])
})
test('parses a universal selector', () => {
const result = fromString('*')
expect(result.selectors).toEqual([{ type: SimpleSelectorType.UNIVERSAL, active: true }])
})
test('parses an attribute selector', () => {
const result = fromString('[data-role=button]')
expect(result.selectors).toEqual([
{ type: SimpleSelectorType.ATTRIBUTE, value: 'data-role', operator: '=', attributeValue: 'button', active: true },
])
})
test('parses an attribute selector with quotes', () => {
const result = fromString('[data-id="123"]')
expect(result.selectors).toEqual([
{ type: SimpleSelectorType.ATTRIBUTE, value: 'data-id', operator: '=', attributeValue: '123', active: true },
])
})
test('parses a tag with a class', () => {
const result = fromString('button.primary')
expect(result.selectors).toEqual([
{ type: SimpleSelectorType.TAG, value: 'button', active: true },
{ type: SimpleSelectorType.CLASS, value: 'primary', active: true },
])
})
test('parses a tag with an ID', () => {
const result = fromString('nav#main-menu')
expect(result.selectors).toEqual([
{ type: SimpleSelectorType.TAG, value: 'nav', active: true },
{ type: SimpleSelectorType.ID, value: 'main-menu', active: true },
])
})
test('parses a selector with a pseudo-class', () => {
const result = fromString('a:hover')
expect(result.selectors).toEqual([{ type: SimpleSelectorType.TAG, value: 'a', active: true }])
expect(result.pseudoClass?.type).toBe('hover')
})
test('parses a selector with multiple classes and a pseudo-class', () => {
const result = fromString('.button.large:focus')
expect(result.selectors).toEqual([
{ type: SimpleSelectorType.CLASS, value: 'button', active: true },
{ type: SimpleSelectorType.CLASS, value: 'large', active: true },
])
expect(result.pseudoClass?.type).toBe('focus')
})
test('parses a selector with a pseudo-class with params', () => {
const result = fromString(':nth-child(2)')
expect(result.selectors).toEqual([])
expect(result.pseudoClass?.type).toBe('nth-child')
expect(result.pseudoClass?.param).toBe('2')
})
})