preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
235 lines (185 loc) • 6.08 kB
text/typescript
/*
* HSThemeSwitch
* @version: 2.6.0
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { IThemeSwitchOptions, IThemeSwitch } from '../theme-switch/interfaces';
import HSBasePlugin from '../base-plugin';
import { ICollectionItem } from '../../interfaces';
class HSThemeSwitch
extends HSBasePlugin<IThemeSwitchOptions>
implements IThemeSwitch
{
public theme: string;
public type: 'change' | 'click';
private themeSet: string[];
private onElementChangeListener: (evt: Event) => void;
private onElementClickListener: () => void;
constructor(
el: HTMLElement | HTMLInputElement,
options?: IThemeSwitchOptions,
) {
super(el, options);
const data = el.getAttribute('data-hs-theme-switch');
const dataOptions: IThemeSwitchOptions = data ? JSON.parse(data) : {};
const concatOptions = {
...dataOptions,
...options,
};
this.theme =
concatOptions?.theme || localStorage.getItem('hs_theme') || 'default';
this.type = concatOptions?.type || 'change';
this.themeSet = ['light', 'dark', 'default'];
this.init();
}
private elementChange(evt: Event) {
const theme = (evt.target as HTMLInputElement).checked ? 'dark' : 'default';
this.setAppearance(theme);
this.toggleObserveSystemTheme();
}
private elementClick(theme: string) {
this.setAppearance(theme);
this.toggleObserveSystemTheme();
}
private init() {
this.createCollection(window.$hsThemeSwitchCollection, this);
if (this.theme !== 'default') this.setAppearance();
if (this.type === 'click') this.buildSwitchTypeOfClick();
else this.buildSwitchTypeOfChange();
}
private buildSwitchTypeOfChange() {
(this.el as HTMLInputElement).checked = this.theme === 'dark';
this.toggleObserveSystemTheme();
this.onElementChangeListener = (evt) => this.elementChange(evt);
this.el.addEventListener('change', this.onElementChangeListener);
}
private buildSwitchTypeOfClick() {
const theme = this.el.getAttribute('data-hs-theme-click-value');
this.toggleObserveSystemTheme();
this.onElementClickListener = () => this.elementClick(theme);
this.el.addEventListener('click', this.onElementClickListener);
}
private setResetStyles() {
const style = document.createElement('style');
style.innerText = `*{transition: unset !important;}`;
style.setAttribute('data-hs-appearance-onload-styles', '');
document.head.appendChild(style);
return style;
}
private addSystemThemeObserver() {
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches }) => {
if (matches) this.setAppearance('dark', false);
else this.setAppearance('default', false);
});
}
private removeSystemThemeObserver() {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener;
}
private toggleObserveSystemTheme() {
if (localStorage.getItem('hs_theme') === 'auto')
this.addSystemThemeObserver();
else this.removeSystemThemeObserver();
}
// Public methods
public setAppearance(
theme = this.theme,
isSaveToLocalStorage = true,
isSetDispatchEvent = true,
) {
const html = document.querySelector('html');
const resetStyles = this.setResetStyles();
if (isSaveToLocalStorage) localStorage.setItem('hs_theme', theme);
if (theme === 'auto')
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
html.classList.remove('light', 'dark', 'default', 'auto');
html.classList.add(theme);
setTimeout(() => resetStyles.remove());
if (isSetDispatchEvent)
window.dispatchEvent(
new CustomEvent('on-hs-appearance-change', { detail: theme }),
);
}
public destroy() {
// Clear listeners
if (this.type === 'change')
this.el.removeEventListener('change', this.onElementChangeListener);
if (this.type === 'click')
this.el.removeEventListener('click', this.onElementClickListener);
window.$hsThemeSwitchCollection = window.$hsThemeSwitchCollection.filter(
({ element }) => element.el !== this.el,
);
}
// Static methods
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsThemeSwitchCollection.find(
(el) =>
el.element.el ===
(typeof target === 'string' ? document.querySelector(target) : target),
);
return elInCollection
? isInstance
? elInCollection
: elInCollection.element.el
: null;
}
static autoInit() {
if (!window.$hsThemeSwitchCollection) window.$hsThemeSwitchCollection = [];
if (window.$hsThemeSwitchCollection)
window.$hsThemeSwitchCollection = window.$hsThemeSwitchCollection.filter(
({ element }) => document.contains(element.el),
);
document
.querySelectorAll('[data-hs-theme-switch]:not(.--prevent-on-load-init)')
.forEach((el: HTMLElement) => {
if (
!window.$hsThemeSwitchCollection.find(
(elC) => (elC?.element?.el as HTMLElement) === el,
)
)
new HSThemeSwitch(el, { type: 'change' });
});
document
.querySelectorAll(
'[data-hs-theme-click-value]:not(.--prevent-on-load-init)',
)
.forEach((el: HTMLElement) => {
if (
!window.$hsThemeSwitchCollection.find(
(elC) => (elC?.element?.el as HTMLElement) === el,
)
)
new HSThemeSwitch(el, { type: 'click' });
});
}
}
declare global {
interface Window {
HSThemeSwitch: Function;
$hsThemeSwitchCollection: ICollectionItem<HSThemeSwitch>[];
}
}
window.addEventListener('load', () => {
HSThemeSwitch.autoInit();
// Uncomment for debug
// console.log('Theme switch collection:', window.$hsThemeSwitchCollection);
});
if (window.$hsThemeSwitchCollection) {
window.addEventListener(
'on-hs-appearance-change',
(evt: Event & { detail: string }) => {
window.$hsThemeSwitchCollection.forEach((el) => {
(el.element.el as HTMLInputElement).checked = evt.detail === 'dark';
});
},
);
}
if (typeof window !== 'undefined') {
window.HSThemeSwitch = HSThemeSwitch;
}
export default HSThemeSwitch;