@siberiaweb/components
Version:
923 lines (731 loc) • 24.6 kB
text/typescript
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 );