@oddbird/css-toggles
Version:
Polyfill for the proposed CSS Toggles syntax
619 lines (611 loc) • 19.3 kB
JavaScript
// node_modules/stylis/src/Enum.js
var COMMENT = "comm";
var RULESET = "rule";
var DECLARATION = "decl";
var IMPORT = "@import";
var KEYFRAMES = "@keyframes";
// node_modules/stylis/src/Utility.js
var abs = Math.abs;
var from = String.fromCharCode;
function trim(value) {
return value.trim();
}
function replace(value, pattern, replacement) {
return value.replace(pattern, replacement);
}
function indexof(value, search) {
return value.indexOf(search);
}
function charat(value, index) {
return value.charCodeAt(index) | 0;
}
function substr(value, begin, end) {
return value.slice(begin, end);
}
function strlen(value) {
return value.length;
}
function sizeof(value) {
return value.length;
}
function append(value, array) {
return array.push(value), value;
}
// node_modules/stylis/src/Tokenizer.js
var line = 1;
var column = 1;
var length = 0;
var position = 0;
var character = 0;
var characters = "";
function node(value, root, parent, type, props, children, length2) {
return { value, root, parent, type, props, children, line, column, length: length2, return: "" };
}
function char() {
return character;
}
function prev() {
character = position > 0 ? charat(characters, --position) : 0;
if (column--, character === 10)
column = 1, line--;
return character;
}
function next() {
character = position < length ? charat(characters, position++) : 0;
if (column++, character === 10)
column = 1, line++;
return character;
}
function peek() {
return charat(characters, position);
}
function caret() {
return position;
}
function slice(begin, end) {
return substr(characters, begin, end);
}
function token(type) {
switch (type) {
case 0:
case 9:
case 10:
case 13:
case 32:
return 5;
case 33:
case 43:
case 44:
case 47:
case 62:
case 64:
case 126:
case 59:
case 123:
case 125:
return 4;
case 58:
return 3;
case 34:
case 39:
case 40:
case 91:
return 2;
case 41:
case 93:
return 1;
}
return 0;
}
function alloc(value) {
return line = column = 1, length = strlen(characters = value), position = 0, [];
}
function dealloc(value) {
return characters = "", value;
}
function delimit(type) {
return trim(slice(position - 1, delimiter(type === 91 ? type + 2 : type === 40 ? type + 1 : type)));
}
function whitespace(type) {
while (character = peek())
if (character < 33)
next();
else
break;
return token(type) > 2 || token(character) > 3 ? "" : " ";
}
function escaping(index, count) {
while (--count && next())
if (character < 48 || character > 102 || character > 57 && character < 65 || character > 70 && character < 97)
break;
return slice(index, caret() + (count < 6 && peek() == 32 && next() == 32));
}
function delimiter(type) {
while (next())
switch (character) {
case type:
return position;
case 34:
case 39:
if (type !== 34 && type !== 39)
delimiter(character);
break;
case 40:
if (type === 41)
delimiter(type);
break;
case 92:
next();
break;
}
return position;
}
function commenter(type, index) {
while (next())
if (type + character === 47 + 10)
break;
else if (type + character === 42 + 42 && peek() === 47)
break;
return "/*" + slice(index, position - 1) + "*" + from(type === 47 ? type : next());
}
function identifier(index) {
while (!token(peek()))
next();
return slice(index, position);
}
// node_modules/stylis/src/Parser.js
function compile(value) {
return dealloc(parse("", null, null, null, [""], value = alloc(value), 0, [0], value));
}
function parse(value, root, parent, rule, rules, rulesets, pseudo, points, declarations) {
var index = 0;
var offset = 0;
var length2 = pseudo;
var atrule = 0;
var property = 0;
var previous = 0;
var variable = 1;
var scanning = 1;
var ampersand = 1;
var character2 = 0;
var type = "";
var props = rules;
var children = rulesets;
var reference = rule;
var characters2 = type;
while (scanning)
switch (previous = character2, character2 = next()) {
case 40:
if (previous != 108 && characters2.charCodeAt(length2 - 1) == 58) {
if (indexof(characters2 += replace(delimit(character2), "&", "&\f"), "&\f") != -1)
ampersand = -1;
break;
}
case 34:
case 39:
case 91:
characters2 += delimit(character2);
break;
case 9:
case 10:
case 13:
case 32:
characters2 += whitespace(previous);
break;
case 92:
characters2 += escaping(caret() - 1, 7);
continue;
case 47:
switch (peek()) {
case 42:
case 47:
append(comment(commenter(next(), caret()), root, parent), declarations);
break;
default:
characters2 += "/";
}
break;
case 123 * variable:
points[index++] = strlen(characters2) * ampersand;
case 125 * variable:
case 59:
case 0:
switch (character2) {
case 0:
case 125:
scanning = 0;
case 59 + offset:
if (property > 0 && strlen(characters2) - length2)
append(property > 32 ? declaration(characters2 + ";", rule, parent, length2 - 1) : declaration(replace(characters2, " ", "") + ";", rule, parent, length2 - 2), declarations);
break;
case 59:
characters2 += ";";
default:
append(reference = ruleset(characters2, root, parent, index, offset, rules, points, type, props = [], children = [], length2), rulesets);
if (character2 === 123)
if (offset === 0)
parse(characters2, root, reference, reference, props, rulesets, length2, points, children);
else
switch (atrule) {
case 100:
case 109:
case 115:
parse(value, reference, reference, rule && append(ruleset(value, reference, reference, 0, 0, rules, points, type, rules, props = [], length2), children), rules, children, length2, points, rule ? props : children);
break;
default:
parse(characters2, reference, reference, reference, [""], children, 0, points, children);
}
}
index = offset = property = 0, variable = ampersand = 1, type = characters2 = "", length2 = pseudo;
break;
case 58:
length2 = 1 + strlen(characters2), property = previous;
default:
if (variable < 1) {
if (character2 == 123)
--variable;
else if (character2 == 125 && variable++ == 0 && prev() == 125)
continue;
}
switch (characters2 += from(character2), character2 * variable) {
case 38:
ampersand = offset > 0 ? 1 : (characters2 += "\f", -1);
break;
case 44:
points[index++] = (strlen(characters2) - 1) * ampersand, ampersand = 1;
break;
case 64:
if (peek() === 45)
characters2 += delimit(next());
atrule = peek(), offset = length2 = strlen(type = characters2 += identifier(caret())), character2++;
break;
case 45:
if (previous === 45 && strlen(characters2) == 2)
variable = 0;
}
}
return rulesets;
}
function ruleset(value, root, parent, index, offset, rules, points, type, props, children, length2) {
var post = offset - 1;
var rule = offset === 0 ? rules : [""];
var size = sizeof(rule);
for (var i = 0, j = 0, k = 0; i < index; ++i)
for (var x = 0, y = substr(value, post + 1, post = abs(j = points[i])), z = value; x < size; ++x)
if (z = trim(j > 0 ? rule[x] + " " + y : replace(y, /&\f/g, rule[x])))
props[k++] = z;
return node(value, root, parent, offset === 0 ? RULESET : type, props, children, length2);
}
function comment(value, root, parent) {
return node(value, root, parent, COMMENT, from(char()), substr(value, 2, -2), 0);
}
function declaration(value, root, parent, length2) {
return node(value, root, parent, DECLARATION, substr(value, 0, length2), substr(value, length2 + 1, -1), length2);
}
// node_modules/stylis/src/Serializer.js
function serialize(children, callback) {
var output = "";
var length2 = sizeof(children);
for (var i = 0; i < length2; i++)
output += callback(children[i], i, children, callback) || "";
return output;
}
function stringify(element, index, children, callback) {
switch (element.type) {
case IMPORT:
case DECLARATION:
return element.return = element.return || element.value;
case COMMENT:
return "";
case KEYFRAMES:
return element.return = element.value + "{" + serialize(element.children, callback) + "}";
case RULESET:
element.value = element.props.join(",");
}
return strlen(children = serialize(element.children, callback)) ? element.return = element.value + "{" + children + "}" : "";
}
// src/store.js
var makeRegex = (parts, opts) => RegExp(parts.map((p) => p.source).join(""), opts);
var toggleRootRe = makeRegex([
/(?<name>[\w-]+)/,
/ */,
/((?<initial>\d+)\/)?/,
/(?<numActive>\d*)?/,
/(\[(?<states>.+)\])?/,
/ */,
/(at +(?<at>[\w-]+))?/,
/ */,
/(?<modifiers>.*)/
]);
var toggleRootMachineRe = makeRegex([
/((?<name>[\w-]+) +)?/,
/(machine\((?<machine>[\w-]+)(?<strict> *, *strict)?\))/,
/ */,
/(at +(?<at>[\w-]+))?/,
/ */,
/(?<modifiers>.*)/
]);
var toggleTriggerRe = makeRegex([
/(?<name>[\w-]+)/,
/ */,
/(do[ (](?<transition>[\w-]+)\)?)?/,
/(?<targetState>[\w-]*)/
]);
var counter = 0;
var uid = () => counter++;
var toggleRoots = {};
var toggleMachines = {};
function withNextSiblings(element) {
const siblings = [element];
let next2 = element.nextElementSibling;
while (next2 !== null) {
siblings.push(next2);
next2 = next2.nextElementSibling;
}
return siblings;
}
function createToggleRoots(ruleValue, selectors) {
const regex = ruleValue.includes("machine(") ? toggleRootMachineRe : toggleRootRe;
let { name, machine, strict, initial, numActive, states, at, modifiers } = regex.exec(ruleValue).groups;
name = name || machine;
if (name === void 0)
return;
let total;
const machineDef = toggleMachines[machine];
if (machineDef !== void 0) {
states = Object.keys(machineDef.states);
total = states.length;
} else if (states !== void 0) {
states = states.split(/ +/);
total = states.length;
} else {
total = parseInt(numActive || 1) + 1;
states = [...Array(total).keys()].map(String);
}
modifiers = modifiers?.split(/ +/) || [];
const group = modifiers.includes("group");
const isNarrow = modifiers.includes("self");
let activeIndex = states.indexOf(initial);
if (activeIndex === -1)
activeIndex = states.indexOf(at);
if (activeIndex === -1)
activeIndex = 0;
let resetTo = 0;
if (modifiers.includes("linear"))
resetTo = total - 1;
if (modifiers.includes("sticky"))
resetTo = 1;
const config = {
name,
resetTo,
group,
isNarrow,
total,
states,
machine,
activeIndex,
strict: Boolean(strict)
};
document.querySelectorAll(selectors).forEach((el) => {
const id = `${name}-${uid()}`;
const elements = isNarrow ? [el] : withNextSiblings(el);
elements.forEach((el2) => el2.dataset.toggleRoot = id);
toggleRoots[id] = { ...config };
});
}
function createToggleTriggers(ruleValue, selectors) {
const { name, targetState, transition } = toggleTriggerRe.exec(ruleValue).groups;
if (name === void 0)
return;
const dispatchToggleEvent = ({ target }) => {
target.dispatchEvent(new CustomEvent("_toggleTrigger", {
bubbles: true,
detail: { toggleRoot: name, targetState, transition }
}));
};
function handleKeyDown(event) {
if (![" ", "Enter", "Spacebar"].includes(event.key))
return;
event.preventDefault();
dispatchToggleEvent(event);
}
document.querySelectorAll(selectors).forEach((el) => {
el.dataset.toggleTrigger = "";
el.addEventListener("click", dispatchToggleEvent);
if (["button", "a", "input"].includes(el.nodeName.toLowerCase()))
return;
el.addEventListener("keydown", handleKeyDown);
el.setAttribute("tabindex", 0);
el.setAttribute("role", "button");
el.setAttribute("aria-pressed", false);
});
}
// src/walkers.js
var pseudoElements = [
"after",
"backdrop",
"before",
"cue",
"cue-region",
"first-letter",
"first-line",
"file-selector-button",
"grammar-error",
"marker",
"placeholder",
"selection",
"spelling-error",
"target-text"
].join("|");
var pseudoElementRe = RegExp(`::?(${pseudoElements})`);
var togglePseudoClassRe = /:toggle\((?<name>[\w-]+) *(?<value>[\w-]*)\)/;
var toggleVisibilityRe = /toggle *(?<name>[\w-]+)/;
function toggleMachineWalker(element) {
const name = (element.props || [])[0];
if (!(element.type === "@machine" && name))
return;
const states = {};
element.children.filter((child) => child.type === "rule").forEach((rule) => {
states[rule.value] = Object.fromEntries(rule.children.filter((child) => child.type === "decl").map((decl) => [decl.props, decl.children]));
});
toggleMachines[name] = { name, states };
}
function togglePseudoClassWalker(element) {
if (element.type !== "rule")
return;
let didReplace = false;
element.props = element.props.map((selector) => {
if (!selector.match(togglePseudoClassRe))
return selector;
const baseSelector = selector.slice(0, selector.search(togglePseudoClassRe)).replace(pseudoElementRe, "");
document.querySelectorAll(baseSelector).forEach((el) => el.dataset.toggle = "");
let replacement;
const { name, value } = togglePseudoClassRe.exec(selector).groups;
if (value) {
replacement = `[data-toggle="${name} ${value}"]`;
} else {
replacement = `[data-toggle^="${name} "]:not([data-toggle="${name} 0"])`;
}
didReplace = true;
return selector.replace(togglePseudoClassRe, replacement);
});
return didReplace;
}
function toggleVisibilityWalker(element) {
if (!(element.type === "decl" && element.props === "toggle-visibility"))
return;
const { name } = toggleVisibilityRe.exec(element.children).groups;
if (name === void 0)
return;
document.querySelectorAll(element.parent.value).forEach((el) => {
el.dataset.toggleVisibility = "";
});
}
function toggleRootWalker(element) {
if (!(element.type === "decl" && element.props === "toggle-root"))
return;
createToggleRoots(element.children, element.parent.value);
}
function toggleTriggerWalker(element) {
if (!(element.type === "decl" && element.props === "toggle-trigger"))
return;
createToggleTriggers(element.children, element.parent.value);
}
function toggleWalker(element) {
if (!(element.type === "decl" && element.props === "toggle"))
return;
const selectors = element.parent.value;
const ruleValue = element.children;
createToggleRoots(ruleValue, selectors);
createToggleTriggers(ruleValue, selectors);
}
// src/css-toggles.js
function renderToggleState(toggleRootId) {
const toggleRoot = toggleRoots[toggleRootId];
const { activeIndex } = toggleRoot;
document.querySelectorAll(`
[data-toggle-root="${toggleRootId}"][data-toggle-visibility],
[data-toggle-root="${toggleRootId}"] [data-toggle-visibility]
`).forEach((el) => {
const closestRoot = el.closest("[data-toggle-root]");
if (closestRoot?.dataset.toggleRoot !== toggleRootId)
return;
el.dataset.toggleVisibility = activeIndex > 0 ? "visible" : "hidden";
});
document.querySelectorAll(`
[data-toggle-root="${toggleRootId}"][data-toggle],
[data-toggle-root="${toggleRootId}"] [data-toggle]
`).forEach((el) => {
const closestRoot = el.closest("[data-toggle-root]");
if (closestRoot?.dataset.toggleRoot !== toggleRootId)
return;
el.dataset.toggle = `${toggleRoot.name} ${toggleRoot.states[activeIndex]}`;
});
document.querySelectorAll(`
[data-toggle-root="${toggleRootId}"][data-toggle-trigger][aria-pressed],
[data-toggle-root="${toggleRootId}"] [data-toggle-trigger][aria-pressed]
`).forEach((el) => {
const closestRoot = el.closest("[data-toggle-root]");
if (closestRoot?.dataset.toggleRoot !== toggleRootId)
return;
el.setAttribute("aria-pressed", activeIndex > 0);
});
}
function initStylesheet(sheetSrc, url) {
let didReplace = false;
function walk(element) {
if (element.type === "comm")
return;
toggleMachineWalker(element);
didReplace |= togglePseudoClassWalker(element);
toggleVisibilityWalker(element);
toggleRootWalker(element);
toggleTriggerWalker(element);
toggleWalker(element);
const size = (element.children || []).length;
for (let i = 0; i < size; i++)
walk(element.children[i]);
}
const cssAst = compile(sheetSrc);
cssAst.forEach(walk);
return didReplace ? serialize(cssAst, stringify) : "";
}
function handleStyleTag(el) {
const newSrc = initStylesheet(el.innerHTML);
if (!newSrc)
return;
el.innerHTML = newSrc;
}
async function handleLinkedStylesheet(el) {
if (el.rel !== "stylesheet")
return;
const srcUrl = new URL(el.href, document.baseURI);
if (srcUrl.origin !== location.origin)
return;
const src = await fetch(srcUrl.toString()).then((r) => r.text());
const newSrc = initStylesheet(src, srcUrl.toString());
if (!newSrc)
return;
const blob = new Blob([newSrc], { type: "text/css" });
el.href = URL.createObjectURL(blob);
}
document.body.addEventListener("_toggleTrigger", (event) => {
const { target } = event;
let { toggleRoot: name, targetState, transition } = event.detail;
const toggleRootElement = target.closest(`[data-toggle-root^="${name}-"]`);
const id = toggleRootElement?.dataset?.toggleRoot || null;
const toggleRoot = toggleRoots[id];
if (toggleRoot === void 0)
return;
const currentState = toggleRoot.states[toggleRoot.activeIndex];
if (toggleRoot.group) {
for (const _id of Object.keys(toggleRoots)) {
if (toggleRoots[_id].name === name) {
toggleRoots[_id].activeIndex = 0;
renderToggleState(_id);
}
}
}
const machine = toggleMachines[toggleRoot.machine];
if (machine !== void 0) {
targetState = machine.states[currentState][transition];
if (targetState === void 0)
return;
}
const nextIndex = toggleRoot.states.indexOf(targetState);
if (nextIndex === -1) {
if (toggleRoot.activeIndex === toggleRoot.total - 1) {
toggleRoot.activeIndex = toggleRoot.resetTo;
} else {
toggleRoot.activeIndex++;
}
} else {
toggleRoot.activeIndex = nextIndex;
}
renderToggleState(id);
});
document.querySelectorAll("style").forEach(handleStyleTag);
Promise.all([...document.querySelectorAll("link")].map(handleLinkedStylesheet)).then(() => {
Object.keys(toggleRoots).forEach(renderToggleState);
});
document.head.insertAdjacentHTML("beforeend", '<style>[data-toggle-visibility="hidden"] { display: none; }</style>');