diff --git a/angular.json b/angular.json index 06d8e963..d0bc60d2 100644 --- a/angular.json +++ b/angular.json @@ -123,5 +123,8 @@ } } } + }, + "cli": { + "analytics": false } } diff --git a/packages/grid/src/core/types/core.ts b/packages/grid/src/core/types/core.ts index efb2eff9..d1197fec 100644 --- a/packages/grid/src/core/types/core.ts +++ b/packages/grid/src/core/types/core.ts @@ -1,5 +1,6 @@ import { WritableSignal } from '@angular/core'; import { AITableAction } from './action'; +import { AITableSelection } from '../../types'; export enum AITableFieldType { // NotSupport = 0, @@ -57,6 +58,7 @@ export interface AITableField { export interface AITableRecord { id: string; + checked?: boolean; value: Record; } @@ -73,6 +75,7 @@ export interface AITable { records: WritableSignal; fields: WritableSignal; actions: AITableAction[]; + selection: WritableSignal; onChange: () => void; apply: (action: AITableAction) => void; } diff --git a/packages/grid/src/core/utils/common.ts b/packages/grid/src/core/utils/common.ts index 03a328f8..3c14408d 100644 --- a/packages/grid/src/core/utils/common.ts +++ b/packages/grid/src/core/utils/common.ts @@ -1,13 +1,18 @@ import { Actions } from '../action'; import { AITable, AITableAction, AITableFields, AITableRecords } from '../types'; import { FLUSHING } from './weak-map'; -import { WritableSignal } from '@angular/core'; +import { WritableSignal, signal } from '@angular/core'; export function createAITable(records: WritableSignal, fields: WritableSignal): AITable { const aiTable: AITable = { records, fields, actions: [], + selection: signal({ + selectedRecords: new Map(), + selectedFields: new Map(), + selectedCells: new Map() + }), onChange: () => {}, apply: (action: AITableAction) => { aiTable.actions.push(action); diff --git a/packages/grid/src/grid.component.html b/packages/grid/src/grid.component.html index 6dacd21f..448ed0d0 100644 --- a/packages/grid/src/grid.component.html +++ b/packages/grid/src/grid.component.html @@ -1,9 +1,14 @@
- +
@for (field of gridData().fields; track field.id) { -
+
{{ field.name }} @@ -18,12 +23,29 @@
@for (record of gridData().records; track record.id; let index = $index) { -
+
- {{ index + 1 }} + + {{ index + 1 }}
@for (field of gridData().fields; track $index) { -
+
@switch (field.type) { @case (AITableFieldType.SingleSelect) { @if (record.value[field.id] | selectOption: field['options']; as selectedOption) { @@ -51,6 +73,7 @@ {{ record.value[field.id] }} } } +
}
diff --git a/packages/grid/src/grid.component.ts b/packages/grid/src/grid.component.ts index ea6665e7..9453afa1 100644 --- a/packages/grid/src/grid.component.ts +++ b/packages/grid/src/grid.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, computed, ElementRef, input, model, OnInit, output, signal, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, ElementRef, input, model, NgZone, OnInit, output, signal } from '@angular/core'; import { CommonModule, NgClass, NgComponentOutlet, NgForOf } from '@angular/common'; import { SelectOptionPipe } from './pipes/grid'; import { ThyTag } from 'ngx-tethys/tag'; @@ -25,12 +25,14 @@ import { ThyRate } from 'ngx-tethys/rate'; import { FormsModule } from '@angular/forms'; import { ThyFlexibleText } from 'ngx-tethys/flexible-text'; import { ThyTooltipModule, ThyTooltipService } from 'ngx-tethys/tooltip'; +import { ThyCheckboxModule } from 'ngx-tethys/checkbox'; import { ThyStopPropagationDirective } from 'ngx-tethys/shared'; import { FieldMenu } from './components/field-menu/field-menu.component'; import { ThyAction } from 'ngx-tethys/action'; import { ThyDropdownDirective, ThyDropdownMenuComponent } from 'ngx-tethys/dropdown'; import { DefaultFieldMenus } from './constants'; import { AI_TABLE_GRID_FIELD_SERVICE_MAP, AITableGridFieldService } from './services/field.service'; +import { AITableGridSelectionService } from './services/selection.servive'; @Component({ selector: 'ai-table-grid', @@ -59,9 +61,10 @@ import { AI_TABLE_GRID_FIELD_SERVICE_MAP, AITableGridFieldService } from './serv FieldMenu, ThyAction, ThyDropdownDirective, - ThyDropdownMenuComponent + ThyDropdownMenuComponent, + ThyCheckboxModule ], - providers: [ThyTooltipService, AITableGridEventService, AITableGridFieldService] + providers: [ThyTooltipService, AITableGridEventService, AITableGridFieldService, AITableGridSelectionService] }) export class AITableGrid implements OnInit { aiRecords = model.required(); @@ -80,6 +83,10 @@ export class AITableGrid implements OnInit { aiTable!: AITable; + get isSelectedAll() { + return this.aiTable.selection().selectedRecords.size === this.aiRecords().length; + } + onChange = output(); aiTableInitialized = output(); @@ -87,19 +94,28 @@ export class AITableGrid implements OnInit { fieldMenus!: AITableFieldMenu[]; gridData = computed(() => { - return buildGridData(this.aiRecords(), this.aiFields()); + return buildGridData(this.aiRecords(), this.aiFields(), this.aiTable.selection()); }); constructor( private elementRef: ElementRef, private aiTableGridEventService: AITableGridEventService, - private aiTableGridFieldService: AITableGridFieldService + public aiTableGridSelectionService: AITableGridSelectionService, + private aiTableGridFieldService: AITableGridFieldService, + private ngZone: NgZone ) {} ngOnInit(): void { this.initAITable(); this.initService(); this.buildFieldMenus(); + this.ngZone.runOutsideAngular(() => { + this.aiTableGridEventService.mousedownEvent$.pipe(this.takeUntilDestroyed).subscribe((event) => { + if ((event as MouseEvent)?.target) { + this.aiTableGridSelectionService.updateSelect(event as MouseEvent); + } + }); + }); } initAITable() { @@ -116,6 +132,7 @@ export class AITableGrid implements OnInit { initService() { this.aiTableGridEventService.initialize(this.aiTable, this.aiFieldConfig()?.fieldPropertyEditor); + this.aiTableGridSelectionService.initialize(this.aiTable); this.aiTableGridEventService.registerEvents(this.elementRef.nativeElement); this.aiTableGridFieldService.initAIFieldConfig(this.aiFieldConfig()); AI_TABLE_GRID_FIELD_SERVICE_MAP.set(this.aiTable, this.aiTableGridFieldService); @@ -129,6 +146,14 @@ export class AITableGrid implements OnInit { Actions.addRecord(this.aiTable, getDefaultRecord(this.aiFields()), [this.aiRecords().length]); } + selectRecord(recordId: string) { + this.aiTableGridSelectionService.selectRecord(recordId); + } + + toggleSelectAll(checked: boolean) { + this.aiTableGridSelectionService.toggleSelectAll(checked); + } + addField(gridColumnBlank: HTMLElement) { const field = signal(createDefaultField(this.aiTable, AITableFieldType.Text)); this.aiTableGridFieldService.editFieldProperty(gridColumnBlank, this.aiTable, field, false); diff --git a/packages/grid/src/pipes/grid.ts b/packages/grid/src/pipes/grid.ts index dc10cbbc..25525bc0 100644 --- a/packages/grid/src/pipes/grid.ts +++ b/packages/grid/src/pipes/grid.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { AITableSelectOption } from '../core';; +import { AITableSelectOption } from '../core'; @Pipe({ name: 'selectOption', diff --git a/packages/grid/src/services/event.service.ts b/packages/grid/src/services/event.service.ts index a9103ece..1479ac14 100644 --- a/packages/grid/src/services/event.service.ts +++ b/packages/grid/src/services/event.service.ts @@ -1,6 +1,6 @@ import { Injectable, Signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { fromEvent } from 'rxjs'; +import { fromEvent, Subject } from 'rxjs'; import { DBL_CLICK_EDIT_TYPE } from '../constants'; import { getRecordOrField } from '../utils'; import { AITable, AITableField, AITableFieldType, AITableRecord } from '../core'; @@ -16,6 +16,8 @@ export class AITableGridEventService { takeUntilDestroyed = takeUntilDestroyed(); + mousedownEvent$ = new Subject(); + constructor(private thyPopover: ThyPopover) {} initialize(aiTable: AITable, aiFieldRenderers?: Partial>) { @@ -29,6 +31,12 @@ export class AITableGridEventService { .subscribe((event) => { this.dblClick(event as MouseEvent); }); + + fromEvent(element, 'mousedown') + .pipe(this.takeUntilDestroyed) + .subscribe((event) => { + this.mousedownEvent$.next(event as MouseEvent); + }); } private dblClick(event: MouseEvent) { diff --git a/packages/grid/src/services/selection.servive.ts b/packages/grid/src/services/selection.servive.ts new file mode 100644 index 00000000..f1da4616 --- /dev/null +++ b/packages/grid/src/services/selection.servive.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@angular/core'; +import { AITable } from '../core'; + +@Injectable() +export class AITableGridSelectionService { + aiTable!: AITable; + + constructor() {} + + initialize(aiTable: AITable) { + this.aiTable = aiTable; + } + + clearSelection() { + this.aiTable.selection.set({ + selectedRecords: new Map(), + selectedFields: new Map(), + selectedCells: new Map() + }); + } + + selectCell(recordId: string, fieldId: string) { + this.clearSelection(); + this.aiTable.selection().selectedCells.set(recordId, { [fieldId]: true }); + } + + selectField(fieldId: string) { + this.clearSelection(); + this.aiTable.selection().selectedFields.set(fieldId, true); + } + + selectRecord(recordId: string) { + if (this.aiTable.selection().selectedRecords.has(recordId)) { + this.aiTable.selection().selectedRecords.delete(recordId); + } else { + this.aiTable.selection().selectedRecords.set(recordId, true); + } + this.aiTable.selection.set({ + selectedRecords: this.aiTable.selection().selectedRecords, + selectedFields: new Map(), + selectedCells: new Map() + }); + } + + toggleSelectAll(checked: boolean) { + this.clearSelection(); + if (checked) { + this.aiTable.records().forEach((item) => { + this.selectRecord(item.id); + }); + } + } + + updateSelect(event: MouseEvent) { + const target = event.target as HTMLElement; + const cellDom = target.closest('.grid-cell'); + const colDom = target.closest('.grid-column-field'); + if (cellDom) { + const fieldId = cellDom.getAttribute('fieldId'); + const recordId = cellDom.getAttribute('recordId'); + fieldId && recordId && this.selectCell(recordId, fieldId); + } + if (colDom) { + const fieldId = colDom.getAttribute('fieldId'); + fieldId && this.selectField(fieldId); + } + } +} diff --git a/packages/grid/src/styles/styles.scss b/packages/grid/src/styles/styles.scss index 1f8ba746..d6867848 100644 --- a/packages/grid/src/styles/styles.scss +++ b/packages/grid/src/styles/styles.scss @@ -22,17 +22,50 @@ .grid-row { &:hover { background-color: variables.$gray-80; + .unchecked-box { + display: block; + } + .grid-row-number { + display: none; + } + } + &.highlight { + background: variables.$secondary-item-active; } } .grid-cell { - height: 44px; + min-height: 44px; + max-height: 148px; + overflow: auto; display: flex; align-items: center; width: 300px; border-left: 1px solid variables.$gray-200; - justify-content: center; + padding-left: 12px; + position: relative; cursor: pointer; + .autofill-container { + position: absolute; + width: 4px; + height: 4px; + right: 0; + bottom: 0; + background: variables.$primary; + cursor: crosshair; + z-index: 100; + display: none; + } + &.highlight { + background: variables.$secondary-item-active; + } + &.selected { + border: 2px solid variables.$primary; + background: variables.$white; + .autofill-container { + display: block; + } + } } .grid-column-blank { flex: 1; @@ -50,6 +83,21 @@ } } } + .grid-row-index { + .checked-box { + display: block; + } + .unchecked-box { + display: none; + } + .grid-row-no-number { + display: none; + } + + .grid-row-number { + display: block; + } + } .grid-row-index, .grid-column-checkbox { diff --git a/packages/grid/src/types/grid.ts b/packages/grid/src/types/grid.ts index 735ce586..09f38c13 100644 --- a/packages/grid/src/types/grid.ts +++ b/packages/grid/src/types/grid.ts @@ -18,10 +18,13 @@ export interface AITableGridData { records: AITableRecord[]; } +export interface AITableSelection { + selectedRecords: Map; + selectedFields: Map; + selectedCells: Map; +} export interface AIFieldConfig { fieldRenderers?: Partial>; fieldPropertyEditor?: any; fieldMenus?: AITableFieldMenu[]; } - - diff --git a/packages/grid/src/utils/build.ts b/packages/grid/src/utils/build.ts index 2d9f22a0..568f6638 100644 --- a/packages/grid/src/utils/build.ts +++ b/packages/grid/src/utils/build.ts @@ -1,10 +1,12 @@ -import { AITableFields, AITableRecords } from '../core';; -import { AITableGridData } from '../types'; +import { AITableFields, AITableRecords } from '../core'; +import { AITableGridData, AITableSelection } from '../types'; -export const buildGridData = (recordValue: AITableRecords, fieldsValue: AITableFields): AITableGridData => { +export const buildGridData = (recordValue: AITableRecords, fieldsValue: AITableFields, selection: AITableSelection): AITableGridData => { return { type: 'grid', fields: fieldsValue, - records: recordValue + records: recordValue.map((item) => { + return { ...item, checked: selection.selectedRecords.has(item.id) }; + }) }; }; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f6875b72..e5376f9e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -47,7 +47,7 @@ const initValue = { 'column-1': '文本 3-1', 'column-2': '3', 'column-3': {}, - 'column-4': null + 'column-4': 1 } } ],