use-theme-editor
Version:
Zero configuration CSS variables based theme editor
133 lines (108 loc) • 3.99 kB
JavaScript
import { scopesByProperty } from './collectRuleVars';
import { rootScopes } from './extractPageVariables';
import { getMatchingScopes } from './getMatchingScopes';
import { getMatchingVars } from './getMatchingVars';
import { HIGHLIGHT_CLASS } from './highlight';
export const toLabel = ({id, className, tagName}) => {
const idPart = !id ? '' : `#${ id }`;
const noClass = !className || typeof className !== 'string';
const classPart = noClass ? '' : `.${ className.replace(HIGHLIGHT_CLASS, '').trim().replaceAll(/ +/g, '\n.') }`;
return tagName.toLowerCase() + idPart + '\n' + classPart;
};
export const sortForUI = (
{name: nameA, maxSpecific: maxSpecificA},
{name: nameB, maxSpecific: maxSpecificB},
) => {
const reg = /--(?<element>\w+(-?-\w+)*)(--(?<state>(active|focus|visited|hover|disabled)))?--(?<prop>\w+(-\w+)*)/;
const {media: mediaA, property: propA} = maxSpecificA;
const {media: mediaB, property: propB} = maxSpecificB;
if (propA !== propB) {
return propA < propB ? -1 : 1;
}
if (mediaA !== mediaB) {
if (!mediaA) {
return -1;
}
if (!mediaB) {
return 1;
}
return mediaA.localeCompare(mediaB, 'en', {numeric: true});
}
const matchA = nameA.match(reg);
const matchB = nameB.match(reg);
if (!matchA && !matchB) {
// Compare names if they don't match regex.
return nameA < nameB ? -1 : 1;
}
if (!matchA) {
return 1;
}
if (!matchB) {
return -1;
}
const {element: elementA, state: stateA } = matchA.groups;
const {element: elementB, state: stateB } = matchB.groups;
if (elementA !== elementB) {
return elementA < elementB ? -1 : 1;
}
return stateA < stateB ? -1 : 1;
};
export const groupVars = (vars, target) => {
const groups = [];
//
const labelCounts = {};
let current,
previous = target,
previousMatches = vars;
// Walk up the tree to the root to assign each variable to the deepest element they apply to. Each time we go up we
// test the remaining variables. If the current element doesn't match all anymore, the non matching are assigned to
// the previous (one level deeper) element.
while (current = previous.parentNode) {
if (previousMatches.length === 0) {
break;
}
const currentMatches = getMatchingVars({ cssVars: previousMatches, target: current });
const previousInlineStyles = {};
let previousHasInlineStyles = false;
for (const propname of previous.style) {
previousHasInlineStyles = true;
previousInlineStyles[propname] = previous.style[propname];
}
const currentMatchesLess = currentMatches.length < previousMatches.length;
if (previousHasInlineStyles || currentMatchesLess) {
const element = previous;
const vars = !currentMatchesLess ? [] : previousMatches.filter(match => !currentMatches.includes(match));
const scopes = !currentMatchesLess ? [] : getMatchingScopes(element, vars);
const labelText = toLabel(element);
const count = labelCounts[labelText] || 0;
labelCounts[labelText] = count + 1;
const label = labelText + (count === 0 ? '' : `#${count}`);
groups.push({
element,
isRootElement: element.tagName === 'HTML' || element.tagName === 'BODY',
label,
vars: vars.map(v => {
let currentScope;
for (const key in scopesByProperty[v.name] || {}) {
if (
scopes &&
!rootScopes.includes(key) &&
scopes.some(s=>s.selector === key)
) {
currentScope = key;
}
}
return ({
...v,
currentScope,
});
}),
scopes,
inlineStyles: previousInlineStyles,
});
previousMatches = currentMatches;
}
previous = current;
}
return groups;
};