Skip to content

Commit

Permalink
Use everywhere, clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
ayoreis committed Jul 3, 2024
1 parent cc71cfd commit b6c871c
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 87 deletions.
2 changes: 0 additions & 2 deletions public/anchor-pseudo-element.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#my-anchor-pseudo-element::before {
content: '::before';

margin-right: 1ch;
background-color: white;

anchor-name: --my-anchor-pseudo-element;
}

Expand Down
123 changes: 61 additions & 62 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ interface AtRuleRaw extends csstree.Atrule {
prelude: csstree.Raw | null;
}

export interface AnchorSelector {
export interface Selector {
selector: string;
pseudoElement?: string;
elementPart: string;
pseudoElementPart?: string;
}

// `key` is the `anchor-name` value
// `value` is an array of all element selectors with that anchor name
type AnchorNames = Record<string, AnchorSelector[]>;
type AnchorNames = Record<string, Selector[]>;

export type InsetProperty =
| 'top'
Expand Down Expand Up @@ -352,25 +353,28 @@ function getAnchorNames(node: DeclarationWithValue) {
);
}

function getAnchorSelectors(selectors: csstree.SelectorList) {
return (selectors.children as csstree.List<csstree.Selector>).map(
(selector) => {
let pseudoElement: string | undefined;
function getSelectors(rule: csstree.SelectorList | undefined) {
if (!rule) return [];

return (rule.children as csstree.List<csstree.Selector>)
.map((selector) => {
let pseudoElementPart: string | undefined;

if (selector.children.last?.type === 'PseudoElementSelector') {
selector = csstree.clone(selector) as csstree.Selector;
pseudoElement = (
selector.children.last as csstree.PseudoElementSelector
).name;
pseudoElementPart = generateCSS(selector.children.last!);
selector.children.pop();
}

const elementPart = generateCSS(selector);

return {
selector: generateCSS(selector),
pseudoElement,
};
},
);
selector: elementPart + (pseudoElementPart ?? ''),
elementPart,
pseudoElementPart,
} satisfies Selector;
})
.toArray();
}

