refactor: decoupling a11y and blur

This commit is contained in:
ZhenShuo Leo
2025-07-21 23:35:34 +08:00
parent 9f07989148
commit 26637ee585
3 changed files with 198 additions and 199 deletions

149
assets/js/a11y.js Normal file
View File

@@ -0,0 +1,149 @@
const getA11ySettings = () => {
const settings = localStorage.getItem("a11ySettings");
return settings
? JSON.parse(settings)
: {
disableBlur: false,
disableImages: false,
fontSize: "16px", // 1rem
underlineLinks: false,
};
};
const saveA11ySettings = (settings) => {
localStorage.setItem("a11ySettings", JSON.stringify(settings));
};
const applyImageState = (imageElement, imageUrl, disableImages) => {
if (!imageElement) return;
if (disableImages) {
imageElement.style.display = "none";
} else {
imageElement.style.display = "";
if (imageUrl && !imageElement.getAttribute("src")) {
imageElement.setAttribute("src", imageUrl);
}
}
};
const applyFontSize = (fontSizePx) => {
document.documentElement.style.fontSize = fontSizePx;
};
const applyUnderlineLinks = (enabled) => {
let styleElement = document.getElementById("a11y-underline-links");
if (enabled) {
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "a11y-underline-links";
styleElement.textContent = "a { text-decoration: underline !important; }";
document.head.appendChild(styleElement);
}
} else {
if (styleElement) {
styleElement.remove();
}
}
};
const applyA11ySettings = () => {
const settings = getA11ySettings();
document.querySelectorAll("script[data-target-id]").forEach((script) => {
const targetId = script.getAttribute("data-target-id");
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url");
const isMenuBlur = targetId === "menu-blur";
setBackgroundBlur(targetId, scrollDivisor, settings.disableBlur, isMenuBlur);
applyImageState(document.getElementById(imageId), imageUrl, settings.disableImages);
});
applyFontSize(settings.fontSize);
applyUnderlineLinks(settings.underlineLinks);
};
const updateA11ySetting = (key, value) => {
const settings = getA11ySettings();
settings[key] = value;
saveA11ySettings(settings);
applyA11ySettings();
};
window.toggleA11yPanel = function (prefix = "") {
const panel = document.getElementById(`${prefix}a11y-panel`);
const overlay = document.getElementById(`${prefix}a11y-overlay`);
const button = document.getElementById(`${prefix}a11y-toggle`);
if (!panel || !overlay || !button) return;
if (overlay.classList.contains("hidden")) {
overlay.classList.remove("hidden");
panel.classList.remove("hidden");
button.setAttribute("aria-pressed", "true");
button.setAttribute("aria-expanded", "true");
} else {
overlay.classList.add("hidden");
panel.classList.add("hidden");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-expanded", "false");
}
};
window.initA11yPanel = function (prefix = "") {
const settings = getA11ySettings();
const checkboxBlur = document.getElementById(`${prefix}disable-blur`);
const checkboxImages = document.getElementById(`${prefix}disable-images`);
const checkboxUnderline = document.getElementById(`${prefix}underline-links`);
const fontSizeSelect = document.getElementById(`${prefix}font-size-select`);
if (!checkboxBlur || !checkboxImages || !checkboxUnderline || !fontSizeSelect) {
console.warn(`One or more a11y elements not found for prefix: ${prefix}`);
return;
}
checkboxBlur.checked = settings.disableBlur;
checkboxImages.checked = settings.disableImages;
fontSizeSelect.value = settings.fontSize;
checkboxUnderline.checked = settings.underlineLinks;
checkboxBlur.addEventListener("change", (e) => updateA11ySetting("disableBlur", e.target.checked));
checkboxImages.addEventListener("change", (e) => updateA11ySetting("disableImages", e.target.checked));
fontSizeSelect.addEventListener("change", (e) => updateA11ySetting("fontSize", e.target.value));
checkboxUnderline.addEventListener("change", (e) => updateA11ySetting("underlineLinks", e.target.checked));
document.querySelectorAll(`.ios-toggle${prefix ? `[id^="${prefix}"]` : ""}`).forEach((toggle) => {
const checkbox = toggle.querySelector('input[type="checkbox"]');
if (!checkbox) return;
const newToggle = toggle.cloneNode(true);
toggle.parentNode.replaceChild(newToggle, toggle);
newToggle.addEventListener("click", () => {
const newCheckbox = newToggle.querySelector('input[type="checkbox"]');
if (newCheckbox) {
newCheckbox.checked = !newCheckbox.checked;
newCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
}
});
});
const overlay = document.getElementById(`${prefix}a11y-overlay`);
const button = document.getElementById(`${prefix}a11y-toggle`);
if (overlay && button) {
document.addEventListener("click", (e) => {
if (e.target === overlay) {
overlay.classList.add("hidden");
const panel = document.getElementById(`${prefix}a11y-panel`);
if (panel) panel.classList.add("hidden");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-expanded", "false");
}
});
}
};
document.querySelectorAll("script[data-target-id]").forEach((script) => {
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url");
const settings = getA11ySettings();
applyImageState(document.getElementById(imageId), imageUrl, settings.disableImages);
});
document.addEventListener("DOMContentLoaded", () => {
applyA11ySettings();
const allPanels = document.querySelectorAll('[id$="a11y-panel"]');
allPanels.forEach((panel) => {
const prefix = panel.id.replace("a11y-panel", "");
window.initA11yPanel(prefix);
});
});

