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

WIP: Allow unapplying and reapplying polyfill. #123

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
118 changes: 97 additions & 21 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<link rel="stylesheet" href="/anchor-implicit.css" />
<link rel="stylesheet" href="/anchor-update.css" />
<link rel="stylesheet" href="/anchor-absolute.css" />
<link rel="stylesheet" href="/anchor-added.css" />
<style>
#my-anchor-style-tag {
anchor-name: --my-anchor-style-tag;
Expand All @@ -43,37 +44,38 @@
}
</style>
<script type="module">
import polyfill from '/src/index-fn.ts';
import { polyfill, restore } from '/src/index-fn.ts';

const btn = document.getElementById('apply-polyfill');
let applied = false;
window.polyfill = polyfill;
window.restore = restore;

if (!('anchorName' in document.documentElement.style)) {
btn.addEventListener('click', () =>
polyfill().then((rules) => {
btn.innerText = 'Polyfill Applied';
btn.setAttribute('disabled', '');
console.log(rules);
}),
);
btn.addEventListener('click', () => {
btn.setAttribute('disabled', '');
if (applied) {
restore().then(() => {
applied = false;
btn.innerText = 'Apply Polyfill';
btn.removeAttribute('disabled');
});
} else {
polyfill().then((rules) => {
console.log(rules);
applied = true;
btn.innerText = 'Unapply Polyfill';
btn.removeAttribute('disabled');
});
}
});
} else {
btn.innerText = 'No Polyfill Needed';
btn.setAttribute('disabled', '');
console.log(
'anchor-positioning is supported in this browser; polyfill skipped.',
);
}

const updateBtn = document.getElementById('toggle-anchor-width');
const updateAnchor = document.getElementById('my-anchor-update');
updateBtn.addEventListener('click', () => {
if (updateAnchor.getAttribute('data-small')) {
updateAnchor.setAttribute('data-large', true);
updateAnchor.removeAttribute('data-small');
} else {
updateAnchor.setAttribute('data-small', true);
updateAnchor.removeAttribute('data-large');
}
});
</script>
<script src="https://unpkg.com/[email protected]/components/prism-core.min.js"></script>
<script src="https://unpkg.com/[email protected]/plugins/autoloader/prism-autoloader.min.js"></script>
Expand Down Expand Up @@ -682,13 +684,27 @@ <h2>
<section id="anchor-update" class="demo-item">
<h2>
<a href="#anchor-update" aria-hidden="true">🔗</a>
Dynamically update anchors
Dynamically updated anchors
</h2>
<div style="position: relative" class="demo-elements">
<div id="my-anchor-update" class="anchor">Anchor</div>
<div id="my-target-update" class="target">Target</div>
</div>
<button id="toggle-anchor-width">Toggle anchor width</button>
<script>
document
.getElementById('toggle-anchor-width')
.addEventListener('click', () => {
const updateAnchor = document.getElementById('my-anchor-update');
if (updateAnchor.getAttribute('data-small')) {
updateAnchor.setAttribute('data-large', true);
updateAnchor.removeAttribute('data-small');
} else {
updateAnchor.setAttribute('data-small', true);
updateAnchor.removeAttribute('data-large');
}
});
</script>
<p class="note">
With polyfill applied: Target and Anchor's right edges line up. Target's
top edge lines up with the bottom edge of the Anchor.
Expand Down Expand Up @@ -768,6 +784,66 @@ <h2>
position: absolute;
top: anchor(--my-anchor-absolute bottom);
left: anchor(--my-anchor-absolute right);
}</code></pre>
</section>
<section id="anchor-added" class="demo-item">
<h2>
<a href="#anchor-added" aria-hidden="true">🔗</a>
Dynamically added anchors
</h2>
<div
style="position: relative"
class="demo-elements"
id="my-anchor-added-container"
>
<div id="my-anchor-added-original" class="anchor">Original Anchor</div>
<div id="my-target-added" class="target">Target</div>
</div>
<button id="add-anchor">Add anchor</button>
<script>
document.getElementById('add-anchor').addEventListener('click', () => {
const addBtn = document.getElementById('add-anchor');
let addedAnchor = document.getElementById('my-anchor-added-new');
if (addedAnchor) {
addedAnchor.remove();
addBtn.innerText = 'Add anchor';
window.polyfill();
} else {
addedAnchor = document.createElement('div');
addedAnchor.id = 'my-anchor-added-new';
addedAnchor.classList.add('anchor');
addedAnchor.innerText = 'Added Anchor';
document
.getElementById('my-anchor-added-container')
.prepend(addedAnchor);
addBtn.innerText = 'Remove anchor';
window.polyfill();
}
});
</script>
<p class="note">
With polyfill applied: Target and original Anchor's right edges line up.
Target's top edge lines up with the top edge of the original Anchor.
<br />
<br />
When another valid Anchor is added earlier in the DOM order and the
polyfill is re-applied, Target position updates relative to the added
Anchor.
</p>
<pre><code class="language-css"
>#my-anchor-added-new {
anchor-name: --my-anchor-added;
margin-bottom: 1rem;
}

