UNPKG

@siberiaweb/components

Version:
1,046 lines (846 loc) 30.4 kB
import { ListPosition } from "./Types"; import CSS from "./CSS"; import DropdownList from "../dropdown-list/DropdownList"; import DropdownListCloseEvent from "../dropdown-list/CloseEvent"; import DropdownListItemClickEvent from "../dropdown-list/ItemClickEvent"; import DropdownListOpenEvent from "../dropdown-list/OpenEvent"; import HandlerTypes from "./HandlerTypes"; import Icon from "../icon/Icon"; import Input from "../input/Input"; import Item from "./Item"; import SelectEvent from "./SelectEvent"; import UnselectEvent from "./UnselectEvent"; import "./Select.css"; /** * Поле выбора. */ export default class Select extends Input { /** * Наименование компонента. */ public static readonly COMPONENT_NAME: string = "sw-select"; /** * Значок для отмены выбора. */ public static readonly ATTR_CLEAR_ICON: string = "clear-icon"; /** * Значок выпадающего списка. */ public static readonly ATTR_DROPDOWN_ICON: string = "dropdown-icon"; /** * Применение фильтра, начиная с первого символа текста позиции. */ public static readonly ATTR_FILTER_BY_FIRST_SYMBOL: string = "filter-by-first-symbol"; /** * Чувствительный к регистру фильтр позиций. */ public static readonly ATTR_FILTER_CASE_SENSITIVE: string = "filter-case-sensitive"; /** * Фильтр позиций отключен. */ public static readonly ATTR_FILTER_DISABLED: string = "filter-disabled"; /** * Размер позиции по вертикали. */ public static readonly ATTR_ITEM_HEIGHT: string = "item-height"; /** * Выпадающий список открыт. */ public static readonly ATTR_DROPDOWN_LIST_OPENED: string = "list-opened"; /** * Выпадающий список располагается сверху поля выбора. */ public static readonly ATTR_DROPDOWN_LIST_ABOVE: string = "list-above"; /** * Выпадающий список располагается снизу поля выбора. */ public static readonly ATTR_DROPDOWN_LIST_BELOW: string = "list-below"; /** * Выпадающий список располагается по центру поля выбора. */ public static readonly ATTR_DROPDOWN_LIST_CENTER: string = "list-center"; /** * Максимальный размер выпадающего списка по вертикали. */ public static readonly ATTR_LIST_MAX_HEIGHT: string = "list-max-height"; /** * Позиция выпадающего списка относительно поля выбора. */ public static readonly ATTR_LIST_POSITION: string = "list-position"; /** * Выпадающий список. */ private readonly dropdownList: DropdownList; /** * Позиции. */ private items: Item[] = []; /** * Позиция для выбора по умолчанию. */ private defaultSelectionItem: Item | null = null; /** * Выбранная позиция. */ private selectedItem: Item | null = null; /** * Значок отмены выбора. */ private readonly clearIcon: Icon; /** * Значок выпадающего списка. */ private readonly dropdownIcon: Icon; /** * Обработчик применения фильтра к позиции. */ public onFilterItem: HandlerTypes.FilterItem | null = null; /** * Наблюдаемые атрибуты. */ public static get observedAttributes(): string[] { return Input.observedAttributes.concat( [ Select.ATTR_CLEAR_ICON, Select.ATTR_DROPDOWN_ICON, Select.ATTR_FILTER_DISABLED, Select.ATTR_ITEM_HEIGHT, Select.ATTR_LIST_MAX_HEIGHT, Select.ATTR_LIST_POSITION ] ); } /** * Создание выпадающего списка. */ private createDropdownList(): DropdownList { let dropdownList: DropdownList = document.createElement( DropdownList.COMPONENT_NAME ) as DropdownList; dropdownList.addEventListener( DropdownListOpenEvent.EVENT_NAME, ( event: Event ): void => { if ( this.disabled ) { event.preventDefault(); return; } this.setListPosition(); this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_OPENED, true ); if ( ( this.selectedItem !== null ) && ( this.selectedItem !== this.defaultSelectionItem ) && ( this.dropdownList.hasItem( this.selectedItem ) ) ) { dropdownList.selectItem( this.selectedItem, "center" ); } else { dropdownList.selectFirstItem(); } } ); dropdownList.addEventListener( DropdownListCloseEvent.EVENT_NAME, (): void => { this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_OPENED, false ); if ( this.selectedItem === null ) { this.getControl().placeholder = ""; this.clear(); } else { if ( this.selectedItem === this.defaultSelectionItem ) { this.getControl().placeholder = this.selectedItem.getText(); this.clear(); } else { this.getControl().placeholder = ""; this.value = this.selectedItem.getText(); } } } ); dropdownList.addEventListener( DropdownListItemClickEvent.EVENT_NAME, ( e: Event ): void => { if ( this.disabled || this.readOnly ) { return; } let event: DropdownListItemClickEvent = e as DropdownListItemClickEvent; if ( this.selectItem( event.getItem() as Item ) ) { dropdownList.close(); } } ); return dropdownList; } /** * Создание значка отмены выбора. */ private createClearIcon(): Icon { let icon: Icon = document.createElement( Icon.COMPONENT_NAME ) as Icon; icon.classList.add( CSS.CLEAR_ICON ); icon.addEventListener( "click", (): void => { if ( this.disabled || this.readOnly ) { return; } this.unselectItem(); this.dropdownList.close(); } ); return icon; } /** * Создание значка выпадающего списка. */ private createDropdownIcon(): Icon { let icon: Icon = document.createElement( Icon.COMPONENT_NAME ) as Icon; icon.classList.add( CSS.DROPDOWN_ICON ); icon.addEventListener( "click", (): void => { if ( this.disabled ) { return; } if ( this.dropdownList.isOpened() ) { this.dropdownList.close(); } else { this.dropdownList.setItems( this.items ); this.dropdownList.open(); } } ); return icon; } /** * Установка позиции выпадающего списка относительно поля выбора. */ private setListPosition(): void { this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_ABOVE, false ); this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_BELOW, false ); this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_CENTER, false ); switch ( this.listPosition ) { case "above": this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_ABOVE, true ); break; case "below": this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_BELOW, true ); break; case "center": this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_CENTER, true ); break; default: let selectRect: DOMRect = this.getBoundingClientRect(); let heightAbove: number = selectRect.top; let heightBelow: number = window.innerHeight - selectRect.bottom; if ( ( heightAbove >= this.dropdownList.maxHeight ) && ( this.dropdownList.maxHeight > heightBelow ) && ( heightBelow < heightAbove ) ) { this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_ABOVE, true ); } else { this.toggleAttribute( Select.ATTR_DROPDOWN_LIST_BELOW, true ); } break; } } /** * Вывод или скрытие значка очистки. */ private checkDisplayClearIcon(): void { if ( !this.displayClearIcon || this.readOnly || ( this.selectedItem === null ) || ( this.selectedItem === this.defaultSelectionItem ) ) { this.clearIcon.remove(); } else { this.addTrailingIcon( this.clearIcon, true ); } } /** * Инициализация элемента управления. */ private initSelectControl(): void { this.getControl().autocomplete = "off"; this.getControl().addEventListener( "blur", (): void => { this.dropdownList.close(); } ); this.getControl().addEventListener( "mousedown", ( event: MouseEvent ) => { if ( ( event.button === 0 ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.close(); } else { this.dropdownList.setItems( this.items ); this.dropdownList.open(); } } } ); this.getControl().addEventListener( "keydown", ( event: KeyboardEvent ): void => { if ( ( event.key === "ArrowDown" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectNextItem(); event.preventDefault(); } } if ( ( event.key === "ArrowDown" ) && event.altKey && !( event.ctrlKey || event.shiftKey ) ) { if ( !this.dropdownList.isOpened() ) { this.dropdownList.setItems( this.items ); this.dropdownList.open(); event.preventDefault(); } } if ( ( event.key === "ArrowUp" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectPrevItem(); event.preventDefault(); } } if ( ( event.key === "ArrowUp" ) && event.altKey && !( event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.close(); event.preventDefault(); } } if ( ( event.key === "PageDown" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectNextPageItem(); event.preventDefault(); } } if ( ( event.key === "PageUp" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectPrevPageItem(); event.preventDefault(); } } if ( ( event.key === "Home" ) && event.ctrlKey && !( event.altKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectFirstItem(); event.preventDefault(); } } if ( ( event.key === "End" ) && event.ctrlKey && !( event.altKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.selectLastItem(); event.preventDefault(); } } if ( ( event.key === "Enter" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.readOnly ) { return; } if ( this.dropdownList.isOpened() ) { let dropdownListSelectedItem: Item | null = this.getDropdownList().getSelectedItem() as Item; if ( ( dropdownListSelectedItem !== null ) && this.selectItem( dropdownListSelectedItem ) ) { this.dropdownList.close(); } event.preventDefault(); } } if ( ( event.key === "Escape" ) && !( event.altKey || event.ctrlKey || event.shiftKey ) ) { if ( this.dropdownList.isOpened() ) { this.dropdownList.close(); event.preventDefault(); } } if ( ( event.key === "Delete" ) && event.ctrlKey && !( event.altKey || event.shiftKey ) ) { if ( this.readOnly ) { return; } this.unselectItem(); event.preventDefault(); } } ); this.getControl().addEventListener( "input", (): void => { this.applyFilter(); this.dropdownList.open(); } ); } /** * Глобальный обработчик изменения фокуса. * * @param event Событие. */ private readonly documentFocusListener = ( event: FocusEvent ): void => { if ( this.dropdownList.isOpened() && ( event.target instanceof Node ) && !this.contains( event.target ) ) { this.dropdownList.close(); } } /** * Глобальный обработчик клика. * * @param event Событие. */ private readonly documentClickListener = ( event: Event ): void => { if ( this.dropdownList.isOpened() && ( event.target instanceof Node ) && !this.contains( event.target ) ) { this.dropdownList.close(); } } /** * @override */ protected firstConnectedCallback() { super.firstConnectedCallback(); this.classList.add( CSS.SELECT ); } /** * @override */ protected connectedCallback() { super.connectedCallback(); document.addEventListener( "focus", this.documentFocusListener, { capture: true } ); document.addEventListener( "click", this.documentClickListener, { capture: true } ); } /** * @override */ protected disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener( "focus", this.documentFocusListener, { capture: true } ); document.removeEventListener( "click", this.documentClickListener, { capture: true } ); } /** * @override */ protected attrAutocompleteChange(): void { super.attrAutocompleteChange(); this.getControl().autocomplete = "off"; } /** * @override */ protected attrReadOnlyChange(): void { super.attrReadOnlyChange(); this.getControl().readOnly = this.readOnly || this.filterDisabled; this.checkDisplayClearIcon(); } /** * Обработка изменения атрибута "clear-icon". */ protected attrClearIconChange(): void { this.checkDisplayClearIcon(); } /** * Обработка изменения атрибута "dropdown-icon". */ protected attrDropdownIconChange(): void { if ( this.displayDropdownIcon ) { this.addTrailingIcon( this.dropdownIcon ); } else { this.dropdownIcon.remove(); } } /** * Обработка изменения атрибута "filter-disabled". */ protected attrFilterDisabledChange(): void { this.getControl().readOnly = this.readOnly || this.filterDisabled; } /** * Обработка изменения атрибута "item-height". * * @param newValue Новое значение. */ protected attrItemHeightChange( newValue: string | null ): void { let value: number = newValue === null ? DropdownList.DEFAULT_ITEM_HEIGHT : parseInt( newValue ); if ( isNaN( value ) ) { value = DropdownList.DEFAULT_ITEM_HEIGHT; } this.dropdownList.itemHeight = value; } /** * Обработка изменения атрибута "list-max-height". * * @param newValue Новое значение. */ protected attrListMaxHeightChange( newValue: string | null ): void { let value: number = newValue === null ? DropdownList.DEFAULT_MAX_HEIGHT : parseInt( newValue ); if ( isNaN( value ) ) { value = DropdownList.DEFAULT_MAX_HEIGHT; } this.dropdownList.maxHeight = value; } /** * Обработка изменения атрибута "list-position". */ protected attrListPositionChange(): void { this.setListPosition(); } /** * @override */ protected attributeChangedCallback( name: string, oldValue: string | null, newValue: string | null ): void { super.attributeChangedCallback( name, oldValue, newValue ); switch ( name ) { case Select.ATTR_CLEAR_ICON: this.attrClearIconChange(); break; case Select.ATTR_DROPDOWN_ICON: this.attrDropdownIconChange(); break; case Select.ATTR_FILTER_DISABLED: this.attrFilterDisabledChange(); break; case Select.ATTR_ITEM_HEIGHT: this.attrItemHeightChange( newValue ); break; case Select.ATTR_LIST_MAX_HEIGHT: this.attrListMaxHeightChange( newValue ); break; case Select.ATTR_LIST_POSITION: this.attrListPositionChange(); break; } } /** * Фильтр позиции. * * @param item Позиция. * @param text Текст. * * @returns Метод возвращает true, если позиция соответствует фильтру и false в противном случае. */ protected filterItem( item: Item, text: string ): boolean { if ( this.onFilterItem !== null ) { return this.onFilterItem( item, text ); } if ( text === "" ) { return true; } let compareText: string = this.filterCaseSensitive ? item.getText() : item.getText().toLowerCase(); let compareFilter: string = this.filterCaseSensitive ? text : text.toLowerCase(); let index = compareText.indexOf( compareFilter ); return this.filterByFirstSymbol ? ( index === 0 ) : ( index !== -1 ); } /** * Выбор позиции. * * @param item Позиция. * * @returns Метод возвращает true, если позиция выбрана и false в противном случае. */ public selectItem( item: Item ): boolean { if ( item !== this.defaultSelectionItem ) { if ( !this.items.includes( item ) || !this.dropdownList.isItemSelectable( item ) ) { return false; } } if ( item === this.selectedItem ) { return true; } if ( !this.dispatchEvent( new SelectEvent( item ) ) ) { return false; } this.selectedItem = item; if ( this.selectedItem === this.defaultSelectionItem ) { this.getControl().placeholder = this.selectedItem.getText(); this.clear(); } else { this.value = this.selectedItem.getText(); } this.checkDisplayClearIcon(); return true; } /** * Выбор позиции по идентификатору. * * @param id Идентификатор. */ public selectItemById( id: any ): void { let item: Item | undefined = this.items.find( ( v: Item ) => { return v.getId() === id; } ); if ( item ) { this.selectItem( item ); } } /** * Отмена выбора позиции. */ public unselectItem(): void { if ( ( this.selectedItem === null ) || ( this.selectedItem === this.defaultSelectionItem ) ) { return; } if ( this.defaultSelectionItem !== null ) { this.selectItem( this.defaultSelectionItem ); } else { this.selectedItem = null; this.getControl().placeholder = ""; this.clear(); this.dispatchEvent( new UnselectEvent() ); } this.checkDisplayClearIcon(); } /** * Установка позиций. * * @param items Позиции. * @param defaultSelectionItem Позиция для выбора по умолчанию. * @param excludeDefaultSelectionItem Исключить позицию для выбора по умолчанию из списка позиций. Опционально. По * умолчанию true. */ public setItems( items: Item[], defaultSelectionItem: Item | null = null, excludeDefaultSelectionItem: boolean = true ): void { this.dropdownList.unselectItem(); this.defaultSelectionItem = null; this.unselectItem(); this.items = Array.from( items ); this.defaultSelectionItem = defaultSelectionItem; if ( this.defaultSelectionItem !== null ) { if ( excludeDefaultSelectionItem ) { let index: number = this.items.indexOf( this.defaultSelectionItem ); if ( index !== -1 ) { this.items.splice( index, 1 ); } } this.selectItem( this.defaultSelectionItem ); } if ( this.dropdownList.isOpened() ) { this.dropdownList.setItems( this.items ); } } /** * Очистка списка позиций. */ public clearItems(): void { this.setItems( [], this.defaultSelectionItem ); } /** * Применение фильтра. */ public applyFilter(): void { if ( this.filterDisabled ) { return; } let text = this.value; let items: Item[] = this.items.filter( ( item ) => { return item.isGroup() || this.filterItem( item, text ); } ); items = items.filter( ( item ) => { if ( !item.isGroup() ) { return true; } return items.find( ( i ) => { return i.getGroup() === item; } ) !== undefined; } ); this.dropdownList.setItems( items ); this.dropdownList.selectFirstItem(); } /** * Получение выпадающего списка. */ public getDropdownList(): DropdownList { return this.dropdownList; } /** * Получение позиций. */ public getItems(): Item[] { return this.items; } /** * Получение позиции для выбора по умолчанию. */ public getDefaultSelectionItem(): Item | null { return this.defaultSelectionItem; } /** * Получение выбранной позиции. */ public getSelectedItem(): Item | null { return this.selectedItem; } /** * Получение идентификатора выбранной позиции. */ public getSelectedId(): any { return this.selectedItem === null ? null : this.selectedItem.getId(); } /** * Получение текста выбранной позиции. */ public getSelectedText(): string | null { return this.selectedItem === null ? null : this.selectedItem.getText(); } /** * Получение признака отображения значка для отмены выбора. */ public get displayClearIcon(): boolean { return this.hasAttribute( Select.ATTR_CLEAR_ICON ); } /** * Установка признака отображения значка для отмены выбора. * * @param value Значение. */ public set displayClearIcon( value: boolean ) { this.toggleAttribute( Select.ATTR_CLEAR_ICON, value ); } /** * Получение признака отображения значка выпадающего списка. */ public get displayDropdownIcon(): boolean { return this.hasAttribute( Select.ATTR_DROPDOWN_ICON ); } /** * Установка признака отображения значка выпадающего списка. * * @param value Значение. */ public set displayDropdownIcon( value: boolean ) { this.toggleAttribute( Select.ATTR_DROPDOWN_ICON, value ); } /** * Получение признака применения фильтра начиная с первого символа текста позиции. */ public get filterByFirstSymbol(): boolean { return this.hasAttribute( Select.ATTR_FILTER_BY_FIRST_SYMBOL ); } /** * Установка признака применения фильтра, начиная с первого символа текста позиции. * * @param value Значение. */ public set filterByFirstSymbol( value: boolean ) { this.toggleAttribute( Select.ATTR_FILTER_BY_FIRST_SYMBOL, value ); } /** * Получение признака отключенного фильтра позиций. */ public get filterDisabled(): boolean { return this.hasAttribute( Select.ATTR_FILTER_DISABLED ); } /** * Установка признака отключенного фильтра позиций. * * @param value Значение. */ public set filterDisabled( value: boolean ) { this.toggleAttribute( Select.ATTR_FILTER_DISABLED, value ); } /** * Получение признака чувствительного к регистру фильтра позиций. */ public get filterCaseSensitive(): boolean { return this.hasAttribute( Select.ATTR_FILTER_CASE_SENSITIVE ); } /** * Установка признака чувствительного к регистру фильтра позиций. * * @param value Значение. */ public set filterCaseSensitive( value: boolean ) { this.toggleAttribute( Select.ATTR_FILTER_CASE_SENSITIVE, value ); } /** * Получение размера позиции по вертикали. */ public get itemHeight(): number { return this.dropdownList.itemHeight; } /** * Установка размера позиции по вертикали. */ public set itemHeight( value: number ) { this.setAttribute( Select.ATTR_ITEM_HEIGHT, value.toString() ); } /** * Получение максимального размера списка по вертикали. */ public get listMaxHeight(): number { return this.dropdownList.maxHeight; } /** * Установка максимального размера списка по вертикали. */ public set listMaxHeight( value: number ) { this.setAttribute( Select.ATTR_LIST_MAX_HEIGHT, value.toString() ); } /** * Получение позиции выпадающего списка относительно поля выбора. */ public get listPosition(): ListPosition { switch ( this.getAttribute( Select.ATTR_LIST_POSITION ) ) { case "above": return "above"; case "below": return "below"; case "center": return "center"; default: return "auto"; } } /** * Установка позиции выпадающего списка относительно поля выбора. * * @param value Значение. */ public set listPosition( value: ListPosition ) { this.setAttribute( Select.ATTR_LIST_POSITION, "above" ); } /** * Конструктор. */ constructor() { super(); this.dropdownList = this.createDropdownList(); this.clearIcon = this.createClearIcon(); this.dropdownIcon = this.createDropdownIcon(); this.initSelectControl(); this.getInlineInput().appendChild( this.dropdownList ); } } Select.define( DropdownList ); Select.define( Icon );