203 lines
6.5 KiB
JavaScript
203 lines
6.5 KiB
JavaScript
/**
|
|
* @package @wcj/dark-mode
|
|
* Web Component that toggles dark mode 🌒
|
|
* Github: https://github.com/jaywcjlove/dark-mode.git
|
|
* Website: https://jaywcjlove.github.io/dark-mode
|
|
*
|
|
* Licensed under the MIT license.
|
|
* @license Copyright © 2022. Licensed under the MIT License
|
|
* @author kenny wong <wowohoo@qq.com>
|
|
*/
|
|
const doc = document;
|
|
const LOCAL_NANE = '_dark_mode_theme_'
|
|
const PERMANENT = 'permanent';
|
|
const COLOR_SCHEME_CHANGE = 'colorschemechange';
|
|
const PERMANENT_COLOR_SCHEME = 'permanentcolorscheme';
|
|
const LIGHT = 'light';
|
|
const DARK = 'dark';
|
|
|
|
// See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html ↵
|
|
// #reflecting-content-attributes-in-idl-attributes.
|
|
const installStringReflection = (obj, attrName, propName = attrName) => {
|
|
Object.defineProperty(obj, propName, {
|
|
enumerable: true,
|
|
get() {
|
|
const value = this.getAttribute(attrName);
|
|
return value === null ? '' : value;
|
|
},
|
|
set(v) {
|
|
this.setAttribute(attrName, v);
|
|
},
|
|
});
|
|
};
|
|
|
|
const installBoolReflection = (obj, attrName, propName = attrName) => {
|
|
Object.defineProperty(obj, propName, {
|
|
enumerable: true,
|
|
get() {
|
|
return this.hasAttribute(attrName);
|
|
},
|
|
set(v) {
|
|
if (v) {
|
|
this.setAttribute(attrName, '');
|
|
} else {
|
|
this.removeAttribute(attrName);
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
class DarkMode extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['mode', LIGHT, DARK, PERMANENT];
|
|
}
|
|
LOCAL_NANE = LOCAL_NANE;
|
|
constructor() {
|
|
super();
|
|
this._initializeDOM();
|
|
}
|
|
connectedCallback() {
|
|
installStringReflection(this, 'mode');
|
|
installStringReflection(this, DARK);
|
|
installStringReflection(this, LIGHT);
|
|
installBoolReflection(this, PERMANENT);
|
|
|
|
const rememberedValue = localStorage.getItem(LOCAL_NANE);
|
|
if (rememberedValue && [LIGHT, DARK].includes(rememberedValue)) {
|
|
this.mode = rememberedValue;
|
|
this.permanent = true;
|
|
}
|
|
if (this.permanent && !rememberedValue) {
|
|
localStorage.setItem(LOCAL_NANE, this.mode);
|
|
}
|
|
const hasNativePrefersColorScheme = [LIGHT, DARK].includes(rememberedValue);
|
|
|
|
if (this.permanent && rememberedValue) {
|
|
this._changeThemeTag();
|
|
} else {
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
this.mode = DARK;
|
|
this._changeThemeTag();
|
|
}
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
this.mode = LIGHT;
|
|
this._changeThemeTag();
|
|
}
|
|
}
|
|
if (!this.permanent && !hasNativePrefersColorScheme) {
|
|
window.matchMedia('(prefers-color-scheme: light)').onchange = (event) => {
|
|
this.mode = event.matches ? LIGHT : DARK;
|
|
this._changeThemeTag();
|
|
}
|
|
window.matchMedia('(prefers-color-scheme: dark)').onchange = (event) => {
|
|
this.mode = event.matches ? DARK : LIGHT;
|
|
this._changeThemeTag();
|
|
}
|
|
}
|
|
const observer = new MutationObserver((mutationsList, observer) => {
|
|
this.mode = doc.documentElement.dataset.colorMode;
|
|
if (this.permanent && hasNativePrefersColorScheme) {
|
|
localStorage.setItem(LOCAL_NANE, this.mode);
|
|
this._dispatchEvent(PERMANENT_COLOR_SCHEME, {
|
|
permanent: this.permanent,
|
|
});
|
|
}
|
|
this._changeContent();
|
|
this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode });
|
|
});
|
|
// Start observing the target node with the above configuration
|
|
observer.observe(doc.documentElement, { attributes: true });
|
|
// After that, stop observing
|
|
// observer.disconnect();
|
|
this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode });
|
|
|
|
this._changeContent();
|
|
}
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'mode' && oldValue !== newValue && [LIGHT, DARK].includes(newValue)) {
|
|
const rememberedValue = localStorage.getItem(LOCAL_NANE);
|
|
if (this.mode === rememberedValue) {
|
|
this.mode = newValue;
|
|
this._changeContent();
|
|
this._changeThemeTag();
|
|
} else if (this.mode && this.mode !== rememberedValue) {
|
|
this._changeContent();
|
|
this._changeThemeTag();
|
|
}
|
|
} else if ((name === LIGHT || name === DARK) && oldValue !== newValue) {
|
|
this._changeContent();
|
|
}
|
|
if (name === 'permanent' && typeof this.permanent === 'boolean') {
|
|
this.permanent ? localStorage.setItem(LOCAL_NANE, this.mode) : localStorage.removeItem(LOCAL_NANE);
|
|
}
|
|
}
|
|
_changeThemeTag() {
|
|
doc.documentElement.setAttribute('data-color-mode', this.mode);
|
|
}
|
|
_changeContent() {
|
|
this.icon.textContent = this.mode === LIGHT ? '🌒' : '🌞';
|
|
this.text.textContent = this.mode === LIGHT ? this.getAttribute(DARK) : this.getAttribute(LIGHT);
|
|
}
|
|
_initializeDOM() {
|
|
var shadow = this.attachShadow({ mode: 'open' });
|
|
this.label = doc.createElement('span');
|
|
this.label.setAttribute('class', 'wrapper');
|
|
this.label.onclick = () => {
|
|
this.mode = this.mode === LIGHT ? DARK : LIGHT;
|
|
if (this.permanent) {
|
|
localStorage.setItem(LOCAL_NANE, this.mode);
|
|
}
|
|
this._changeThemeTag();
|
|
this._changeContent();
|
|
}
|
|
shadow.appendChild(this.label);
|
|
this.icon = doc.createElement('span');
|
|
this.label.appendChild(this.icon);
|
|
|
|
this.text = doc.createElement('span');
|
|
this.label.appendChild(this.text);
|
|
|
|
const textContent = `
|
|
[data-color-mode*='dark'], [data-color-mode*='dark'] body {
|
|
color-scheme: dark;
|
|
--color-theme-bg: #0d1117;
|
|
--color-theme-text: #c9d1d9;
|
|
background-color: var(--color-theme-bg);
|
|
color: var(--color-theme-text);
|
|
}
|
|
|
|
[data-color-mode*='light'], [data-color-mode*='light'] body {
|
|
color-scheme: light;
|
|
--color-theme-bg: #fff;
|
|
--color-theme-text: #24292f;
|
|
background-color: var(--color-theme-bg);
|
|
color: var(--color-theme-text);
|
|
}`;
|
|
|
|
const STYLE_ID = '_dark_mode_style_';
|
|
const styleDom = doc.getElementById(STYLE_ID);
|
|
|
|
if (!styleDom) {
|
|
var initstyle = doc.createElement('style');
|
|
initstyle.id = STYLE_ID;
|
|
initstyle.textContent = textContent;
|
|
doc.head.appendChild(initstyle);
|
|
}
|
|
|
|
var style = doc.createElement('style');
|
|
style.textContent = `
|
|
.wrapper { cursor: pointer; user-select: none; position: relative; }
|
|
.wrapper > span + span { margin-left: .4rem; }
|
|
`;
|
|
shadow.appendChild(style);
|
|
}
|
|
_dispatchEvent(type, value) {
|
|
this.dispatchEvent(new CustomEvent(type, {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: value,
|
|
}));
|
|
}
|
|
}
|
|
|
|
customElements.define('dark-mode', DarkMode); |