storybook-addon-pseudo-states
Version:
Storybook Pseudo-states addon: Manipulate CSS pseudo states
238 lines (230 loc) • 11.7 kB
JavaScript
import {
EXCLUDED_PSEUDO_ELEMENT_PATTERNS,
EXCLUDED_PSEUDO_ESCAPE_SEQUENCE,
PARAM_KEY,
PSEUDO_STATES,
__export
} from "./chunk-FLRMKMJW.js";
// src/preview.ts
var preview_exports = {};
__export(preview_exports, {
decorators: () => decorators,
initialGlobals: () => initialGlobals
});
// src/preview/withPseudoState.ts
import {
DOCS_RENDERED,
FORCE_REMOUNT,
FORCE_RE_RENDER,
GLOBALS_UPDATED,
STORY_CHANGED,
STORY_RENDERED,
UPDATE_GLOBALS
} from "storybook/internal/core-events";
import { addons, useEffect, useMemo, useRef } from "storybook/preview-api";
// src/preview/splitSelectors.ts
var isAtRule = (selector) => selector.indexOf("@") === 0, splitSelectors = (selectors) => {
if (isAtRule(selectors))
return [selectors];
let result = [], parentheses = 0, brackets = 0, selector = "";
for (let i = 0, len = selectors.length; i < len; i++) {
let char = selectors[i];
if (char === "(")
parentheses += 1;
else if (char === ")")
parentheses -= 1;
else if (char === "[")
brackets += 1;
else if (char === "]")
brackets -= 1;
else if (char === "," && !parentheses && !brackets) {
result.push(selector.trim()), selector = "";
continue;
}
selector += char;
}
return result.push(selector.trim()), result;
};
// src/preview/rewriteStyleSheet.ts
var pseudoStates = Object.values(PSEUDO_STATES), pseudoStatesPattern = `${EXCLUDED_PSEUDO_ESCAPE_SEQUENCE}:(${pseudoStates.join("|")})`, matchOne = new RegExp(pseudoStatesPattern), matchAll = new RegExp(pseudoStatesPattern, "g"), warnings = /* @__PURE__ */ new Set(), warnOnce = (message) => {
warnings.has(message) || (console.warn(message), warnings.add(message));
}, replacePseudoStates = (selector, allClass) => {
let negativeLookbehind = `(?<!(?:${EXCLUDED_PSEUDO_ELEMENT_PATTERNS.join("|")})\\S*)`;
return pseudoStates.reduce(
(acc, state) => acc.replace(
new RegExp(`${negativeLookbehind}${EXCLUDED_PSEUDO_ESCAPE_SEQUENCE}:${state}`, "g"),
`.pseudo-${state}${allClass ? "-all" : ""}`
),
selector
);
}, replacePseudoStatesWithAncestorSelector = (selector, forShadowDOM, additionalHostSelectors) => {
let extracted = extractPseudoStates(selector);
if (extracted.states.length === 0 && !additionalHostSelectors)
return selector;
let selectors = `${additionalHostSelectors ?? ""}${extracted.states.map((s) => `.pseudo-${s}-all`).join("")}`, { withoutPseudoStates } = extracted;
return withoutPseudoStates = withoutPseudoStates.replace(":host-context(*)", "").trimStart(), withoutPseudoStates.startsWith(":host-context(") ? withoutPseudoStates.replace(/(?<=:host-context\(\S+)\)/, `)${selectors}`) : forShadowDOM ? `:host(${selectors}) ${withoutPseudoStates}` : `${selectors} ${withoutPseudoStates}`;
}, extractPseudoStates = (selector) => {
let states = /* @__PURE__ */ new Set(), withoutPseudoStates = selector.replace(matchAll, (_, state) => (states.add(state), "")).replaceAll(/(?<!is)\(\)/g, "(*)").replace(/([>+~])\s*(?=$|[,)])/g, "$1 *").replace(/(?<=[\s(]),\s+|(,\s+)+(?=\))/g, "") || "*";
return {
states: Array.from(states),
withoutPseudoStates
};
}, rewriteNotSelectors = (selector, forShadowDOM) => [...selector.matchAll(/:not\((?:[^()]|\([^()]+\)|\((?:[^()]|\([^()]+\))+\))+\)/g)].reduce(
(acc, [originalNot]) => {
let selectorList = originalNot.match(/^:not\((.+)\)$/)?.[1] ?? "", rewrittenNot = rewriteNotSelector(selectorList, forShadowDOM);
return acc.replace(originalNot, rewrittenNot);
},
selector
), rewriteNotSelector = (negatedSelectorList, forShadowDOM) => {
let rewrittenSelectors = [];
for (let negatedSelector of negatedSelectorList.split(/,\s*/))
rewrittenSelectors.push(replacePseudoStatesWithAncestorSelector(negatedSelector, forShadowDOM));
return `:not(${rewrittenSelectors.join(", ")})`;
}, rewriteRule = ({ cssText, selectorText }, forShadowDOM) => cssText.replace(
selectorText,
splitSelectors(selectorText).flatMap((selector) => {
if (selector.includes(".pseudo-"))
return [];
let replacementSelectors = [selector];
if (!matchOne.test(selector))
return replacementSelectors;
let classSelector = replacePseudoStates(selector);
classSelector !== selector && replacementSelectors.push(classSelector);
let ancestorSelector = "";
if (selector.startsWith(":host(")) {
let matches = selector.match(/^:host\((\S+)\)\s+(.+)$/);
if (matches && matchOne.test(matches[2])) {
let hostInnerSelector = matches[1], descendantSelector = matches[2];
hostInnerSelector = replacePseudoStates(hostInnerSelector, !0), descendantSelector = rewriteNotSelectors(descendantSelector, !0), ancestorSelector = replacePseudoStatesWithAncestorSelector(
descendantSelector,
!0,
hostInnerSelector
);
} else
ancestorSelector = replacePseudoStates(selector, !0);
} else {
let withNotsReplaced = rewriteNotSelectors(selector, forShadowDOM);
ancestorSelector = replacePseudoStatesWithAncestorSelector(
withNotsReplaced,
forShadowDOM
);
}
return replacementSelectors.push(ancestorSelector), replacementSelectors;
}).join(", ")
), rewriteStyleSheet = (sheet, forShadowDOM = !1) => {
try {
let count = rewriteRuleContainer(sheet, 1e3, forShadowDOM);
return count >= 1e3 && warnOnce("Reached maximum of 1000 pseudo selectors per sheet, skipping the rest."), count > 0;
} catch (e) {
return String(e).includes("cssRules") ? warnOnce(`Can't access cssRules, likely due to CORS restrictions: ${sheet.href}`) : console.error(e, sheet.href), !1;
}
}, rewriteRuleContainer = (ruleContainer, rewriteLimit, forShadowDOM) => {
let count = 0, index = -1;
for (let cssRule of ruleContainer.cssRules) {
index++;
let numRewritten = 0;
if (cssRule.__processed)
numRewritten = cssRule.__pseudoStatesRewrittenCount;
else {
let styleRule = cssRule;
if ("selectorText" in styleRule && matchOne.test(styleRule.selectorText)) {
let newRule = rewriteRule(styleRule, forShadowDOM);
ruleContainer.deleteRule(index), ruleContainer.insertRule(newRule, index), styleRule = ruleContainer.cssRules[index], numRewritten = 1;
}
"cssRules" in styleRule && styleRule.cssRules.length && (numRewritten = rewriteRuleContainer(
styleRule,
rewriteLimit - count,
forShadowDOM
)), cssRule.__processed = !0, cssRule.__pseudoStatesRewrittenCount = numRewritten;
}
if (count += numRewritten, count >= rewriteLimit)
break;
}
return count;
};
// src/preview/withPseudoState.ts
var channel = addons.getChannel(), shadowHosts = /* @__PURE__ */ new Set(), applyClasses = (element, classnames) => {
Object.values(PSEUDO_STATES).forEach((state) => {
element.classList.remove(`pseudo-${state}`), element.classList.remove(`pseudo-${state}-all`);
}), classnames.forEach((classname) => element.classList.add(classname));
};
function querySelectorPiercingShadowDOM(root, selector) {
let results = [];
return root.querySelectorAll("*").forEach((el) => {
el.shadowRoot && results.push(...querySelectorPiercingShadowDOM(el.shadowRoot, selector));
}), results.push(...root.querySelectorAll(selector).values()), results;
}
var applyParameter = (rootElement, parameter = {}) => {
let map = /* @__PURE__ */ new Map([[rootElement, /* @__PURE__ */ new Set()]]), add = (target, state) => map.set(target, /* @__PURE__ */ new Set([...map.get(target) || [], state]));
Object.entries(parameter || {}).forEach(([state, value]) => {
typeof value == "boolean" ? value && add(rootElement, `${state}-all`) : typeof value == "string" ? querySelectorPiercingShadowDOM(rootElement, value).forEach((el) => add(el, state)) : Array.isArray(value) && value.forEach(
(sel) => querySelectorPiercingShadowDOM(rootElement, sel).forEach((el) => add(el, state))
);
}), map.forEach((states, target) => {
let classnames = /* @__PURE__ */ new Set();
states.forEach((key) => {
let keyWithoutAll = key.replace("-all", "");
PSEUDO_STATES[key] ? classnames.add(`pseudo-${PSEUDO_STATES[key]}`) : PSEUDO_STATES[keyWithoutAll] && classnames.add(`pseudo-${PSEUDO_STATES[keyWithoutAll]}-all`);
}), applyClasses(target, classnames);
});
}, updateShadowHost = (shadowHost) => {
let classnames = /* @__PURE__ */ new Set();
shadowHost.className.split(" ").filter((classname) => classname.match(/^pseudo-(.(?!-all))+$/)).forEach((classname) => classnames.add(classname));
for (let node = shadowHost.parentNode; node; ) {
if (node instanceof ShadowRoot) {
node = node.host;
continue;
}
if (node instanceof Element) {
let element = node;
element.className && element.className.split(" ").filter((classname) => classname.match(/^pseudo-.+-all$/) !== null).forEach((classname) => classnames.add(classname));
}
node = node.parentNode;
}
applyClasses(shadowHost, classnames);
}, pseudoConfig = (parameter) => {
let { rootSelector, ...pseudoStateConfig } = parameter || {};
return pseudoStateConfig;
}, equals = (a = {}, b = {}) => a !== null && b !== null && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(
(key) => JSON.stringify(a[key]) === JSON.stringify(b[key])
), withPseudoState = (StoryFn, { viewMode, parameters, id, globals: globalsArgs }) => {
let { pseudo: parameter } = parameters, { pseudo: globals } = globalsArgs, { rootSelector } = parameter || {}, rootElement = useMemo(() => rootSelector ? document.querySelector(rootSelector) : viewMode === "docs" ? document.getElementById(`story--${id}`) : document.getElementById("storybook-root") || // Storybook 7.0+
document.getElementById("root"), [rootSelector, viewMode, id]), globalsRef = useRef(globals);
return useEffect(() => {
let config = pseudoConfig(parameter);
viewMode === "story" && Object.keys(config).length > 0 && !equals(config, globalsRef.current) && channel.emit(UPDATE_GLOBALS, {
globals: { pseudo: config }
});
}, [parameter, viewMode]), useEffect(() => {
if (!rootElement)
return;
let timeout = setTimeout(() => {
applyParameter(rootElement, globals || pseudoConfig(parameter)), shadowHosts.forEach(updateShadowHost);
}, 0);
return () => clearTimeout(timeout);
}, [rootElement, globals, parameter]), StoryFn();
}, rewriteStyleSheets = (shadowRoot) => {
let styleSheets = Array.from(shadowRoot ? shadowRoot.styleSheets : document.styleSheets);
shadowRoot?.adoptedStyleSheets?.length && (styleSheets = shadowRoot.adoptedStyleSheets), styleSheets.forEach((sheet) => rewriteStyleSheet(sheet, !!shadowRoot)), shadowRoot && shadowHosts && shadowHosts.add(shadowRoot.host);
};
channel.on(STORY_CHANGED, () => shadowHosts.clear());
channel.on(STORY_RENDERED, () => rewriteStyleSheets());
channel.on(GLOBALS_UPDATED, () => rewriteStyleSheets());
channel.on(FORCE_RE_RENDER, () => rewriteStyleSheets());
channel.on(FORCE_REMOUNT, () => rewriteStyleSheets());
channel.on(DOCS_RENDERED, () => rewriteStyleSheets());
Element.prototype.attachShadow && (Element.prototype._attachShadow = Element.prototype.attachShadow, Element.prototype.attachShadow = function(init) {
let shadowRoot = this._attachShadow({ ...init, mode: "open" });
return requestAnimationFrame(() => {
rewriteStyleSheets(shadowRoot), shadowHosts.has(shadowRoot.host) && updateShadowHost(shadowRoot.host);
}), shadowRoot;
});
// src/preview.ts
var decorators = [withPseudoState], initialGlobals = {
[PARAM_KEY]: !1
};
export {
decorators,
initialGlobals,
preview_exports
};