dark-reader-aware-theme-toggle-button
Version:
Dark mode toggle button that detects the Dark Reader extension and steps aside
118 lines (115 loc) • 3.89 kB
JavaScript
// src/is-dark-reader-active.ts
function isDarkReaderActive(doc = document) {
const html = doc.documentElement;
return html.hasAttribute("data-darkreader-scheme") || html.hasAttribute("data-darkreader-mode") || doc.querySelector('meta[name="darkreader"]') != null || doc.getElementById("dark-reader-style") != null;
}
// src/watch-dark-reader.ts
function watchDarkReader(onChange, doc = document) {
const html = doc.documentElement;
const check = () => onChange(isDarkReaderActive(doc));
check();
const htmlObserver = new MutationObserver(check);
htmlObserver.observe(html, {
attributes: true,
attributeFilter: ["data-darkreader-scheme", "data-darkreader-mode"]
});
let headObserver = null;
if (doc.head) {
headObserver = new MutationObserver(check);
headObserver.observe(doc.head, { childList: true, subtree: true });
}
return () => {
htmlObserver.disconnect();
headObserver?.disconnect();
};
}
// src/theme.ts
function getStorage(doc) {
return doc.defaultView?.localStorage ?? null;
}
function prefersDark(doc) {
return doc.defaultView?.matchMedia("(prefers-color-scheme: dark)").matches ?? false;
}
function getTheme(options = {}, doc = document) {
const { storageKey = "theme", respectSystemPreference = false } = options;
const saved = getStorage(doc)?.getItem(storageKey);
if (saved === "dark" || saved === "light") return saved;
if (respectSystemPreference && prefersDark(doc)) return "dark";
return "light";
}
function setTheme(theme, options = {}, doc = document) {
const { storageKey = "theme", attribute = "data-theme", darkValue = "dark" } = options;
const html = doc.documentElement;
if (theme === "dark") {
html.setAttribute(attribute, darkValue);
} else {
html.removeAttribute(attribute);
}
getStorage(doc)?.setItem(storageKey, theme);
}
function getCurrentTheme(options = {}, doc = document) {
const { attribute = "data-theme", darkValue = "dark" } = options;
return doc.documentElement.getAttribute(attribute) === darkValue ? "dark" : "light";
}
function toggleTheme(options = {}, doc = document) {
const next = getCurrentTheme(options, doc) === "dark" ? "light" : "dark";
setTheme(next, options, doc);
return next;
}
function initThemeEarly(options = {}, doc = document) {
const theme = getTheme(options, doc);
setTheme(theme, options, doc);
return theme;
}
// src/mount-theme-toggle-button.ts
function resolveElement(ref, doc) {
return typeof ref === "string" ? doc.querySelector(ref) : ref;
}
function mountThemeToggleButton(options, doc = document) {
const button = resolveElement(options.button, doc);
if (!button) {
throw new Error("mountThemeToggleButton: button element not found");
}
const labelEl = options.label ? resolveElement(options.label, doc) : null;
const labels = options.labels ?? { light: "Dark", dark: "Light" };
const hideWhenDR = options.hideWhenDarkReaderActive !== false;
const updateLabel = (theme2) => {
const text = theme2 === "dark" ? labels.dark : labels.light;
if (labelEl) {
labelEl.textContent = text;
} else {
button.textContent = text;
}
};
const theme = getTheme(options, doc);
setTheme(theme, options, doc);
updateLabel(theme);
options.onThemeChange?.(theme);
const onClick = () => {
const next = toggleTheme(options, doc);
updateLabel(next);
options.onThemeChange?.(next);
};
button.addEventListener("click", onClick);
let unwatchDR;
if (hideWhenDR) {
unwatchDR = watchDarkReader((active) => {
button.style.visibility = active ? "hidden" : "";
}, doc);
}
return () => {
button.removeEventListener("click", onClick);
unwatchDR?.();
};
}
export {
getCurrentTheme,
getTheme,
initThemeEarly,
isDarkReaderActive,
mountThemeToggleButton,
setTheme,
toggleTheme,
watchDarkReader
};
//# sourceMappingURL=index.js.map