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
23 changes: 19 additions & 4 deletions packages/grid/src/grid.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="grid-header d-flex">
<div class="grid-column-checkbox grid-cell">
<input type="checkbox" />
<label thyCheckbox thyLabelText="" [(ngModel)]="isSelectedAll" (ngModelChange)="toggleSelectAll()"></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 (click)="selectCol(field)" [attr.fieldId]="field.id">
{{ field.name }}
<a thyAction thyActiveClass="active" thyIcon="more-vertical" [thyDropdown]="fieldMenu" href="javascript:;">
<thy-dropdown-menu #fieldMenu>
Expand All @@ -20,10 +20,24 @@
@for (record of gridData().records; track record.id; let index = $index) {
<div class="grid-row d-flex">
<div class="grid-row-index">
{{ index + 1 }}
<label
[ngClass]="record.checked ? 'checked-box' : 'unchecked-box'"
thyCheckbox
thyLabelText=""
[(ngModel)]="record.checked"
(ngModelChange)="selectRow(record)"
></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"
[attr.type]="[field.type]"
[attr.fieldId]="[field.id]"
[attr.recordId]="[record.id]"
#cell
(click)="selectCell(record.id, field.id)"
>
@switch (field.type) {
@case (AITableFieldType.SingleSelect) {
@if (record.value[field.id] | selectOption: field['options']; as selectedOption) {
Expand Down Expand Up @@ -51,6 +65,7 @@
{{ record.value[field.id] }}
}
}
<div class="autofill-container"></div>
</div>
}
<div class="grid-column-blank"></div>
Expand Down
81 changes: 75 additions & 6 deletions packages/grid/src/grid.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { ChangeDetectionStrategy, Component, computed, ElementRef, input, model, OnInit, output, signal, viewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
input,
model,
OnInit,
output,
signal,
viewChild
} from '@angular/core';
import { CommonModule, NgClass, NgComponentOutlet, NgForOf } from '@angular/common';
import { SelectOptionPipe } from './pipes/grid';
import { SelectedOneFieldPipe, SelectOptionPipe } from './pipes/grid';
import { ThyTag } from 'ngx-tethys/tag';
import { ThyPopoverModule } from 'ngx-tethys/popover';
import { ThyPopover, ThyPopoverModule } from 'ngx-tethys/popover';
Copy link
Contributor

Choose a reason for hiding this comment

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

没用的引用删除一下

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { buildGridData } from './utils';
import { AIFieldConfig, AITableFieldMenu, AITableRowHeight } from './types';
Expand All @@ -15,6 +27,8 @@ import {
AITableFields,
AITableFieldType,
AITableRecords,
AITableField,
AITableRecord,
createDefaultField
} from './core';
import { ThyIcon } from 'ngx-tethys/icon';
Expand All @@ -25,12 +39,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 +75,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,6 +97,12 @@ export class AITableGrid implements OnInit {

aiTable!: AITable;

isSelectedAll = false;

get selection() {
return this.aiTableGridSelectionService.selection();
}

onChange = output<AITableChangeOptions>();

aiTableInitialized = output<AITable>();
Expand All @@ -93,8 +116,18 @@ export class AITableGrid implements OnInit {
constructor(
private elementRef: ElementRef,
private aiTableGridEventService: AITableGridEventService,
private thyPopover: ThyPopover,
public aiTableGridSelectionService: AITableGridSelectionService,
private aiTableGridFieldService: AITableGridFieldService
) {}
) {
effect(
() => {
this.aiTable.selection.set(this.aiTableGridSelectionService.selection());
Copy link
Contributor

Choose a reason for hiding this comment

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

aiTableGridSelectionService 中的 selection 和 aiTable.selection 中应该是同一个 signal ,这样就不需要维护两套 signal 数据

console.log('跟新啦', this.aiTable.selection());
Copy link
Contributor

Choose a reason for hiding this comment

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

console

},
{ allowSignalWrites: true }
);
}

ngOnInit(): void {
this.initAITable();
Expand Down Expand Up @@ -129,6 +162,42 @@ export class AITableGrid implements OnInit {
Actions.addRecord(this.aiTable, getDefaultRecord(this.aiFields()), [this.aiRecords().length]);
}

toggleAllCheckbox(checked: boolean) {
const data = this.gridData().records.map((item) => {
return { ...item, checked: checked };
});
this.gridData().records = data;
Copy link
Contributor

Choose a reason for hiding this comment

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

gridData 中的数据应该是只从 buildGridData 中而来,外部不应该有修改它属性的逻辑。

如果是要修改 checked ,当 toggleAllCheckbox 事件触发时 :

  1. 应该优先修改 aiTable.selection 中的数据(这里是否需要通过 Actions.xxx 待定,当前在 AITableGridSelectionService 中修改也没问题)
  2. 同时在 buildGridData 中的数据源中加上 aiTable.selection ,并增加根据 aiTable.selection 计算出 checked 的逻辑
  3. 当 selection 数据发生变化时,gridData 会自动计算出正确的 checked

}

selectCell(recordId: string, fieldId: string) {
this.toggleAllCheckbox(false);
this.isSelectedAll = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

这里的 isSelectedAll 最好也是通过 aiTable.selection 计算得到

this.aiTableGridSelectionService.selectCell(recordId, fieldId);
}

selectCol(field: any) {
this.toggleAllCheckbox(false);
this.isSelectedAll = false;
this.aiTableGridSelectionService.selectCol(field.id);
}

selectRow(record: AITableRecord) {
this.aiTableGridSelectionService.selectRow(record.id);
this.isSelectedAll = this.selection.selectedRecords.size === this.aiRecords().length;
}

toggleSelectAll() {
this.aiTableGridSelectionService.clearSelection();
if (this.isSelectedAll) {
this.toggleAllCheckbox(true);
this.aiRecords().forEach((item) => {
this.selectRow(item);
});
} else {
this.toggleAllCheckbox(false);
}
}

addField(gridColumnBlank: HTMLElement) {
const field = signal(createDefaultField(this.aiTable, AITableFieldType.Text));
this.aiTableGridFieldService.editFieldProperty(gridColumnBlank, this.aiTable, field, false);
Expand Down
12 changes: 11 additions & 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 All @@ -10,3 +10,13 @@ export class SelectOptionPipe implements PipeTransform {
return options.find((item) => item.id === id);
}
}

@Pipe({
Copy link
Contributor

Choose a reason for hiding this comment

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

没用到的删除

name: 'selectedOneField',
standalone: true
})
export class SelectedOneFieldPipe implements PipeTransform {
transform(fields: any) {
return Object.keys(fields ?? {}).length === 1;
}
}
119 changes: 117 additions & 2 deletions packages/grid/src/services/event.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Signal } from '@angular/core';
import { Injectable, Renderer2, Signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent } from 'rxjs';
import { DBL_CLICK_EDIT_TYPE } from '../constants';
Expand All @@ -16,7 +16,12 @@ export class AITableGridEventService {

takeUntilDestroyed = takeUntilDestroyed();

constructor(private thyPopover: ThyPopover) {}
lastClickCellElement?: HTMLElement;

constructor(
private thyPopover: ThyPopover,
private renderer: Renderer2
) {}

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

fromEvent<MouseEvent>(element, 'click')
Copy link
Contributor

Choose a reason for hiding this comment

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

选中单元格的交互应该是 mousedown

Copy link
Contributor

Choose a reason for hiding this comment

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

这里的逻辑有些奇怪,gridComponent 中已经针对行和列以及单元格的点击事件做处理了,这里又监听了 click 并且再区分点击的是什么,如果用操作 dom 的方式设置高亮在 gridComponent 中就可以实现

Copy link
Contributor Author

Choose a reason for hiding this comment

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

这个可以在 gridComponent 中实现,我觉得 这个高亮方案 不一定行 我心里觉得 ngClass 更好, 我就拆开写了,不行就删除了 ,不会影响我的 gridComponent 文件, gridComponent 文件 的点击事件 都很分散

.pipe(this.takeUntilDestroyed)
.subscribe((event) => {
this.click(event as MouseEvent);
});
}

updateCellClass(dom: Element, operation: string) {
const rowDom = dom.closest('.grid-row');
const highlightCells = rowDom?.querySelectorAll('.grid-cell');
highlightCells?.forEach((cell) => {
operation === 'remove' ? this.renderer.removeClass(cell, 'highlight') : this.renderer.addClass(cell, 'highlight');
});
operation === 'remove' ? this.renderer.removeClass(dom, 'isSelected') : this.renderer.addClass(dom, 'isSelected');
}

updateColClass(dom: Element, operation: string) {
const fieldId = dom.getAttribute('fieldid');
const tableElement = dom.closest('ai-table-grid');
const cells = tableElement?.querySelectorAll(`[fieldid="${fieldId}"]`);
cells?.forEach((cell) => {
operation === 'add' ? this.renderer.addClass(cell, 'highlight') : this.renderer.removeClass(cell, 'highlight');
});
}

updateAllClass(dom: Element, operation: string, checked: boolean) {
const tableElement = dom.closest('ai-table-grid');
const rows = tableElement?.querySelectorAll('.grid-row');
if (checked && operation === 'add') {
rows?.forEach((row) => {
this.renderer.addClass(row, 'highlight');
});
}
if (operation === 'remove') {
rows?.forEach((row) => {
this.renderer.removeClass(row, 'highlight');
});
}
}

private click(event: MouseEvent) {
const cell = this.cellType(event.target as HTMLElement);
if (this.lastClickCellElement) {
const lastCell = this.cellType(this.lastClickCellElement);
if (lastCell?.type === 'cell') {
this.updateCellClass(this.lastClickCellElement, 'remove');
}

if (lastCell?.type === 'row' && cell?.type !== 'row' && cell.type) {
const tableElement = lastCell.element.closest('ai-table-grid');
const checkboxes = tableElement?.querySelectorAll('.checked-box');
checkboxes?.forEach((box) => {
const row = box.closest('.grid-row');
this.renderer.removeClass(row, 'highlight');
});
}

if (lastCell.type === 'col') {
this.updateColClass(lastCell.element, 'remove');
}

if (lastCell?.type === 'all' && cell?.type !== 'row' && cell.type) {
this.updateAllClass(cell.element, 'remove', false);
}
}

if (cell?.type === 'cell') {
this.updateCellClass(cell.element, 'add');
}

if (cell?.type === 'row') {
const rowDom = cell.element.closest('.grid-row');
if ((event.target as HTMLInputElement).checked) {
this.renderer.addClass(rowDom, 'highlight');
} else {
this.renderer.removeClass(rowDom, 'highlight');
}
}

if (cell?.type === 'col') {
this.updateColClass(cell.element, 'add');
}

if (cell?.type === 'all') {
this.updateAllClass(cell.element, 'add', (event.target as HTMLInputElement).checked);
}

this.lastClickCellElement = event.target as HTMLElement;
Copy link
Contributor

Choose a reason for hiding this comment

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

这里不能直接赋值吧,会出现点击到的不是 cell 的情况

}

private cellType(cell: HTMLElement) {
const cellDom = cell.closest('.grid-cell');
const rowDom = cell.closest('.grid-row-index');
const colDom = cell.closest('.grid-column-field');
const checkAllDom = cell.closest('.grid-column-checkbox');
if (cellDom && cellDom.getAttribute('fieldid') && cellDom.getAttribute('recordid')) {
return { type: 'cell', element: cellDom };
}
if (rowDom && cell.tagName === 'INPUT') {
return { type: 'row', element: rowDom };
}
if (colDom) {
return { type: 'col', element: colDom };
}

if (checkAllDom && cell.tagName === 'INPUT') {
return { type: 'all', element: checkAllDom };
}
return {};
}

private dblClick(event: MouseEvent) {
Expand Down
Loading