let anchorNames: AnchorNames = {};
Expand Down Expand Up @@ -425,11 +429,15 @@ function getAnchorFunctionData(

function getPositionFallbackDeclaration(
node: csstree.Declaration,
rule?: csstree.Raw,
selectorList?: csstree.SelectorList,
) {
if (isFallbackDeclaration(node) && node.value.children.first && rule?.value) {
if (
isFallbackDeclaration(node) &&
node.value.children.first &&
selectorList
) {
const name = getDeclarationValue(node);
return { name, selector: rule.value };
return { name, selector: selectorList };
}
return {};
}
Expand Down Expand Up @@ -485,8 +493,10 @@ async function getAnchorEl(
} else if (customPropName) {
anchorName = getCSSPropertyValue(targetEl, customPropName);
} else if (anchorAttr) {
const elementPart = `#${CSS.escape(anchorAttr)}`;

return await validatedForPositioning(targetEl, [
{ selector: `#${CSS.escape(anchorAttr)}` },
{ selector: elementPart, elementPart },
]);
}
}
Expand Down Expand Up @@ -532,14 +542,22 @@ export async function parseCSS(styleData: StyleData[]) {
csstree.walk(ast, {
visit: 'Declaration',
enter(node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const selectors = getSelectors(rule);

// Parse `position-fallback` declaration
const { name, selector } = getPositionFallbackDeclaration(node, rule);
if (name && selector && fallbacks[name]) {
validPositions[selector] = { fallbacks: fallbacks[name].blocks };
if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
const { name } = getPositionFallbackDeclaration(node);
if (name && selectors.length && fallbacks[name]) {
const selectorList = selectors
.map(({ selector }) => selector)
.join(', ');

for (const { selector } of selectors) {
if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
}
}

// Add each `@try` block, scoped to a unique data-attr
for (const block of fallbacks[name].blocks) {
const dataAttr = `[data-anchor-polyfill="${block.uuid}"]`;
Expand All @@ -565,7 +583,7 @@ export async function parseCSS(styleData: StyleData[]) {
},
});
// Store mapping of data-attr to target selector
fallbackTargets[dataAttr] = selector;
fallbackTargets[dataAttr] = selectorList;
}
changed = true;
}
Expand All @@ -582,19 +600,14 @@ export async function parseCSS(styleData: StyleData[]) {
let changed = false;
const ast = getAST(styleObj.css);
csstree.walk(ast, function (node) {
const selectorList = this.rule?.prelude as
| csstree.SelectorList
| undefined;

const anchorSelectors = selectorList
? getAnchorSelectors(selectorList)
: undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const selectors = getSelectors(rule);

// Parse `anchor-name` declaration
if (isAnchorNameDeclaration(node) && anchorSelectors) {
if (isAnchorNameDeclaration(node) && selectors.length) {
for (const name of getAnchorNames(node)) {
anchorNames[name] ??= [];
anchorNames[name].push(...anchorSelectors);
anchorNames[name].push(...selectors);
}
}

Expand All @@ -604,20 +617,16 @@ export async function parseCSS(styleData: StyleData[]) {
data,
changed: updated,
} = getAnchorFunctionData(node, this.declaration);
if (prop && data && anchorSelectors) {
if (prop && data && selectors.length) {
// This will override earlier declarations
// with the same exact rule selector
// *and* the same exact declaration property:
// (e.g. multiple `top: anchor(...)` declarations
// for the same `.foo {...}` selector)
for (const { selector, pseudoElement } of anchorSelectors) {
const fullSelector = pseudoElement
? `${selector}::${pseudoElement}`
: selector;

for (const { selector } of selectors) {
anchorFunctions[selector] = {
...anchorFunctions[fullSelector],
[prop]: [...(anchorFunctions[fullSelector]?.[prop] ?? []), data],
...anchorFunctions[selector],
[prop]: [...(anchorFunctions[selector]?.[prop] ?? []), data],
};
}
}
Expand Down Expand Up @@ -672,11 +681,11 @@ export async function parseCSS(styleData: StyleData[]) {
csstree.walk(ast, {
visit: 'Function',
enter(node) {
const rule = this.rule?.prelude as csstree.Raw | undefined;
const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const declaration = this.declaration;
const prop = declaration?.property;
if (
rule?.value &&
rule?.children &&
isVarFunction(node) &&
declaration &&
prop &&
Expand Down Expand Up @@ -742,15 +751,12 @@ export async function parseCSS(styleData: StyleData[]) {
csstree.walk(ast, {
visit: 'Function',
enter(node) {
const selectorList = this.rule?.prelude as
| csstree.SelectorList
| undefined;

const rule = this.rule?.prelude as csstree.SelectorList | undefined;
const declaration = this.declaration;
const prop = declaration?.property;

if (
selectorList?.children &&
rule?.children &&
isVarFunction(node) &&
declaration &&
prop &&
Expand Down Expand Up @@ -855,25 +861,18 @@ export async function parseCSS(styleData: StyleData[]) {
// properties, the value will be different each time. So we append
// the property to the uuid, and update the CSS property to point
// to the new uuid...
const anchorSelectors = getAnchorSelectors(selectorList);
const selectors = getSelectors(rule);

for (const anchorFnData of [...anchorFns, ...referencedFns]) {
const data = { ...anchorFnData };
const uuidWithProp = `--anchor-${nanoid(12)}-${prop}`;
const uuid = data.uuid;
data.uuid = uuidWithProp;

for (const { selector, pseudoElement } of anchorSelectors) {
const fullSelector = pseudoElement
? `${selector}::${pseudoElement}`
: selector;

anchorFunctions[fullSelector] = {
...anchorFunctions[fullSelector],
[prop]: [
...(anchorFunctions[fullSelector]?.[prop] ?? []),
data,
],
for (const { selector } of selectors) {
anchorFunctions[selector] = {
...anchorFunctions[selector],
[prop]: [...(anchorFunctions[selector]?.[prop] ?? []), data],
};
}
// Store new name with declaration prop appended,
Expand Down
45 changes: 22 additions & 23 deletions src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import { platform, type VirtualElement } from '@floating-ui/dom';

import { type AnchorSelector, getCSSPropertyValue } from './parse.js';
import { getCSSPropertyValue,type Selector } from './parse.js';

export interface PseudoElement extends VirtualElement {
fakePseudoElement: HTMLElement;
removeFake(): void;
removeFakePseudoElement(): void;
}

function getAnchorsBySelectors(selectors: AnchorSelector[]) {
const result = [];
function getAnchorsBySelectors(selectors: Selector[]) {
const result: (HTMLElement | PseudoElement)[] = [];

for (const { selector, pseudoElement } of selectors) {
if (pseudoElement && !['before', 'after'].includes(pseudoElement)) {
for (const { selector, elementPart, pseudoElementPart } of selectors) {
if (
pseudoElementPart &&
!['::before', '::after'].includes(pseudoElementPart)
) {
continue;
}

const elements = Array.from(
document.querySelectorAll<HTMLElement>(selector),
document.querySelectorAll<HTMLElement>(elementPart),
);

if (!pseudoElement) {
if (!pseudoElementPart) {
result.push(...elements);
continue;
}

for (const element of elements) {
const originalStyles = Array.from(document.adoptedStyleSheets);
const styleSheet = new CSSStyleSheet();
const styles = getComputedStyle(element, `::${pseudoElement}`);
const styles = getComputedStyle(element, pseudoElementPart);
const fakePseudoElement = document.createElement('div');

for (const property of Array.from(styles)) {
Expand All @@ -39,18 +42,16 @@ function getAnchorsBySelectors(selectors: AnchorSelector[]) {

fakePseudoElement.textContent = styles.content.slice(1, -1);

styleSheet.insertRule(
`${selector}::${pseudoElement} { display: none !important; }`,
);
styleSheet.insertRule(`${selector} { display: none !important; }`);
document.adoptedStyleSheets = [...originalStyles, styleSheet];

switch (pseudoElement) {
case 'before': {
switch (pseudoElementPart) {
case '::before': {
element.insertAdjacentElement('afterbegin', fakePseudoElement);
break;
}

case 'after': {
case '::after': {
element.insertAdjacentElement('beforeend', fakePseudoElement);
break;
}
Expand All @@ -63,22 +64,22 @@ function getAnchorsBySelectors(selectors: AnchorSelector[]) {
result.push({
fakePseudoElement,

removeFake() {
removeFakePseudoElement() {
fakePseudoElement.remove();
document.adoptedStyleSheets = originalStyles;
},

getBoundingClientRect() {
// NOTE this only takes into account viewport scroll
// but not any of it's parents, doing that on each scroll event would be expensive
// NOTE this only takes into account viewport scroll and not any of it's parents,
// traversing parents on each scroll event would be expensive
return DOMRect.fromRect({
x: boundingClientRect.x - (window.scrollX - startingScrollX),
y: boundingClientRect.y - (window.scrollY - startingScrollY),
width: boundingClientRect.width,
height: boundingClientRect.height,
});
},
} satisfies PseudoElement);
});
}
}

Expand Down Expand Up @@ -217,8 +218,6 @@ export async function isAcceptableAnchorElement(
}
}

// TODO el is either an element or a part-like pseudo-element.

// el is not in the skipped contents of another element.
{
let currentParent = el.parentElement;
Expand All @@ -245,7 +244,7 @@ export async function isAcceptableAnchorElement(
*/
export async function validatedForPositioning(
targetEl: HTMLElement | null,
anchorSelectors: AnchorSelector[],
anchorSelectors: Selector[],
) {
if (
!(
Expand All @@ -269,7 +268,7 @@ export async function validatedForPositioning(
targetEl,
)
) {
if (isPseudoElement) anchor.removeFake();
if (isPseudoElement) anchor.removeFakePseudoElement();

return anchor;
}
Expand Down

0 comments on commit b6c871c

Please sign in to comment.