UNPKG

@gez/date-time-kit

Version:

293 lines (267 loc) 9.62 kB
import type { Lang } from '../../i18n'; import { debounce, smallScreenObserver } from '../../utils'; import { scrollbarStyleStr, styleStr } from './css'; type EmitType = Record<string, any>; export type Emit2EventMap<Emit extends EmitType> = { [K in keyof Emit]: CustomEvent<Emit[K]>; }; export type ListenerFn< Emit extends EmitType, K extends keyof Emit | keyof HTMLElementEventMap > = ( this: HTMLElement, ev: K extends keyof Emit ? CustomEvent<Emit[K]> : HTMLElementEventMap[K & keyof HTMLElementEventMap] ) => any; export type EventListenerObj< Emit extends EmitType, K extends keyof Emit | keyof HTMLElementEventMap > = { handleEvent: ListenerFn<Emit, K> }; export type EventListenerOrListenerObj< Emit extends EmitType, K extends keyof Emit | keyof HTMLElementEventMap > = ListenerFn<Emit, K> | EventListenerObj<Emit, K>; type getAttrType<Attr, K extends keyof Attr> = Extract< Attr[K], string > extends never ? string : Extract<Attr[K], string>; // tagName to template element cache const templateCache = new Map<string, HTMLTemplateElement>(); export interface BaseAttrs { /** * The language of the component. * @type `Lang` */ lang?: Lang; } export interface BaseEmits { 'dt-attribute-changed': { name: string; oldValue: string | null; newValue: string | null; }; } if (typeof document === 'object') { try { const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(scrollbarStyleStr); document.adoptedStyleSheets.unshift(styleSheet); } catch { const styleEle = document.createElement('style'); styleEle.innerHTML = scrollbarStyleStr; document.head.prepend(styleEle); } } const HTMLElementBase = (() => { if (typeof HTMLElement === 'function') return HTMLElement; return class {} as typeof HTMLElement; })(); type Elements = HTMLElement | HTMLElement[]; export class UiBase< Attr extends BaseAttrs = BaseAttrs, Emit extends BaseEmits = BaseEmits > extends HTMLElementBase { public static readonly tagName: string = ''; protected static _definePromise: Promise<CustomElementConstructor> | null = null; public static define() { if (this._definePromise) return this._definePromise; if (typeof customElements === 'undefined') { return; } const tagName = this.tagName; if (!tagName) throw new Error('UiBase.define: tagName is not defined.'); this._definePromise = customElements.whenDefined(tagName); customElements.define(tagName, this); return this._definePromise; } // TODO: use override keyword in subclasses static get observedAttributes(): string[] { return ['lang'] satisfies (keyof BaseAttrs)[]; } protected static _style = ''; protected static _template = ''; private get _constructor() { return this.constructor as typeof UiBase; } private _initTemplate() { const { tagName } = this; if (templateCache.has(tagName)) return templateCache.get(tagName)!; const templateEle = document.createElement('template'); templateEle.innerHTML = `<style>${scrollbarStyleStr}${styleStr}${ this._constructor._style }</style>${this._constructor._template}`; templateCache.set(tagName, templateEle); return templateEle; } get _staticEls(): Record<string, Elements> { return Object.create(null); } get _dynamicEls(): Record<string, Elements | undefined> { return Object.create(null); } private _staticElsCache: this['_staticEls'] & this['_dynamicEls']; protected get _els(): this['_staticEls'] & this['_dynamicEls'] { return Object.assign({}, this._staticElsCache, this._dynamicEls); } constructor() { super(); const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = ''; shadowRoot.appendChild(this._initTemplate().content.cloneNode(true)); this._staticElsCache = this._staticEls; } protected _getAttr<K extends keyof Attr>( qualifiedName: K ): getAttrType<Attr, K> | null; protected _getAttr<K extends keyof Attr>( qualifiedName: K, defaultValue: getAttrType<Attr, K> | string ): getAttrType<Attr, K>; protected _getAttr<K extends keyof Attr>( qualifiedName: K, defaultValue?: getAttrType<Attr, K> | string ): getAttrType<Attr, K> | null { const attr = this.getAttribute(qualifiedName as string); return ( attr === null && defaultValue !== void 0 ? defaultValue : attr ) as getAttrType<Attr, K> | null; } protected $<E extends HTMLElement = HTMLElement>( selector: string | TemplateStringsArray, ...args: unknown[] ) { if (typeof selector !== 'string') selector = String.raw(selector, ...args); return [...(this.shadowRoot?.querySelectorAll<E>(selector) || [])]; } protected $0<E extends HTMLElement = HTMLElement>( selector: string | TemplateStringsArray, ...args: unknown[] ): E | undefined { return this.$<E>(selector, ...args)[0]; } private _unbindFnCache: (() => void)[] = []; protected _bindEvt<Ele>( elsOrSelector: Ele | Ele[] | string | TemplateStringsArray, ...strSlot: unknown[] ): Ele extends string | TemplateStringsArray ? HTMLElement['addEventListener'] : Ele extends { addEventListener: infer F } ? F : never { const els = typeof elsOrSelector === 'string' ? this.$(elsOrSelector) : !Array.isArray(elsOrSelector) ? [elsOrSelector] : typeof elsOrSelector[0] === 'string' ? this.$(elsOrSelector as any, ...strSlot) : (elsOrSelector as HTMLElement[]); return ((...args: Parameters<HTMLElement['addEventListener']>) => { els.forEach((el) => { el.addEventListener(...args); this._unbindFnCache.push(() => el.removeEventListener(...args)); }); }) as any; } protected _onAttrChanged( _name: string, _oldVal: string | null, _newVal: string | null ) {} attributeChangedCallback( name: string, oldValue: string | null, newValue: string | null ) { if (oldValue === newValue) return; this._onAttrChanged(name, oldValue, newValue); this.dispatchEvent( 'dt-attribute-changed', { name, oldValue, newValue }, true ); if (name === 'lang') this.$<UiBase>`[dt]`.forEach((ele) => { if (newValue) ele.setAttribute('lang', newValue); else ele.removeAttribute('lang'); }); } /** return `false | void` means not continue */ connectedCallback(): boolean | void { this.setAttribute('dt', ''); smallScreenObserver.observe(this, this._onScreenSizeChanged.bind(this)); return !!this.shadowRoot; } /** return `false | void` means not continue */ disconnectedCallback(): boolean | void { smallScreenObserver.unobserve(this); this._unbindFnCache.forEach((fn) => fn()); this._unbindFnCache = []; return !!this.shadowRoot; } connectedMoveCallback() {} adoptedCallback() {} protected _onScreenSizeChanged(isSmall: boolean) {} protected get _isSmallScreen() { return smallScreenObserver.isSmall; } public dispatchEvent(event: Event): boolean; public dispatchEvent<K extends keyof Emit>( type: K, data: Emit[K], global?: boolean ): boolean; public dispatchEvent(type: string, data?: any, global?: boolean): boolean; dispatchEvent(type: string | Event, data?: any, global = false): boolean { return type instanceof Event ? super.dispatchEvent(type) : super.dispatchEvent( new CustomEvent(type, { ...(global ? { bubbles: true, cancelable: true, composed: true } : {}), detail: data }) ); } protected _stopEvent = (e: Event) => e.stopPropagation(); public addEventListener<K extends keyof Emit | keyof HTMLElementEventMap>( type: K | string, listener: EventListenerOrListenerObj<Emit, K>, options?: boolean | EventListenerOptions ): void { super.addEventListener( type as string, listener as EventListenerOrEventListenerObject, options ); } public removeEventListener< K extends keyof Emit | keyof HTMLElementEventMap >( type: K | string, listener: EventListenerOrListenerObj<Emit, K>, options?: boolean | EventListenerOptions ): void { super.removeEventListener( type as string, listener as EventListenerOrEventListenerObject, options ); } protected _genRenderFn<F extends (...args: any) => void>(fn: F) { return debounce((...args: Parameters<F>) => { if (!this.isConnected) return; fn(...args); }, 0) as F; } }