New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@oddbird/css-anchor-positioning

Package Overview
Dependencies
Maintainers
0
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@oddbird/css-anchor-positioning - npm Package Compare versions

Comparing version 0.2.0 to 0.3.0

2

dist/fetch.d.ts
import { type StyleData } from './utils.js';
export declare function isStyleLink(link: HTMLLinkElement): link is HTMLLinkElement;
export declare function fetchCSS(): Promise<StyleData[]>;
export declare function fetchCSS(elements?: HTMLElement[], excludeInlineStyles?: boolean): Promise<StyleData[]>;

@@ -17,2 +17,2 @@ import { type Rect } from '@floating-ui/dom';

export declare const getPixelValue: ({ targetEl, targetProperty, anchorRect, anchorSide, anchorSize, fallback, }: GetPixelValueOpts) => Promise<string>;
export declare function polyfill(animationFrame?: boolean): Promise<AnchorPositions>;
export declare function polyfill(useAnimationFrameOrOption?: boolean | AnchorPositioningPolyfillOptions): Promise<AnchorPositions>;
{
"name": "@oddbird/css-anchor-positioning",
"version": "0.2.0",
"version": "0.3.0",
"description": "Polyfill for the proposed CSS anchor positioning spec",

@@ -81,5 +81,5 @@ "license": "BSD-3-Clause",

"dependencies": {
"@floating-ui/dom": "^1.6.10",
"@floating-ui/dom": "^1.6.11",
"@types/css-tree": "^2.3.8",
"css-tree": "^2.3.1",
"css-tree": "^3.0.0",
"nanoid": "^5.0.7"

@@ -92,28 +92,27 @@ },

"@types/selenium-webdriver": "^4.1.26",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"@vitest/coverage-istanbul": "^2.0.5",
"@vitest/coverage-istanbul": "^2.1.3",
"@vitest/eslint-plugin": "^1.1.7",
"async": "^3.2.6",
"browserslist": "^4.23.3",
"browserslist": "^4.24.0",
"browserstack-local": "^1.5.5",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"fetch-mock": "^11.1.3",
"jsdom": "^25.0.0",
"liquidjs": "^10.16.7",
"fetch-mock": "^11.1.5",
"jsdom": "^25.0.1",
"liquidjs": "^10.17.0",
"node-fetch": "^2.6.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"selenium-webdriver": "^4.24.0",
"stylelint": "^16.9.0",
"selenium-webdriver": "^4.25.0",
"stylelint": "^16.10.0",
"stylelint-config-standard": "^36.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.5.4",
"vite": "^5.4.3",
"vitest": "^2.0.5"
"typescript": "^5.6.3",
"typescript-eslint": "^8.9.0",
"vite": "^5.4.9",
"vitest": "^2.1.3"
},