#my-anchor-added-original {
anchor-name: --my-anchor-added;
}

#my-target-added {
position: absolute;
left: anchor(--my-anchor-added right);
top: anchor(--my-anchor-added top);
}</code></pre>
</section>
<section id="sponsor">
Expand Down
14 changes: 14 additions & 0 deletions public/anchor-added.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#my-anchor-added-new {
anchor-name: --my-anchor-added;
margin-bottom: 1rem;
}

#my-anchor-added-original {
anchor-name: --my-anchor-added;
}

#my-target-added {
position: absolute;
left: anchor(--my-anchor-added right);
top: anchor(--my-anchor-added top);
}
2 changes: 1 addition & 1 deletion public/demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ footer p {

/* prettier-ignore */
[href*='://']::after {
content: ' ↗';
content: '\a0↗';
}

nav a:any-link,
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const POLYFILL_ID_ATTR = 'data-anchor-polyfill-id';
export const INLINE_STYLES_ID_ATTR = 'data-anchor-polyfill-inline-styles-id';
export const TARGET_STYLE_ATTR = 'data-anchor-polyfill-target-style';
19 changes: 9 additions & 10 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid/non-secure';

import { INLINE_STYLES_ID_ATTR } from './constants.js';
import { type StyleData } from './utils.js';

export function isStyleLink(link: HTMLLinkElement): link is HTMLLinkElement {
Expand All @@ -26,27 +27,25 @@ async function fetchLinkedStylesheets(
// fetch css and add to array
const response = await fetch(data.url.toString());
const css = await response.text();
return { ...data, css } as StyleData;
return { ...data, css, original: css } as StyleData;
}),
);
}

// Searches for all elements with inline style attributes that include `anchor`.
// For each element found, adds a new 'data-has-inline-styles' attribute with a
// random UUID value, and then formats the styles in the same manner as CSS from
// style tags.
// For each element found, adds a new data attribute with a random UUID value,
// and then formats the styles in the same manner as CSS from style tags.
function fetchInlineStyles() {
const elementsWithInlineAnchorStyles: NodeListOf<HTMLElement> =
document.querySelectorAll('[style*="anchor"]');
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 });
el.setAttribute(INLINE_STYLES_ID_ATTR, selector);
const styles = el.getAttribute('style') as string;
const css = `[${INLINE_STYLES_ID_ATTR}="${selector}"] { ${styles} }`;
inlineStyles.push({ el, css, original: styles });
});

return inlineStyles;
Expand All @@ -65,7 +64,7 @@ export async function fetchCSS(): Promise<StyleData[]> {
}
}
if (el.tagName.toLowerCase() === 'style') {
sources.push({ el, css: el.innerHTML });
sources.push({ el, css: el.innerHTML, original: el.innerHTML });
}
});

Expand Down
4 changes: 3 additions & 1 deletion src/index-fn.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { polyfill } from './polyfill.js';
import { polyfill, restore } from './polyfill.js';

export { polyfill, restore };

export default polyfill;
14 changes: 9 additions & 5 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as csstree from 'css-tree';
import { nanoid } from 'nanoid/non-secure';

import { POLYFILL_ID_ATTR, TARGET_STYLE_ATTR } from './constants.js';
import {
type DeclarationWithValue,
generateCSS,
Expand Down Expand Up @@ -524,7 +525,7 @@ export async function parseCSS(styleData: StyleData[]) {
}
// Add each `@try` block, scoped to a unique data-attr
for (const block of fallbacks[name].blocks) {
const dataAttr = `[data-anchor-polyfill="${block.uuid}"]`;
const dataAttr = `[${POLYFILL_ID_ATTR}="${block.uuid}"]`;
this.stylesheet?.children.prependData({
type: 'Rule',
prelude: {
Expand Down Expand Up @@ -927,7 +928,7 @@ export async function parseCSS(styleData: StyleData[]) {
for (const [targetSel, anchorFns] of Object.entries(anchorFunctions)) {
let targets: NodeListOf<HTMLElement>;
if (
targetSel.startsWith('[data-anchor-polyfill=') &&
targetSel.startsWith(`[${POLYFILL_ID_ATTR}=`) &&
fallbackTargets[targetSel]
) {
// If we're dealing with a `@position-fallback` `@try` block,
Expand All @@ -951,13 +952,16 @@ export async function parseCSS(styleData: StyleData[]) {
...(inlineStyles.get(targetEl) ?? {}),
[anchorObj.uuid]: uuid,
});
const original = targetEl.getAttribute('style') ?? '';
// Point original uuid to new uuid
targetEl.setAttribute(
'style',
`${anchorObj.uuid}: var(${uuid}); ${
targetEl.getAttribute('style') ?? ''
}`,
`${anchorObj.uuid}: var(${uuid}); ${original}`,
);
if (!targetEl.hasAttribute(TARGET_STYLE_ATTR)) {
// Store original styles (for potential later restoration)
targetEl.setAttribute(TARGET_STYLE_ATTR, original);
}
// Populate new data for each anchor/target combo
validPositions[targetSel] = {
...validPositions[targetSel],
Expand Down
Loading
Loading