UNPKG

@progress/kendo-angular-listbox

Version:
1,239 lines (1,229 loc) 84 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i0 from '@angular/core'; import { EventEmitter, Injectable, Directive, Input, HostBinding, HostListener, forwardRef, isDevMode, Component, ContentChild, ViewChild, ViewChildren, Output, NgModule } from '@angular/core'; import { validatePackage } from '@progress/kendo-licensing'; import { Subscription } from 'rxjs'; import { getter } from '@progress/kendo-common'; import { caretAltUpIcon, caretAltDownIcon, caretAltRightIcon, caretAltLeftIcon, caretDoubleAltRightIcon, caretDoubleAltLeftIcon, xIcon } from '@progress/kendo-svg-icons'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { normalizeKeys, Keys, isPresent as isPresent$1, TemplateContextDirective, isChanged, ResizeBatchService } from '@progress/kendo-angular-common'; import { take } from 'rxjs/operators'; import * as i1 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { IconsService } from '@progress/kendo-angular-icons'; import { PopupService } from '@progress/kendo-angular-popup'; /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-listbox', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1765468206, version: '21.3.0', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/?utm_medium=product&utm_source=kendoangular&utm_campaign=kendo-ui-angular-purchase-license-keys-warning' }; /** * @hidden */ class ListBoxSelectionService { selectedIndices = []; selectionMode = 'single'; lastSelectedOrUnselectedIndex = null; rangeSelectionTargetIndex = null; rangeSelectionAnchorIndex = null; isItemDisabled = () => false; onSelect = new EventEmitter(); select(index, ctrlKey = false, shiftKey = false) { if (this.isItemDisabled(index)) { return; } const previousSelection = [...this.selectedIndices]; let selectedIndices = []; let deselectedIndices = null; const previousTargetIndex = this.rangeSelectionTargetIndex; if (this.selectionMode === 'single') { if (ctrlKey) { const isSelected = this.isSelected(index); if (isSelected) { this.selectedIndices = []; deselectedIndices = [index]; } else { this.selectedIndices = [index]; selectedIndices = [index]; if (previousSelection.length > 0) { deselectedIndices = previousSelection; } } this.lastSelectedOrUnselectedIndex = index; } else { this.selectedIndices = [index]; selectedIndices = [index]; this.lastSelectedOrUnselectedIndex = index; this.rangeSelectionAnchorIndex = index; if (previousSelection.length > 0 && previousSelection[0] !== index) { deselectedIndices = previousSelection; } } } else if (this.selectionMode === 'multiple') { if (shiftKey) { let anchorIndex = this.rangeSelectionAnchorIndex ?? this.lastSelectedOrUnselectedIndex ?? 0; if (index === anchorIndex) { this.selectedIndices = [anchorIndex]; this.rangeSelectionTargetIndex = index; selectedIndices = this.selectedIndices.filter(i => !previousSelection.includes(i)); const nowDeselected = previousSelection.filter(i => !this.selectedIndices.includes(i)); deselectedIndices = nowDeselected.length > 0 ? nowDeselected : null; } else { if (previousTargetIndex !== null && previousTargetIndex !== anchorIndex) { const previousDirection = previousTargetIndex > anchorIndex ? 'down' : 'up'; const currentDirection = index > anchorIndex ? 'down' : 'up'; if (previousDirection !== currentDirection) { this.rangeSelectionAnchorIndex = previousTargetIndex; anchorIndex = previousTargetIndex; } } const startIndex = Math.min(anchorIndex, index); const endIndex = Math.max(anchorIndex, index); this.selectedIndices = []; for (let i = startIndex; i <= endIndex; i++) { if (!this.isItemDisabled(i)) { this.selectedIndices.push(i); } } this.rangeSelectionTargetIndex = index; selectedIndices = this.selectedIndices.filter(i => !previousSelection.includes(i)); const nowDeselected = previousSelection.filter(i => !this.selectedIndices.includes(i)); deselectedIndices = nowDeselected.length > 0 ? nowDeselected : null; } } else if (ctrlKey) { const indexInSelection = this.selectedIndices.indexOf(index); if (indexInSelection === -1) { this.selectedIndices.push(index); selectedIndices = [index]; } else { this.selectedIndices.splice(indexInSelection, 1); deselectedIndices = [index]; } this.lastSelectedOrUnselectedIndex = index; this.rangeSelectionAnchorIndex = index; this.rangeSelectionTargetIndex = index; } else { this.selectedIndices = [index]; selectedIndices = [index]; this.lastSelectedOrUnselectedIndex = index; this.rangeSelectionAnchorIndex = index; this.rangeSelectionTargetIndex = index; const nowDeselected = previousSelection.filter(i => i !== index); deselectedIndices = nowDeselected.length > 0 ? nowDeselected : null; } } this.onSelect.next({ selectedIndices: selectedIndices.length > 0 ? selectedIndices : null, deselectedIndices }); } selectRange(targetIndex) { const anchorIndex = this.lastSelectedOrUnselectedIndex ?? 0; const startIndex = Math.min(anchorIndex, targetIndex); const endIndex = Math.max(anchorIndex, targetIndex); this.selectedIndices = []; for (let i = startIndex; i <= endIndex; i++) { if (!this.isItemDisabled(i)) { this.selectedIndices.push(i); } } } setSelectedIndices(indices) { this.selectedIndices = indices.filter(i => !this.isItemDisabled(i)); } addToSelectedIndices(index) { if (this.isItemDisabled(index)) { return; } if (this.selectionMode === 'single') { this.selectedIndices = [index]; } else if (this.selectedIndices.indexOf(index) === -1) { this.selectedIndices = [...this.selectedIndices, index]; } } selectAll(totalItems) { if (this.selectionMode === 'multiple') { this.selectedIndices = []; for (let i = 0; i < totalItems; i++) { if (!this.isItemDisabled(i)) { this.selectedIndices.push(i); } } } } areAllSelected(totalItems) { const allSelectableItems = Array.from({ length: totalItems }, (_, i) => i).filter(i => !this.isItemDisabled(i)); return this.selectedIndices.length === allSelectableItems.length && allSelectableItems.length > 0; } isSelected(index) { return this.selectedIndices.indexOf(index) !== -1; } clearSelection() { this.selectedIndices = []; this.lastSelectedOrUnselectedIndex = null; this.rangeSelectionAnchorIndex = null; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListBoxSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListBoxSelectionService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListBoxSelectionService, decorators: [{ type: Injectable }] }); /** * Allows you to customize the rendering of each item in the Kendo UI ListBox for Angular. * * Place an `<ng-template>` with the `kendoListBoxItemTemplate` directive inside your `<kendo-listbox>` component. * The template context exposes the current data item as `let-dataItem`. * * @example * ```typescript * @Component({ * selector: 'my-app', * template: ` * <kendo-listbox [data]="listBoxItems"> * <ng-template kendoListBoxItemTemplate let-dataItem> * <span>{{ dataItem }} item</span> * </ng-template> * </kendo-listbox> * ` * }) * export class AppComponent { } * ``` */ class ItemTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ItemTemplateDirective, isStandalone: true, selector: "[kendoListBoxItemTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListBoxItemTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * @hidden */ const DEFAULT_TOOLBAR_POSITION = 'right'; /** * @hidden */ const allTools = [ { name: 'moveUp', label: 'Move Up', icon: 'caret-alt-up', svgIcon: caretAltUpIcon }, { name: 'moveDown', label: 'Move Down', icon: 'caret-alt-down', svgIcon: caretAltDownIcon }, { name: 'transferTo', label: 'Transfer To', icon: 'caret-alt-right', svgIcon: caretAltRightIcon }, { name: 'transferFrom', label: 'Transfer From', icon: 'caret-alt-left', svgIcon: caretAltLeftIcon }, { name: 'transferAllTo', label: 'Transfer All To', icon: 'caret-double-alt-right', svgIcon: caretDoubleAltRightIcon }, { name: 'transferAllFrom', label: 'Transfer All From', icon: 'caret-double-alt-left', svgIcon: caretDoubleAltLeftIcon }, { name: 'remove', label: 'Remove', icon: 'x', svgIcon: xIcon } ]; /** * @hidden */ const sizeClassMap = { small: 'sm', medium: 'md', large: 'lg' }; /** * @hidden */ const actionsClasses = { left: 'k-listbox-actions-left', right: 'k-listbox-actions-right', top: 'k-listbox-actions-top', bottom: 'k-listbox-actions-bottom' }; /** * @hidden */ const isPresent = (value) => value !== null && value !== undefined; /** * @hidden */ const isObject = (value) => isPresent(value) && typeof value === 'object'; /** * @hidden */ const fieldAccessor = (dataItem, field) => { if (!isPresent(dataItem)) { return null; } if (!isPresent(field) || !isObject(dataItem)) { return dataItem; } // creates a field accessor supporting nested fields processing const valueFrom = getter(field); return valueFrom(dataItem); }; /** * @hidden */ const defaultItemDisabled = () => false; /** * @hidden */ const getTools = (names) => { return names.map(tool => allTools.find(meta => meta.name === tool)); }; /* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * @hidden */ class KeyboardNavigationService { renderer; zone; selectedListboxItemIndex = 0; focusedListboxItemIndex = 0; focusedToolIndex = 0; onDeleteEvent = new EventEmitter(); onMoveSelectedItem = new EventEmitter(); onTransferAllEvent = new EventEmitter(); onShiftSelectedItem = new EventEmitter(); onSelectionChange = new EventEmitter(); onSelectAll = new EventEmitter(); onSelectToEnd = new EventEmitter(); constructor(renderer, zone) { this.renderer = renderer; this.zone = zone; } onKeyDown(event, toolsRef, toolbar, childListbox, parentListbox, listboxItems, currentListbox) { const target = event.target; const keyCode = normalizeKeys(event); const ctrlOrMetaKey = event.ctrlKey || event.metaKey; const parentListboxToolbar = parentListbox?.selectedTools; const tool = toolsRef.find(elem => elem.element === target); const activeToolbar = toolbar.length > 0 ? toolbar : parentListboxToolbar; if (toolsRef.length > 0 || parentListbox?.tools.toArray().length > 0) { const focusNextTool = (keyCode === Keys.ArrowDown || keyCode === Keys.ArrowRight); const focusPrevTool = (keyCode === Keys.ArrowUp || keyCode === Keys.ArrowLeft); if ((focusNextTool || focusPrevTool) && tool) { const dir = focusPrevTool ? 'up' : 'down'; this.handleToolbarArrows(toolsRef, dir); } else if (keyCode === Keys.F10) { event.preventDefault(); this.onF10Key(toolsRef); } else if (keyCode === Keys.Delete && activeToolbar.some(tool => tool.name === 'remove')) { this.onDeleteEvent.emit(this.selectedListboxItemIndex); } } if (ctrlOrMetaKey && (event.key === 'a' || event.key === 'A')) { event.preventDefault(); this.onSelectAll.emit(); return; } if (ctrlOrMetaKey && event.shiftKey && (keyCode === Keys.Home || keyCode === Keys.End)) { event.preventDefault(); const direction = keyCode === Keys.Home ? 'home' : 'end'; this.onSelectToEnd.emit({ direction }); return; } const isTargetListboxItem = listboxItems.find(elem => elem.nativeElement === target); if (isTargetListboxItem) { let isTransferToolVisible; if (activeToolbar) { isTransferToolVisible = activeToolbar.some(tool => tool.name.startsWith('transfer')); } if ((keyCode === Keys.ArrowRight || keyCode === Keys.ArrowLeft) && ctrlOrMetaKey && isTransferToolVisible) { this.onArrowLeftOrRight(keyCode, parentListbox, childListbox, event, listboxItems, currentListbox); } else if ((keyCode === Keys.ArrowUp || keyCode === Keys.ArrowDown)) { this.onArrowUpOrDown(keyCode, ctrlOrMetaKey, event, activeToolbar, listboxItems); } else if ((event.metaKey && keyCode === Keys.Enter) || (event.ctrlKey && keyCode === Keys.Space)) { this.onSelectChange(event, listboxItems); } else if (keyCode === Keys.Space) { this.onSpaceKey(event, listboxItems); } } } changeTabindex(previousItem, currentItem, shouldBlur = true) { if (previousItem) { this.renderer.setAttribute(previousItem, 'tabindex', '-1'); if (shouldBlur) { previousItem.blur(); } } if (currentItem) { this.renderer.setAttribute(currentItem, 'tabindex', '0'); currentItem.focus(); } } handleToolbarArrows(toolsRef, dir) { const topReached = dir === 'up' && this.focusedToolIndex <= 0; const bottomReached = dir === 'down' && this.focusedToolIndex >= toolsRef.length - 1; if (topReached || bottomReached) { return; } const offset = dir === 'up' ? -1 : 1; this.focusedToolIndex += offset; const prevItem = toolsRef[this.focusedToolIndex + (offset * -1)]?.element; const currentItem = toolsRef[this.focusedToolIndex]?.element; this.changeTabindex(prevItem, currentItem); } onSpaceKey(event, listboxItems) { event.stopImmediatePropagation(); event.preventDefault(); event.stopPropagation(); const ctrlKey = event.ctrlKey || event.metaKey; const shiftKey = event.shiftKey; if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) { const previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { return; } this.changeTabindex(previousItem, currentItem); } this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex, ctrlKey, shiftKey }); this.selectedListboxItemIndex = this.focusedListboxItemIndex; } onArrowUpOrDown(keyCode, ctrlOrMetaKey, event, activeToolbar, listboxItems) { event.preventDefault(); const dir = keyCode === Keys.ArrowUp ? 'moveUp' : 'moveDown'; if (ctrlOrMetaKey) { let isMoveToolVisible; if (activeToolbar) { isMoveToolVisible = activeToolbar.some(tool => tool.name.startsWith('move')); } if (event.shiftKey && isMoveToolVisible) { this.onMoveSelectedItem.emit(dir); return; } this.changeFocusedItem(dir, listboxItems); return; } if (event.shiftKey) { this.onShiftArrow(dir, listboxItems); return; } dir === 'moveUp' ? this.onArrowUp(listboxItems) : this.onArrowDown(listboxItems); if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) { this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex: this.focusedListboxItemIndex }); this.focusedListboxItemIndex = this.selectedListboxItemIndex; } } onArrowLeftOrRight(keyCode, parentListbox, childListbox, event, listboxItems, currentListbox) { event.preventDefault(); if (event.shiftKey) { this.transferAllItems(keyCode, childListbox, parentListbox); return; } const isArrowRight = keyCode === Keys.ArrowRight; const sourceListbox = isArrowRight ? currentListbox : childListbox || parentListbox?.childListbox; const hasSelection = sourceListbox?.selectedIndices && sourceListbox.selectedIndices.length > 0; if (hasSelection) { this.transferItem(keyCode, childListbox, parentListbox, listboxItems); } } onSelectChange(event, listboxItems) { event.stopImmediatePropagation(); event.preventDefault(); const areIndexesEqual = this.selectedListboxItemIndex === this.focusedListboxItemIndex; const canDeselect = (this.selectedListboxItemIndex || this.selectedListboxItemIndex === 0) && areIndexesEqual; let previousItem; let currentItem; let prevIndex; if (canDeselect) { previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; this.selectedListboxItemIndex = null; } else { previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { return; } prevIndex = this.selectedListboxItemIndex; this.selectedListboxItemIndex = this.focusedListboxItemIndex; } this.changeTabindex(previousItem, currentItem, !!currentItem); const ctrlKey = event.ctrlKey || event.metaKey; this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex, ctrlKey }); } onF10Key(tools) { if (this.focusedToolIndex && this.focusedToolIndex > -1) { if (this.focusedToolIndex >= tools.length) { tools[tools.length - 1].element.focus(); } else { tools[this.focusedToolIndex].element.focus(); } } else { tools[0]?.element.focus(); } } transferAllItems(keyCode, childListbox, parentListbox) { const isArrowRight = keyCode === Keys.ArrowRight; const actionToPerform = isArrowRight ? 'transferAllTo' : 'transferAllFrom'; this.onTransferAllEvent.emit(actionToPerform); const adjustTabindex = (items) => { items.forEach(item => { if (item.nativeElement.getAttribute('tabindex') === '0') { this.changeTabindex(item.nativeElement, null); } }); }; this.zone.onStable.pipe(take(1)).subscribe(() => { const childListboxNav = childListbox?.keyboardNavigationService || parentListbox?.childListbox.keyboardNavigationService; let currentItem; if (isArrowRight) { if (childListbox) { const childListBoxItems = childListbox.listboxItems.toArray(); const childListboxItemsLength = childListBoxItems.length - 1; currentItem = childListBoxItems[childListboxItemsLength].nativeElement; childListboxNav.focusedListboxItemIndex = childListboxItemsLength; childListboxNav.selectedListboxItemIndex = childListboxItemsLength; this.focusedListboxItemIndex = 0; this.selectedListboxItemIndex = 0; adjustTabindex(childListBoxItems); } } else { if (parentListbox) { const parentListboxNav = parentListbox.keyboardNavigationService; const parentListBoxItems = parentListbox.listboxItems.toArray(); const parentListboxItemsLength = parentListBoxItems.length - 1; currentItem = parentListBoxItems[parentListboxItemsLength].nativeElement; parentListboxNav.focusedListboxItemIndex = parentListboxItemsLength; parentListboxNav.selectedListboxItemIndex = parentListboxItemsLength; childListboxNav.focusedListboxItemIndex = 0; childListboxNav.selectedListboxItemIndex = 0; adjustTabindex(parentListBoxItems); } } this.changeTabindex(null, currentItem); }); } transferItem(keyCode, childListbox, parentListbox, listboxItems) { const isArrowRight = keyCode === Keys.ArrowRight; const actionToPerform = isArrowRight ? 'transferTo' : 'transferFrom'; this.onShiftSelectedItem.emit(actionToPerform); const adjustTabindex = (items, firstItem, currentItem) => { items.forEach(item => { if (item.nativeElement.getAttribute('tabindex') === '0') { this.changeTabindex(item.nativeElement, firstItem); } }); this.changeTabindex(null, currentItem); }; this.zone.onStable.pipe(take(1)).subscribe(() => { if (isArrowRight) { if (childListbox) { const childListBoxItems = childListbox.listboxItems.toArray(); const childListboxNav = childListbox.keyboardNavigationService; const childListboxItemsLength = childListbox.listboxItems.length - 1; const parentListboxFirstItem = listboxItems[0].nativeElement; const currentItem = childListBoxItems[childListboxItemsLength].nativeElement; childListboxNav.focusedListboxItemIndex = childListboxItemsLength; childListboxNav.selectedListboxItemIndex = childListboxItemsLength; this.focusedListboxItemIndex = 0; this.selectedListboxItemIndex = 0; adjustTabindex(childListBoxItems, parentListboxFirstItem, currentItem); } } else { if (parentListbox) { const parentListBoxItems = parentListbox.listboxItems.toArray(); const childListboxNav = parentListbox.childListbox.keyboardNavigationService; const parentListboxNav = parentListbox.keyboardNavigationService; const parentListboxItemsLength = parentListbox.listboxItems.length - 1; const childListboxFirstItem = listboxItems[0].nativeElement; const currentItem = parentListBoxItems[parentListboxItemsLength].nativeElement; parentListboxNav.focusedListboxItemIndex = parentListboxItemsLength; parentListboxNav.selectedListboxItemIndex = parentListboxItemsLength; childListboxNav.focusedListboxItemIndex = 0; childListboxNav.selectedListboxItemIndex = 0; adjustTabindex(parentListBoxItems, childListboxFirstItem, currentItem); } } }); } changeFocusedItem(dir, listboxItems) { const previousIndex = this.focusedListboxItemIndex; const previousItem = listboxItems[previousIndex].nativeElement; if (this.focusedListboxItemIndex > 0 && dir === 'moveUp') { this.focusedListboxItemIndex -= 1; } else if (this.focusedListboxItemIndex < listboxItems.length - 1 && dir === 'moveDown') { this.focusedListboxItemIndex += 1; } const currentItem = listboxItems[this.focusedListboxItemIndex].nativeElement; this.changeTabindex(previousItem, currentItem); } onShiftArrow(dir, listboxItems) { const previousFocusIndex = this.focusedListboxItemIndex; if (dir === 'moveUp' && this.focusedListboxItemIndex > 0) { this.focusedListboxItemIndex -= 1; } else if (dir === 'moveDown' && this.focusedListboxItemIndex < listboxItems.length - 1) { this.focusedListboxItemIndex += 1; } if (previousFocusIndex !== this.focusedListboxItemIndex) { const previousItem = listboxItems[previousFocusIndex]?.nativeElement; let currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { const step = dir === 'moveDown' ? 1 : -1; const nextEnabledIndex = this.findNextEnabledIndex(this.focusedListboxItemIndex, listboxItems, step); if (nextEnabledIndex === -1) { this.focusedListboxItemIndex = previousFocusIndex; return; } this.focusedListboxItemIndex = nextEnabledIndex; currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; } this.changeTabindex(previousItem, currentItem); this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex, shiftKey: true }); this.selectedListboxItemIndex = this.focusedListboxItemIndex; } } onArrowDown(listboxItems) { if (this.focusedListboxItemIndex < listboxItems.length - 1) { this.selectedListboxItemIndex = this.focusedListboxItemIndex + 1; const previousItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; let currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { currentItem = this.calculateNextActiveItem(listboxItems, 1); if (!currentItem) { return; } } this.changeTabindex(previousItem, currentItem); } } onArrowUp(listboxItems) { if (this.focusedListboxItemIndex > 0) { this.selectedListboxItemIndex = this.focusedListboxItemIndex - 1; const previousItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement; let currentItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement; if (this.isItemDisabled(currentItem)) { currentItem = this.calculateNextActiveItem(listboxItems, -1); if (!currentItem) { return; } } this.changeTabindex(previousItem, currentItem); } } isItemDisabled(item) { return item.getAttribute('aria-disabled') === 'true'; } findNextEnabledIndex(startIndex, listboxItems, step) { let index = startIndex; while (index >= 0 && index < listboxItems.length) { const item = listboxItems[index]?.nativeElement; if (!this.isItemDisabled(item)) { return index; } index += step; } return -1; } calculateNextActiveItem(listboxItems, step) { this.selectedListboxItemIndex = this.findNextEnabledIndex(this.selectedListboxItemIndex, listboxItems, step); if (this.selectedListboxItemIndex === -1) { this.selectedListboxItemIndex = this.focusedListboxItemIndex; return null; } return listboxItems[this.selectedListboxItemIndex]?.nativeElement; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: KeyboardNavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.NgZone }] }); /** * @hidden */ class ItemSelectableDirective { selectionService; index; constructor(selectionService) { this.selectionService = selectionService; } get selectedClassName() { return this.selectionService.isSelected(this.index); } onClick(event) { event.stopPropagation(); const ctrlKey = event.ctrlKey || event.metaKey; const shiftKey = event.shiftKey; if (shiftKey) { event.preventDefault(); } this.selectionService.select(this.index, ctrlKey, shiftKey); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemSelectableDirective, deps: [{ token: ListBoxSelectionService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ItemSelectableDirective, isStandalone: true, selector: "[kendoListBoxItemSelectable]", inputs: { index: "index" }, host: { listeners: { "mousedown": "onClick($event)" }, properties: { "class.k-selected": "this.selectedClassName" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemSelectableDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListBoxItemSelectable]', standalone: true }] }], ctorParameters: () => [{ type: ListBoxSelectionService }], propDecorators: { index: [{ type: Input }], selectedClassName: [{ type: HostBinding, args: ['class.k-selected'] }], onClick: [{ type: HostListener, args: ['mousedown', ['$event']] }] } }); /** * @hidden */ class Messages extends ComponentMessages { /** * The text of the `Move Up` button title. */ moveUp; /** * The text of the `Move Down` button title. */ moveDown; /** * The text of the `Remove` button tittle. */ remove; /** * The text of the `Transfer To` button title. */ transferTo; /** * The text of the `Transfer From` button title. */ transferFrom; /** * The text of the `Transfer All To` button title. */ transferAllTo; /** * The text of the `Transfer All From` button title. */ transferAllFrom; /** * The text displayed when there are no items. */ noDataText; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: Messages, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: Messages, inputs: { moveUp: "moveUp", moveDown: "moveDown", remove: "remove", transferTo: "transferTo", transferFrom: "transferFrom", transferAllTo: "transferAllTo", transferAllFrom: "transferAllFrom", noDataText: "noDataText" }, usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: Messages, decorators: [{ type: Directive }], propDecorators: { moveUp: [{ type: Input }], moveDown: [{ type: Input }], remove: [{ type: Input }], transferTo: [{ type: Input }], transferFrom: [{ type: Input }], transferAllTo: [{ type: Input }], transferAllFrom: [{ type: Input }], noDataText: [{ type: Input }] } }); /** * @hidden */ class LocalizedMessagesDirective extends Messages { service; constructor(service) { super(); this.service = service; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalizedMessagesDirective, deps: [{ token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LocalizedMessagesDirective, isStandalone: true, selector: "[kendoListBoxLocalizedMessages]", providers: [ { provide: Messages, useExisting: forwardRef(() => LocalizedMessagesDirective) } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocalizedMessagesDirective, decorators: [{ type: Directive, args: [{ providers: [ { provide: Messages, useExisting: forwardRef(() => LocalizedMessagesDirective) } ], selector: '[kendoListBoxLocalizedMessages]', standalone: true }] }], ctorParameters: () => [{ type: i1.LocalizationService }] }); /* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ const DEFAULT_SIZE = 'medium'; let idx = 0; /** * Represents the Kendo UI ListBox component for Angular. * Provides a list of items from which you can select and transfer data between connected ListBoxes * ([see overview]({% slug overview_listbox %})). * * @example * ```typescript * @Component({ * selector: 'my-app', * template: ` * <kendo-listbox * [data]="items" * textField="name" * [toolbar]="true"> * </kendo-listbox> * ` * }) * export class AppComponent { * items = [ * { name: 'Item 1' }, * { name: 'Item 2' } * ]; * } * ``` * * @remarks * Supported children components are: {@link CustomMessagesComponent}. */ class ListBoxComponent { keyboardNavigationService; selectionService; hostElement; renderer; zone; localization; cdr; /** * @hidden */ listboxClassName = true; /** * @hidden */ direction; /** * @hidden */ itemTemplate; /** * @hidden */ listboxElement; /** * @hidden */ listboxItems; /** * @hidden */ toolbarElement; /** * @hidden */ tools; /** * Specifies the field of the data item that provides the text content of the nodes. */ textField; /** * Sets the selection mode of the ListBox. * * @default 'single' */ set selectable(mode) { this._selectable = mode; this.selectionService.selectionMode = mode; } get selectable() { return this._selectable; } /** * Specifies the data that the ListBox displays. * * @default [] */ data = []; /** * Specifies the size of the component. * */ set size(size) { const newSize = size ? size : DEFAULT_SIZE; this.renderer.removeClass(this.hostElement.nativeElement, `k-listbox-${sizeClassMap[this.size]}`); this.setSizingClass(newSize); this._size = size; } get size() { return this._size; } /** * Configures the toolbar of the ListBox. * Specifies whether to display a toolbar and which tools and position to use. * * @default false */ set toolbar(config) { let position = DEFAULT_TOOLBAR_POSITION; if (typeof config === 'boolean') { this.selectedTools = config ? allTools : []; } else { this.selectedTools = config.tools ? getTools(config.tools) : allTools; if (config.position) { position = config.position; } } this.setToolbarClass(position); } /** * Specifies the value of the `aria-label` attribute of the Listbox element. * * @default 'Listbox' */ listboxLabel = 'Listbox'; /** * Specifies the value of the `aria-label` attribute of the Listbox toolbar element. * * @default 'Toolbar' */ listboxToolbarLabel = 'Toolbar'; /** * Specifies a function that determines if a specific item is disabled. */ itemDisabled = defaultItemDisabled; /** * Fires when you select a different ListBox item. * Also fires when you move a node, because moving changes its index. */ selectionChange = new EventEmitter(); /** * Fires when you click a ListBox item. */ action = new EventEmitter(); /** * @hidden */ getChildListbox = new EventEmitter(); /** * @hidden */ get listClasses() { return `k-list k-list-${sizeClassMap[this.size]}`; } /** * @hidden */ messageFor(key) { return this.localization.get(key); } /** * @hidden */ selectedTools = []; /** * @hidden */ listboxId; /** * @hidden */ toolbarId; /** * @hidden */ childListbox; /** * @hidden */ parentListbox; /** * @hidden */ caretAltLeftIcon = caretAltLeftIcon; /** * @hidden */ caretAltRightIcon = caretAltRightIcon; localizationSubscription; _size = DEFAULT_SIZE; subs = new Subscription(); shouldFireFocusIn = false; _selectable = 'single'; constructor(keyboardNavigationService, selectionService, hostElement, renderer, zone, localization, cdr) { this.keyboardNavigationService = keyboardNavigationService; this.selectionService = selectionService; this.hostElement = hostElement; this.renderer = renderer; this.zone = zone; this.localization = localization; this.cdr = cdr; validatePackage(packageMetadata); this.setToolbarClass(DEFAULT_TOOLBAR_POSITION); this.setSizingClass(this.size); this.direction = localization.rtl ? 'rtl' : 'ltr'; this.selectionService.isItemDisabled = (index) => { return this.itemDisabled(this.data[index]); }; } ngOnInit() { // This event emitter gives us the connectedWith value from the DataBinding directive this.getChildListbox.emit(); if (this.childListbox) { // This allows us to know to which parent Listbox the child Listbox is connected to this.childListbox.parentListbox = this; } this.localizationSubscription = this.localization.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; }); this.subs.add(this.localizationSubscription); } ngAfterViewInit() { const toolsRef = this.tools.toArray(); const hostEl = this.hostElement.nativeElement; const navService = this.keyboardNavigationService; this.setIds(); this.initSubscriptions(navService, hostEl, toolsRef); } ngOnDestroy() { this.subs.unsubscribe(); } /** * @hidden */ performAction(actionName) { const isActionTransferFrom = actionName === 'transferFrom' || actionName === 'transferAllFrom'; const isListboxChild = this.parentListbox && !this.childListbox; const isListboxParentAndChild = !!(this.parentListbox && this.childListbox); const isListboxParent = !!(this.childListbox || (!this.childListbox && !this.parentListbox)); this.shouldFireFocusIn = false; if (isListboxChild || (isListboxParentAndChild && isActionTransferFrom)) { this.parentListbox.action.next(actionName); } else if (isListboxParent || (isListboxParentAndChild && !isActionTransferFrom)) { this.action.next(actionName); } const toolsRef = this.tools.toArray() || this.parentListbox.tools.toArray(); const focusedToolIndex = toolsRef.findIndex(elem => elem.nativeElement === document.activeElement); if ((this.selectedTools.length > 0 || this.parentListbox.selectedTools.length > 0) && focusedToolIndex > -1) { const navService = this.keyboardNavigationService || this.parentListbox.keyboardNavigationService; const selectedTools = this.selectedTools || this.parentListbox.selectedTools; const prevTool = toolsRef[navService.focusedToolIndex]?.element; navService.focusedToolIndex = selectedTools.findIndex(tool => tool.name === actionName); const currentTool = toolsRef[navService.focusedToolIndex]?.element; navService.changeTabindex(prevTool, currentTool); } this.cdr.markForCheck(); this.zone.runOutsideAngular(() => setTimeout(() => { this.shouldFireFocusIn = true; })); } /** * Selects multiple ListBox nodes programmatically. */ select(indices) { const validIndices = indices.filter(index => index >= 0 && index < this.data.length); this.selectionService.setSelectedIndices(validIndices); } /** * Selects a ListBox node programmatically. * * @hidden */ selectItem(index) { if (index >= 0 && index < this.data.length) { this.select([index]); } } /** * Clears the ListBox selection programmatically. */ clearSelection() { this.selectionService.clearSelection(); } /** * Gets the indexes of the currently selected items in the ListBox. */ get selectedIndices() { return this.selectionService.selectedIndices; } /** * @hidden */ get getListboxId() { const id = ++idx; const listboxId = `k-listbox-${id}`; return listboxId; } /** * @hidden */ getText(dataItem) { if (typeof dataItem !== 'string' && !this.textField && isDevMode()) { throw new Error('Missing textField input. When passing an array of objects as data, please set the textField input of the ListBox accordingly.'); } return fieldAccessor(dataItem, this.textField); } /** * @hidden */ toolIcon(icon) { return this.direction === 'ltr' ? icon : icon === 'caret-alt-left' ? 'caret-alt-right' : icon === 'caret-alt-right' ? 'caret-alt-left' : icon; } /** * @hidden */ toolSVGIcon(icon) { return this.direction === 'ltr' ? icon : icon === this.caretAltLeftIcon ? this.caretAltRightIcon : icon === this.caretAltRightIcon ? this.caretAltLeftIcon : icon; } initSubscriptions(navService, hostEl, toolsRef) { this.subs.add(navService.onShiftSelectedItem.subscribe((actionToPerform) => this.performAction(actionToPerform))); this.subs.add(navService.onTransferAllEvent.subscribe((actionToPerform) => this.performAction(actionToPerform))); this.subs.add(this.selectionService.onSelect.subscribe((event) => { this.shouldFireFocusIn = false; this.selectionChange.next(event); const newFocusIndex = isPresent$1(this.selectionService.rangeSelectionTargetIndex) ? this.selectionService.rangeSelectionTargetIndex : this.selectionService.lastSelectedOrUnselectedIndex; if (isPresent$1(newFocusIndex)) { const listboxItems = this.listboxItems.toArray(); const previousItem = listboxItems[navService.focusedListboxItemIndex]?.nativeElement; const currentItem = listboxItems[newFocusIndex]?.nativeElement; if (previousItem && currentItem) { navService.changeTabindex(previousItem, currentItem, false); navService.focusedListboxItemIndex = newFocusIndex; navService.selectedListboxItemIndex = newFocusIndex; } } this.cdr.markForCheck(); this.zone.runOutsideAngular(() => setTimeout(() => { this.shouldFireFocusIn = true; })); })); this.subs.add(navService.onDeleteEvent.subscribe((index) => this.onDeleteEvent(index, navService))); this.subs.add(navService.onMoveSelectedItem.subscribe((dir) => this.performAction(dir))); this.subs.add(navService.onSelectAll.subscribe(() => { if (this.selectable === 'multiple') { const previousSelection = [...this.selectionService.selectedIndices]; const allSelected = this.selectionService.areAllSelected(this.data.length); if (allSelected) { this.selectionService.clearSelection(); this.selectionChange.next({ selectedIndices: null, deselectedIndices: previousSelection.length > 0 ? previousSelection : null }); } else { this.selectionService.selectAll(this.data.length); const selectedIndices = this.selectionService.selectedIndices.filter(i => !previousSelection.includes(i)); this.selectionChange.next({ selectedIndices: selectedIndices.length > 0 ? selectedIndices : null, deselectedIndices: null }); } this.cdr.markForCheck(); } })); this.subs.add(navService.onSelectToEnd.subscribe(({ direction }) => { if (this.selectable === 'multiple') { this.shouldFireFocusIn = false; const previousSelection = [...this.selectionService.selectedIndices]; const targetIndex = direction === 'home' ? 0 : this.data.length - 1; this.selectionService.selectRange(targetIndex); const selectedIndices = this.selectionService.selectedIndices.filter(i => !previousSelection.includes(i)); const deselectedIndices = previousSelection.filter(i => !this.selectionService.selectedIndices.includes(i)); this.selectionChange.next({ selectedIndices: selectedIndices.length > 0 ? selectedIndices : null, deselectedIndices: deselectedIndices.length > 0 ? deselectedIndices : null }); const listboxItems = this.listboxItems.toArray(); const currentItem = listboxItems[navService.focusedListboxItemIndex]?.nativeEl