UNPKG

@progress/kendo-angular-listbox

Version:
662 lines (647 loc) 29 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ /* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, isDevMode, NgZone, Output, QueryList, Renderer2, ViewChild, ViewChildren } from '@angular/core'; import { validatePackage } from '@progress/kendo-licensing'; import { Subscription } from 'rxjs'; import { packageMetadata } from './package-metadata'; import { ListBoxSelectionService } from './selection.service'; import { ItemTemplateDirective } from './item-template.directive'; import { defaultItemDisabled, fieldAccessor, getTools } from './util'; import { allTools, DEFAULT_TOOLBAR_POSITION, sizeClassMap, actionsClasses } from './constants'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { KeyboardNavigationService } from './keyboard-navigation.service'; import { take } from 'rxjs/operators'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { caretAltLeftIcon, caretAltRightIcon } from '@progress/kendo-svg-icons'; import { ItemSelectableDirective } from './item-selectable.directive'; import { NgIf, NgFor } from '@angular/common'; import { LocalizedMessagesDirective } from './localization/localized-messages.directive'; import { TemplateContextDirective } from '@progress/kendo-angular-common'; import * as i0 from "@angular/core"; import * as i1 from "./keyboard-navigation.service"; import * as i2 from "./selection.service"; import * as i3 from "@progress/kendo-angular-l10n"; const DEFAULT_SIZE = 'medium'; let idx = 0; /** * Represents the [Kendo UI ListBox component for Angular]({% slug overview_listbox %}). */ export class ListBoxComponent { keyboardNavigationService; selectionService; hostElement; renderer; zone; localization; changeDetector; /** * @hidden */ listboxClassName = true; /** * @hidden */ direction; /** * @hidden */ itemTemplate; /** * @hidden */ listboxElement; /** * @hidden */ listboxItems; /** * @hidden */ toolbarElement; /** * @hidden */ tools; /** * The fields of the data item that provide the text content of the nodes. */ textField; /** * The data which will be displayed by the ListBox. */ data = []; /** * Sets the size of the component. * * The possible values are: * - `'small'` * - `'medium'` (default) * - `'large'` */ 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; } /** * Sets whether a toolbar should be displayed with the ListBox, as well as what tools and position should be used. */ 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); } /** * The value of the aria-label attribute of the Listbox element. */ listboxLabel = 'Listbox'; /** * The value of the aria-label attribute of the Listbox toolbar element. */ listboxToolbarLabel = 'Toolbar'; /** * A function which determines if a specific item is disabled. */ itemDisabled = defaultItemDisabled; /** * Fires when the user selects a different ListBox item. Also fires when a node is moved, since that also changes its index. */ selectionChange = new EventEmitter(); /** * Fires when the user clicks a ListBox item. */ actionClick = 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 = allTools; /** * @hidden */ listboxId; /** * @hidden */ toolbarId; /** * @hidden */ childListbox; /** * @hidden */ parentListbox; /** * @hidden */ caretAltLeftIcon = caretAltLeftIcon; /** * @hidden */ caretAltRightIcon = caretAltRightIcon; localizationSubscription; _size = DEFAULT_SIZE; subs = new Subscription(); shouldFireFocusIn = true; constructor(keyboardNavigationService, selectionService, hostElement, renderer, zone, localization, changeDetector) { this.keyboardNavigationService = keyboardNavigationService; this.selectionService = selectionService; this.hostElement = hostElement; this.renderer = renderer; this.zone = zone; this.localization = localization; this.changeDetector = changeDetector; validatePackage(packageMetadata); this.setToolbarClass(DEFAULT_TOOLBAR_POSITION); this.setSizingClass(this.size); this.direction = localization.rtl ? 'rtl' : 'ltr'; } 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; } if (this.selectedIndex) { this.keyboardNavigationService.focusedToolIndex = this.selectedIndex; } 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)); if (isListboxChild || (isListboxParentAndChild && isActionTransferFrom)) { this.parentListbox.actionClick.next(actionName); } else if (isListboxParent || (isListboxParentAndChild && !isActionTransferFrom)) { this.actionClick.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); } } /** * Programmatically selects a ListBox node. */ selectItem(index) { this.selectionService.selectedIndex = index; } /** * Programmatically clears the ListBox selection. */ clearSelection() { this.selectionService.clearSelection(); } /** * The index of the currently selected item in the ListBox. */ get selectedIndex() { return this.selectionService.selectedIndex; } /** * @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; } onClickEvent(prevIndex, index) { this.shouldFireFocusIn = false; this.selectionChange.next({ index, prevIndex: this.keyboardNavigationService.selectedListboxItemIndex }); this.keyboardNavigationService.selectedListboxItemIndex = index; this.keyboardNavigationService.focusedListboxItemIndex = index; this.zone.onStable.pipe(take(1)).subscribe(() => { const listboxItems = this.listboxItems.toArray(); const previousItem = prevIndex ? listboxItems[prevIndex].nativeElement : listboxItems[0].nativeElement; const currentItem = listboxItems[index].nativeElement; this.keyboardNavigationService.changeTabindex(previousItem, currentItem); }); this.zone.onStable.pipe(take(1)).subscribe(() => { this.shouldFireFocusIn = true; }); } 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((e) => this.onClickEvent(e.prevIndex, e.index))); this.subs.add(navService.onDeleteEvent.subscribe((index) => this.onDeleteEvent(index, navService))); this.subs.add(navService.onMoveSelectedItem.subscribe((dir) => this.performAction(dir))); if (this.listboxElement) { this.subs.add(this.renderer.listen(this.listboxElement.nativeElement, 'focusin', (event) => this.onFocusIn(event))); } this.subs.add(this.renderer.listen(hostEl, 'keydown', (event) => navService.onKeyDown(event, toolsRef, this.selectedTools, this.childListbox, this.parentListbox, this.listboxItems.toArray()))); this.subs.add(navService.onSelectionChange.subscribe((indexes) => { const { prevIndex, index } = indexes; this.selectionService.selectedIndex = index; this.selectionChange.next({ index, prevIndex }); this.changeDetector.markForCheck(); })); } onFocusIn(event) { const navService = this.keyboardNavigationService; if (navService.focusedListboxItemIndex === navService.selectedListboxItemIndex && this.shouldFireFocusIn) { const items = this.listboxItems.toArray(); const index = items.findIndex(elem => elem.nativeElement === event.target); if (index === -1) { return; } this.selectionService.selectedIndex = index; this.selectionChange.next({ index, prevIndex: null }); const previousItem = items[navService.selectedListboxItemIndex]?.nativeElement; const currentItem = items[index]?.nativeElement; this.renderer.setAttribute(previousItem, 'tabindex', '-1'); this.renderer.setAttribute(currentItem, 'tabindex', '0'); } } setIds() { if (!this.listboxElement) { return; } const listbox = this.listboxElement.nativeElement; this.listboxId = this.getListboxId; this.renderer.setAttribute(listbox, 'id', this.listboxId); if (this.selectedTools.length > 0 || this.parentListbox?.selectedTools.length > 0) { const toolbar = this.toolbarElement?.nativeElement; const parentToolbar = this.parentListbox?.toolbarElement?.nativeElement; if (this.parentListbox && this.childListbox) { this.zone.onStable.pipe(take(1)).subscribe(() => { this.toolbarId = `${this.parentListbox.listboxId} ${this.listboxId} ${this.childListbox.listboxId}`; this.renderer.setAttribute(toolbar, 'aria-controls', this.toolbarId); }); } else if (this.childListbox && !this.parentListbox) { this.zone.onStable.pipe(take(1)).subscribe(() => { this.toolbarId = this.toolbarId = `${this.listboxId} ${this.childListbox.listboxId}`; this.renderer.setAttribute(toolbar, 'aria-controls', this.toolbarId); }); } else if (this.parentListbox && this.selectedTools.length > 0) { this.toolbarId = `${this.parentListbox.listboxId} ${this.listboxId}`; this.parentListbox.toolbarId = this.toolbarId = `${this.parentListbox.listboxId} ${this.listboxId}`; this.renderer.setAttribute(toolbar, 'aria-controls', this.toolbarId); parentToolbar && this.renderer.setAttribute(parentToolbar, 'aria-controls', this.parentListbox.toolbarId); } else if (!this.parentListbox && !this.childListbox) { this.toolbarId = this.listboxId; this.renderer.setAttribute(toolbar, 'aria-controls', this.toolbarId); } } } onDeleteEvent(index, navService) { this.selectionService.selectedIndex = index; this.performAction('remove'); const listboxItems = this.listboxItems.toArray(); const setIndex = index + 1 === listboxItems.length ? { index: index - 1, tabindex: index - 1 } : { index, tabindex: index + 1 }; navService.changeTabindex(null, listboxItems[setIndex['tabindex']]?.nativeElement); this.selectionChange.next({ index: setIndex['index'], prevIndex: null }); navService.selectedListboxItemIndex = setIndex['index']; navService.focusedListboxItemIndex = setIndex['index']; navService.focusedListboxItem = setIndex['index']; this.selectionService.selectedIndex = setIndex['index']; } setToolbarClass(pos) { Object.keys(actionsClasses).forEach((className) => { if (pos === className) { this.renderer.addClass(this.hostElement.nativeElement, actionsClasses[className]); } else { this.renderer.removeClass(this.hostElement.nativeElement, actionsClasses[className]); } }); } setSizingClass(size) { this.renderer.addClass(this.hostElement.nativeElement, `k-listbox-${sizeClassMap[size]}`); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListBoxComponent, deps: [{ token: i1.KeyboardNavigationService }, { token: i2.ListBoxSelectionService }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i3.LocalizationService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ListBoxComponent, isStandalone: true, selector: "kendo-listbox", inputs: { textField: "textField", data: "data", size: "size", toolbar: "toolbar", listboxLabel: "listboxLabel", listboxToolbarLabel: "listboxToolbarLabel", itemDisabled: "itemDisabled" }, outputs: { selectionChange: "selectionChange", actionClick: "actionClick", getChildListbox: "getChildListbox" }, host: { properties: { "class.k-listbox": "this.listboxClassName", "attr.dir": "this.direction" } }, providers: [ ListBoxSelectionService, KeyboardNavigationService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.listbox' }, ], queries: [{ propertyName: "itemTemplate", first: true, predicate: ItemTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "listboxElement", first: true, predicate: ["listbox"], descendants: true }, { propertyName: "toolbarElement", first: true, predicate: ["toolbar"], descendants: true }, { propertyName: "listboxItems", predicate: ["listboxItems"], descendants: true }, { propertyName: "tools", predicate: ["tools"], descendants: true }], ngImport: i0, template: ` <ng-container kendoListBoxLocalizedMessages i18n-moveUp="kendo.listbox.moveUp|The title of the Move Up button" moveUp="Move Up" i18n-moveDown="kendo.listbox.moveDown|The title of the Move Down button" moveDown="Move Down" i18n-transferTo="kendo.listbox.transferTo|The title of the Transfer To button" transferTo="Transfer To" i18n-transferAllTo="kendo.listbox.transferAllTo|The title of the Transfer All To button" transferAllTo="Transfer All To" i18n-transferFrom="kendo.listbox.transferFrom|The title of the Transfer From button" transferFrom="Transfer From" i18n-transferAllFrom="kendo.listbox.transferAllFrom|The title of the Transfer All From button" transferAllFrom="Transfer All From" i18n-remove="kendo.listbox.remove|The title of the Remove button" remove="Remove" i18n-noDataText="kendo.listbox.noDataText|The text displayed when there are no items" noDataText="No data found." > </ng-container> <div #toolbar class="k-listbox-actions" *ngIf="selectedTools.length > 0" role="toolbar" [attr.aria-label]="listboxToolbarLabel" > <button #tools *ngFor="let tool of selectedTools; let i = index" kendoButton [attr.tabindex]="i === 0 ? '0' : '-1'" [size]="this.size" [icon]="toolIcon(tool.icon)" [svgIcon]="toolSVGIcon(tool.svgIcon)" [attr.title]="messageFor(tool.name)" (click)="performAction(tool.name)" role="button" type="button" ></button> </div> <div class="k-list-scroller k-selectable"> <div class="{{ listClasses }}"> <div *ngIf="data.length > 0" class="k-list-content" > <ul #listbox class="k-list-ul" role="listbox" [attr.aria-label]="listboxLabel" [attr.aria-multiselectable]="false" > <li #listboxItems *ngFor="let item of data; let i = index" kendoListBoxItemSelectable class="k-list-item" [attr.tabindex]="i === 0 ? '0' : '-1'" role="option" [attr.aria-selected]="selectedIndex === i" [index]="i" [class.k-disabled]="itemDisabled(item)" > <ng-template *ngIf="itemTemplate; else defaultItemTemplate" [templateContext]="{ templateRef: itemTemplate.templateRef, $implicit: item }" > </ng-template> <ng-template #defaultItemTemplate> <span class="k-list-item-text">{{ getText(item) }}</span> </ng-template> </li> </ul> </div> <span *ngIf="data.length === 0" class="k-nodata" >{{ messageFor('noDataText') }}</span> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoListBoxLocalizedMessages]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: ItemSelectableDirective, selector: "[kendoListBoxItemSelectable]", inputs: ["index"] }, { kind: "directive", type: TemplateContextDirective, selector: "[templateContext]", inputs: ["templateContext"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListBoxComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-listbox', providers: [ ListBoxSelectionService, KeyboardNavigationService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.listbox' }, ], template: ` <ng-container kendoListBoxLocalizedMessages i18n-moveUp="kendo.listbox.moveUp|The title of the Move Up button" moveUp="Move Up" i18n-moveDown="kendo.listbox.moveDown|The title of the Move Down button" moveDown="Move Down" i18n-transferTo="kendo.listbox.transferTo|The title of the Transfer To button" transferTo="Transfer To" i18n-transferAllTo="kendo.listbox.transferAllTo|The title of the Transfer All To button" transferAllTo="Transfer All To" i18n-transferFrom="kendo.listbox.transferFrom|The title of the Transfer From button" transferFrom="Transfer From" i18n-transferAllFrom="kendo.listbox.transferAllFrom|The title of the Transfer All From button" transferAllFrom="Transfer All From" i18n-remove="kendo.listbox.remove|The title of the Remove button" remove="Remove" i18n-noDataText="kendo.listbox.noDataText|The text displayed when there are no items" noDataText="No data found." > </ng-container> <div #toolbar class="k-listbox-actions" *ngIf="selectedTools.length > 0" role="toolbar" [attr.aria-label]="listboxToolbarLabel" > <button #tools *ngFor="let tool of selectedTools; let i = index" kendoButton [attr.tabindex]="i === 0 ? '0' : '-1'" [size]="this.size" [icon]="toolIcon(tool.icon)" [svgIcon]="toolSVGIcon(tool.svgIcon)" [attr.title]="messageFor(tool.name)" (click)="performAction(tool.name)" role="button" type="button" ></button> </div> <div class="k-list-scroller k-selectable"> <div class="{{ listClasses }}"> <div *ngIf="data.length > 0" class="k-list-content" > <ul #listbox class="k-list-ul" role="listbox" [attr.aria-label]="listboxLabel" [attr.aria-multiselectable]="false" > <li #listboxItems *ngFor="let item of data; let i = index" kendoListBoxItemSelectable class="k-list-item" [attr.tabindex]="i === 0 ? '0' : '-1'" role="option" [attr.aria-selected]="selectedIndex === i" [index]="i" [class.k-disabled]="itemDisabled(item)" > <ng-template *ngIf="itemTemplate; else defaultItemTemplate" [templateContext]="{ templateRef: itemTemplate.templateRef, $implicit: item }" > </ng-template> <ng-template #defaultItemTemplate> <span class="k-list-item-text">{{ getText(item) }}</span> </ng-template> </li> </ul> </div> <span *ngIf="data.length === 0" class="k-nodata" >{{ messageFor('noDataText') }}</span> </div> </div> `, standalone: true, imports: [LocalizedMessagesDirective, NgIf, NgFor, ButtonComponent, ItemSelectableDirective, TemplateContextDirective] }] }], ctorParameters: function () { return [{ type: i1.KeyboardNavigationService }, { type: i2.ListBoxSelectionService }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i3.LocalizationService }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { listboxClassName: [{ type: HostBinding, args: ['class.k-listbox'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], itemTemplate: [{ type: ContentChild, args: [ItemTemplateDirective] }], listboxElement: [{ type: ViewChild, args: ['listbox'] }], listboxItems: [{ type: ViewChildren, args: ['listboxItems'] }], toolbarElement: [{ type: ViewChild, args: ['toolbar'] }], tools: [{ type: ViewChildren, args: ['tools'] }], textField: [{ type: Input }], data: [{ type: Input }], size: [{ type: Input }], toolbar: [{ type: Input }], listboxLabel: [{ type: Input }], listboxToolbarLabel: [{ type: Input }], itemDisabled: [{ type: Input }], selectionChange: [{ type: Output }], actionClick: [{ type: Output }], getChildListbox: [{ type: Output }] } });