@progress/kendo-angular-listbox
Version:
Kendo UI for Angular ListBox
1,239 lines (1,229 loc) • 84 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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