|
1 | 1 | import { nanoid } from 'nanoid/non-secure'; |
2 | 2 |
|
3 | | -import { POLYFILLED_STYLE_ATTRIBUTE } from './cascade.js'; |
| 3 | +import { POLYFILLED_STYLE_ATTRIBUTE, SHIFTED_PROPERTIES } from './cascade.js'; |
4 | 4 | import { querySelectorAllRoots } from './dom.js'; |
5 | 5 | import { |
6 | 6 | type AnchorPositioningRoot, |
@@ -63,44 +63,71 @@ async function fetchLinkedStylesheets( |
63 | 63 | return results.filter((loaded) => loaded !== null); |
64 | 64 | } |
65 | 65 |
|
66 | | -const ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY = '[style*="anchor"]'; |
67 | | -const ELEMENTS_WITH_INLINE_POSITION_AREA = '[style*="position-area"]'; |
68 | | -// Searches for all elements with inline style attributes that include `anchor`. |
69 | | -// For each element found, adds a new 'data-has-inline-styles' attribute with a |
70 | | -// random UUID value, and then formats the styles in the same manner as CSS from |
71 | | -// style tags. |
| 66 | +// Inline styles are collected so that `cascadeCSS` can shift their declarations |
| 67 | +// into custom properties, like it does for the rest of the CSS. That has to |
| 68 | +// cover every property the polyfill later reads back through |
| 69 | +// `getCSSPropertyValue` — insets, margins, sizing, padding, self-alignment, |
| 70 | +// `position-area` — and not just the anchor-specific ones: a target can take |
| 71 | +// its `position-area` from a stylesheet while setting its margin inline. |
| 72 | +// `anchor` is matched on its own as well, for `anchor()`/`anchor-size()` values. |
| 73 | +// |
| 74 | +// Matching tests the `style` attribute against a single regex rather than |
| 75 | +// handing `querySelectorAll` one `[style*="..."]` clause per property. Engines |
| 76 | +// do not bucket attribute-substring selectors by attribute presence, so a |
| 77 | +// ~50-clause query runs every substring test against every element in the |
| 78 | +// document; querying `[style]` and filtering here is an order of magnitude |
| 79 | +// faster, and scales with the number of styled elements rather than with the |
| 80 | +// size of the document. |
| 81 | +// |
| 82 | +// Built on first use rather than at module evaluation: `cascade.js` and this |
| 83 | +// module are part of an import cycle, so `SHIFTED_PROPERTIES` is not |
| 84 | +// necessarily initialized yet when this module is evaluated. |
| 85 | +let inlineAnchorStylesRegex: RegExp | undefined; |
| 86 | +/** |
| 87 | + * Checks if the given element has inline styles used by the polyfill, including |
| 88 | + * margin, inset, sizing, padding, self-alignment, `position-area`, and anchor |
| 89 | + * properties. |
| 90 | + * |
| 91 | + * @param el The element to check. |
| 92 | + * @returns True if the element has inline styles used by the polyfill. |
| 93 | + */ |
| 94 | +export function hasInlineAnchorStyles(el: HTMLElement) { |
| 95 | + if (!inlineAnchorStylesRegex) { |
| 96 | + // While there is overlap in the terms (`margin` and `margin-block-start`), |
| 97 | + // reducing the list to only the shortest distinct terms doesn't |
| 98 | + // significantly improve performance. |
| 99 | + const terms = ['anchor', ...Object.keys(SHIFTED_PROPERTIES)]; |
| 100 | + // Match at a declaration boundary, so a term appearing in a *value* does |
| 101 | + // not count: `float: left` and `line-height: 1.5` are not styles we read. |
| 102 | + inlineAnchorStylesRegex = new RegExp( |
| 103 | + `(?:^|;)\\s*(?:${terms.join('|')})`, |
| 104 | + 'i', |
| 105 | + ); |
| 106 | + } |
| 107 | + return inlineAnchorStylesRegex.test(el.getAttribute('style') ?? ''); |
| 108 | +} |
| 109 | +// Searches for all elements with inline style attributes that contain |
| 110 | +// declarations used by the polyfill. For each element found, adds a new |
| 111 | +// 'data-has-inline-styles' attribute with a random UUID value, and then formats |
| 112 | +// the styles in the same manner as CSS from style tags. |
72 | 113 | function fetchInlineStyles(elements?: HTMLElement[]) { |
73 | | - const elementsWithInlineAnchorStyles: HTMLElement[] = elements |
74 | | - ? elements.filter( |
75 | | - (el) => |
76 | | - el instanceof HTMLElement && |
77 | | - (el.matches(ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY) || |
78 | | - el.matches(ELEMENTS_WITH_INLINE_POSITION_AREA)), |
79 | | - ) |
80 | | - : Array.from( |
81 | | - document.querySelectorAll( |
82 | | - [ |
83 | | - ELEMENTS_WITH_INLINE_ANCHOR_STYLES_QUERY, |
84 | | - ELEMENTS_WITH_INLINE_POSITION_AREA, |
85 | | - ].join(','), |
86 | | - ), |
87 | | - ); |
| 114 | + const elementsWithInlineAnchorStyles: HTMLElement[] = ( |
| 115 | + elements ?? Array.from(document.querySelectorAll<HTMLElement>('[style]')) |
| 116 | + ).filter((el) => el instanceof HTMLElement && hasInlineAnchorStyles(el)); |
88 | 117 | const inlineStyles: Partial<StyleData>[] = []; |
89 | 118 |
|
90 | | - elementsWithInlineAnchorStyles |
91 | | - .filter((el) => el instanceof HTMLElement) |
92 | | - .forEach((el) => { |
93 | | - const dataAttribute = 'data-has-inline-styles'; |
94 | | - // Reuse an existing id rather than minting a new one each run: a |
95 | | - // concurrent run (e.g. another shadow root being polyfilled) may already |
96 | | - // be relying on this element's id in an anchor selector, and re-stamping |
97 | | - // it would invalidate that selector. |
98 | | - const selector = el.getAttribute(dataAttribute) ?? nanoid(12); |
99 | | - el.setAttribute(dataAttribute, selector); |
100 | | - const styles = el.getAttribute('style'); |
101 | | - const css = `[${dataAttribute}="${selector}"] { ${styles} }`; |
102 | | - inlineStyles.push({ el, css }); |
103 | | - }); |
| 119 | + elementsWithInlineAnchorStyles.forEach((el) => { |
| 120 | + const dataAttribute = 'data-has-inline-styles'; |
| 121 | + // Reuse an existing id rather than minting a new one each run: a |
| 122 | + // concurrent run (e.g. another shadow root being polyfilled) may already |
| 123 | + // be relying on this element's id in an anchor selector, and re-stamping |
| 124 | + // it would invalidate that selector. |
| 125 | + const selector = el.getAttribute(dataAttribute) ?? nanoid(12); |
| 126 | + el.setAttribute(dataAttribute, selector); |
| 127 | + const styles = el.getAttribute('style'); |
| 128 | + const css = `[${dataAttribute}="${selector}"] { ${styles} }`; |
| 129 | + inlineStyles.push({ el, css }); |
| 130 | + }); |
104 | 131 |
|
105 | 132 | return inlineStyles; |
106 | 133 | } |
|
0 commit comments