@siberiaweb/components
Version:
1,046 lines (846 loc) • 30.4 kB
text/typescript
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 );