From 9044236874dcba9d7e9b540aed8e66cc2881e7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20de=20l=27Hamaide?= Date: Sat, 12 Apr 2025 19:38:13 +0200 Subject: [PATCH] feat(module:transfer): support multiple row selection with Shift key --- components/transfer/transfer.component.ts | 46 ++++++++++++++++++--- components/transfer/transfer.spec.ts | 49 +++++++++++------------ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/components/transfer/transfer.component.ts b/components/transfer/transfer.component.ts index 97e7d8014bc..985c20fca0e 100644 --- a/components/transfer/transfer.component.ts +++ b/components/transfer/transfer.component.ts @@ -10,6 +10,7 @@ import { Component, ElementRef, EventEmitter, + HostListener, Input, OnChanges, OnDestroy, @@ -220,9 +221,31 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { // left leftDataSource: TransferItem[] = []; + lastLeftCheckedIndex?: number; // right rightDataSource: TransferItem[] = []; + lastRightCheckedIndex?: number; + + isShiftPressed = false; + + @HostListener('window:keydown.shift') + onTriggerShiftDown(): void { + this.isShiftPressed = true; + } + + @HostListener('window:keyup.shift') + onTriggerShiftUp(): void { + this.isShiftPressed = false; + } + + @HostListener('mousedown', ['$event']) + onTriggerMouseDown(event: MouseEvent): void { + const isInsideTransfer = (event.target as HTMLElement).closest('.ant-transfer-list'); + if (event.shiftKey && isInsideTransfer) { + event.preventDefault(); + } + } private splitDataSource(): void { this.leftDataSource = []; @@ -249,6 +272,23 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { handleRightSelect = (item: TransferItem): void => this.handleSelect('right', !!item.checked, item); handleSelect(direction: TransferDirection, checked: boolean, item?: TransferItem): void { + if (item) { + const datasource = direction === 'left' ? this.leftDataSource : this.rightDataSource; + const currentIndex = datasource.findIndex(i => i.key === item.key); + const lastCheckedIndex = this[direction === 'left' ? 'lastLeftCheckedIndex' : 'lastRightCheckedIndex'] ?? -1; + if (this.isShiftPressed && lastCheckedIndex > -1) { + const start = Math.min(lastCheckedIndex, currentIndex); + const end = Math.max(lastCheckedIndex, currentIndex); + for (let i = start; i <= end; i++) { + const item = datasource[i]; + if (!item.disabled) { + item.checked = checked; + } + } + this.markForCheckAllList(); + } + this[direction === 'left' ? 'lastLeftCheckedIndex' : 'lastRightCheckedIndex'] = currentIndex; + } const list = this.getCheckedData(direction); const count = list.filter(i => !i.disabled).length; this.updateOperationStatus(direction, count); @@ -301,11 +341,7 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { } targetDatasource.splice(0, 0, ...list); this.updateOperationStatus(oppositeDirection); - this.nzChange.emit({ - from: oppositeDirection, - to: direction, - list - }); + this.nzChange.emit({ from: oppositeDirection, to: direction, list }); this.markForCheckAllList(); } diff --git a/components/transfer/transfer.spec.ts b/components/transfer/transfer.spec.ts index b2403c49de5..2ecdd4b7964 100644 --- a/components/transfer/transfer.spec.ts +++ b/components/transfer/transfer.spec.ts @@ -39,9 +39,7 @@ describe('transfer', () => { let pageObject: TransferPageObject; beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideNzIconsTesting(), provideNoopAnimations()] - }); + TestBed.configureTestingModule({ providers: [provideNzIconsTesting(), provideNoopAnimations()] }); fixture = TestBed.createComponent(TestTransferComponent); debugElement = fixture.debugElement; instance = debugElement.componentInstance; @@ -210,6 +208,22 @@ describe('transfer', () => { expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(0); }); + it('should be checkboxes are toggle select via shift key', () => { + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(0); + pageObject.checkItem('right', 0); + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(1); + window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift' })); + expect(instance.comp.isShiftPressed).toBeTrue(); + fixture.detectChanges(); + const multiSelectEndIndex = 9; + pageObject.checkItem('right', multiSelectEndIndex); + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe( + COUNT - LEFTCOUNT - DISABLED - multiSelectEndIndex + 1 + ); + window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Shift' })); + expect(instance.comp.isShiftPressed).toBeFalse(); + }); + describe('#notFoundContent', () => { it('should be the left and right list have data', () => { instance.nzDataSource = [{ title: `content0`, direction: 'right' }, { title: `content1` }]; @@ -680,22 +694,12 @@ class TestTransferComponent implements OnInit, AbstractTestTransferComponent { }) class TestTransferCustomRenderComponent implements OnInit, AbstractTestTransferComponent { @ViewChild('comp', { static: false }) comp!: NzTransferComponent; - nzDataSource: Array<{ - key: string; - title: string; - description: string; - direction: TransferDirection; - icon: string; - }> = []; + nzDataSource: Array<{ key: string; title: string; description: string; direction: TransferDirection; icon: string }> = + []; ngOnInit(): void { - const ret: Array<{ - key: string; - title: string; - description: string; - direction: TransferDirection; - icon: string; - }> = []; + const ret: Array<{ key: string; title: string; description: string; direction: TransferDirection; icon: string }> = + []; for (let i = 0; i < COUNT; i++) { ret.push({ key: i.toString(), @@ -710,21 +714,14 @@ class TestTransferCustomRenderComponent implements OnInit, AbstractTestTransferC } // https://github.com/NG-ZORRO/ng-zorro-antd/issues/996 -@Component({ - imports: [NzTransferModule], - template: `` -}) +@Component({ imports: [NzTransferModule], template: `` }) class Test996Component implements OnInit { @ViewChild(NzTransferComponent, { static: true }) comp!: NzTransferComponent; list: NzSafeAny[] = []; ngOnInit(): void { for (let i = 0; i < 2; i++) { - this.list.push({ - key: i.toString(), - title: `content${i + 1}`, - disabled: i % 3 < 1 - }); + this.list.push({ key: i.toString(), title: `content${i + 1}`, disabled: i % 3 < 1 }); } [0, 1].forEach(idx => (this.list[idx].direction = 'right'));