@gez/date-time-kit
Version:
293 lines (267 loc) • 9.62 kB
text/typescript
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;
}
}