UNPKG

@progress/kendo-angular-listbox

Version:
281 lines (280 loc) 12.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Directive, Input, NgZone } from '@angular/core'; import { isChanged } from '@progress/kendo-angular-common'; import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { ListBoxComponent } from './listbox.component'; import { isPresent } from './util'; import * as i0 from "@angular/core"; import * as i1 from "./listbox.component"; /** * Represents the data-binding directive for the Kendo UI ListBox for Angular. * Manages the functionality of the ListBox tools out of the box and modifies the provided data accordingly. * * @example * ```typescript * @Component({ * selector: 'my-app', * template: ` * <kendo-listbox * kendoListBoxDataBinding * [connectedWith]="targetListBox" * [data]="sourceData"> * </kendo-listbox> * ` * }) * export class AppComponent { } * ``` * * @remarks * Applied to: {@link ListBoxComponent}. */ export class DataBindingDirective { listbox; zone; /** * Specifies the `ListBoxComponent` instance with which the current ListBox connects. * When you link two listboxes through this input, you can transfer items between them. */ connectedWith; actionSub = new Subscription(); selectedBoxSub = new Subscription(); connectedWithSub = new Subscription(); selectedBox; constructor(listbox, zone) { this.listbox = listbox; this.zone = zone; this.selectedBox = this.listbox; this.connectedWithSub.add(this.listbox.getChildListbox.subscribe(() => { this.listbox.childListbox = this.connectedWith; })); this.actionSub.add(this.listbox.action.subscribe((actionName) => { switch (actionName) { case 'moveUp': { this.moveVertically('up'); break; } case 'moveDown': { this.moveVertically('down'); break; } case 'transferFrom': { this.transferSelectedItems(this.connectedWith, this.listbox); break; } case 'transferTo': { this.transferSelectedItems(this.listbox, this.connectedWith); break; } case 'transferAllTo': { this.transferAll(this.listbox, this.connectedWith); break; } case 'transferAllFrom': { this.transferAll(this.connectedWith, this.listbox); break; } case 'remove': { this.removeSelectedItems(); break; } default: { break; } } })); } /** * @hidden */ ngOnChanges(changes) { if (isChanged('connectedWith', changes, false)) { if (!changes['connectedWith'].firstChange) { this.selectedBoxSub.unsubscribe(); this.selectedBoxSub = new Subscription(); } this.selectedBoxSub.add(this.listbox.selectionChange.subscribe(() => { this.selectedBox = this.listbox; const connectedNavService = this.connectedWith.keyboardNavigationService; const connectedSelService = this.connectedWith.selectionService; let lastSelectedIndex = 0; if (connectedSelService.selectedIndices.length > 0) { lastSelectedIndex = connectedSelService.rangeSelectionTargetIndex ?? connectedSelService.lastSelectedOrUnselectedIndex ?? 0; } this.connectedWith.clearSelection(); if (this.connectedWith.data?.length > 0) { const validIndex = Math.min(lastSelectedIndex, this.connectedWith.data.length - 1); this.updateListBoxIndices(connectedNavService, connectedSelService, validIndex, false); } else { this.updateListBoxIndices(connectedNavService, connectedSelService, 0, false); } })); this.selectedBoxSub.add(this.connectedWith.selectionChange.subscribe(() => { this.selectedBox = this.connectedWith; const listboxNavService = this.listbox.keyboardNavigationService; const listboxSelService = this.listbox.selectionService; let lastSelectedIndex = 0; if (listboxSelService.selectedIndices.length > 0) { lastSelectedIndex = listboxSelService.rangeSelectionTargetIndex ?? listboxSelService.lastSelectedOrUnselectedIndex ?? 0; } this.listbox.clearSelection(); if (this.listbox.data?.length > 0) { const validIndex = Math.min(lastSelectedIndex, this.listbox.data.length - 1); this.updateListBoxIndices(listboxNavService, listboxSelService, validIndex, false); } else { this.updateListBoxIndices(listboxNavService, listboxSelService, 0, false); } })); } } /** * @hidden */ ngOnDestroy() { if (this.actionSub) { this.actionSub.unsubscribe(); this.actionSub = null; } if (this.selectedBoxSub) { this.selectedBoxSub.unsubscribe(); this.selectedBoxSub = null; } } moveVertically(dir) { const selectedIndices = this.selectedBox.selectedIndices; if (!isPresent(selectedIndices) || selectedIndices.length === 0) { return; } const sortedIndices = [...selectedIndices].sort((a, b) => a - b); const topIndex = sortedIndices[0]; const bottomIndex = sortedIndices[sortedIndices.length - 1]; const topReached = dir === 'up' && topIndex <= 0; const bottomReached = dir === 'down' && bottomIndex >= this.selectedBox.data.length - 1; if (topReached || bottomReached) { return; } const data = this.selectedBox.data; const newSelectedIndices = []; if (dir === 'up') { for (const index of sortedIndices) { const newIndex = index - 1; [data[newIndex], data[index]] = [data[index], data[newIndex]]; newSelectedIndices.push(newIndex); } } else { for (let i = sortedIndices.length - 1; i >= 0; i--) { const index = sortedIndices[i]; const newIndex = index + 1; [data[newIndex], data[index]] = [data[index], data[newIndex]]; newSelectedIndices.push(newIndex); } } newSelectedIndices.sort((a, b) => a - b); this.selectedBox.selectionService.setSelectedIndices(newSelectedIndices); const navigation = this.selectedBox.keyboardNavigationService; const currentFocusedIndex = navigation.focusedListboxItemIndex; const focusedItemIndexInSelection = sortedIndices.indexOf(currentFocusedIndex); let newFocusIndex; if (focusedItemIndexInSelection !== -1) { newFocusIndex = newSelectedIndices[focusedItemIndexInSelection]; } else { newFocusIndex = dir === 'up' ? topIndex - 1 : bottomIndex + 1; } this.zone.onStable.pipe(take(1)).subscribe(() => { const listboxItems = this.selectedBox.listboxItems.toArray(); const previousItem = listboxItems[currentFocusedIndex]?.nativeElement; const currentItem = listboxItems[newFocusIndex]?.nativeElement; navigation.changeTabindex(previousItem, currentItem); navigation.focusedListboxItemIndex = newFocusIndex; navigation.selectedListboxItemIndex = newFocusIndex; }); } removeSelectedItems() { const itemIndices = this.selectedBox.selectedIndices; if (!isPresent(itemIndices) || itemIndices.length === 0) { return; } this.selectedBox.data = this.selectedBox.data.filter((_, index) => !itemIndices.includes(index)); this.selectedBox.selectionService.clearSelection(); } transferSelectedItems(source, target) { const selectedIndices = source?.data && source?.selectedIndices; if (!target || !source || !isPresent(selectedIndices) || selectedIndices.length === 0) { return; } const sourceLastIndex = source.selectionService.rangeSelectionTargetIndex ?? source.selectionService.lastSelectedOrUnselectedIndex ?? 0; target.data.push(...selectedIndices.map(index => source.data[index])); source.data = source.data.filter((_, index) => !selectedIndices.includes(index)); source.clearSelection(); const removedBeforeAnchor = selectedIndices.filter(i => i < sourceLastIndex).length; const adjustedAnchorIndex = Math.max(0, Math.min(sourceLastIndex - removedBeforeAnchor, source.data.length - 1)); const sourceNavService = source.keyboardNavigationService; const sourceSelService = source.selectionService; if (source.data.length > 0) { this.updateListBoxIndices(sourceNavService, sourceSelService, adjustedAnchorIndex); } const targetIndex = target.data.length - 1; target.select([targetIndex]); const targetNavService = target.keyboardNavigationService; const targetSelService = target.selectionService; this.updateListBoxIndices(targetNavService, targetSelService, targetIndex); this.selectedBox = target; } transferAll(source, target) { if (!target || !source || source.data?.length === 0) { return; } const itemsToTransfer = source.data.filter((item) => !source.itemDisabled(item)); if (itemsToTransfer.length === 0) { return; } source.data = source.data.filter((item) => source.itemDisabled(item)); target.data.push(...itemsToTransfer); source.clearSelection(); const sourceNavService = source.keyboardNavigationService; const sourceSelService = source.selectionService; if (source.data.length === 0) { this.updateListBoxIndices(sourceNavService, sourceSelService, 0); } else { sourceNavService.focusedListboxItemIndex = 0; sourceNavService.selectedListboxItemIndex = -1; sourceSelService.rangeSelectionAnchorIndex = null; sourceSelService.lastSelectedOrUnselectedIndex = null; } const targetIndex = target.data.length - 1; target.select([targetIndex]); const targetNavService = target.keyboardNavigationService; const targetSelService = target.selectionService; this.updateListBoxIndices(targetNavService, targetSelService, targetIndex); this.selectedBox = target; } updateListBoxIndices = (keyboardNavService, selectionService, index, setFocusedIndex = true) => { if (setFocusedIndex) { keyboardNavService.focusedListboxItemIndex = index; } keyboardNavService.selectedListboxItemIndex = index; selectionService.rangeSelectionAnchorIndex = index; selectionService.lastSelectedOrUnselectedIndex = index; }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBindingDirective, deps: [{ token: i1.ListBoxComponent }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: DataBindingDirective, isStandalone: true, selector: "[kendoListBoxDataBinding]", inputs: { connectedWith: "connectedWith" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBindingDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListBoxDataBinding]', standalone: true }] }], ctorParameters: () => [{ type: i1.ListBoxComponent }, { type: i0.NgZone }], propDecorators: { connectedWith: [{ type: Input }] } });