UNPKG

@siberiaweb/components

Version:
923 lines (731 loc) 24.6 kB
import CSS from "./CSS"; import Icon from "../icon/Icon"; import MaskInterface from "@siberiaweb/mask-interface/lib/MaskInterface"; import WebComponent from "@siberiaweb/webcomponent/lib/WebComponent"; import "./Input.css"; /** * Поле ввода. */ export default class Input extends WebComponent { /** * Наименование компонента. */ public static readonly COMPONENT_NAME: string = "sw-input"; /** * Автоматическое заполнение. */ public static readonly ATTR_AUTOCOMPLETE: string = "autocomplete"; /** * Поле ввода заполнено автоматически. */ public static readonly ATTR_AUTOFILLED: string = "autofilled"; /** * Автоматическая установка фокуса после загрузки страницы. */ public static readonly ATTR_AUTOFOCUS: string = "autofocus"; /** * Элемент отключен. */ public static readonly ATTR_DISABLED: string = "disabled"; /** * Фокус ввода на элементе. */ public static readonly ATTR_FOCUSED: string = "focused"; /** * Вспомогательный текст. */ public static readonly ATTR_HELPER_TEXT: string = "helper-text"; /** * Метка. */ public static readonly ATTR_LABEL: string = "label"; /** * Максимальная длина вводимого значения. */ public static readonly ATTR_MAX_LENGTH: string = "maxlength"; /** * Уникальное имя элемента формы. */ public static readonly ATTR_NAME: string = "name"; /** * Значение предназначено только для чтения. */ public static readonly ATTR_READ_ONLY: string = "readonly"; /** * Значение обязательно для заполнения. */ public static readonly ATTR_REQUIRED: string = "required"; /** * Проверка правописания. */ public static readonly ATTR_SPELLCHECK: string = "spellcheck"; /** * Индекс последовательности перехода. */ public static readonly ATTR_TAB_INDEX: string = "tabindex"; /** * В поле ввода введен текст или указан заполнитель. */ public static readonly ATTR_TEXT_ENTERED: string = "text-entered"; /** * Значение. */ public static readonly ATTR_VALUE: string = "value"; /** * Значение по умолчанию для автоматического заполнения. */ public static readonly DEFAULT_AUTOCOMPLETE: string = "off"; /** * Значение по умолчанию для максимальной длины вводимого значения. */ public static readonly DEFAULT_MAX_LENGTH: number = 524288; /** * Максимальная длина вводимого значения. */ private _maxLength: number = Input.DEFAULT_MAX_LENGTH; /** * Элемент управления. */ private readonly control: HTMLInputElement; /** * Метка. */ private readonly labelElement: HTMLLabelElement; /** * Встроенный ввод. */ private readonly inlineInput: HTMLDivElement; /** * Контейнер поля ввода. */ private readonly controlContainer: HTMLDivElement; /** * Контейнер значков, располагающихся слева от поля ввода. */ private readonly leadingIconContainer: HTMLDivElement; /** * Контейнер значков, располагающихся справа от поля ввода. */ private readonly trailingIconContainer: HTMLDivElement; /** * Вспомогательный контейнер. */ private readonly helperContainer: HTMLDivElement; /** * Маска. */ private mask: MaskInterface | null = null; /** * Наблюдаемые атрибуты. */ public static get observedAttributes(): string[] { return WebComponent.observedAttributes.concat( [ Input.ATTR_AUTOCOMPLETE, Input.ATTR_AUTOFOCUS, Input.ATTR_DISABLED, Input.ATTR_HELPER_TEXT, Input.ATTR_LABEL, Input.ATTR_MAX_LENGTH, Input.ATTR_NAME, Input.ATTR_READ_ONLY, Input.ATTR_REQUIRED, Input.ATTR_SPELLCHECK, Input.ATTR_TAB_INDEX, Input.ATTR_VALUE ] ); } /** * Создание встроенного ввода. */ private createInlineInput(): HTMLDivElement { let container: HTMLDivElement = document.createElement( "div" ); container.classList.add( CSS.INLINE_INPUT ); return container; } /** * Создание контейнера поля ввода. */ private createControlContainer(): HTMLDivElement { let container: HTMLDivElement = document.createElement( "div" ); container.classList.add( CSS.CONTROL_CONTAINER ); return container; } /** * Создание контейнера значков, располагающихся слева от поля ввода. */ private createLeadingIconContainer(): HTMLDivElement { let container: HTMLDivElement = document.createElement( "div" ); container.classList.add( CSS.ICONS_CONTAINER, CSS.LEADING_ICONS_CONTAINER ); return container; } /** * Создание контейнера значков, располагающихся справа от поля ввода. */ private createTrailingIconContainer(): HTMLDivElement { let container: HTMLDivElement = document.createElement( "div" ); container.classList.add( CSS.ICONS_CONTAINER, CSS.TRAILING_ICONS_CONTAINER ); return container; } /** * Создание вспомогательного контейнера. */ private createHelperContainer(): HTMLDivElement { let container: HTMLDivElement = document.createElement( "div" ); container.classList.add( CSS.HELPER_CONTAINER ); return container; } /** * Создание элемента управления. */ private createControl(): HTMLInputElement { let control: HTMLInputElement = document.createElement( "input" ); control.autocomplete = Input.DEFAULT_AUTOCOMPLETE; control.spellcheck = false; control.type = "text"; control.addEventListener( "focus", (): void => { this.toggleAttribute( Input.ATTR_FOCUSED, true ); } ); control.addEventListener( "blur", (): void => { this.toggleAttribute( Input.ATTR_FOCUSED, false ); } ); let lastKeydown: string | null = null; control.addEventListener( "keydown", ( event: KeyboardEvent ): void => { lastKeydown = event.key; } ); let savedValue: string = control.value; control.addEventListener( "input", (): void => { if ( this.mask !== null ) { let selectionStart: number | null = control.selectionStart; let selectionEnd: number | null = control.selectionEnd; let caretAtEnd: boolean = control.value.length === control.selectionStart; control.value = this.mask.getMaskedValue( control.value ); control.selectionStart = selectionStart; control.selectionEnd = selectionEnd; if ( savedValue === control.value ) { if ( lastKeydown === "Delete" ) { control.selectionStart = selectionStart === null ? null : ++selectionStart; control.selectionEnd = selectionEnd === null ? null : ++selectionEnd; } } else if ( caretAtEnd ) { control.selectionStart = control.value.length; control.selectionEnd = control.value.length; } savedValue = control.value; } this.updateInputState(); } ); control.addEventListener( "animationstart", ( event: AnimationEvent ) => { switch ( event.animationName ) { case CSS.AUTOFILL_START: this.toggleAttribute( Input.ATTR_AUTOFILLED, true ); break; case CSS.AUTOFILL_END: this.toggleAttribute( Input.ATTR_AUTOFILLED, false ); break; } } ); new MutationObserver( (): void => { this.updateInputState(); } ) .observe( control, { attributeFilter: [ "placeholder" ], attributes: true } ); return control; } /** * Создание метки. */ private createLabel(): HTMLLabelElement { return document.createElement( "label" ); } /** * Добавление значка. * * @param container Контейнер значков. * @param icon Значок. * @param first Добавить значок первым. */ private addIcon( container: HTMLDivElement, icon: Icon, first: boolean ): Icon { if ( container.contains( icon ) ) { return icon; } if ( ( container.children.length === 0 ) || !first ) { container.appendChild( icon ); } else { container.insertBefore( icon, container.children[ 0 ] ); } icon.addEventListener( "mousedown", ( event: MouseEvent ) : void => { // Элемент управления не теряет фокус при нажатии на значок. event.preventDefault(); } ); return icon; } /** * Обновление состояния ввода. */ protected updateInputState(): void { if ( this.isEmpty() ) { this.toggleAttribute( Input.ATTR_TEXT_ENTERED, this.control.placeholder.length !== 0 ); } else { this.toggleAttribute( Input.ATTR_TEXT_ENTERED, true ); } } /** * @override */ protected firstConnectedCallback() { super.firstConnectedCallback(); this.classList.add( CSS.INPUT ); } /** * Обработка изменения атрибута "autocomplete". */ protected attrAutocompleteChange(): void { this.control.autocomplete = this.autocomplete; } /** * Обработка изменения атрибута "autofocus". */ protected attrAutofocusChange(): void { if ( this.hasAttribute( Input.ATTR_AUTOFOCUS ) ) { window.requestAnimationFrame( () => { this.control.focus(); } ); } } /** * Обработка изменения атрибута "disabled". */ protected attrDisabledChange(): void { this.control.disabled = this.disabled; } /** * Обработка изменения атрибута "helper-text. */ protected attrHelperTextChange(): void { this.helperContainer.classList.remove( CSS.ERROR_MESSAGE ); this.helperContainer.innerText = this.helperText; } /** * Обработка изменения атрибута "label-text". */ protected attrLabelTextChange(): void { this.labelElement.innerText = this.label; } /** * Обработка изменения атрибута "maxlength". * * @param newValue Значение. */ protected attrMaxLengthChange( newValue: string | null ): void { let value: number = newValue === null ? Input.DEFAULT_MAX_LENGTH : parseInt( newValue ); if ( isNaN( value ) || ( value < 0 ) || ( value > Input.DEFAULT_MAX_LENGTH ) ) { value = Input.DEFAULT_MAX_LENGTH; } this._maxLength = value; this.control.maxLength = this._maxLength; } /** * Обработка изменения атрибута "name". */ protected attrNameChange(): void { this.control.name = this.name; } /** * Обработка изменения атрибута "readonly". */ protected attrReadOnlyChange(): void { this.control.readOnly = this.readOnly; } /** * Обработка изменения атрибута "required". */ protected attrRequiredChange(): void { this.control.required = this.required; } /** * Обработка изменения атрибута "spellcheck". */ protected attrSpellcheckChange(): void { this.control.spellcheck = this.spellcheck; } /** * Признак необходимости пропуска обработки изменения атрибута "tabindex". */ private skipAttrTabIndexChange: boolean = false; /** * Обработка изменения атрибута "tabindex". * * @param newValue Новое значение. */ protected attrTabIndexChange( newValue: string | null ): void { if ( this.skipAttrTabIndexChange ) { this.skipAttrTabIndexChange = false; return; } if ( newValue === null ) { this.control.removeAttribute( "tabindex" ); } else { let value: number = parseInt( newValue ); if ( isNaN( value ) ) { value = 0; } this.control.tabIndex = value; } this.skipAttrTabIndexChange = true; this.tabIndex = -1; } /** * Обработка изменения атрибута "value". * * @param newValue Новое значение. */ protected attrValueChange( newValue: string | null ): void { this.control.value = newValue === null ? "" : newValue; this.updateInputState(); } /** * @override */ protected attributeChangedCallback( name: string, oldValue: string | null, newValue: string | null ) { super.attributeChangedCallback( name, oldValue, newValue ); switch ( name ) { case Input.ATTR_AUTOCOMPLETE: this.attrAutocompleteChange(); break; case Input.ATTR_AUTOFOCUS: this.attrAutofocusChange(); break; case Input.ATTR_DISABLED: this.attrDisabledChange(); break; case Input.ATTR_HELPER_TEXT: this.attrHelperTextChange(); break; case Input.ATTR_LABEL: this.attrLabelTextChange(); break; case Input.ATTR_MAX_LENGTH: this.attrMaxLengthChange( newValue ); break; case Input.ATTR_NAME: this.attrNameChange(); break; case Input.ATTR_READ_ONLY: this.attrReadOnlyChange(); break; case Input.ATTR_REQUIRED: this.attrRequiredChange(); break; case Input.ATTR_SPELLCHECK: this.attrSpellcheckChange(); break; case Input.ATTR_TAB_INDEX: this.attrTabIndexChange( newValue ); break; case Input.ATTR_VALUE: this.attrValueChange( newValue ); break; } } /** * Получение встроенного поля ввода. */ protected getInlineInput(): HTMLDivElement { return this.inlineInput; } /** * Добавление значка слева от поля ввода. * * @param icon Значок. * @param first Добавить значок первым. Опционально. По умолчанию false. */ public addLeadingIcon( icon: Icon, first: boolean = false ): Icon { return this.addIcon( this.leadingIconContainer, icon, first ); } /** * Добавление значка справа от поля ввода. * * @param icon Значок. * @param first Добавить значок первым. Опционально. По умолчанию false. */ public addTrailingIcon( icon: Icon, first: boolean = false ): Icon { return this.addIcon( this.trailingIconContainer, icon, first ); } /** * Вывод сообщения об ошибке. * * @param message Сообщение. * @param hideOnInput Скрыть сообщение при вводе. Опционально. По умолчанию true. */ public showErrorMessage( message: string, hideOnInput: boolean = true ): void { this.helperContainer.classList.add( CSS.ERROR_MESSAGE ); this.helperContainer.innerText = message; if ( hideOnInput ) { this.control.addEventListener( "input", (): void => { this.hideErrorMessage(); }, { once: true } ); } } /** * Скрытие сообщения об ошибке. */ public hideErrorMessage(): void { this.helperText = this.helperText; } /** * Получение элемента управления. */ public getControl(): HTMLInputElement { return this.control; } /** * @override */ public focus( options?: FocusOptions ): void { this.control.focus( options ); } /** * @override */ public blur(): void { this.control.blur(); } /** * Установка маски. * * @param value Значение. */ public setMask( value: MaskInterface ) { this.mask = value; this.control.maxLength = this.mask.getMaxLength(); } /** * Получение чистого значения. * * @return Метод возвращает чистое значение, если для поля ввода установлена маска, иначе просто значение. */ public getCleanValue(): string { return this.mask === null ? this.value : this.mask.getCleanValue( this.value ); } /** * Получение признака автоматического заполнения. */ public get autocomplete(): string { return this.getAttributeOrDefault( Input.ATTR_AUTOCOMPLETE, Input.DEFAULT_AUTOCOMPLETE ); } /** * Установка признака автоматического заполнения. * * @param value Значение. */ public set autocomplete( value: string ) { this.setAttribute( Input.ATTR_AUTOCOMPLETE, value ); } /** * Получение признака, что элемент отключен. */ public get disabled(): boolean { return this.hasAttribute( Input.ATTR_DISABLED ); } /** * Установка признака, что элемент отключен. * * @param value Значение. */ public set disabled( value: boolean ) { this.toggleAttribute( Input.ATTR_DISABLED, value ); } /** * Получение вспомогательного текста. */ public get helperText(): string { return this.getAttributeOrDefault( Input.ATTR_HELPER_TEXT, "" ); } /** * Установка вспомогательного текста. * * @param value Значение. */ public set helperText( value: string ) { this.setAttribute( Input.ATTR_HELPER_TEXT, value ); } /** * Получение метки. */ public get label(): string { return this.getAttributeOrDefault( Input.ATTR_LABEL, "" ); } /** * Установка метки. * * @param value Значение. */ public set label( value: string ) { this.setAttribute( Input.ATTR_LABEL, value ); } /** * Получение максимальной длины вводимого значения. */ public get maxLength(): number { return this._maxLength; } /** * Установка максимальной длины вводимого значения. */ public set maxLength( value: number ) { this.setAttribute( Input.ATTR_MAX_LENGTH, value.toString() ); } /** * Получение уникального имени элемента формы. */ public get name(): string { return this.getAttributeOrDefault( Input.ATTR_NAME, "" ); } /** * Установка уникального имени элемента формы. * * @param value Значение. */ public set name( value: string ) { this.setAttribute( Input.ATTR_NAME, value ); } /** * Получение признака только для чтения. */ public get readOnly(): boolean { return this.hasAttribute( Input.ATTR_READ_ONLY ); } /** * Установка признака только для чтения. * * @param value Значение. */ public set readOnly( value: boolean ) { this.toggleAttribute( Input.ATTR_READ_ONLY, value ); } /** * Получение признака, что значение обязательно для заполнения. */ public get required(): boolean { return this.hasAttribute( Input.ATTR_REQUIRED ); } /** * Установка признака, что значение обязательно для заполнения. * * @param value Значение. */ public set required( value: boolean ) { this.toggleAttribute( Input.ATTR_REQUIRED, value ); } /** * Получение значения. */ public get value(): string { return this.control.value; } /** * Установка значения. * * @param value Значение. */ public set value( value: string ) { this.setAttribute( Input.ATTR_VALUE, value ); } /** * Очистка. */ public clear(): void { this.value = ""; } /** * Проверка, что поле ввода пусто. */ public isEmpty(): boolean { return this.value.length === 0; } /** * Конструктор. */ constructor() { super(); this.control = this.createControl(); this.labelElement = this.createLabel(); this.inlineInput = this.createInlineInput(); this.controlContainer = this.createControlContainer() this.leadingIconContainer = this.createLeadingIconContainer(); this.trailingIconContainer = this.createTrailingIconContainer(); this.helperContainer = this.createHelperContainer(); this.controlContainer.appendChild( this.control ); this.inlineInput.appendChild( this.leadingIconContainer ); this.inlineInput.appendChild( this.labelElement ); this.inlineInput.appendChild( this.controlContainer ); this.inlineInput.appendChild( this.trailingIconContainer ); this.lightDOMFragment.appendChild( this.inlineInput ); this.lightDOMFragment.appendChild( this.helperContainer ); } } Input.define( Icon );