View File

@@ -1,209 +1,52 @@
(() => {
const scripts = document.querySelectorAll("script[data-target-id]");
const getA11ySettings = () => {
const settings = localStorage.getItem("a11ySettings");
return settings
? JSON.parse(settings)
: {
disableBlur: false,
disableImages: false,
fontSize: "16px",
underlineLinks: false,
};
};
const saveA11ySettings = (settings) => {
localStorage.setItem("a11ySettings", JSON.stringify(settings));
};
const getScrollOpacity = (scrollDivisor) => {
const scrollY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
return scrollY / scrollDivisor;
};
const applyBlurState = (blurElement, scrollDivisor, targetId, imageElement, imageUrl) => {
if (!blurElement) return;
const settings = getA11ySettings();
const isMenuBlur = targetId === "menu-blur";
if (settings.disableBlur) {
blurElement.setAttribute("aria-hidden", "true");
if (!isMenuBlur) {
blurElement.style.display = "none";
blurElement.style.opacity = "0";
} else {
blurElement.style.display = "";
blurElement.style.opacity = getScrollOpacity(scrollDivisor);
}
function setBackgroundBlur(targetId, scrollDivisor = 300, disableBlur = false, isMenuBlur = false) {
if (!targetId) {
console.error("data-target-id is null");
return;
}
const blurElement = document.getElementById(targetId);
if (!blurElement) return;
if (disableBlur) {
blurElement.setAttribute("aria-hidden", "true");
if (!isMenuBlur) {
blurElement.style.display = "none";
blurElement.style.opacity = "0";
} else {
blurElement.style.display = "";
blurElement.style.opacity = getScrollOpacity(scrollDivisor);
blurElement.removeAttribute("aria-hidden");
}
if (imageElement) {
if (settings.disableImages) {
imageElement.style.display = "none";
} else {
imageElement.style.display = "";
if (imageUrl && !imageElement.getAttribute("src")) {
imageElement.setAttribute("src", imageUrl);
}
}
} else {
blurElement.style.display = "";
blurElement.removeAttribute("aria-hidden");
}
const updateBlur = () => {
if (!disableBlur || isMenuBlur) {
const scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
blurElement.style.opacity = scroll / scrollDivisor;
}
};
blurElement.setAttribute("role", "presentation");
blurElement.setAttribute("tabindex", "-1");
window.addEventListener("scroll", updateBlur);
updateBlur();
}
const applyFontSize = (fontSizePx) => {
document.documentElement.style.fontSize = fontSizePx;
};
document.querySelectorAll("script[data-target-id]").forEach((script) => {
const targetId = script.getAttribute("data-target-id");
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
const isMenuBlur = targetId === "menu-blur";
const settings = JSON.parse(localStorage.getItem("a11ySettings") || "{}");
const disableBlur = settings.disableBlur || false;
setBackgroundBlur(targetId, scrollDivisor, disableBlur, isMenuBlur);
});
const applyUnderlineLinks = (enabled) => {
let styleElement = document.getElementById("a11y-underline-links");
if (enabled) {
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "a11y-underline-links";
styleElement.textContent = "a { text-decoration: underline !important; }";
document.head.appendChild(styleElement);
}
} else {
if (styleElement) {
styleElement.remove();
}
}
};
const applyA11ySettings = () => {
const settings = getA11ySettings();
scripts.forEach((script) => {
const targetId = script.getAttribute("data-target-id");
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
// Prevent disableImages FOUC
// Note: I tried putting this in a11y.js but it did not work, and placing it here prevents FOUC
(() => {
const settings = JSON.parse(localStorage.getItem("a11ySettings") || "{}");
if (settings.disableImages) {
document.querySelectorAll("script[data-image-id]").forEach((script) => {
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url");
const blurElement = document.getElementById(targetId);
const imageElement = imageId ? document.getElementById(imageId) : null;
applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl);
const image = imageId && document.getElementById(imageId);
if (image) image.style.display = "none";
});
applyFontSize(settings.fontSize);
applyUnderlineLinks(settings.underlineLinks);
};
const updateA11ySetting = (key, value) => {
const settings = getA11ySettings();
settings[key] = value;
saveA11ySettings(settings);
applyA11ySettings();
};
window.toggleA11yPanel = function (prefix = "") {
const panel = document.getElementById(`${prefix}a11y-panel`);
const overlay = document.getElementById(`${prefix}a11y-overlay`);
const button = document.getElementById(`${prefix}a11y-toggle`);
if (!panel || !overlay || !button) return;
if (overlay.classList.contains("hidden")) {
overlay.classList.remove("hidden");
panel.classList.remove("hidden");
button.setAttribute("aria-pressed", "true");
button.setAttribute("aria-expanded", "true");
} else {
overlay.classList.add("hidden");
panel.classList.add("hidden");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-expanded", "false");
}
};
window.initA11yPanel = function (prefix = "") {
const settings = getA11ySettings();
const checkboxBlur = document.getElementById(`${prefix}disable-blur`);
const checkboxImages = document.getElementById(`${prefix}disable-images`);
const checkboxUnderline = document.getElementById(`${prefix}underline-links`);
const fontSizeSelect = document.getElementById(`${prefix}font-size-select`);
if (!checkboxBlur || !checkboxImages || !checkboxUnderline || !fontSizeSelect) return;
checkboxBlur.checked = settings.disableBlur;
checkboxImages.checked = settings.disableImages;
fontSizeSelect.value = settings.fontSize;
checkboxUnderline.checked = settings.underlineLinks;
checkboxBlur.addEventListener("change", (e) => {
updateA11ySetting("disableBlur", e.target.checked);
});
checkboxImages.addEventListener("change", (e) => {
updateA11ySetting("disableImages", e.target.checked);
});
fontSizeSelect.addEventListener("change", (e) => {
updateA11ySetting("fontSize", e.target.value);
});
checkboxUnderline.addEventListener("change", (e) => {
updateA11ySetting("underlineLinks", e.target.checked);
});
document.querySelectorAll(".ios-toggle").forEach((toggle) => {
const checkbox = toggle.querySelector('input[type="checkbox"]');
if (!checkbox || !checkbox.id.startsWith(prefix)) return;
toggle.addEventListener("click", () => {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event("change"));
});
});
const overlay = document.getElementById(`${prefix}a11y-overlay`);
const button = document.getElementById(`${prefix}a11y-toggle`);
if (overlay && button) {
document.addEventListener("click", (e) => {
if (e.target === overlay) {
overlay.classList.add("hidden");
const panel = document.getElementById(`${prefix}a11y-panel`);
if (panel) panel.classList.add("hidden");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-expanded", "false");
}
});
}
};
scripts.forEach((script) => {
const targetId = script.getAttribute("data-target-id");
const scrollDivisor = Number(script.getAttribute("data-scroll-divisor") || 300);
const imageId = script.getAttribute("data-image-id");
const imageUrl = script.getAttribute("data-image-url");
const blurElement = document.getElementById(targetId);
const imageElement = imageId ? document.getElementById(imageId) : null;
if (blurElement) {
blurElement.setAttribute("role", "presentation");
blurElement.setAttribute("tabindex", "-1");
applyBlurState(blurElement, scrollDivisor, targetId, imageElement, imageUrl);
window.addEventListener("scroll", () => {
const settings = getA11ySettings();
if (!settings.disableBlur || targetId === "menu-blur") {
blurElement.style.opacity = getScrollOpacity(scrollDivisor);
}
});
}
});
document.addEventListener("DOMContentLoaded", () => {
applyA11ySettings();
const allPanels = document.querySelectorAll('[id$="a11y-panel"]');
allPanels.forEach((panel) => {
const prefix = panel.id.replace("a11y-panel", "");
window.initA11yPanel(prefix);
});
});
}
})();

View File

@@ -69,6 +69,13 @@
type="text/javascript"
src="{{ $jsAppearance.RelPermalink }}"
integrity="{{ $jsAppearance.Data.Integrity }}"></script>
{{ if .Site.Params.enableA11y | default false }}
{{ $jsA11y := resources.Get "js/a11y.js" }}
{{ $jsA11y = $jsA11y | resources.Minify | resources.Fingerprint (site.Params.fingerprintAlgorithm | default "sha512") }}
<script
src="{{ $jsA11y.RelPermalink }}"
integrity="{{ $jsA11y.Data.Integrity }}"></script>
{{ end }}
{{ if .Site.Params.enableSearch | default false }}
{{ $jsFuse := resources.Get "lib/fuse/fuse.min.js" }}
{{ $jsSearch := resources.Get "js/search.js" }}