Skip to content

Commit

Permalink
feat(grid): fix highlight bind #WIK-16060
Browse files Browse the repository at this point in the history
  • Loading branch information
cmm-va committed Jul 19, 2024
1 parent d89fefa commit d96e94b
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 56 deletions.
3 changes: 2 additions & 1 deletion 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 @@ -74,7 +75,7 @@ export interface AITable {
records: WritableSignal<AITableRecords>;
fields: WritableSignal<AITableFields>;
actions: AITableAction[];
selection: Map<string, {}>;
selection: WritableSignal<AITableSelection>;
onChange: () => void;
apply: (action: AITableAction) => void;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/grid/src/core/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,14 +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: new Map(),
selection: signal({
selectedRecords: new Map(),
selectedFields: new Map(),
selectedCells: new Map()
}),
onChange: () => {},
apply: (action: AITableAction) => {
aiTable.actions.push(action);
Expand Down
15 changes: 3 additions & 12 deletions packages/grid/src/grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<label thyCheckbox thyLabelText="" [(ngModel)]="isSelectedAll" (ngModelChange)="toggleSelectAll()"></label>
</div>
@for (field of gridData().fields; track field.id) {
<div class="grid-cell grid-column-field" (click)="selectCol(field)" [ngClass]="selectedHeader.has(field.name) ? 'highlight' : ''">
<div class="grid-cell grid-column-field" (click)="selectCol(field)" [attr.fieldId]="field.id">
{{ field.name }}
</div>
}
Expand All @@ -12,28 +12,19 @@
<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-index" [ngClass]="selection.has(record.id) && record.checked ? 'highlight' : ''">
<div class="grid-row-index">
<label
[ngClass]="record.checked ? 'checked-box' : 'unchecked-box'"
thyCheckbox
thyLabelText=""
[(ngModel)]="record.checked"
(ngModelChange)="selectRow()"
(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"
[ngClass]="{
highlight:
selection.get(record.id)?.hasOwnProperty(field.id) ||
((selection.get(record.id) | selectedOneField) && selection.size === 1 && selection.has(record.id)),
isSelected:
(selection.get(record.id) | selectedOneField) &&
selection.size === 1 &&
selection.get(record.id)?.hasOwnProperty(field.id)
}"
[attr.type]="[field.type]"
[attr.fieldId]="[field.id]"
[attr.recordId]="[record.id]"
Expand Down
61 changes: 29 additions & 32 deletions packages/grid/src/grid.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ChangeDetectionStrategy, Component, computed, ElementRef, input, model, OnInit, output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, input, model, OnInit, output } from '@angular/core';
import { CommonModule, NgClass, NgComponentOutlet, NgForOf } from '@angular/common';
import { SelectedOneFieldPipe, SelectOptionPipe } from './pipes/grid';
import { ThyTag } from 'ngx-tethys/tag';
import { ThyPopover, ThyPopoverModule } from 'ngx-tethys/popover';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { buildGridData } from './utils';
import { AITableGridCellRenderSchema, AITableRowHeight } from './types';
import { AITableGridCellRenderSchema, AITableRowHeight, AITableSelection } from './types';
import {
Actions,
createAITable,
Expand All @@ -15,7 +15,8 @@ import {
AITableFields,
AITableFieldType,
AITableRecords,
AITableField
AITableField,
AITableRecord
} from './core';
import { ThyIcon } from 'ngx-tethys/icon';
import { AITableGridEventService } from './services/event.service';
Expand All @@ -27,6 +28,7 @@ import { ThyFlexibleText } from 'ngx-tethys/flexible-text';
import { ThyTooltipModule, ThyTooltipService } from 'ngx-tethys/tooltip';
import { ThyStopPropagationDirective } from 'ngx-tethys/shared';
import { ThyCheckboxModule } from 'ngx-tethys/checkbox';
import { AITableGridSelectionService } from './services/selection.servive';

@Component({
selector: 'ai-table-grid',
Expand Down Expand Up @@ -55,7 +57,7 @@ import { ThyCheckboxModule } from 'ngx-tethys/checkbox';
ThyCheckboxModule,
ThyStopPropagationDirective
],
providers: [ThyTooltipService, AITableGridEventService]
providers: [ThyTooltipService, AITableGridEventService, AITableGridSelectionService]
})
export class AITableGridComponent implements OnInit {
aiRecords = model.required<AITableRecords>();
Expand All @@ -76,9 +78,9 @@ export class AITableGridComponent implements OnInit {

isSelectedAll = false;

selection = new Map<string, {}>();

selectedHeader = new Set();
get selection() {
return this.aiTableGridSelectionService.selection();
}

onChange = output<AITableChangeOptions>();

Expand All @@ -89,8 +91,17 @@ export class AITableGridComponent implements OnInit {
constructor(
private elementRef: ElementRef,
private aiTableGridEventService: AITableGridEventService,
private thyPopover: ThyPopover
) {}
private thyPopover: ThyPopover,
public aiTableGridSelectionService: AITableGridSelectionService
) {
effect(
() => {
this.aiTable.selection.set(this.aiTableGridSelectionService.selection());
console.log('跟新啦', this.aiTable.selection());
},
{ allowSignalWrites: true }
);
}

ngOnInit(): void {
this.initAITable();
Expand Down Expand Up @@ -120,43 +131,29 @@ export class AITableGridComponent implements OnInit {
this.gridData().records = data;
}

clearSelection() {
this.selection.clear();
this.selectedHeader.clear();
}

selectCell(recordId: string, fieldId: string) {
this.clearSelection();
this.toggleAllCheckbox(false);
this.selection.set(recordId, { [fieldId]: '' });
this.isSelectedAll = false;
this.aiTableGridSelectionService.selectCell(recordId, fieldId);
}

selectCol(field: any) {
this.clearSelection();
this.toggleAllCheckbox(false);
// 选择表头
this.selectedHeader.add(field.name);
this.aiRecords().forEach((item) => {
const value = item.value[field.id];
this.selection.set(item.id, { [field.id]: value });
});
this.isSelectedAll = false;
this.aiTableGridSelectionService.selectCol(field.id);
}

selectRow() {
this.clearSelection();
this.gridData().records.forEach((record) => {
if (record.checked) {
this.selection.set(record.id, record.value);
}
});
selectRow(record: AITableRecord) {
this.aiTableGridSelectionService.selectRow(record.id);
this.isSelectedAll = this.selection.selectedRecords.size === this.aiRecords().length;
}

toggleSelectAll() {
this.clearSelection();
this.aiTableGridSelectionService.clearSelection();
if (this.isSelectedAll) {
this.toggleAllCheckbox(true);
this.aiRecords().forEach((item) => {
this.selection.set(item.id, item.value);
this.selectRow(item);
});
} else {
this.toggleAllCheckbox(false);
Expand Down
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, Signal } from '@angular/core';
import { Injectable, Renderer2, signal, 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')
.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;
}

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
41 changes: 37 additions & 4 deletions packages/grid/src/services/selection.servive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import { Injectable } from '@angular/core';
import { AITable } from '../core';
import { Injectable, WritableSignal, signal } from '@angular/core';
import { AITableSelection } from '../types';

@Injectable()
export class AITableGridSelectionService {
aiTable!: AITable;
selection: WritableSignal<AITableSelection> = signal({
selectedRecords: new Map(),
selectedFields: new Map(),
selectedCells: new Map()
});

setState() {}
clearSelection() {
this.selection.set({
selectedRecords: new Map(),
selectedFields: new Map(),
selectedCells: new Map()
});
}

selectCell(recordId: string, fieldId: string) {
this.clearSelection();
this.selection().selectedCells.set(recordId, { [fieldId]: true });
}

selectCol(fieldId: string) {
this.clearSelection();
this.selection().selectedFields.set(fieldId, true);
}

selectRow(recordId: string) {
if (this.selection().selectedRecords.has(recordId)) {
this.selection().selectedRecords.delete(recordId);
} else {
this.selection().selectedRecords.set(recordId, true);
}
this.selection.set({
selectedRecords: this.selection().selectedRecords,
selectedFields: new Map(),
selectedCells: new Map()
});
}
}
Loading

0 comments on commit d96e94b

Please sign in to comment.