UNPKG

@dom-native/ui

Version:

Minimalist, stylable, and extensible DOM UI base components based dom-native

192 lines (164 loc) 5.41 kB
import { elem, getAttr, html, on, setAttr } from 'dom-native'; import { BaseFieldElement } from './d-base-field.js'; import { svgSymbolEl } from './d-ico-symbol.js'; const SHADOW_CONTENT = html` <slot name="icon-lead"></slot> <slot name="icon-trail"></slot> <slot name="label"></slot> <slot name="label-trail"></slot> <slot name="text-trail"></slot> <div class="box" part="box"></div> `; /** * Base field element for d-input and d-text (text area) * * Attributes: * - See BaseFieldElement. * - `password?`: set input as password * * Properties: * - See BaseFieldElement. * - `password: boolean`: reflective of attribute. * * CSS: * - See BaseFieldElement. * * Content: * - none * * Events: * - `CHANGE` see BaseFieldElement. * */ export abstract class BaseInputElement extends BaseFieldElement { // --- Component Key Children (on demand for more DOM mutation resiliency) protected ctrlEl!: HTMLElement | HTMLInputElement | HTMLTextAreaElement; protected labelEl!: HTMLElement | null; protected labelTrailEl?: HTMLElement; // --- Input value are always text abstract get value(): string | null; abstract set value(val: string | null) constructor() { super(); // Base shadow content const content = SHADOW_CONTENT.cloneNode(true); // -- Add the input control to the shadow dom const ctrlEl = this.createCtrlEl(); setAttr(ctrlEl, { part: 'ctrl' }); ctrlEl.classList.add('ctrl'); // Carry over the attributes let [spellcheck, autocorrect, autocomplete] = getAttr(ctrlEl, "spellcheck", "autocorrect", "autocomplete") setAttr(ctrlEl, { spellcheck, autocorrect, autocomplete }); this.ctrlEl = ctrlEl; content.appendChild(this.ctrlEl); // Forward d-input attributes to HTMLInputElement ctrl element const [readonly, disabled, placeholder] = getAttr(this, 'readonly', 'disabled', 'placeholder'); if (isValueElement(this.ctrlEl)) { setAttr(this.ctrlEl, { readonly, disabled, placeholder }); } // Build the shadow and append const shadow = this.attachShadow({ mode: 'open' }); shadow.append(content); } //#region ---------- Lifecycle ---------- // Component initialization (will be called once by BaseHTMLElement on first connectedCallback) init() { super.init(); // --- Add icon-lead if needed const icoLead = this.iconLead; if (icoLead) { this.classList.add("has-icon-lead"); this.appendChild(svgSymbolEl(icoLead, { slot: "icon-lead" })); } // --- Add the icon-trail if needed const icoTrail = this.iconTrail; if (icoTrail) { this.classList.add("has-icon-trail"); this.appendChild(svgSymbolEl(icoTrail, { slot: "icon-trail" })); } const [label, labelTrail, textTrail] = getAttr(this, 'label', 'label-trail', 'text-trail'); // add the label-trail if (labelTrail) { this.appendChild(elem('label', { slot: 'label-trail', $: { textContent: labelTrail } })); } // add the label if (label) { this.appendChild(elem('label', { slot: 'label', $: { textContent: label } })); } // add the text-trail if (textTrail) { this.classList.add("has-text-trail"); this.appendChild(elem('div', { slot: 'text-trail', $: { textContent: textTrail } })); } // --- Set the states const value = this.getInitialValue(); this.noValue = (!value); this.value = value; // --- Bind internal component events on(this.ctrlEl, 'keyup, change, focusin, focusout', (evt) => { const m_input = this; switch (evt.type) { case 'keyup': // if exec, triggerCancel if (evt.key === 'Escape') { m_input.triggerCancel(); } break; case 'focusin': this.dFocus = true; break; case 'focusout': this.dFocus = false; break; case 'change': // here we forward the value from the input to this component state value to make srue all get changed. if (isValueElement(this.ctrlEl)) { this.value = this.ctrlEl.value; } else { this.value = this.textContent; } break; } }); // TODO: minor bug when user re-click on label when input is empty, it toggle focus off. on(this.shadowRoot, 'click', 'label', (evt) => { this.ctrlEl.focus(); }); } attributeChangedCallback(name: string, oldVal: any, newVal: any) { super.attributeChangedCallback(name, oldVal, newVal); // always if (this.initialized) { switch (name) { case 'readonly': if (isValueElement(this.ctrlEl)) { setAttr(this.ctrlEl, { readonly: newVal }); } break; case 'disabled': if (isValueElement(this.ctrlEl)) { setAttr(this.ctrlEl, { disabled: newVal }); } break; case 'placeholder': if (isValueElement(this.ctrlEl)) { setAttr(this.ctrlEl, { placeholder: newVal }); } else { // TODO - check if this.value = this.value; } break; } } } //#endregion ---------- /Lifecycle ---------- //#region ---------- HTML Element Overrides ---------- focus() { this.ctrlEl?.focus(); } //#endregion ---------- /HTML Element Overrides ---------- abstract createCtrlEl(): HTMLElement | HTMLInputElement | HTMLTextAreaElement abstract getInitialValue(): string | null } export function isValueElement(obj: HTMLElement | HTMLInputElement | HTMLTextAreaElement): obj is HTMLInputElement | HTMLTextAreaElement { return (obj instanceof HTMLInputElement || obj instanceof HTMLTextAreaElement); }