@progress/kendo-angular-listbox
Version:
Kendo UI for Angular ListBox
281 lines (280 loc) • 12.8 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 { 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
}] } });