diff --git a/.gitignore b/.gitignore index fd87fe19e..bf8d6ccc0 100644 --- a/.gitignore +++ b/.gitignore @@ -95,5 +95,5 @@ packages/vtable-docs/public/zh/documents packages/vtable-docs/public/en/documents packages/vtable-docs/public/*.html -#git-hook +# git-hook common/scripts/pre-commit \ No newline at end of file diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index c5b202df5..cf4594ea7 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -78,6 +78,7 @@ import { import { MenuHandler } from '../menu/dom/MenuHandler'; import type { BaseTableAPI, BaseTableConstructorOptions, IBaseTableProtected } from '../ts-types/base-table'; import { FocusInput } from './FouseInput'; +import { defaultPixelRatio } from '../tools/pixel-ratio'; const { toBoxArray } = utilStyle; const { isTouchEvent } = event; const rangeReg = /^\$(\d+)\$(\d+)$/; @@ -156,7 +157,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { menu, select: click, customRender, - pixelRatio = 1 + pixelRatio = defaultPixelRatio } = options; this.options = options; this._widthMode = widthMode; @@ -700,7 +701,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * @param pixelRatio */ setPixelRatio(pixelRatio: number) { - // do nothing + this.internalProps.pixelRatio = pixelRatio; + this.scenegraph.setPixelRatio(pixelRatio); } /** * 窗口尺寸发生变化 或者像数比变化 @@ -2030,7 +2032,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * 清除选中单元格 */ clearSelected() { - // do nothing + this.stateManeger.updateSelectPos(-1, -1); } /** * 选中单元格 和鼠标选中单元格效果一致 @@ -2038,7 +2040,8 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * @param row */ selectCell(col: number, row: number) { - // do nothing + this.stateManeger.updateSelectPos(col, row); + this.stateManeger.endSelectCells(); } abstract isListTable(): boolean; diff --git a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts index fbc712736..3bef8dceb 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/proxy.ts @@ -3,6 +3,7 @@ import type { Group } from '../../graphic/group'; import type { WrapText } from '../../graphic/text'; import { updateCellHeightForColumn } from '../../layout/update-height'; import type { Scenegraph } from '../../scenegraph'; +import { emptyGroup } from '../../scenegraph'; import { getProp } from '../../utils/get-prop'; import { getPadding } from '../../utils/padding'; import { createColGroup } from '../column'; @@ -251,6 +252,8 @@ export class SceneProxy { // 不改变row,更新body group范围 this.updateBody(y); } + + this.scenegraph.updateNextFrame(); } updateBody(y: number) { @@ -304,7 +307,7 @@ export class SceneProxy { for (let col = this.bodyLeftCol; col <= this.bodyRightCol; col++) { for (let row = syncTopRow; row <= syncBottomRow; row++) { // const cellGroup = this.table.scenegraph.getCell(col, row); - const cellGroup = this.highPerformanceGetCell(col, row); + const cellGroup = this.highPerformanceGetCell(col, row, distStartRow, distEndRow); this.updateCellGroupContent(cellGroup); } } @@ -367,7 +370,7 @@ export class SceneProxy { for (let col = this.bodyLeftCol; col <= this.bodyRightCol; col++) { for (let row = syncTopRow; row <= syncBottomRow; row++) { // const cellGroup = this.table.scenegraph.getCell(col, row); - const cellGroup = this.highPerformanceGetCell(col, row); + const cellGroup = this.highPerformanceGetCell(col, row, distStartRow, distEndRow); this.updateCellGroupContent(cellGroup); } } @@ -398,8 +401,6 @@ export class SceneProxy { (this.table as any).scenegraph.bodyGroup.firstChild.lastChild.row ); - this.scenegraph.renderSceneGraph(); - if (!this.table.internalProps.autoRowHeight) { await this.progress(); } @@ -524,7 +525,10 @@ export class SceneProxy { } } - highPerformanceGetCell(col: number, row: number) { + highPerformanceGetCell(col: number, row: number, rowStart: number = this.rowStart, rowEnd: number = this.rowEnd) { + if (row < rowStart || row > rowEnd) { + return emptyGroup; + } if (this.cellCache.get(col)) { const cacheCellGoup = this.cellCache.get(col); if ((cacheCellGoup._next || cacheCellGoup._prev) && Math.abs(cacheCellGoup.row - row) < row) { diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index f1a3402fb..237dc8155 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -10,7 +10,6 @@ import { createRowHeaderColGroup } from './group-creater/column'; import type { WrapText } from './graphic/text'; -import { updateAutoColWidth } from './layout/auto-width'; import { updateAutoRowHeight } from './layout/auto-height'; import { getCellMergeInfo } from './utils/get-cell-merge'; import { updateColWidth } from './layout/update-width'; @@ -22,21 +21,23 @@ import { createFrameBorder } from './style/frame-border'; import { ResizeColumnHotSpotSize } from '../tools/global'; import splitModule from './graphic/contributions'; import { getProp } from './utils/get-prop'; -import { createCellContent, dealWithIcon } from './utils/text-icon-layout'; +import { dealWithIcon } from './utils/text-icon-layout'; import { SceneProxy } from './group-creater/progress/proxy'; import { SortOrder } from '../state/state'; -import type { ListTable } from '../ListTable'; import type { TooltipOptions } from '../ts-types/tooltip'; import { computeColWidth, computeColsWidth } from './layout/compute-col-width'; -import { getStyleTheme } from './group-creater/column-helper'; import { moveHeaderPosition } from './layout/move-cell'; import { updateCell } from './group-creater/cell-helper'; import type { BaseTableAPI } from '../ts-types/base-table'; +import { updateAllSelectComponent, updateCellSelectBorder } from './select/update-select-border'; +import { createCellSelectBorder } from './select/create-select-border'; +import { moveSelectingRangeComponentsToSelectedRangeComponents } from './select/move-select-border'; +import { deleteAllSelectBorder, deleteLastSelectedRangeComponents } from './select/delete-select-border'; container.load(splitModule); -const groupForDebug = new Group({}); -groupForDebug.role = 'empty'; +export const emptyGroup = new Group({}); +emptyGroup.role = 'empty'; /** * @description: 表格场景树,存储和管理表格全部的场景图元 * @return {*} @@ -74,7 +75,8 @@ export class Scenegraph { width: table.canvas.width, height: table.canvas.height, disableDirtyBounds: false, - background: table.theme.underlayBackgroundColor + background: table.theme.underlayBackgroundColor, + dpr: table.internalProps.pixelRatio }); this.stage.defaultLayer.setTheme({ @@ -349,7 +351,14 @@ export class Scenegraph { cell = this.getCell(range.start.col, range.start.row); } - return cell || groupForDebug; + return cell || emptyGroup; + } + + highPerformanceGetCell(col: number, row: number): Group { + if (!this.isPivot && !this.transpose && !this.table.isHeader(col, row)) { + return this.proxy.highPerformanceGetCell(col, row); + } + return this.getCell(col, row); } getColGroup(col: number, isCornerOrColHeader = false): Group { @@ -396,179 +405,7 @@ export class Scenegraph { this.stage.renderNextFrame(); } resetAllSelectComponent() { - this.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { - const [startCol, startRow, endCol, endRow] = key.split('-'); - let cellsBounds; - for (let i = parseInt(startCol, 10); i <= parseInt(endCol, 10); i++) { - for (let j = parseInt(startRow, 10); j <= parseInt(endRow, 10); j++) { - const cellGroup = this.getCell(i, j); - cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新,这里强制更新一下 - const bounds = cellGroup.globalAABBBounds; - if (!cellsBounds) { - cellsBounds = bounds; - } else { - cellsBounds.union(bounds); - } - } - } - selectComp.rect.setAttributes({ - x: cellsBounds.x1 - this.tableGroup.attribute.x, - y: cellsBounds.y1 - this.tableGroup.attribute.y, - width: cellsBounds.width(), - height: cellsBounds.height(), - visible: true - }); - - //#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级 - const isNearRowHeader = - // this.table.scrollLeft === 0 && - parseInt(startCol, 10) === this.table.frozenColCount; - const isNearColHeader = - // this.table.scrollTop === 0 && - parseInt(startRow, 10) === this.table.frozenRowCount; - if ( - (isNearRowHeader && selectComp.rect.attribute.stroke[3]) || - (isNearColHeader && selectComp.rect.attribute.stroke[0]) - ) { - if (isNearRowHeader) { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'columnHeader' ? this.cornerHeaderGroup : this.rowHeaderGroup - ); - } - if (isNearColHeader) { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'rowHeader' ? this.cornerHeaderGroup : this.colHeaderGroup - ); - } - //#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面,所有需要调整选中框rect的 边界 - if ( - selectComp.rect.attribute.x < this.rowHeaderGroup.attribute.width && - this.table.scrollLeft > 0 && - (selectComp.role === 'body' || selectComp.role === 'columnHeader') - ) { - selectComp.rect.setAttributes({ - x: selectComp.rect.attribute.x + (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x), - width: selectComp.rect.attribute.width - (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x) - }); - } - if ( - selectComp.rect.attribute.y < this.colHeaderGroup.attribute.height && - this.table.scrollTop > 0 && - (selectComp.role === 'body' || selectComp.role === 'rowHeader') - ) { - selectComp.rect.setAttributes({ - y: selectComp.rect.attribute.y + (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y), - height: - selectComp.rect.attribute.height - (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y) - }); - } - //#endregion - } else { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'body' - ? this.bodyGroup - : selectComp.role === 'columnHeader' - ? this.colHeaderGroup - : selectComp.role === 'rowHeader' - ? this.rowHeaderGroup - : this.cornerHeaderGroup - ); - } - //#endregion - }); - this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { - const [startCol, startRow, endCol, endRow] = key.split('-'); - let cellsBounds; - for (let i = parseInt(startCol, 10); i <= parseInt(endCol, 10); i++) { - for (let j = parseInt(startRow, 10); j <= parseInt(endRow, 10); j++) { - const cellGroup = this.getCell(i, j); - cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新,这里强制更新一下 - const bounds = cellGroup.globalAABBBounds; - if (!cellsBounds) { - cellsBounds = bounds; - } else { - cellsBounds.union(bounds); - } - } - } - selectComp.rect.setAttributes({ - x: cellsBounds.x1 - this.tableGroup.attribute.x, - y: cellsBounds.y1 - this.tableGroup.attribute.y, - width: cellsBounds.width(), - height: cellsBounds.height(), - visible: true - }); - - //#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级 - const isNearRowHeader = - // this.table.scrollLeft === 0 && - parseInt(startCol, 10) === this.table.frozenColCount; - const isNearColHeader = - // this.table.scrollTop === 0 && - parseInt(startRow, 10) === this.table.frozenRowCount; - if ( - (isNearRowHeader && selectComp.rect.attribute.stroke[3]) || - (isNearColHeader && selectComp.rect.attribute.stroke[0]) - ) { - if (isNearRowHeader) { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'columnHeader' ? this.cornerHeaderGroup : this.rowHeaderGroup - ); - } - if (isNearColHeader) { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'rowHeader' ? this.cornerHeaderGroup : this.colHeaderGroup - ); - } - //#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面,所有需要调整选中框rect的 边界 - if ( - selectComp.rect.attribute.x < this.rowHeaderGroup.attribute.width && - this.table.scrollLeft > 0 && - (selectComp.role === 'body' || selectComp.role === 'columnHeader') - ) { - selectComp.rect.setAttributes({ - x: selectComp.rect.attribute.x + (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x), - width: selectComp.rect.attribute.width - (this.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x) - }); - } - if ( - selectComp.rect.attribute.y < this.colHeaderGroup.attribute.height && - this.table.scrollTop > 0 && - (selectComp.role === 'body' || selectComp.role === 'rowHeader') - ) { - selectComp.rect.setAttributes({ - y: selectComp.rect.attribute.y + (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y), - height: - selectComp.rect.attribute.height - (this.colHeaderGroup.attribute.height - selectComp.rect.attribute.y) - }); - } - //#endregion - } else { - this.tableGroup.insertAfter( - selectComp.rect, - selectComp.role === 'body' - ? this.bodyGroup - : selectComp.role === 'columnHeader' - ? this.colHeaderGroup - : selectComp.role === 'rowHeader' - ? this.rowHeaderGroup - : this.cornerHeaderGroup - ); - } - //#endregion - }); - } - - removeInteractionBorder(col: number, row: number) { - const cellGroup = this.getCell(col, row); - cellGroup.setAttribute('highlightStroke', undefined); - cellGroup.setAttribute('highlightStrokeArrayWidth', undefined); - cellGroup.setAttribute('highlightStrokeArrayColor', undefined); + updateAllSelectComponent(this); } hideHoverIcon(col: number, row: number) { @@ -626,6 +463,13 @@ export class Scenegraph { (cellGroup?.firstChild as any)?.activate?.(this.table); } + removeInteractionBorder(col: number, row: number) { + const cellGroup = this.getCell(col, row); + cellGroup.setAttribute('highlightStroke', undefined); + cellGroup.setAttribute('highlightStrokeArrayWidth', undefined); + cellGroup.setAttribute('highlightStrokeArrayColor', undefined); + } + createCellSelectBorder( start_Col: number, start_Row: number, @@ -635,238 +479,22 @@ export class Scenegraph { selectId: string, //整体区域${endRow}-${startCol}${startRow}${endCol}${endRow}作为其编号 strokes?: boolean[] ) { - const startCol = Math.min(start_Col, end_Col); - const startRow = Math.min(start_Row, end_Row); - const endCol = Math.max(start_Col, end_Col); - const endRow = Math.max(start_Row, end_Row); - - let cellsBounds; - for (let i = startCol; i <= endCol; i++) { - for (let j = startRow; j <= endRow; j++) { - const cellGroup = this.getCell(i, j); - if (cellGroup.role === 'shadow-cell') { - continue; - } - const bounds = cellGroup.globalAABBBounds; - if (!cellsBounds) { - cellsBounds = bounds; - } else { - cellsBounds.union(bounds); - } - } - } - - const theme = this.table.theme; - // 框选外边框 - const bodyClickBorderColor = theme.selectionStyle?.cellBorderColor; - const bodyClickLineWidth = theme.selectionStyle?.cellBorderLineWidth; - const rect = createRect({ - pickable: false, - fill: true, - fillColor: (theme.selectionStyle?.cellBgColor as any) ?? 'rgba(0, 0, 255,0.1)', - strokeColor: bodyClickBorderColor as string, - lineWidth: bodyClickLineWidth as number, - stroke: strokes, - x: cellsBounds.x1 - this.tableGroup.attribute.x, - y: cellsBounds.y1 - this.tableGroup.attribute.y, - width: cellsBounds.width(), - height: cellsBounds.height(), - visible: true - }); - this.lastSelectId = selectId; - this.selectingRangeComponents.set(`${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`, { - rect, - role: selectRangeType - }); - this.tableGroup.insertAfter( - rect, - selectRangeType === 'body' - ? this.bodyGroup - : selectRangeType === 'columnHeader' - ? this.colHeaderGroup - : selectRangeType === 'rowHeader' - ? this.rowHeaderGroup - : this.cornerHeaderGroup - ); + createCellSelectBorder(this, start_Col, start_Row, end_Col, end_Row, selectRangeType, selectId, strokes); } moveSelectingRangeComponentsToSelectedRangeComponents() { - this.selectingRangeComponents.forEach((rangeComponent, key) => { - if (this.selectedRangeComponents.get(key)) { - this.selectedRangeComponents.get(key).rect.delete(); - } - this.selectedRangeComponents.set(key, rangeComponent); - }); - this.selectingRangeComponents = new Map(); - this.updateNextFrame(); + moveSelectingRangeComponentsToSelectedRangeComponents(this); } /** 按住shift 则继续上次选中范围 需要将现有的删除掉 */ deleteLastSelectedRangeComponents() { - this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { - const lastSelectId = key.split('-')[4]; - if (lastSelectId === this.lastSelectId) { - selectComp.rect.delete(); - this.selectedRangeComponents.delete(key); - } - }); + deleteLastSelectedRangeComponents(this); } deleteAllSelectBorder() { - this.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { - selectComp.rect.delete(); - }); - this.selectedRangeComponents = new Map(); + deleteAllSelectBorder(this); } updateCellSelectBorder(newStartCol: number, newStartRow: number, newEndCol: number, newEndRow: number) { - let startCol = Math.min(newEndCol, newStartCol); - let startRow = Math.min(newEndRow, newStartRow); - let endCol = Math.max(newEndCol, newStartCol); - let endRow = Math.max(newEndRow, newStartRow); - //#region region 校验四周的单元格有没有合并的情况,如有则扩大范围 - const extendSelectRange = () => { - let isExtend = false; - for (let col = startCol; col <= endCol; col++) { - if (col === startCol) { - for (let row = startRow; row <= endRow; row++) { - const mergeInfo = getCellMergeInfo(this.table, col, row); - if (mergeInfo && mergeInfo.start.col < startCol) { - startCol = mergeInfo.start.col; - isExtend = true; - break; - } - } - } - if (!isExtend && col === endCol) { - for (let row = startRow; row <= endRow; row++) { - const mergeInfo = getCellMergeInfo(this.table, col, row); - if (mergeInfo && mergeInfo.end.col > endCol) { - endCol = mergeInfo.end.col; - isExtend = true; - break; - } - } - } - - if (isExtend) { - break; - } - } - if (!isExtend) { - for (let row = startRow; row <= endRow; row++) { - if (row === startRow) { - for (let col = startCol; col <= endCol; col++) { - const mergeInfo = getCellMergeInfo(this.table, col, row); - if (mergeInfo && mergeInfo.start.row < startRow) { - startRow = mergeInfo.start.row; - isExtend = true; - break; - } - } - } - if (!isExtend && row === endRow) { - for (let col = startCol; col <= endCol; col++) { - const mergeInfo = getCellMergeInfo(this.table, col, row); - if (mergeInfo && mergeInfo.end.row > endRow) { - endRow = mergeInfo.end.row; - isExtend = true; - break; - } - } - } - - if (isExtend) { - break; - } - } - } - if (isExtend) { - extendSelectRange(); - } - }; - extendSelectRange(); - //#endregion - this.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { - selectComp.rect.delete(); - }); - this.selectingRangeComponents = new Map(); - - let needRowHeader = false; - let needColumnHeader = false; - let needBody = false; - let needCornerHeader = false; - if (startCol <= this.table.frozenColCount - 1 && startRow <= this.table.frozenRowCount - 1) { - needCornerHeader = true; - } - if (startCol <= this.table.frozenColCount - 1 && endRow >= this.table.frozenRowCount) { - needRowHeader = true; - } - if (startRow <= this.table.frozenRowCount - 1 && endCol >= this.table.frozenColCount) { - needColumnHeader = true; - } - if (endCol >= this.table.frozenColCount && endRow >= this.table.frozenRowCount) { - needBody = true; - } - - // TODO 可以尝试不拆分三个表头和body【前提是theme中合并配置】 用一个SelectBorder 需要结合clip,并动态设置border的范围【依据区域范围 已经是否跨表头及body】 - if (needCornerHeader) { - const cornerEndCol = Math.min(endCol, this.table.frozenColCount - 1); - const cornerEndRow = Math.min(endRow, this.table.frozenRowCount - 1); - const strokeArray = [true, !needColumnHeader, !needRowHeader, true]; - this.createCellSelectBorder( - startCol, - startRow, - cornerEndCol, - cornerEndRow, - 'cornerHeader', - `${startCol}${startRow}${endCol}${endRow}`, - strokeArray - ); - } - if (needColumnHeader) { - const columnHeaderStartCol = Math.max(startCol, this.table.frozenColCount); - const columnHeaderEndRow = Math.min(endRow, this.table.frozenRowCount - 1); - const strokeArray = [true, true, !needBody, !needCornerHeader]; - this.createCellSelectBorder( - columnHeaderStartCol, - startRow, - endCol, - columnHeaderEndRow, - 'columnHeader', - `${startCol}${startRow}${endCol}${endRow}`, - strokeArray - ); - } - if (needRowHeader) { - const columnHeaderStartRow = Math.max(startRow, this.table.frozenRowCount); - const columnHeaderEndCol = Math.min(endCol, this.table.frozenColCount - 1); - const strokeArray = [!needCornerHeader, !needBody, true, true]; - this.createCellSelectBorder( - startCol, - columnHeaderStartRow, - columnHeaderEndCol, - endRow, - 'rowHeader', - `${startCol}${startRow}${endCol}${endRow}`, - strokeArray - ); - } - if (needBody) { - const columnHeaderStartCol = Math.max(startCol, this.table.frozenColCount); - const columnHeaderStartRow = Math.max(startRow, this.table.frozenRowCount); - const strokeArray = [!needColumnHeader, true, true, !needRowHeader]; - this.createCellSelectBorder( - columnHeaderStartCol, - columnHeaderStartRow, - endCol, - endRow, - 'body', - `${startCol}${startRow}${endCol}${endRow}`, - strokeArray - ); - } + updateCellSelectBorder(this, newStartCol, newStartRow, newEndCol, newEndRow); } - // hideCellsSelectBorder() { - // this.component.selectBorder.setAttribute('visible', false); - // } /** * @description: 获取指定单元格指定位置的icon mark @@ -1151,6 +779,8 @@ export class Scenegraph { // 更新滚动条状态 this.component.updateScrollBar(); + + this.updateNextFrame(); } /** @@ -1769,6 +1399,16 @@ export class Scenegraph { } updateCell(col, row, this.table); } + + setPixelRatio(pixelRatio: number) { + // this.stage.setDpr(pixelRatio); + // 这里因为本时刻部分节点有更新bounds标记,直接render回导致开启DirtyBounds,无法完整重绘画布; + // 所以这里先关闭DirtyBounds,等待下一帧再开启 + this.stage.disableDirtyBounds(); + this.stage.window.setDpr(pixelRatio); + this.stage.render(); + this.stage.enableDirtyBounds(); + } } function showIcon(scene: Scenegraph, cellGroup: Group, visibleTime: 'mouseenter_cell' | 'click_cell') { diff --git a/packages/vtable/src/scenegraph/select/create-select-border.ts b/packages/vtable/src/scenegraph/select/create-select-border.ts new file mode 100644 index 000000000..01f12c49a --- /dev/null +++ b/packages/vtable/src/scenegraph/select/create-select-border.ts @@ -0,0 +1,68 @@ +import { createRect } from '@visactor/vrender'; +import type { CellType } from '../../ts-types'; +import type { Scenegraph } from '../scenegraph'; + +export function createCellSelectBorder( + scene: Scenegraph, + start_Col: number, + start_Row: number, + end_Col: number, + end_Row: number, + selectRangeType: CellType, + selectId: string, //整体区域${endRow}-${startCol}${startRow}${endCol}${endRow}作为其编号 + strokes?: boolean[] +) { + const startCol = Math.min(start_Col, end_Col); + const startRow = Math.min(start_Row, end_Row); + const endCol = Math.max(start_Col, end_Col); + const endRow = Math.max(start_Row, end_Row); + + let cellsBounds; + for (let i = startCol; i <= endCol; i++) { + for (let j = startRow; j <= endRow; j++) { + const cellGroup = scene.highPerformanceGetCell(i, j); + if (cellGroup.role === 'shadow-cell') { + continue; + } + const bounds = cellGroup.globalAABBBounds; + if (!cellsBounds) { + cellsBounds = bounds; + } else { + cellsBounds.union(bounds); + } + } + } + + const theme = scene.table.theme; + // 框选外边框 + const bodyClickBorderColor = theme.selectionStyle?.cellBorderColor; + const bodyClickLineWidth = theme.selectionStyle?.cellBorderLineWidth; + const rect = createRect({ + pickable: false, + fill: true, + fillColor: (theme.selectionStyle?.cellBgColor as any) ?? 'rgba(0, 0, 255,0.1)', + strokeColor: bodyClickBorderColor as string, + lineWidth: bodyClickLineWidth as number, + stroke: strokes, + x: cellsBounds.x1 - scene.tableGroup.attribute.x, + y: cellsBounds.y1 - scene.tableGroup.attribute.y, + width: cellsBounds.width(), + height: cellsBounds.height(), + visible: true + }); + scene.lastSelectId = selectId; + scene.selectingRangeComponents.set(`${startCol}-${startRow}-${endCol}-${endRow}-${selectId}`, { + rect, + role: selectRangeType + }); + scene.tableGroup.insertAfter( + rect, + selectRangeType === 'body' + ? scene.bodyGroup + : selectRangeType === 'columnHeader' + ? scene.colHeaderGroup + : selectRangeType === 'rowHeader' + ? scene.rowHeaderGroup + : scene.cornerHeaderGroup + ); +} diff --git a/packages/vtable/src/scenegraph/select/delete-select-border.ts b/packages/vtable/src/scenegraph/select/delete-select-border.ts new file mode 100644 index 000000000..24fb8dabf --- /dev/null +++ b/packages/vtable/src/scenegraph/select/delete-select-border.ts @@ -0,0 +1,21 @@ +import type { IRect } from '@visactor/vrender'; +import type { Scenegraph } from '../scenegraph'; +import type { CellType } from '../../ts-types'; + +/** 按住shift 则继续上次选中范围 需要将现有的删除掉 */ +export function deleteLastSelectedRangeComponents(scene: Scenegraph) { + scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { + const lastSelectId = key.split('-')[4]; + if (lastSelectId === scene.lastSelectId) { + selectComp.rect.delete(); + scene.selectedRangeComponents.delete(key); + } + }); +} + +export function deleteAllSelectBorder(scene: Scenegraph) { + scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { + selectComp.rect.delete(); + }); + scene.selectedRangeComponents = new Map(); +} diff --git a/packages/vtable/src/scenegraph/select/move-select-border.ts b/packages/vtable/src/scenegraph/select/move-select-border.ts new file mode 100644 index 000000000..ed1b0a903 --- /dev/null +++ b/packages/vtable/src/scenegraph/select/move-select-border.ts @@ -0,0 +1,12 @@ +import type { Scenegraph } from '../scenegraph'; + +export function moveSelectingRangeComponentsToSelectedRangeComponents(scene: Scenegraph) { + scene.selectingRangeComponents.forEach((rangeComponent, key) => { + if (scene.selectedRangeComponents.get(key)) { + scene.selectedRangeComponents.get(key).rect.delete(); + } + scene.selectedRangeComponents.set(key, rangeComponent); + }); + scene.selectingRangeComponents = new Map(); + scene.updateNextFrame(); +} diff --git a/packages/vtable/src/scenegraph/select/update-select-border.ts b/packages/vtable/src/scenegraph/select/update-select-border.ts new file mode 100644 index 000000000..8ee348c16 --- /dev/null +++ b/packages/vtable/src/scenegraph/select/update-select-border.ts @@ -0,0 +1,265 @@ +import type { IRect } from '@visactor/vrender'; +import type { Scenegraph } from '../scenegraph'; +import type { CellType } from '../../ts-types'; +import { getCellMergeInfo } from '../utils/get-cell-merge'; + +export function updateAllSelectComponent(scene: Scenegraph) { + scene.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { + updateComponent(selectComp, key, scene); + }); + scene.selectedRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { + updateComponent(selectComp, key, scene); + }); +} + +function updateComponent(selectComp: { rect: IRect; role: CellType }, key: string, scene: Scenegraph) { + const [startColStr, startRowStr, endColStr, endRowStr] = key.split('-'); + const startCol = parseInt(startColStr, 10); + const startRow = parseInt(startRowStr, 10); + const endCol = parseInt(endColStr, 10); + const endRow = parseInt(endRowStr, 10); + let cellsBounds; + for (let i = startCol; i <= endCol; i++) { + for (let j = startRow; j <= endRow; j++) { + const cellGroup = scene.highPerformanceGetCell(i, j); + if (cellGroup.role !== 'cell') { + continue; + } + cellGroup.AABBBounds.width(); // hack: globalAABBBounds可能不会自动更新,这里强制更新一下 + const bounds = cellGroup.globalAABBBounds; + if (!cellsBounds) { + cellsBounds = bounds; + } else { + cellsBounds.union(bounds); + } + } + } + if (!cellsBounds) { + // 选中区域在实际单元格区域外,不显示选择框 + selectComp.rect.setAttributes({ + visible: false + }); + } else { + selectComp.rect.setAttributes({ + x: cellsBounds.x1 - scene.tableGroup.attribute.x, + y: cellsBounds.y1 - scene.tableGroup.attribute.y, + width: cellsBounds.width(), + height: cellsBounds.height(), + visible: true + }); + } + + //#region 判断是不是按着表头部分的选中框 因为绘制层级的原因 线宽会被遮住一半,因此需要动态调整层级 + const isNearRowHeader = + // scene.table.scrollLeft === 0 && + startCol === scene.table.frozenColCount; + const isNearColHeader = + // scene.table.scrollTop === 0 && + startRow === scene.table.frozenRowCount; + if ( + (isNearRowHeader && selectComp.rect.attribute.stroke[3]) || + (isNearColHeader && selectComp.rect.attribute.stroke[0]) + ) { + if (isNearRowHeader) { + scene.tableGroup.insertAfter( + selectComp.rect, + selectComp.role === 'columnHeader' ? scene.cornerHeaderGroup : scene.rowHeaderGroup + ); + } + if (isNearColHeader) { + scene.tableGroup.insertAfter( + selectComp.rect, + selectComp.role === 'rowHeader' ? scene.cornerHeaderGroup : scene.colHeaderGroup + ); + } + //#region 调整层级后 滚动情况下会出现绘制范围出界 如body的选中框 渲染在了rowheader上面,所有需要调整选中框rect的 边界 + if ( + selectComp.rect.attribute.x < scene.rowHeaderGroup.attribute.width && + scene.table.scrollLeft > 0 && + (selectComp.role === 'body' || selectComp.role === 'columnHeader') + ) { + selectComp.rect.setAttributes({ + x: selectComp.rect.attribute.x + (scene.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x), + width: selectComp.rect.attribute.width - (scene.rowHeaderGroup.attribute.width - selectComp.rect.attribute.x) + }); + } + if ( + selectComp.rect.attribute.y < scene.colHeaderGroup.attribute.height && + scene.table.scrollTop > 0 && + (selectComp.role === 'body' || selectComp.role === 'rowHeader') + ) { + selectComp.rect.setAttributes({ + y: selectComp.rect.attribute.y + (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y), + height: selectComp.rect.attribute.height - (scene.colHeaderGroup.attribute.height - selectComp.rect.attribute.y) + }); + } + //#endregion + } else { + scene.tableGroup.insertAfter( + selectComp.rect, + selectComp.role === 'body' + ? scene.bodyGroup + : selectComp.role === 'columnHeader' + ? scene.colHeaderGroup + : selectComp.role === 'rowHeader' + ? scene.rowHeaderGroup + : scene.cornerHeaderGroup + ); + } + //#endregion +} + +export function updateCellSelectBorder( + scene: Scenegraph, + newStartCol: number, + newStartRow: number, + newEndCol: number, + newEndRow: number +) { + let startCol = Math.min(newEndCol, newStartCol); + let startRow = Math.min(newEndRow, newStartRow); + let endCol = Math.max(newEndCol, newStartCol); + let endRow = Math.max(newEndRow, newStartRow); + //#region region 校验四周的单元格有没有合并的情况,如有则扩大范围 + const extendSelectRange = () => { + let isExtend = false; + for (let col = startCol; col <= endCol; col++) { + if (col === startCol) { + for (let row = startRow; row <= endRow; row++) { + const mergeInfo = getCellMergeInfo(scene.table, col, row); + if (mergeInfo && mergeInfo.start.col < startCol) { + startCol = mergeInfo.start.col; + isExtend = true; + break; + } + } + } + if (!isExtend && col === endCol) { + for (let row = startRow; row <= endRow; row++) { + const mergeInfo = getCellMergeInfo(scene.table, col, row); + if (mergeInfo && mergeInfo.end.col > endCol) { + endCol = mergeInfo.end.col; + isExtend = true; + break; + } + } + } + + if (isExtend) { + break; + } + } + if (!isExtend) { + for (let row = startRow; row <= endRow; row++) { + if (row === startRow) { + for (let col = startCol; col <= endCol; col++) { + const mergeInfo = getCellMergeInfo(scene.table, col, row); + if (mergeInfo && mergeInfo.start.row < startRow) { + startRow = mergeInfo.start.row; + isExtend = true; + break; + } + } + } + if (!isExtend && row === endRow) { + for (let col = startCol; col <= endCol; col++) { + const mergeInfo = getCellMergeInfo(scene.table, col, row); + if (mergeInfo && mergeInfo.end.row > endRow) { + endRow = mergeInfo.end.row; + isExtend = true; + break; + } + } + } + + if (isExtend) { + break; + } + } + } + if (isExtend) { + extendSelectRange(); + } + }; + extendSelectRange(); + //#endregion + scene.selectingRangeComponents.forEach((selectComp: { rect: IRect; role: CellType }, key: string) => { + selectComp.rect.delete(); + }); + scene.selectingRangeComponents = new Map(); + + let needRowHeader = false; + let needColumnHeader = false; + let needBody = false; + let needCornerHeader = false; + if (startCol <= scene.table.frozenColCount - 1 && startRow <= scene.table.frozenRowCount - 1) { + needCornerHeader = true; + } + if (startCol <= scene.table.frozenColCount - 1 && endRow >= scene.table.frozenRowCount) { + needRowHeader = true; + } + if (startRow <= scene.table.frozenRowCount - 1 && endCol >= scene.table.frozenColCount) { + needColumnHeader = true; + } + if (endCol >= scene.table.frozenColCount && endRow >= scene.table.frozenRowCount) { + needBody = true; + } + + // TODO 可以尝试不拆分三个表头和body【前提是theme中合并配置】 用一个SelectBorder 需要结合clip,并动态设置border的范围【依据区域范围 已经是否跨表头及body】 + if (needCornerHeader) { + const cornerEndCol = Math.min(endCol, scene.table.frozenColCount - 1); + const cornerEndRow = Math.min(endRow, scene.table.frozenRowCount - 1); + const strokeArray = [true, !needColumnHeader, !needRowHeader, true]; + scene.createCellSelectBorder( + startCol, + startRow, + cornerEndCol, + cornerEndRow, + 'cornerHeader', + `${startCol}${startRow}${endCol}${endRow}`, + strokeArray + ); + } + if (needColumnHeader) { + const columnHeaderStartCol = Math.max(startCol, scene.table.frozenColCount); + const columnHeaderEndRow = Math.min(endRow, scene.table.frozenRowCount - 1); + const strokeArray = [true, true, !needBody, !needCornerHeader]; + scene.createCellSelectBorder( + columnHeaderStartCol, + startRow, + endCol, + columnHeaderEndRow, + 'columnHeader', + `${startCol}${startRow}${endCol}${endRow}`, + strokeArray + ); + } + if (needRowHeader) { + const columnHeaderStartRow = Math.max(startRow, scene.table.frozenRowCount); + const columnHeaderEndCol = Math.min(endCol, scene.table.frozenColCount - 1); + const strokeArray = [!needCornerHeader, !needBody, true, true]; + scene.createCellSelectBorder( + startCol, + columnHeaderStartRow, + columnHeaderEndCol, + endRow, + 'rowHeader', + `${startCol}${startRow}${endCol}${endRow}`, + strokeArray + ); + } + if (needBody) { + const columnHeaderStartCol = Math.max(startCol, scene.table.frozenColCount); + const columnHeaderStartRow = Math.max(startRow, scene.table.frozenRowCount); + const strokeArray = [!needColumnHeader, true, true, !needRowHeader]; + scene.createCellSelectBorder( + columnHeaderStartCol, + columnHeaderStartRow, + endCol, + endRow, + 'body', + `${startCol}${startRow}${endCol}${endRow}`, + strokeArray + ); + } +} diff --git a/packages/vtable/src/scenegraph/utils/get-cell-merge.ts b/packages/vtable/src/scenegraph/utils/get-cell-merge.ts index acae3a808..8e58fc586 100644 --- a/packages/vtable/src/scenegraph/utils/get-cell-merge.ts +++ b/packages/vtable/src/scenegraph/utils/get-cell-merge.ts @@ -1,4 +1,4 @@ -import type { CellRange } from '../../ts-types'; +import type { CellRange, TextColumnDefine } from '../../ts-types'; import type { BaseTableAPI } from '../../ts-types/base-table'; /** @@ -9,6 +9,10 @@ import type { BaseTableAPI } from '../../ts-types/base-table'; * @return {false | CellRange} */ export function getCellMergeInfo(table: BaseTableAPI, col: number, row: number): false | CellRange { + // 先判断非表头且非cellMerge配置,返回false + if (!table.isHeader(col, row) && (table.getBodyColumnDefine(col, row) as TextColumnDefine).mergeCell !== true) { + return false; + } const range = table.getCellRange(col, row); const isMerge = range.start.col !== range.end.col || range.start.row !== range.end.row; if (!isMerge) { diff --git a/packages/vtable/src/tools/pixel-ratio.ts b/packages/vtable/src/tools/pixel-ratio.ts new file mode 100644 index 000000000..6162868b0 --- /dev/null +++ b/packages/vtable/src/tools/pixel-ratio.ts @@ -0,0 +1,18 @@ +import { isNode } from './helper'; + +export let defaultPixelRatio = 1; +/* + * @Description: 设置像素比 + */ +function setPixelRatio(): void { + if (isNode) { + defaultPixelRatio = 1; + } else { + defaultPixelRatio = Math.ceil(window.devicePixelRatio || 1); + if (defaultPixelRatio > 1 && defaultPixelRatio % 2 !== 0) { + // 非整数倍的像素比,向上取整 + defaultPixelRatio += 1; + } + } +} +setPixelRatio();