-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 3 commits
d89fefa
d96e94b
0f12102
aef4cc8
035e0ac
70306d5
8f7e85a
e19395a
489bb06
2013963
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,5 +123,8 @@ | |
} | ||
} | ||
} | ||
}, | ||
"cli": { | ||
"analytics": false | ||
} | ||
} |
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'; | ||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; | ||
import { buildGridData } from './utils'; | ||
import { AIFieldConfig, AITableFieldMenu, AITableRowHeight } from './types'; | ||
|
@@ -15,6 +27,8 @@ import { | |
AITableFields, | ||
AITableFieldType, | ||
AITableRecords, | ||
AITableField, | ||
AITableRecord, | ||
createDefaultField | ||
} from './core'; | ||
import { ThyIcon } from 'ngx-tethys/icon'; | ||
|
@@ -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', | ||
|
@@ -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>(); | ||
|
@@ -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>(); | ||
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aiTableGridSelectionService 中的 selection 和 aiTable.selection 中应该是同一个 signal ,这样就不需要维护两套 signal 数据 |
||
console.log('跟新啦', this.aiTable.selection()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. console |
||
}, | ||
{ allowSignalWrites: true } | ||
); | ||
} | ||
|
||
ngOnInit(): void { | ||
this.initAITable(); | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gridData 中的数据应该是只从 buildGridData 中而来,外部不应该有修改它属性的逻辑。 如果是要修改 checked ,当 toggleAllCheckbox 事件触发时 :
|
||
} | ||
|
||
selectCell(recordId: string, fieldId: string) { | ||
this.toggleAllCheckbox(false); | ||
this.isSelectedAll = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
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', | ||
|
@@ -10,3 +10,13 @@ export class SelectOptionPipe implements PipeTransform { | |
return options.find((item) => item.id === id); | ||
} | ||
} | ||
|
||
@Pipe({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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'; | ||
|
@@ -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; | ||
|
@@ -29,6 +34,116 @@ export class AITableGridEventService { | |
.subscribe((event) => { | ||
this.dblClick(event as MouseEvent); | ||
}); | ||
|
||
fromEvent<MouseEvent>(element, 'click') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 选中单元格的交互应该是 mousedown There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里的逻辑有些奇怪,gridComponent 中已经针对行和列以及单元格的点击事件做处理了,这里又监听了 click 并且再区分点击的是什么,如果用操作 dom 的方式设置高亮在 gridComponent 中就可以实现 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
没用的引用删除一下