From 48edc5a463eef55585e36a8b69f88bc57b6171d2 Mon Sep 17 00:00:00 2001 From: 3720 Date: Tue, 30 Jul 2024 19:15:08 +0800 Subject: [PATCH] fix(database): cell selection doesn't support multiple cells with keyboard (#7769) --- .../view/presets/table/controller/hotkeys.ts | 44 ++++- .../presets/table/controller/selection.ts | 182 +++++++++++++++++- 2 files changed, 217 insertions(+), 9 deletions(-) diff --git a/packages/blocks/src/database-block/data-view/view/presets/table/controller/hotkeys.ts b/packages/blocks/src/database-block/data-view/view/presets/table/controller/hotkeys.ts index 14cd211017ba..f4be4f6e3cf4 100644 --- a/packages/blocks/src/database-block/data-view/view/presets/table/controller/hotkeys.ts +++ b/packages/blocks/src/database-block/data-view/view/presets/table/controller/hotkeys.ts @@ -194,34 +194,64 @@ export class TableHotkeysController implements ReactiveController { }, 'Shift-ArrowUp': context => { + const selection = this.selectionController.selection; + if (!selection || selection.isEditing) { + return false; + } + + if (this.selectionController.isSelectedRowOnly()) { + this.selectionController.navigateRowSelection('up', true); + } else { + this.selectionController.selectionAreaUp(); + } + + context.get('keyboardState').raw.preventDefault(); + return true; + }, + + 'Shift-ArrowDown': context => { + const selection = this.selectionController.selection; + if (!selection || selection.isEditing) { + return false; + } + + if (this.selectionController.isSelectedRowOnly()) { + this.selectionController.navigateRowSelection('down', true); + } else { + this.selectionController.selectionAreaDown(); + } + + context.get('keyboardState').raw.preventDefault(); + return true; + }, + + 'Shift-ArrowLeft': context => { const selection = this.selectionController.selection; if ( !selection || selection.isEditing || - !this.selectionController.isSelectedRowOnly() + this.selectionController.isSelectedRowOnly() ) { return false; } - if (this.selectionController.isSelectedRowOnly()) - this.selectionController.navigateRowSelection('up', true); + this.selectionController.selectionAreaLeft(); context.get('keyboardState').raw.preventDefault(); return true; }, - 'Shift-ArrowDown': context => { + 'Shift-ArrowRight': context => { const selection = this.selectionController.selection; if ( !selection || selection.isEditing || - !this.selectionController.isSelectedRowOnly() + this.selectionController.isSelectedRowOnly() ) { return false; } - if (this.selectionController.isSelectedRowOnly()) - this.selectionController.navigateRowSelection('down', true); + this.selectionController.selectionAreaRight(); context.get('keyboardState').raw.preventDefault(); return true; diff --git a/packages/blocks/src/database-block/data-view/view/presets/table/controller/selection.ts b/packages/blocks/src/database-block/data-view/view/presets/table/controller/selection.ts index 24b08562d4ee..7a9bd68394aa 100644 --- a/packages/blocks/src/database-block/data-view/view/presets/table/controller/selection.ts +++ b/packages/blocks/src/database-block/data-view/view/presets/table/controller/selection.ts @@ -23,6 +23,8 @@ import { fillSelectionWithFocusCellData, } from './drag-to-fill.js'; +type TableViewSelectionSetOptions = Omit; + export class TableSelectionController implements ReactiveController { __dragToFillElement = new DragToFillElement(); @@ -306,6 +308,21 @@ export class TableSelectionController implements ReactiveController { }; } + focusToArea(selection: TableViewSelection) { + return { + ...selection, + rowsSelection: selection.rowsSelection ?? { + start: selection.focus.rowIndex, + end: selection.focus.rowIndex, + }, + columnsSelection: selection.columnsSelection ?? { + start: selection.focus.columnIndex, + end: selection.focus.columnIndex, + }, + isEditing: false, + } satisfies TableViewSelectionSetOptions; + } + focusToCell(position: 'left' | 'right' | 'up' | 'down') { if (!this.selection) { return; @@ -403,6 +420,12 @@ export class TableSelectionController implements ReactiveController { }; } + getSelectionAreaBorder(position: 'left' | 'right' | 'top' | 'bottom') { + return this.__selectionElement.selectionRef.value?.querySelector( + `.area-border.area-${position}` + ); + } + hostConnected() { requestAnimationFrame(() => { this.tableContainer.append(this.__selectionElement); @@ -562,6 +585,132 @@ export class TableSelectionController implements ReactiveController { }; } + selectionAreaDown() { + if (!this.selection) { + return; + } + const newSelection = this.focusToArea(this.selection); + if (newSelection.rowsSelection.start === newSelection.focus.rowIndex) { + newSelection.rowsSelection.end = Math.min( + this.rows(newSelection.groupKey).length - 1, + newSelection.rowsSelection.end + 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('bottom')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.rowsSelection.start += 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('top')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaLeft() { + if (!this.selection) { + return; + } + const newSelection = this.focusToArea(this.selection); + if (newSelection.columnsSelection.end === newSelection.focus.columnIndex) { + newSelection.columnsSelection.start = Math.max( + 0, + newSelection.columnsSelection.start - 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('left')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.columnsSelection.end -= 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('right')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaRight() { + if (!this.selection) { + return; + } + const newSelection = this.focusToArea(this.selection); + if ( + newSelection.columnsSelection.start === newSelection.focus.columnIndex + ) { + const max = + this.rows(newSelection.groupKey) + ?.item(0) + .querySelectorAll('affine-database-cell-container').length - 1; + newSelection.columnsSelection.end = Math.min( + max, + newSelection.columnsSelection.end + 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('right')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.columnsSelection.start += 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('left')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaUp() { + if (!this.selection) { + return; + } + const newSelection = this.focusToArea(this.selection); + if (newSelection.rowsSelection.end === newSelection.focus.rowIndex) { + newSelection.rowsSelection.start = Math.max( + 0, + newSelection.rowsSelection.start - 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('top')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.rowsSelection.end -= 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('bottom')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + startDrag( evt: PointerEvent, cell: DatabaseCellContainer, @@ -814,7 +963,7 @@ export class TableSelectionController implements ReactiveController { return this._tableViewSelection; } - set selection(data: Omit | undefined) { + set selection(data: TableViewSelectionSetOptions | undefined) { if (!data) { this.clearSelection(); return; @@ -879,6 +1028,30 @@ class SelectionElement extends ShadowlessElement { outline: none; } + .area-border { + position: absolute; + pointer-events: none; + } + .area-left { + left: 0; + height: 100%; + width: 1px; + } + .area-right { + right: 0; + height: 100%; + width: 1px; + } + .area-top { + top: 0; + width: 100%; + height: 1px; + } + .area-bottom { + bottom: 0; + width: 100%; + height: 1px; + } @media print { affine-database-selection { display: none; @@ -892,7 +1065,12 @@ class SelectionElement extends ShadowlessElement { override render() { return html` -
+
+
+
+
+
+
`; }