Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(selection): add selection feature #WIK-16060 #12

Merged
merged 10 commits into from
Jul 24, 2024
3 changes: 3 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}
3 changes: 3 additions & 0 deletions packages/grid/src/core/types/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { WritableSignal } from '@angular/core';
import { AITableAction } from './action';
import { AITableSelection } from '../../types';

export enum AITableFieldType {
// NotSupport = 0,
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface AITableField {

export interface AITableRecord {
id: string;
checked?: boolean;
value: Record<string, any>;
}

Expand All @@ -73,6 +75,7 @@ export interface AITable {
records: WritableSignal<AITableRecords>;
fields: WritableSignal<AITableFields>;
actions: AITableAction[];
selection: WritableSignal<AITableSelection>;
onChange: () => void;
apply: (action: AITableAction) => void;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/grid/src/core/utils/common.ts
Original file line number Diff line number Diff line change
@@ -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<AITableRecords>, fields: WritableSignal<AITableFields>): 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);
Expand Down
33 changes: 28 additions & 5 deletions packages/grid/src/grid.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<div class="grid-header d-flex">
<div class="grid-column-checkbox grid-cell">
<input type="checkbox" />
<label thyCheckbox thyLabelText="" [ngModel]="isSelectedAll" (ngModelChange)="toggleSelectAll($event)"></label>
</div>
@for (field of gridData().fields; track field.id) {
<div class="grid-cell grid-field" #fieldAction>
<div
class="grid-cell grid-column-field"
#fieldAction
[attr.fieldId]="field.id"
[ngClass]="{ highlight: aiTable.selection().selectedFields.has(field.id) }"
>
{{ field.name }}
<a thyAction thyActiveClass="active" thyIcon="more-vertical" [thyDropdown]="fieldMenu" href="javascript:;">
<thy-dropdown-menu #fieldMenu>
Expand All @@ -18,12 +23,29 @@
</div>
<div class="grid-body d-flex">
@for (record of gridData().records; track record.id; let index = $index) {
<div class="grid-row d-flex">
<div class="grid-row d-flex" [ngClass]="{ highlight: aiTable.selection().selectedRecords.has(record.id) }">
<div class="grid-row-index">
{{ index + 1 }}
<label
[ngClass]="record.checked ? 'checked-box' : 'unchecked-box'"
thyCheckbox
thyLabelText=""
[ngModel]="record.checked"
(ngModelChange)="selectRecord(record.id)"
></label>
<span [ngClass]="record.checked ? 'grid-row-no-number' : 'grid-row-number'"> {{ index + 1 }} </span>
</div>
@for (field of gridData().fields; track $index) {
<div class="grid-cell" [attr.type]="[field.type]" [attr.fieldId]="[field.id]" [attr.recordId]="[record.id]" #cell>
<div
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]"
#cell
>
@switch (field.type) {
@case (AITableFieldType.SingleSelect) {
@if (record.value[field.id] | selectOption: field['options']; as selectedOption) {
Expand Down Expand Up @@ -51,6 +73,7 @@
{{ record.value[field.id] }}
}
}
<div class="autofill-container"></div>
</div>
}
<div class="grid-column-blank"></div>
Expand Down
35 changes: 30 additions & 5 deletions packages/grid/src/grid.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -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<AITableRecords>();
Expand All @@ -80,26 +83,39 @@ export class AITableGrid implements OnInit {

aiTable!: AITable;

get isSelectedAll() {
return this.aiTable.selection().selectedRecords.size === this.aiRecords().length;
}

onChange = output<AITableChangeOptions>();

aiTableInitialized = output<AITable>();

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() {
Expand All @@ -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);
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/grid/src/pipes/grid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AITableSelectOption } from '../core';;
import { AITableSelectOption } from '../core';

@Pipe({
name: 'selectOption',
Expand Down
10 changes: 9 additions & 1 deletion packages/grid/src/services/event.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,6 +16,8 @@ export class AITableGridEventService {

takeUntilDestroyed = takeUntilDestroyed();

mousedownEvent$ = new Subject<MouseEvent>();

constructor(private thyPopover: ThyPopover) {}

initialize(aiTable: AITable, aiFieldRenderers?: Partial<Record<AITableFieldType, AITableGridCellRenderSchema>>) {
Expand All @@ -29,6 +31,12 @@ export class AITableGridEventService {
.subscribe((event) => {
this.dblClick(event as MouseEvent);
});

fromEvent<MouseEvent>(element, 'mousedown')
.pipe(this.takeUntilDestroyed)
.subscribe((event) => {
this.mousedownEvent$.next(event as MouseEvent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

click 中的不应该是 mousedownEvent

});
}

private dblClick(event: MouseEvent) {
Expand Down
68 changes: 68 additions & 0 deletions packages/grid/src/services/selection.servive.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
52 changes: 50 additions & 2 deletions packages/grid/src/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
Loading