From 70f6b3e26b13a4c6219354d8326ee7ce1b4a47d6 Mon Sep 17 00:00:00 2001 From: minlovehua <321512939@qq.com> Date: Fri, 3 Jan 2025 14:18:53 +0800 Subject: [PATCH] feat(grid): support drag select cells and support right click selected areas to delete records --- .../context-menu/context-menu.component.ts | 17 +----- packages/grid/src/core/types/ai-table.ts | 23 ++++---- packages/grid/src/core/utils/common.ts | 3 +- packages/grid/src/dom-grid.component.html | 8 +-- packages/grid/src/grid.component.html | 1 + packages/grid/src/grid.component.ts | 40 ++++++++++++-- .../creations/create-active-cell-border.ts | 6 ++- .../src/renderer/creations/create-cells.ts | 12 ++++- .../grid/src/renderer/renderer.component.html | 1 + .../grid/src/renderer/renderer.component.ts | 6 +++ .../grid/src/services/selection.service.ts | 54 +++++++++++++++---- packages/grid/src/types/grid.ts | 5 +- .../state/src/constants/context-menu-item.ts | 8 +-- 13 files changed, 128 insertions(+), 56 deletions(-) diff --git a/packages/grid/src/components/context-menu/context-menu.component.ts b/packages/grid/src/components/context-menu/context-menu.component.ts index 57b9c1ee..77eb92ff 100644 --- a/packages/grid/src/components/context-menu/context-menu.component.ts +++ b/packages/grid/src/components/context-menu/context-menu.component.ts @@ -1,12 +1,6 @@ import { NgClass } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; -import { - ThyDropdownAbstractMenu, - ThyDropdownMenuComponent, - ThyDropdownMenuItemDirective, - ThyDropdownMenuItemIconDirective, - ThyDropdownMenuItemNameDirective -} from 'ngx-tethys/dropdown'; +import { ThyDropdownAbstractMenu, ThyDropdownMenuItemDirective } from 'ngx-tethys/dropdown'; import { ThyIcon } from 'ngx-tethys/icon'; import { AITable } from '../../core'; import { AITableContextMenuItem } from '../../types'; @@ -20,14 +14,7 @@ import { AITableGridSelectionService } from '../../services/selection.service'; host: { class: 'context-menu' }, - imports: [ - ThyDropdownMenuComponent, - ThyDropdownMenuItemDirective, - ThyDropdownMenuItemNameDirective, - ThyDropdownMenuItemIconDirective, - ThyIcon, - NgClass - ] + imports: [ThyDropdownMenuItemDirective, ThyIcon, NgClass] }) export class AITableContextMenu extends ThyDropdownAbstractMenu { private aiTableGridSelectionService = inject(AITableGridSelectionService); diff --git a/packages/grid/src/core/types/ai-table.ts b/packages/grid/src/core/types/ai-table.ts index c2b1f3fb..b0b2aef1 100644 --- a/packages/grid/src/core/types/ai-table.ts +++ b/packages/grid/src/core/types/ai-table.ts @@ -30,19 +30,18 @@ export const AITable = { return aiTable.records(); }, getActiveCell(aiTable: AITable): { recordId: string; fieldId: string } | null { - const selection = aiTable.selection(); - let selectedCells = []; - for (let [recordId, fieldIds] of selection.selectedCells.entries()) { - for (let fieldId of Object.keys(fieldIds)) { - if ((fieldIds as { [key: string]: boolean })[fieldId]) { - selectedCells.push({ - recordId, - fieldId - }); - } - } + return aiTable.selection().activeCell; + }, + getSelectedRecordIds(aiTable: AITable): string[] { + const selectedRecords = aiTable.selection().selectedRecords; + const selectedCells = aiTable.selection().selectedCells; + if (selectedRecords.size > 0) { + return [...selectedRecords.keys()]; + } else if (selectedCells.size > 0) { + return [...selectedCells].map((item) => item.split(':')[0]); + } else { + return []; } - return selectedCells ? selectedCells[0] : null; }, isCellVisible(aiTable: AITable, cell: { recordId: string; fieldId: string }) { const visibleRowIndexMap = aiTable.context!.visibleRowsIndexMap(); diff --git a/packages/grid/src/core/utils/common.ts b/packages/grid/src/core/utils/common.ts index 0a318c3c..9ffb67eb 100644 --- a/packages/grid/src/core/utils/common.ts +++ b/packages/grid/src/core/utils/common.ts @@ -8,7 +8,8 @@ export function createAITable(records: WritableSignal, fields: W selection: signal({ selectedRecords: new Map(), selectedFields: new Map(), - selectedCells: new Map() + selectedCells: new Set(), + activeCell: null }), matchedCells: signal([]), recordsMap: computed(() => { diff --git a/packages/grid/src/dom-grid.component.html b/packages/grid/src/dom-grid.component.html index eb946a84..ce74c621 100644 --- a/packages/grid/src/dom-grid.component.html +++ b/packages/grid/src/dom-grid.component.html @@ -44,13 +44,13 @@ @for (field of gridData().fields; track field._id) { +
) { + this.setDragStatus(false, null); + } + stageContextmenu(e: KoEventObject) { const mouseEvent = e.event.evt; mouseEvent.preventDefault(); @@ -412,15 +431,30 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy { (e) => e.target instanceof Element && !this.containerElement().contains(e.target) && - !(e.target.closest(AI_TABLE_PROHIBIT_CLEAR_SELECTION_CLASS) && this.aiTable.selection().selectedRecords.size > 0) + !( + e.target.closest(AI_TABLE_PROHIBIT_CLEAR_SELECTION_CLASS) && + AITable.getSelectedRecordIds(this.aiTable).length > 0 + ) ), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { + this.setDragStatus(false, null); this.aiTableGridSelectionService.clearSelection(); }); } + private setDragStatus( + isDragging: boolean, + startCell: { + recordId: string; + fieldId: string; + } | null + ) { + this.isDragSelecting = isDragging; + this.dragStartCell = startCell; + } + private resetScrolling = () => { this.aiTable.context!.setScrollState({ isScrolling: false diff --git a/packages/grid/src/renderer/creations/create-active-cell-border.ts b/packages/grid/src/renderer/creations/create-active-cell-border.ts index 4691b98b..257481f6 100644 --- a/packages/grid/src/renderer/creations/create-active-cell-border.ts +++ b/packages/grid/src/renderer/creations/create-active-cell-border.ts @@ -16,7 +16,11 @@ export const createActiveCellBorder = (config: AITableCellsConfig) => { let activeCellBorder: RectConfig | null = null; let frozenActiveCellBorder: RectConfig | null = null; - if (activeCell != null) { + if ( + activeCell != null && + aiTable.context!.visibleRowsIndexMap().has(activeCell.recordId) && + aiTable.context!.visibleColumnsMap().has(activeCell.fieldId) + ) { const { fieldId } = activeCell; const { rowIndex, columnIndex } = AITable.getCellIndex(aiTable, activeCell)!; diff --git a/packages/grid/src/renderer/creations/create-cells.ts b/packages/grid/src/renderer/creations/create-cells.ts index 13b50602..44f9030d 100644 --- a/packages/grid/src/renderer/creations/create-cells.ts +++ b/packages/grid/src/renderer/creations/create-cells.ts @@ -82,17 +82,25 @@ export const createCells = (config: AITableCellsDrawerConfig) => { const isSelected = aiTable.selection().selectedFields.has(fieldId); const isHoverRow = isHover && targetName !== AI_TABLE_FIELD_HEAD; const activeCell = AITable.getActiveCell(aiTable); - const isSiblingActiveCell = recordId === activeCell?.recordId && fieldId !== activeCell?.fieldId; - const isActiveCell = recordId === activeCell?.recordId; + const selectedCells = aiTable.selection().selectedCells; + const isSiblingActiveCell = + selectedCells.size === 1 && + selectedCells.has(`${activeCell?.recordId}:${activeCell?.fieldId}`) && + recordId === activeCell?.recordId && + fieldId !== activeCell?.fieldId; + const isActiveCell = recordId === activeCell?.recordId && fieldId === activeCell?.fieldId; let matchedCellsMap: { [key: string]: boolean } = {}; aiTable.matchedCells().forEach((key) => { matchedCellsMap[key] = true; }); const isMatchedCell = matchedCellsMap[`${recordId}-${fieldId}`]; + const isSelectedCell = selectedCells.has(`${recordId}:${fieldId}`); if (isMatchedCell) { background = colors.itemMatchBgColor; + } else if (isSelectedCell && !isActiveCell) { + background = colors.itemActiveBgColor; } else if (isCheckedRow || isSelected || isSiblingActiveCell) { background = colors.itemActiveBgColor; } else if (isHoverRow && !isActiveCell) { diff --git a/packages/grid/src/renderer/renderer.component.html b/packages/grid/src/renderer/renderer.component.html index 01b9fa72..24d04271 100644 --- a/packages/grid/src/renderer/renderer.component.html +++ b/packages/grid/src/renderer/renderer.component.html @@ -2,6 +2,7 @@ [config]="stageConfig()" (koMousemove)="stageMousemove($event)" (koMousedown)="stageMousedown($event)" + (koMouseup)="stageMouseup($event)" (koContextmenu)="stageContextmenu($event)" (koClick)="stageClick($event)" (koDblclick)="stageDblclick($event)" diff --git a/packages/grid/src/renderer/renderer.component.ts b/packages/grid/src/renderer/renderer.component.ts index dbf5847b..988c2d16 100644 --- a/packages/grid/src/renderer/renderer.component.ts +++ b/packages/grid/src/renderer/renderer.component.ts @@ -48,6 +48,8 @@ export class AITableRenderer { koMousedown = output>(); + koMouseup = output>(); + koContextmenu = output>(); koWheel = output>(); @@ -209,6 +211,10 @@ export class AITableRenderer { this.koMousedown.emit(e as KoEventObject); } + stageMouseup(e: KoEventObject) { + this.koMouseup.emit(e as KoEventObject); + } + stageContextmenu(e: KoEventObject) { this.koContextmenu.emit(e as KoEventObject); } diff --git a/packages/grid/src/services/selection.service.ts b/packages/grid/src/services/selection.service.ts index c9de4ef4..29f9ffd3 100644 --- a/packages/grid/src/services/selection.service.ts +++ b/packages/grid/src/services/selection.service.ts @@ -15,17 +15,13 @@ export class AITableGridSelectionService { this.aiTable.selection.set({ selectedRecords: new Map(), selectedFields: new Map(), - selectedCells: new Map() + selectedCells: new Set(), + activeCell: null }); } - selectCell(recordId: string, fieldId: string) { - const fields = this.aiTable.selection().selectedCells.get(recordId); - if (fields?.hasOwnProperty(fieldId)) { - return; - } - this.clearSelection(); - this.aiTable.selection().selectedCells.set(recordId, { [fieldId]: true }); + setActiveCell(recordId: string, fieldId: string) { + this.aiTable.selection().activeCell = { recordId, fieldId }; } selectField(fieldId: string) { @@ -45,7 +41,8 @@ export class AITableGridSelectionService { this.aiTable.selection.set({ selectedRecords: this.aiTable.selection().selectedRecords, selectedFields: new Map(), - selectedCells: new Map() + selectedCells: new Set(), + activeCell: null }); } @@ -70,7 +67,7 @@ export class AITableGridSelectionService { if (cellDom) { const fieldId = cellDom.getAttribute('fieldId'); const recordId = cellDom.getAttribute('recordId'); - fieldId && recordId && this.selectCell(recordId, fieldId); + fieldId && recordId && this.selectCells(recordId, fieldId); } if (colDom && !fieldAction) { const fieldId = colDom.getAttribute('fieldId'); @@ -80,4 +77,41 @@ export class AITableGridSelectionService { this.clearSelection(); } } + + selectCells(startRecordId: string, startFieldId: string, endRecordId?: string, endFieldId?: string) { + if ( + !this.aiTable.context!.visibleRowsIndexMap().has(startRecordId) || + !this.aiTable.context!.visibleColumnsMap().has(startFieldId) + ) { + return; + } + + const selectedCells = new Set(); + if (!endRecordId || !endFieldId) { + selectedCells.add(`${startRecordId}:${startFieldId}`); + } else { + const startRowIndex = this.aiTable.context!.visibleRowsIndexMap().get(startRecordId)!; + const endRowIndex = this.aiTable.context!.visibleRowsIndexMap().get(endRecordId)!; + const startColIndex = this.aiTable.context!.visibleColumnsMap().get(startFieldId)!; + const endColIndex = this.aiTable.context!.visibleColumnsMap().get(endFieldId)!; + + const minRowIndex = Math.min(startRowIndex, endRowIndex); + const maxRowIndex = Math.max(startRowIndex, endRowIndex); + const minColIndex = Math.min(startColIndex, endColIndex); + const maxColIndex = Math.max(startColIndex, endColIndex); + + const rows = this.aiTable.context!.linearRows(); + const fields = AITable.getVisibleFields(this.aiTable); + + for (let i = minRowIndex; i <= maxRowIndex; i++) { + for (let j = minColIndex; j <= maxColIndex; j++) { + selectedCells.add(`${rows[i]._id}:${fields[j]._id}`); + } + } + } + + this.clearSelection(); + this.setActiveCell(startRecordId, startFieldId); + this.aiTable.selection().selectedCells = selectedCells; + } } diff --git a/packages/grid/src/types/grid.ts b/packages/grid/src/types/grid.ts index 94b30abc..3e4c9bf6 100644 --- a/packages/grid/src/types/grid.ts +++ b/packages/grid/src/types/grid.ts @@ -2,7 +2,7 @@ import { Signal, WritableSignal } from '@angular/core'; import { Dictionary } from 'ngx-tethys/types'; import { AITable, AITableField, AITableFieldType, AITableRecord, Coordinate, FieldValue, UpdateFieldValueOptions } from '../core'; import { AITableFieldMenuItem } from './field'; -import { AITableLinearRow, AITableContextMenuItem } from './row'; +import { AITableLinearRow } from './row'; export interface AITableGridCellRenderSchema { editor?: any; @@ -18,7 +18,8 @@ export interface AITableGridData { export interface AITableSelection { selectedRecords: Map; selectedFields: Map; - selectedCells: Map; + selectedCells: Set; // `${recordId}:${fieldId}` + activeCell: { recordId: string; fieldId: string } | null; } export interface AIFieldConfig { diff --git a/packages/state/src/constants/context-menu-item.ts b/packages/state/src/constants/context-menu-item.ts index 0c1840a4..ccf4026b 100644 --- a/packages/state/src/constants/context-menu-item.ts +++ b/packages/state/src/constants/context-menu-item.ts @@ -1,4 +1,4 @@ -import { AITable, AITableContextMenuItem, AITableGridSelectionService, getDetailByTargetName } from '@ai-table/grid'; +import { AITable, AITableContextMenuItem, AITableGridSelectionService } from '@ai-table/grid'; import { Actions } from '../action'; import { AIViewTable } from '../types'; @@ -12,11 +12,7 @@ export const RemoveRecordsItem: AITableContextMenuItem = { position: { x: number; y: number }, aiTableGridSelectionService: AITableGridSelectionService ) => { - let selectedRecordIds = [...aiTable.selection().selectedRecords.keys()]; - if (!selectedRecordIds.length) { - const recordId = getDetailByTargetName(targetName).recordId as string; - selectedRecordIds = [recordId]; - } + let selectedRecordIds = AITable.getSelectedRecordIds(aiTable); selectedRecordIds.forEach((id: string) => { Actions.removeRecord(aiTable as AIViewTable, [id]);