Skip to content

Commit

Permalink
feat(grid): support drag select cells and support right click selecte…
Browse files Browse the repository at this point in the history
…d areas to delete records
  • Loading branch information
minlovehua committed Jan 3, 2025
1 parent bc5d894 commit 70f6b3e
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down
23 changes: 11 additions & 12 deletions packages/grid/src/core/types/ai-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion packages/grid/src/core/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function createAITable(records: WritableSignal<AITableRecords>, fields: W
selection: signal({
selectedRecords: new Map(),
selectedFields: new Map(),
selectedCells: new Map()
selectedCells: new Set(),
activeCell: null
}),
matchedCells: signal([]),
recordsMap: computed(() => {
Expand Down
8 changes: 4 additions & 4 deletions packages/grid/src/dom-grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@
</span>
</div>
@for (field of gridData().fields; track field._id) {
<!-- [ngClass]="{
highlight: aiTable.selection().selectedCells.has(record._id) || aiTable.selection().selectedFields.has(field._id),
selected: aiTable.selection().selectedCells.get(record._id)?.hasOwnProperty(field._id)
}" -->
<div
#cell
class="grid-cell"
[ngClass]="{
highlight: aiTable.selection().selectedCells.has(record._id) || aiTable.selection().selectedFields.has(field._id),
selected: aiTable.selection().selectedCells.get(record._id)?.hasOwnProperty(field._id)
}"
[attr.type]="[field.type]"
[attr.fieldId]="[field._id]"
[attr.recordId]="[record._id]"
Expand Down
1 change: 1 addition & 0 deletions packages/grid/src/grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[config]="rendererConfig()"
(koMousemove)="stageMousemove($event)"
(koMousedown)="stageMousedown($event)"
(koMouseup)="stageMouseup($event)"
(koContextmenu)="stageContextmenu($event)"
(koClick)="stageClick($event)"
(koDblclick)="stageDblclick($event)"
Expand Down
40 changes: 37 additions & 3 deletions packages/grid/src/grid.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ import { AITableGridMatchCellService } from './services/match-cell.service';
export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy {
private viewContainerRef = inject(ViewContainerRef);

private isDragSelecting = false;

private dragStartCell: { recordId: string; fieldId: string } | null = null;

timer!: number | null;

resizeObserver!: ResizeObserver;
Expand Down Expand Up @@ -198,6 +202,16 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy {
handleMouseStyle(curMousePosition.realTargetName, curMousePosition.areaType, this.containerElement());
context!.setPointPosition(curMousePosition);
this.timer = null;

if (this.isDragSelecting) {
const { fieldId, recordId } = getDetailByTargetName(curMousePosition.realTargetName);
if (fieldId && recordId) {
const startCell = this.dragStartCell;
if (startCell) {
this.aiTableGridSelectionService.selectCells(startCell.recordId, startCell.fieldId, recordId, fieldId);
}
}
}
});
}

Expand All @@ -210,7 +224,7 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy {
mouseEvent.button === AITableMouseDownType.Right &&
recordId &&
fieldId &&
this.aiTable.selection().selectedRecords.has(recordId)
(this.aiTable.selection().selectedRecords.has(recordId) || this.aiTable.selection().selectedCells.has(`${recordId}:${fieldId}`))
) {
return;
}
Expand All @@ -224,7 +238,8 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy {
case AI_TABLE_CELL:
if (!recordId || !fieldId) return;
this.aiTableGridEventService.closeCellEditor();
this.aiTableGridSelectionService.selectCell(recordId, fieldId);
this.setDragStatus(true, { recordId, fieldId });
this.aiTableGridSelectionService.selectCells(recordId, fieldId);
return;
case AI_TABLE_ROW_ADD_BUTTON:
case AI_TABLE_FIELD_ADD_BUTTON:
Expand All @@ -236,6 +251,10 @@ export class AITableGrid extends AITableGridBase implements OnInit, OnDestroy {
}
}

stageMouseup(e: KoEventObject<MouseEvent>) {
this.setDragStatus(false, null);
}

stageContextmenu(e: KoEventObject<MouseEvent>) {
const mouseEvent = e.event.evt;
mouseEvent.preventDefault();
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)!;

Expand Down
12 changes: 10 additions & 2 deletions packages/grid/src/renderer/creations/create-cells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/grid/src/renderer/renderer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
[config]="stageConfig()"
(koMousemove)="stageMousemove($event)"
(koMousedown)="stageMousedown($event)"
(koMouseup)="stageMouseup($event)"
(koContextmenu)="stageContextmenu($event)"
(koClick)="stageClick($event)"
(koDblclick)="stageDblclick($event)"
Expand Down
6 changes: 6 additions & 0 deletions packages/grid/src/renderer/renderer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export class AITableRenderer {

koMousedown = output<KoEventObject<MouseEvent>>();

koMouseup = output<KoEventObject<MouseEvent>>();

koContextmenu = output<KoEventObject<MouseEvent>>();

koWheel = output<KoEventObject<WheelEvent>>();
Expand Down Expand Up @@ -209,6 +211,10 @@ export class AITableRenderer {
this.koMousedown.emit(e as KoEventObject<MouseEvent>);
}

stageMouseup(e: KoEventObject<MouseEvent>) {
this.koMouseup.emit(e as KoEventObject<MouseEvent>);
}

stageContextmenu(e: KoEventObject<MouseEvent>) {
this.koContextmenu.emit(e as KoEventObject<MouseEvent>);
}
Expand Down
54 changes: 44 additions & 10 deletions packages/grid/src/services/selection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
});
}

Expand All @@ -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');
Expand All @@ -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<string>();
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;
}
}
5 changes: 3 additions & 2 deletions packages/grid/src/types/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,7 +18,8 @@ export interface AITableGridData {
export interface AITableSelection {
selectedRecords: Map<string, boolean>;
selectedFields: Map<string, boolean>;
selectedCells: Map<string, {}>;
selectedCells: Set<string>; // `${recordId}:${fieldId}`
activeCell: { recordId: string; fieldId: string } | null;
}

export interface AIFieldConfig {
Expand Down
8 changes: 2 additions & 6 deletions packages/state/src/constants/context-menu-item.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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]);
Expand Down

0 comments on commit 70f6b3e

Please sign in to comment.