import { LightningElement, api } from "lwc"; import cx from "classnames"; import { IconSize, IconSymbol } from "typings/custom"; import { isMac } from "dxUtils/devices"; import { reflectBooleanAttribute, setContainerInnerHtml } from "dxUtils/lwc"; import { isValidEmail } from "./validators"; type ValidatorMap = { [key: string]: { validator: (value: string | null) => boolean; errorMessage: string; }; }; const typeValidatorMap: ValidatorMap = { email: { validator: isValidEmail, errorMessage: "Enter valid email" } }; // @ts-ignore Dark Magic (TM): we have custom getters on aria-keyshortcuts that cause problems for typescript export default class Input extends LightningElement { @api ariaLabel!: string; @api autocomplete: string | null = null; @api clearable: boolean = false; @api iconSize: IconSize = "medium"; @api iconSymbol: IconSymbol | null = null; @api label?: string; @api descriptionMarkup?: string; @api loading: boolean = false; @api missingErrorMessage: string = "Complete this field"; @api formatErrorMessage?: string; @api placeholder!: string; @api readOnly: boolean = false; @api role: string | null = null; @api shortcutKey: string | null = null; @api size: "small" | "large" | "override" = "small"; @api submitLabel?: string | null = null; @api type: string = "text"; @api validityOnBlur = false; @api get value() { return this._value; } set value(value: string | null) { this._value = value || ""; } @api get disabled() { return this._disabled; } set disabled(value: boolean) { this._disabled = value; reflectBooleanAttribute( this as unknown as LightningElement, "disabled", value ); } @api get name() { return this._name; } set name(name: string | undefined) { this._name = name; this.setAttribute("name", name); // reflect to HTML } @api get required(): boolean { return this._required; } set required(value: boolean) { this._required = value; reflectBooleanAttribute( this as unknown as LightningElement, "required", value ); } @api get errorMessage() { return this._errorMessage; } set errorMessage(value) { this._errorMessage = value; } @api focus() { if (this.input) { this.input.focus(); } } @api reportValidity(): boolean { if (this.isRequiredButEmpty) { this._errorMessage = this.missingErrorMessage; return false; } const typeValidator = typeValidatorMap[this.type]; if (typeValidator && !typeValidator.validator(this.value)) { this._errorMessage = this.formatErrorMessage || typeValidator.errorMessage; return false; } this._errorMessage = ""; return true; } @api checkValidity(): boolean { if (this.isRequiredButEmpty) { return false; } const typeValidator = typeValidatorMap[this.type]; if (typeValidator && !typeValidator.validator(this.value)) { return false; } return true; } private _errorMessage: string = ""; private focused: boolean = false; private input: HTMLInputElement | null = null; private _value: string | null = null; private _disabled: boolean = false; private _name?: string; private _required: boolean = false; private _loading: boolean = false; private isMac: boolean = false; private get showClearButton(): boolean { return this.clearable && !!this.value; } private get showShortcutKey(): boolean { return !this.showClearButton && !!this.shortcutKey; } private get ariaKeyShortcuts() { return this.shortcutKey ? `meta+${this.shortcutKey}` : null; } private get inputLoading() { return this.loading || this._loading; } private get inputInvalid() { const typeValidator = typeValidatorMap[this.type]; return ( this.isRequiredButEmpty && typeValidator && !typeValidator.validator(this.value) ); } private get inputDisabled() { return this.inputLoading || this.disabled; } private get className() { return cx("input-group", `size-${this.size}`); } private get descriptionClassName() { return cx("description", !this.descriptionMarkup && "hide"); } private get inputContainerClassName() { return cx( "input-container", this.focused && "state-focused", (this.showClearButton || this.showShortcutKey) && "has-floating-item", !!this.errorMessage && "has-error" ); } private get buttonDisabled() { return this.inputLoading || this.inputInvalid || this.disabled; } private get commandKey() { return this.isMac ? "command" : "control"; } private get shortcutImgSrc() { return `https://developer.salesforce.com/ns-assets/${this.commandKey}.svg`; } private get shortcutImgAlt() { return `${this.commandKey}-${this.shortcutKey} is the search shortcut`; } private get isRequiredButEmpty() { return this.required && (!this.value || !this.value.trim()); } constructor() { super(); this.isMac = isMac(); } connectedCallback() { window.addEventListener("keydown", this.onWindowKeydown); } disconnectedCallback() { window.removeEventListener("keydown", this.onWindowKeydown); } renderedCallback() { if (!this.input) { this.input = this.template.querySelector("input"); } const container = this.template.querySelector(".description"); setContainerInnerHtml(container!, this.descriptionMarkup!); } onClick() { if (this.input) { this.input.focus(); } } onInputFocus() { this.focused = true; } onInputBlur() { this.focused = false; if (this.validityOnBlur) { this.reportValidity(); } } onInputChange(e: InputEvent) { if (!e || !e.target) { return; } this.dispatchChange((e.target).value || ""); } onButtonClick(e: Event) { if (!this.buttonDisabled) { e.stopPropagation(); } this.submit(); } dispatchChange(value: string) { this._value = value; this.dispatchEvent(new CustomEvent("change", { detail: this.value })); } clear(e?: Event) { if (e) { e.stopPropagation(); } this.dispatchChange(""); this.dispatchEvent(new CustomEvent("clear")); if (this.input) { this.input.blur(); } } submit = async () => { if (this.inputInvalid) { return; } this.dispatchEvent(new CustomEvent("submit", { detail: this.value })); }; private onKeyDown(e: KeyboardEvent) { switch (e.key) { case "Escape": this.clear(); break; case "Enter": this.submit(); break; default: } } private onWindowKeydown = (e: KeyboardEvent) => { const shortcutPressed = this.isMac ? e.metaKey : e.ctrlKey; if (this.shortcutKey && shortcutPressed && e.key === this.shortcutKey) { e.preventDefault(); this.focus(); } }; }