Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor anchor resolution logic out of parsing #224

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 32 additions & 126 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import * as csstree from 'css-tree';
import { nanoid } from 'nanoid/non-secure';

import {
AnchorScopeValue,
getCSSPropertyValue,
type PseudoElement,
type Selector,
} from './dom.js';
import { type Selector } from './dom.js';
import {
type DeclarationWithValue,
generateCSS,
Expand All @@ -15,15 +10,14 @@ import {
isDeclaration,
type StyleData,
} from './utils.js';
import { validatedForPositioning } from './validate.js';

interface AtRuleRaw extends csstree.Atrule {
prelude: csstree.Raw | null;
}

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

export type InsetProperty =
| 'top'
Expand Down Expand Up @@ -128,9 +122,18 @@ const ANCHOR_SIZES: AnchorSize[] = [
'self-inline',
];

/**
* Parsed anchor data to be used by the rest of the polyfill.
*/
export interface ParsedAnchorData {
anchorFunctions: AnchorFunctionDeclarations;
tryBlocks: TryBlocks;
tryBlockTargets: FallbackTargets;
anchorNames: AnchorSelectors;
anchorScopes: AnchorSelectors;
}

export interface AnchorFunction {
targetEl?: HTMLElement | null;
anchorEl?: HTMLElement | PseudoElement | null;
anchorName?: string;
anchorSide?: AnchorSide;
anchorSize?: AnchorSize;
Expand All @@ -147,16 +150,10 @@ export type AnchorFunctionDeclaration = Partial<

// `key` is the target element selector
// `value` is an object with all anchor-function declarations on that element
type AnchorFunctionDeclarations = Record<string, AnchorFunctionDeclaration>;

interface AnchorPosition {
declarations?: AnchorFunctionDeclaration;
fallbacks?: TryBlock[];
}

// `key` is the target element selector
// `value` is an object with all anchor-positioning data for that element
export type AnchorPositions = Record<string, AnchorPosition>;
export type AnchorFunctionDeclarations = Record<
string,
AnchorFunctionDeclaration
>;

export interface TryBlock {
uuid: string;
Expand All @@ -169,7 +166,9 @@ export interface TryBlock {

// `key` is the `@try` block uuid
// `value` is the target element selector
type FallbackTargets = Record<string, string>;
export type FallbackTargets = Record<string, string>;

export type TryBlocks = Record<string, TryBlock[]>;

type Fallbacks = Record<
// `key` is the `position-fallback` value (name)
Expand Down Expand Up @@ -461,55 +460,11 @@ function getPositionFallbackRules(node: csstree.Atrule) {
return {};
}

async function getAnchorEl(
targetEl: HTMLElement | null,
anchorObj: AnchorFunction,
) {
let anchorName = anchorObj.anchorName;
const customPropName = anchorObj.customPropName;
if (targetEl && !anchorName) {
const anchorAttr = targetEl.getAttribute('anchor');
const positionAnchorProperty = getCSSPropertyValue(
targetEl,
'position-anchor',
);

if (positionAnchorProperty) {
anchorName = positionAnchorProperty;
} else if (customPropName) {
anchorName = getCSSPropertyValue(targetEl, customPropName);
} else if (anchorAttr) {
const elementPart = `#${CSS.escape(anchorAttr)}`;

return await validatedForPositioning(
targetEl,
null,
[{ selector: elementPart, elementPart }],
[],
);
}
}
const anchorSelectors = anchorName ? anchorNames[anchorName] || [] : [];
const allScopeSelectors = anchorName
? anchorScopes[AnchorScopeValue.All] || []
: [];
const anchorNameScopeSelectors = anchorName
? anchorScopes[anchorName] || []
: [];
return await validatedForPositioning(
targetEl,
anchorName || null,
anchorSelectors,
[...allScopeSelectors, ...anchorNameScopeSelectors],
);
}

export async function parseCSS(styleData: StyleData[]) {
export function parseCSS(styleData: StyleData[]) {
const anchorFunctions: AnchorFunctionDeclarations = {};
const fallbackTargets: FallbackTargets = {};
const tryBlockTargets: FallbackTargets = {};
const fallbacks: Fallbacks = {};
// Final data merged together under target-element selector key
const validPositions: AnchorPositions = {};
const tryBlocks: TryBlocks = {};
resetStores();

// First, find all uses of `@position-fallback`
Expand Down Expand Up @@ -549,7 +504,7 @@ export async function parseCSS(styleData: StyleData[]) {
const name = getPositionFallbackDeclaration(node);
if (name && selectors.length && fallbacks[name]) {
for (const { selector } of selectors) {
validPositions[selector] = { fallbacks: fallbacks[name].blocks };
tryBlocks[selector] = fallbacks[name].blocks;
if (!fallbacks[name].targets.includes(selector)) {
fallbacks[name].targets.push(selector);
}
Expand Down Expand Up @@ -580,7 +535,7 @@ export async function parseCSS(styleData: StyleData[]) {
},
});
// Store mapping of data-attr to target selector
fallbackTargets[dataAttr] = selectors
tryBlockTargets[dataAttr] = selectors
.map(({ selector }) => selector)
.join(', ');
}
Expand Down Expand Up @@ -965,60 +920,11 @@ export async function parseCSS(styleData: StyleData[]) {
}
}

// Store inline style custom property mappings for each target element
const inlineStyles = new Map<HTMLElement, Record<string, string>>();
// Store any `anchor()` fns
for (const [targetSel, anchorFns] of Object.entries(anchorFunctions)) {
let targets: NodeListOf<HTMLElement>;
if (
targetSel.startsWith('[data-anchor-polyfill=') &&
fallbackTargets[targetSel]
) {
// If we're dealing with a `@position-fallback` `@try` block,
// then the targets are places where that `position-fallback` is used.
targets = document.querySelectorAll(fallbackTargets[targetSel]);
} else {
targets = document.querySelectorAll(targetSel);
}
for (const [targetProperty, anchorObjects] of Object.entries(anchorFns) as [
InsetProperty | SizingProperty,
AnchorFunction[],
][]) {
for (const anchorObj of anchorObjects) {
for (const targetEl of targets) {
// For every target element, find a valid anchor element
const anchorEl = await getAnchorEl(targetEl, anchorObj);
const uuid = `--anchor-${nanoid(12)}`;
// Store new mapping, in case inline styles have changed and will
// be overwritten -- in which case new mappings will be re-added
inlineStyles.set(targetEl, {
...(inlineStyles.get(targetEl) ?? {}),
[anchorObj.uuid]: uuid,
});
// Point original uuid to new uuid
targetEl.setAttribute(
'style',
`${anchorObj.uuid}: var(${uuid}); ${
targetEl.getAttribute('style') ?? ''
}`,
);
// Populate new data for each anchor/target combo
validPositions[targetSel] = {
...validPositions[targetSel],
declarations: {
...validPositions[targetSel]?.declarations,
[targetProperty]: [
...(validPositions[targetSel]?.declarations?.[
targetProperty as InsetProperty
] ?? []),
{ ...anchorObj, anchorEl, targetEl, uuid },
],
},
};
}
}
}
}

return { rules: validPositions, inlineStyles, anchorScopes };
return {
anchorFunctions,
tryBlocks,
tryBlockTargets,
anchorNames,
anchorScopes,
};
}
24 changes: 16 additions & 8 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
import { getCSSPropertyValue } from './dom.js';
import { fetchCSS } from './fetch.js';
import {
type AnchorFunction,
type AnchorFunctionDeclaration,
type AnchorPositions,
type AnchorSide,
type AnchorSize,
type InsetProperty,
Expand All @@ -22,6 +20,11 @@
type SizingProperty,
type TryBlock,
} from './parse.js';
import {

Check warning on line 23 in src/polyfill.ts

View workflow job for this annotation

GitHub Actions / Lint

Imports "ResolvedAnchorFunction" and "ResolvedAnchorPositions" are only used as type
resolveAnchors,
ResolvedAnchorFunction,
ResolvedAnchorPositions,
} from './resolve.js';
import { transformCSS } from './transform.js';

const platformWithCache = { ...platform, _c: new Map() };
Expand Down Expand Up @@ -286,7 +289,7 @@

for (const [property, anchorValues] of Object.entries(declarations) as [
InsetProperty | SizingProperty,
AnchorFunction[],
ResolvedAnchorFunction[],
][]) {
for (const anchorValue of anchorValues) {
const anchor = anchorValue.anchorEl;
Expand Down Expand Up @@ -393,7 +396,10 @@
}
}

async function position(rules: AnchorPositions, useAnimationFrame = false) {
async function position(
rules: ResolvedAnchorPositions,
useAnimationFrame = false,
) {
for (const pos of Object.values(rules)) {
// Handle `anchor()` and `anchor-size()` functions...
await applyAnchorPositions(pos.declarations ?? {}, useAnimationFrame);
Expand Down Expand Up @@ -423,15 +429,17 @@
styleData = await transformCSS(styleData);
}
// parse CSS
const { rules, inlineStyles } = await parseCSS(styleData);
const parsedCSS = parseCSS(styleData);
// resolve anchor elements
const { resolvedAnchors, inlineStyles } = await resolveAnchors(parsedCSS);

if (Object.values(rules).length) {
if (Object.values(resolvedAnchors).length) {
// update source code
await transformCSS(styleData, inlineStyles, true);

// calculate position values
await position(rules, useAnimationFrame);
await position(resolvedAnchors, useAnimationFrame);
}

return rules;
return resolvedAnchors;
}
Loading