@@ -120,0 +119,0 @@ "resolutions": {

@@ -45,8 +45,5 @@ # CSS Anchor Positioning Polyfill

The polyfill accepts one argument (type: `boolean`, default: `false`), which
determines whether anchor calculations should [update on every animation
frame](https://floating-ui.com/docs/autoUpdate#animationframe) (e.g. when the
anchor element is animated using `transform`s), in addition to always updating
on scroll/resize. While this option is optimized for performance, it should be
used sparingly.
The polyfill supports a small number of options. When using the default version
of the polyfill that executes automatically, options can be set by setting the
value of `window.ANCHOR_POSITIONING_POLYFILL_OPTIONS`.

@@ -56,5 +53,8 @@ ```js

if (!("anchorName" in document.documentElement.style)) {
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js");
polyfill(true);
window.ANCHOR_POSITIONING_POLYFILL_OPTIONS = {
elements: undefined,
excludeInlineStyles: false,
useAnimationFrame: false,
};
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}

@@ -64,5 +64,4 @@ </script>

When using the default version of the polyfill that executes automatically, this
option can be set by setting the value of
`window.UPDATE_ANCHOR_ON_ANIMATION_FRAME`.
When manually applying the polyfill, options can be set by passing an object as
an argument.

@@ -72,4 +71,9 @@ ```js

if (!("anchorName" in document.documentElement.style)) {
window.UPDATE_ANCHOR_ON_ANIMATION_FRAME = true;
import("https://unpkg.com/@oddbird/css-anchor-positioning");
const { default: polyfill } = await import("https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js");
polyfill({
elements: undefined,
excludeInlineStyles: false,
useAnimationFrame: false,
});
}

@@ -79,2 +83,36 @@ </script>

### elements
type: `HTMLElements[]`, default: `undefined`
If set, the polyfill will only be applied to the specified elements instead of
to all styles. Any specified `<link>` and `<style>` elements will be polyfilled.
By default, all inline styles in the document will also be polyfilled, but if
`excludeInlineStyles` is true, only inline styles on specified elements will be
polyfilled.
### excludeInlineStyles
type: `boolean`, default: `false`
When not defined or set to `false`, the polyfill will be applied to all elements
that have eligible inline styles, regardless of whether the `elements` option is
defined. When set to `true`, elements with eligible inline styles listed in the
`elements` option will still be polyfilled, but no other elements in the
document will be implicitly polyfilled.
### useAnimationFrame
type: `boolean`, default: `false`
Determines whether anchor calculations should [update on every animation
frame](https://floating-ui.com/docs/autoUpdate#animationframe) (e.g. when the
anchor element is animated using `transform`s), in addition to always updating
on scroll/resize. While this option is optimized for performance, it should be
used sparingly.
For legacy support, this option can also be set by setting the value of
`window.UPDATE_ANCHOR_ON_ANIMATION_FRAME`, or, when applying the polyfill
manually, by passing a single boolean with `polyfill(true)`.
## Limitations

@@ -84,3 +122,3 @@

were paused to allow the syntax to solidify. Now that browsers are working on
implementation, we would like to bring it up to date.
implementation, we are in the process of bringing it up to date.

@@ -97,4 +135,4 @@ While this polyfill supports many basic use cases, it doesn't (yet) support the

- a `position-area` as a `try-tactic`
- Fallback does does not support anchor functions that are nested or passed
through custom properties.
- Fallback does not support percentage anchor-side values, nor anchor
functions that are passed through custom properties.
- Polyfill allows anchoring in scroll more permissively than the spec allows,

@@ -101,0 +139,0 @@ for instance without a default `position-anchor`.

export {};
declare global {
interface AnchorPositioningPolyfillOptions {
// Whether to use `requestAnimationFrame()` when updating target elements’
// positions
useAnimationFrame?: boolean;
// An array of explicitly targeted elements to polyfill
elements?: HTMLElement[];
// Whether to exclude elements with eligible inline styles. When not defined
// or set to `false`, the polyfill will be applied to all elements that have
// eligible inline styles, regardless of whether the `elements` option is
// defined. When set to `true`, elements with eligible inline styles listed
// in the `elements` option will still be polyfilled, but no other elements
// in the document will be implicitly polyfilled.
excludeInlineStyles?: boolean;
}
interface Window {
UPDATE_ANCHOR_ON_ANIMATION_FRAME?: boolean;
ANCHOR_POSITIONING_POLYFILL_OPTIONS?: AnchorPositioningPolyfillOptions;
CHECK_LAYOUT_DELAY?: boolean;
}
}

@@ -32,2 +32,4 @@ import * as csstree from 'css-tree';

// https://github.com/import-js/eslint-plugin-import/issues/3019
// eslint-disable-next-line import/namespace
interface AtRuleRaw extends csstree.Atrule {

@@ -425,11 +427,16 @@ prelude: csstree.Raw | null;

// todo: This does not support anchor functions that are nested or passed
// through custom properties.
if (isAnchorFunction(valueAst.children.first)) {
valueAst.children.first.children.forEach((item) => {
if (isIdentifier(item) && isAnchorSide(item.name)) {
item.name = mapAnchorSide(item.name, tactic);
// todo: This does not support percentage anchor-side values, nor anchor
// functions that are passed through custom properties.
csstree.walk(valueAst, {
visit: 'Function',
enter(node) {
if (isAnchorFunction(node)) {
node.children.forEach((item) => {
if (isIdentifier(item) && isAnchorSide(item.name)) {
item.name = mapAnchorSide(item.name, tactic);
}
});
}
});
}
},
});

@@ -436,0 +443,0 @@ if (key === 'position-area') {

@@ -5,2 +5,4 @@ import { nanoid } from 'nanoid/non-secure';

const INVALID_MIME_TYPE_ERROR = 'InvalidMimeType';
export function isStyleLink(link: HTMLLinkElement): link is HTMLLinkElement {

@@ -22,3 +24,3 @@ return Boolean(

): Promise<StyleData[]> {
return Promise.all(
const results = await Promise.all(
sources.map(async (data) => {

@@ -28,10 +30,35 @@ if (!data.url) {

}
// TODO: Add MutationObserver to watch for disabled links being enabled
// https://github.com/oddbird/css-anchor-positioning/issues/246
if ((data.el as HTMLLinkElement | undefined)?.disabled) {
// Do not fetch or parse disabled stylesheets
return null;
}
// fetch css and add to array
const response = await fetch(data.url.toString());
const css = await response.text();
return { ...data, css } as StyleData;
try {
const response = await fetch(data.url.toString());
const type = response.headers.get('content-type');
if (!type?.startsWith('text/css')) {
const error = new Error(
`Error loading ${data.url}: expected content-type "text/css", got "${type}".`,
);
error.name = INVALID_MIME_TYPE_ERROR;
throw error;
}
const css = await response.text();
return { ...data, css } as StyleData;
} catch (error) {
if (error instanceof Error && error.name === INVALID_MIME_TYPE_ERROR) {
// eslint-disable-next-line no-console
console.warn(error);
return null;
}
throw error;
}
}),
);
return results.filter((loaded) => loaded !== null);
}
const ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY = '[style*="anchor"]';
// Searches for all elements with inline style attributes that include `anchor`.

@@ -41,15 +68,24 @@ // For each element found, adds a new 'data-has-inline-styles' attribute with a

// style tags.
function fetchInlineStyles() {
const elementsWithInlineAnchorStyles: NodeListOf<HTMLElement> =
document.querySelectorAll('[style*="anchor"]');
function fetchInlineStyles(elements?: HTMLElement[]) {
const elementsWithInlineAnchorStyles: HTMLElement[] = elements
? elements.filter(
(el) =>
el instanceof HTMLElement &&
el.matches(ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY),
)
: Array.from(
document.querySelectorAll(ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY),
);
const inlineStyles: Partial<StyleData>[] = [];
elementsWithInlineAnchorStyles.forEach((el) => {
const selector = nanoid(12);
const dataAttribute = 'data-has-inline-styles';
el.setAttribute(dataAttribute, selector);
const styles = el.getAttribute('style');
const css = `[${dataAttribute}="${selector}"] { ${styles} }`;
inlineStyles.push({ el, css });
});
elementsWithInlineAnchorStyles
.filter((el) => el instanceof HTMLElement)
.forEach((el) => {
const selector = nanoid(12);
const dataAttribute = 'data-has-inline-styles';
el.setAttribute(dataAttribute, selector);
const styles = el.getAttribute('style');
const css = `[${dataAttribute}="${selector}"] { ${styles} }`;
inlineStyles.push({ el, css });
});

@@ -59,22 +95,29 @@ return inlineStyles;

export async function fetchCSS(): Promise<StyleData[]> {
const elements: NodeListOf<HTMLElement> =
document.querySelectorAll('link, style');
export async function fetchCSS(
elements?: HTMLElement[],
excludeInlineStyles?: boolean,
): Promise<StyleData[]> {
const targetElements: HTMLElement[] =
elements ?? Array.from(document.querySelectorAll('link, style'));
const sources: Partial<StyleData>[] = [];
elements.forEach((el) => {
if (el.tagName.toLowerCase() === 'link') {
const url = getStylesheetUrl(el as HTMLLinkElement);
if (url) {
sources.push({ el, url });
targetElements
.filter((el) => el instanceof HTMLElement)
.forEach((el) => {
if (el.tagName.toLowerCase() === 'link') {
const url = getStylesheetUrl(el as HTMLLinkElement);
if (url) {
sources.push({ el, url });
}
}
}
if (el.tagName.toLowerCase() === 'style') {
sources.push({ el, css: el.innerHTML });
}
});
if (el.tagName.toLowerCase() === 'style') {
sources.push({ el, css: el.innerHTML });
}
});
const inlines = fetchInlineStyles();
const elementsForInlines = excludeInlineStyles ? (elements ?? []) : undefined;
const inlines = fetchInlineStyles(elementsForInlines);
return await fetchLinkedStylesheets([...sources, ...inlines]);
}

@@ -446,9 +446,31 @@ import {

export async function polyfill(animationFrame?: boolean) {
function normalizePolyfillOptions(
useAnimationFrameOrOption: boolean | AnchorPositioningPolyfillOptions = {},
) {
const options =
typeof useAnimationFrameOrOption === 'boolean'
? { useAnimationFrame: useAnimationFrameOrOption }
: useAnimationFrameOrOption;
const useAnimationFrame =
animationFrame === undefined
options.useAnimationFrame === undefined
? Boolean(window.UPDATE_ANCHOR_ON_ANIMATION_FRAME)
: animationFrame;
: options.useAnimationFrame;
if (!Array.isArray(options.elements)) {
options.elements = undefined;
}
return Object.assign(options, { useAnimationFrame });
}
// Support a boolean option for backwards compatibility.
export async function polyfill(
useAnimationFrameOrOption?: boolean | AnchorPositioningPolyfillOptions,
) {
const options = normalizePolyfillOptions(
useAnimationFrameOrOption ?? window.ANCHOR_POSITIONING_POLYFILL_OPTIONS,
);
// fetch CSS from stylesheet and inline style
let styleData = await fetchCSS();
let styleData = await fetchCSS(options.elements, options.excludeInlineStyles);

@@ -468,3 +490,3 @@ // pre parse CSS styles that we need to cascade

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

@@ -471,0 +493,0 @@

import { type StyleData } from './utils.js';
const excludeAttributes = [
'crossorigin',
'href',
'integrity',
'referrerpolicy',
];
export async function transformCSS(

@@ -15,3 +22,3 @@ styleData: StyleData[],

el.innerHTML = css;
} else if (el.tagName.toLowerCase() === 'link') {
} else if (el instanceof HTMLLinkElement) {
// Create new link

@@ -21,11 +28,18 @@ const blob = new Blob([css], { type: 'text/css' });

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
for (const name of el.getAttributeNames()) {
if (!name.startsWith('on') && !excludeAttributes.includes(name)) {
const attr = el.getAttribute(name);
if (attr !== null) {
link.setAttribute(name, attr);
}
}
}
link.setAttribute('href', url);
const promise = new Promise((res) => {
link.onload = res;
});
el.replaceWith(link);
el.insertAdjacentElement('beforebegin', link);
// Wait for new stylesheet to be loaded
await promise;
URL.revokeObjectURL(url);
el.remove();
updatedObject.el = link;

@@ -32,0 +46,0 @@ } else if (el.hasAttribute('data-has-inline-styles')) {

@@ -8,2 +8,4 @@ import * as csstree from 'css-tree';

// https://github.com/import-js/eslint-plugin-import/issues/3019
// eslint-disable-next-line import/namespace
export interface DeclarationWithValue extends csstree.Declaration {

@@ -10,0 +12,0 @@ value: csstree.Value;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc