import type { PropertyValues } from 'lit' import { css, html } from 'lit' import { property, query, queryAssignedNodes, state } from 'lit/decorators.js' import type { DirectiveClass, DirectiveResult } from 'lit/directive.js' import { classMap } from 'lit/directives/class-map.js' import { ifDefined } from 'lit/directives/if-defined.js' import { live } from 'lit/directives/live.js' import { map } from 'lit/directives/map.js' import { when } from 'lit/directives/when.js' import { v4 as uuidv4 } from 'uuid' import { localizeManager, localized } from '../../../dependencies' import { customElementIfNotExist } from '../../../utils/customElementIfNotExist' import { dispatchCustomEvent } from '../../../utils/dispatchCustomEvent' import { redispatchEvent } from '../../../utils/redispatchEvent' import { UsBaseElement } from '../../base/UsBaseElement' import { parseRules } from '../utils/parseRules' import rules from '../utils/rules' import type { IInputProps } from './model/IInputProps' import UsInput_css from './input.pcss' import vars_css from './vars.pcss' // import '../../icon/UsIcon' import '../../tooltip/UsTooltip' import '../messages/validMessage' declare global { interface HTMLElementTagNameMap { 'us-input': UsInput } } @localized() @customElementIfNotExist('us-input') export class UsInput extends UsBaseElement implements IInputProps { static styles = [ UsBaseElement.styles, css([vars_css] as TemplateStringsArray | any), css([UsInput_css] as TemplateStringsArray | any), ] private _validation = '' @property({ type: String }) get validation(): string { return this._validation } set validation(value: string) { this._validation = value if (value) this.parseValidation(value) } @property({ type: Boolean }) error = false @property({ type: String }) placeholder = '' @property({ type: String }) type = 'text' @property({ type: Boolean, reflect: true }) readonly = false @property({ type: Boolean, reflect: true }) disabled = false @property({ type: Boolean }) required = false @property({ type: String }) pattern = '' @property({ type: String }) label = '' @property({ type: String }) value = '' @property({ type: String }) name = '' @property({ type: String }) minValue = '' @property({ type: String }) maxValue = '' @property({ type: String }) hint = '' @property({ type: Boolean }) textArea = false @property({ type: Boolean }) range = false @property({ type: Boolean }) active!: boolean @property({ type: Boolean }) hovered = false @property({ type: Number }) maxLength: number | undefined = undefined @property({ type: Boolean }) search = false @property({ type: Boolean }) showCounter = false @property({ type: String }) icon = '' @property({ type: Boolean }) inbuilt = false @property({ type: Boolean }) showTooltip = false @property({ type: String }) tooltipTitleText = '' @property({ type: String }) tooltipLinkName = '' @property({ type: String }) tooltipLinkHref = '' @property({ type: Number }) textAreaRows = 4 @property({ type: String, attribute: 'data-testid', reflect: true }) dataTestId = 'text_field' @property({ type: String }) widgetView = '' @property({ type: Boolean }) isEyeView = false @state() errorsList: Array = [] @state() isCustomErrorMsgAdded = false @state() isToggleEye = false isFirstUpdated = true usedRules: Array = [] get isValueLengthExceedsMaxLength() { return this.value.length > this.maxLength! } @query('#usInputId') inputElement: HTMLInputElement | undefined @queryAssignedNodes({ slot: 'error', flatten: true }) customErrorMsgEl!: Array inputId = uuidv4() async firstUpdated() { await new Promise(resolve => setTimeout(resolve, 0)) this.validate() this.isFirstUpdated = false } willUpdate(changedProperties: PropertyValues) { if ((changedProperties.has('active') && !this.active)) this.validate() if (changedProperties.has('error') && this.error) this.dispatchErrorEvent() } render() { return html` ${when(this.range, () => this.renderRangeInput(), () => html` ${when(this.type === 'password' && !this.textArea, () => this.renderPasswordInput())} ${when(this.type !== 'password' && !this.textArea, () => this.renderInput())} ${when(this.textArea, () => this.renderTextarea())} `)} ${when(this.showCounter, () => this.renderCounter())} ${this.isErrorMsgShow() ? html` ${map(this.errorsList, error => html`
${error}
`) }` : html``}
${when(this.isHintShow(), () => html` ${this.hint} `)}
` } renderInput() { return html`
${this.renderIcon()}
` } renderPasswordInput() { return html`
${when(this.isEyeView, () => html` `, () => this.renderIcon())}
` } renderRangeInput() { const placeholderFrom = `${this.placeholder} ${localizeManager.input_range_from()}` const placeholderTo = `${this.placeholder} ${localizeManager.input_range_to()}` return html`
${this.renderIcon('minValue')}
${this.renderIcon('maxValue')}
` } renderTextarea() { return html`
${this.renderIcon()}
` } renderIcon(valueName = 'value') { return html` !this.icon ? this.resetSearchValue(valueName, event) : null} > ` } renderCounter() { const counterClasses = classMap({ counter: true, counter_error: this.isValueLengthExceedsMaxLength, }) return html`
${this.value.length} / ${this.maxLength}
` } getLabelClasses(): DirectiveResult { return classMap({ label_hide: !this.label, label_disabled: this.disabled, }) } getInputClasses(): DirectiveResult { return classMap({ input_disabled: this.disabled, input_readonly: this.readonly, input_inbuilt: this.inbuilt, input__error: this.error || this.isValueLengthExceedsMaxLength, input__value_color_black: this.value, }) } getInputContainerClasses(): DirectiveResult { return classMap({ usinput__container: true, usinput__container_readonly: this.readonly, usinput__container_disabled: this.disabled, usinput__container_inbuilt: this.inbuilt, }) } isErrorMsgShow() { return (!this.isFirstUpdated && this.error && !this.isCustomErrorMsgAdded) } isHintShow() { return this.hint && !this.error && !this.isCustomErrorMsgAdded } parseValidation(validation: string) { this.usedRules = parseRules(validation) } onSlotChange() { const text = this.customErrorMsgEl.map((node) => { return node.textContent ? node.textContent : '' }).join('').trim() this.isCustomErrorMsgAdded = !!text this.requestUpdate() } handleChange(event: Event) { this.validate() this.redispatchEvent(event) this.dispatchEventChange(this.error) } handleSearch() { this.dispatchEvent(new CustomEvent('clear')) } handleBlur() { this.validate() } dispatchEventChange(error: boolean) { const isValid = !error const options = { detail: { id: this.inputId, isValid, }, bubbles: true, composed: true, } this.dispatchEvent(new CustomEvent('validHandler', options)) } handleInput(event: InputEvent) { const target = event.target as (HTMLInputElement | HTMLTextAreaElement) this.value = target.value this.redispatchEvent(event) if (this.error) this.validate() if (this.textArea && target instanceof HTMLTextAreaElement) this.error = this.isValueLengthExceedsMaxLength } handleMinInput(event) { const value = event.target.value this.minValue = value this.dispatchRangeInputEvent() } handleMaxInput(event) { const value = event.target.value this.maxValue = value this.dispatchRangeInputEvent() } dispatchRangeInputEvent() { dispatchCustomEvent('input', { min: this.minValue, max: this.maxValue, }, this) } handleChangeRangeMin() { this.validateRangeMin() this.dispatchRangeChangeEvent() } handleChangeRangeMax() { this.validateRangeMax() this.dispatchRangeChangeEvent() } dispatchRangeChangeEvent() { dispatchCustomEvent('change', { min: this.minValue, max: this.maxValue, }, this) } validateRangeMin() { this.validate('minValue') } validateRangeMax() { this.validate('maxValue') } resetSearchValue(valueName, event: MouseEvent) { const target = event.target as HTMLInputElement const inputContainer = target.closest('.usinput__container') as HTMLDivElement const field = this.textArea ? inputContainer.querySelector('textarea') as HTMLTextAreaElement : inputContainer.querySelector('input') as HTMLInputElement this[valueName] = '' this.handleSearch() field?.focus() this.validate() this.dispatchEventChange(this.error) if (this.textArea && field instanceof HTMLTextAreaElement) this.error = this.isValueLengthExceedsMaxLength } togglePasswordView() { this.isToggleEye = !this.isToggleEye } redispatchEvent(event: Event) { redispatchEvent(this, event) } validate(valueName = 'value') { if (this.isCustomErrorMsgAdded || this.active) return const trimmedValue = this[valueName]?.trim() || '' let isValid = true const errorMessages: Array = [] this.usedRules.forEach((rule) => { const [ruleName, modifier = null] = rule.split(':') const ruleValidate = rules[ruleName](trimmedValue, modifier) if (!ruleValidate) { const errorMessage = localizeManager.rulesMessages[ruleName](trimmedValue, modifier) || localizeManager.input_validate() errorMessages.push(errorMessage) } isValid = isValid && ruleValidate }) this.errorsList = [...errorMessages] if (this.isFirstUpdated) this.dispatchEventChange(!isValid) else this.error = !isValid return isValid } dispatchErrorEvent() { dispatchCustomEvent('validation', { hasError: this.error, errorMessages: this.errorsList }, this) } }