UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

562 lines 25.6 kB
import { Rect, } from '@antv/g'; import { cloneDeep, isEmpty, isNil, map, throttle } from 'lodash'; import { ColCell, DataCell, RowCell } from '../../cell'; import { FRONT_GROUND_GROUP_BRUSH_SELECTION_Z_INDEX, FrozenGroupArea, InteractionStateName, InterceptType, S2Event, ScrollDirection, } from '../../common/constant'; import { BRUSH_AUTO_SCROLL_INITIAL_CONFIG, InteractionBrushSelectionStage, ScrollDirectionRowIndexDiff, } from '../../common/constant/interaction'; import { isFrozenCol, isFrozenRow, isFrozenTrailingCol, isFrozenTrailingRow, } from '../../facet/utils'; import { getCellsTooltipData } from '../../utils'; import { getCellMeta, getScrollOffsetForCol, getScrollOffsetForRow, } from '../../utils/interaction'; import { BaseEvent } from '../base-interaction'; export class BaseBrushSelection extends BaseEvent { constructor() { super(...arguments); this.displayedCells = []; this.brushRangeCells = []; this.brushSelectionStage = InteractionBrushSelectionStage.UN_DRAGGED; this.brushSelectionMinimumMoveDistance = 5; this.scrollAnimationComplete = true; this.mouseMoveDistanceFromCanvas = 0; this.setMoveDistanceFromCanvas = (delta, needScrollForX, needScrollForY) => { let deltaVal = 0; if (needScrollForX) { deltaVal = delta.x; } if (needScrollForY) { const deltaY = delta.y; if (needScrollForX) { deltaVal = Math.max(deltaY, deltaVal); } else { deltaVal = deltaY; } } this.mouseMoveDistanceFromCanvas = Math.abs(deltaVal); }; this.formatBrushPointForScroll = (delta, isRowHeader = false) => { var _a, _b, _c, _d; const { x, y } = delta; const { facet } = this.spreadsheet; const { minX, maxX } = isRowHeader ? facet.cornerBBox : facet.panelBBox; const { minY, maxY } = facet.panelBBox; let newX = ((_a = this.endBrushPoint) === null || _a === void 0 ? void 0 : _a.x) + x; let newY = ((_b = this.endBrushPoint) === null || _b === void 0 ? void 0 : _b.y) + y; // 有滚动条才需要滚动 let needScrollForX = isRowHeader ? !!facet.hRowScrollBar : !!facet.hScrollBar; let needScrollForY = !!facet.vScrollBar; const vScrollBarWidth = (_d = (_c = facet.vScrollBar) === null || _c === void 0 ? void 0 : _c.getBBox()) === null || _d === void 0 ? void 0 : _d.width; // 额外加缩进,保证 getShape 在 panelBox 内 const extraPixel = 2; if (newX > maxX) { newX = maxX - vScrollBarWidth - extraPixel; } else if (newX < minX) { newX = minX + extraPixel; } else { needScrollForX = false; } if (newY > maxY) { newY = maxY - extraPixel; } else if (newY <= minY) { newY = minY + extraPixel; } else { needScrollForY = false; } return { x: { value: newX, needScroll: needScrollForX, }, y: { value: newY, needScroll: needScrollForY, }, }; }; this.autoScrollIntervalId = null; // 矩形相交算法: 通过判断两矩形左右上下的线是否相交 this.rectanglesIntersect = (rect1, rect2) => rect1.maxX > rect2.minX && rect1.minX < rect2.maxX && rect1.minY < rect2.maxY && rect1.maxY > rect2.minY; this.autoScrollConfig = cloneDeep(BRUSH_AUTO_SCROLL_INITIAL_CONFIG); this.validateYIndex = (yIndex) => { var _a, _b; const { facet } = this.spreadsheet; const frozenGroupAreas = facet.frozenGroupAreas; let min = 0; const frozenRowRange = (_a = frozenGroupAreas === null || frozenGroupAreas === void 0 ? void 0 : frozenGroupAreas.frozenRow) === null || _a === void 0 ? void 0 : _a.range; if (frozenRowRange === null || frozenRowRange === void 0 ? void 0 : frozenRowRange[1]) { min = frozenRowRange[1] + 1; } if (yIndex < min) { return null; } let max = facet.getCellRange().end; const frozenTrailingRowRange = (_b = frozenGroupAreas === null || frozenGroupAreas === void 0 ? void 0 : frozenGroupAreas.frozenTrailingRow) === null || _b === void 0 ? void 0 : _b.range; if (frozenTrailingRowRange === null || frozenTrailingRowRange === void 0 ? void 0 : frozenTrailingRowRange[0]) { max = frozenTrailingRowRange[0] - 1; } if (yIndex > max) { return null; } return yIndex; }; this.validateXIndex = (xIndex) => { const { facet } = this.spreadsheet; const frozenGroupAreas = facet.frozenGroupAreas; let min = 0; const frozenColRange = frozenGroupAreas[FrozenGroupArea.Col].range; if (frozenColRange === null || frozenColRange === void 0 ? void 0 : frozenColRange[1]) { min = frozenColRange[1] + 1; } if (xIndex < min) { return null; } let max = facet.getColLeafNodes().length - 1; const frozenTrailingColRange = frozenGroupAreas[FrozenGroupArea.TrailingCol].range; if (frozenTrailingColRange === null || frozenTrailingColRange === void 0 ? void 0 : frozenTrailingColRange[0]) { max = frozenTrailingColRange[0] - 1; } if (xIndex > max) { return null; } return xIndex; }; this.adjustNextColIndexWithFrozen = (colIndex, dir) => { const { facet } = this.spreadsheet; const colLength = facet.getColLeafNodes().length; const { colCount, trailingColCount } = facet.getFrozenOptions(); const panelIndexes = facet .panelScrollGroupIndexes; if (colCount > 0 && dir === ScrollDirection.SCROLL_UP && isFrozenCol(colIndex, colCount)) { return panelIndexes[0]; } if (trailingColCount > 0 && dir === ScrollDirection.SCROLL_DOWN && isFrozenTrailingCol(colIndex, trailingColCount, colLength)) { return panelIndexes[1]; } return colIndex; }; this.adjustNextRowIndexWithFrozen = (rowIndex, dir) => { const { facet } = this.spreadsheet; const cellRange = facet.getCellRange(); const { rowCount, trailingRowCount } = facet.getFrozenOptions(); const panelIndexes = facet .panelScrollGroupIndexes; if (rowCount > 0 && dir === ScrollDirection.SCROLL_UP && isFrozenRow(rowIndex, cellRange.start, rowCount)) { return panelIndexes[2]; } if (trailingRowCount > 0 && dir === ScrollDirection.SCROLL_DOWN && isFrozenTrailingRow(rowIndex, cellRange.end, trailingRowCount)) { return panelIndexes[3]; } return rowIndex; }; this.getWillScrollRowIndexDiff = (dir) => { return dir === ScrollDirection.SCROLL_DOWN ? ScrollDirectionRowIndexDiff.SCROLL_DOWN : ScrollDirectionRowIndexDiff.SCROLL_UP; }; this.getDefaultWillScrollToRowIndex = (dir) => { const rowIndex = this.adjustNextRowIndexWithFrozen(this.endBrushPoint.rowIndex, dir); const nextRowIndex = rowIndex + this.getWillScrollRowIndexDiff(dir); return this.validateYIndex(nextRowIndex); }; this.getWillScrollToRowIndex = (dir) => { return this.getDefaultWillScrollToRowIndex(dir); }; this.getNextScrollDelta = (config) => { const { scrollX = 0, scrollY = 0 } = this.spreadsheet.facet.getScrollOffset(); let x = 0; let y = 0; if (config.y.scroll) { const dir = config.y.value > 0 ? ScrollDirection.SCROLL_DOWN : ScrollDirection.SCROLL_UP; const willScrollToRowIndex = this.getWillScrollToRowIndex(dir); if (isNil(willScrollToRowIndex)) { y = 0; } else { const scrollOffsetY = getScrollOffsetForRow(willScrollToRowIndex, dir, this.spreadsheet) - scrollY; const isInvalidScroll = isNil(scrollOffsetY) || Number.isNaN(scrollOffsetY); y = isInvalidScroll ? 0 : scrollOffsetY; } } if (config.x.scroll) { const dir = config.x.value > 0 ? ScrollDirection.SCROLL_DOWN : ScrollDirection.SCROLL_UP; const colIndex = this.adjustNextColIndexWithFrozen(this.endBrushPoint.colIndex, dir); const nextIndex = this.validateXIndex(colIndex + (config.x.value > 0 ? 1 : -1)); x = isNil(nextIndex) ? 0 : getScrollOffsetForCol(nextIndex, dir, this.spreadsheet) - scrollX; } return { x, y, }; }; this.onScrollAnimationComplete = () => { this.scrollAnimationComplete = true; if (this.brushSelectionStage !== InteractionBrushSelectionStage.UN_DRAGGED) { this.renderPrepareSelected(this.endBrushPoint); } }; this.autoScroll = (isRowHeader = false) => { if (this.brushSelectionStage === InteractionBrushSelectionStage.UN_DRAGGED || !this.scrollAnimationComplete) { return; } const config = this.autoScrollConfig; const scrollOffset = this.spreadsheet.facet.getScrollOffset(); const key = isRowHeader ? 'rowHeaderOffsetX' : 'offsetX'; const offsetCfg = { rowHeaderOffsetX: { value: scrollOffset.rowHeaderScrollX, animate: true, }, offsetX: { value: scrollOffset.scrollX, animate: true, }, offsetY: { value: scrollOffset.scrollY, animate: true, }, }; const { x: deltaX, y: deltaY } = this.getNextScrollDelta(config); if (deltaY === 0 && deltaX === 0) { this.clearAutoScroll(); return; } if (config.y.scroll) { offsetCfg.offsetY.value += deltaY; } if (config.x.scroll) { const offset = offsetCfg[key]; offset.value += deltaX; if (offset.value < 0) { offset.value = 0; } } this.scrollAnimationComplete = false; // x 轴滚动速度慢 const ratio = config.x.scroll ? 1 : 3; const duration = Math.max(16, 300 - this.mouseMoveDistanceFromCanvas * ratio); this.spreadsheet.facet.scrollWithAnimation(offsetCfg, duration, this.onScrollAnimationComplete); }; this.handleScroll = throttle((x, y, isRowHeader = false) => { if (this.brushSelectionStage === InteractionBrushSelectionStage.UN_DRAGGED) { return; } const { x: { value: newX, needScroll: needScrollForX }, y: { value: newY, needScroll: needScrollForY }, } = this.formatBrushPointForScroll({ x, y }, isRowHeader); const config = this.autoScrollConfig; if (needScrollForY) { config.y.value = y; config.y.scroll = true; } if (needScrollForX) { config.x.value = x; config.x.scroll = true; } this.setMoveDistanceFromCanvas({ x, y }, needScrollForX, needScrollForY); this.renderPrepareSelected({ x: newX, y: newY, }); if (needScrollForY || needScrollForX) { this.clearAutoScroll(); this.autoScroll(isRowHeader); this.autoScrollIntervalId = setInterval(() => { this.autoScroll(isRowHeader); }, 16); } }, 30); this.clearAutoScroll = () => { if (this.autoScrollIntervalId) { clearInterval(this.autoScrollIntervalId); this.autoScrollIntervalId = null; this.resetScrollDelta(); } }; this.onUpdateCells = (_, defaultOnUpdateCells) => defaultOnUpdateCells(); // 刷选过程中高亮的cell this.showPrepareSelectedCells = () => { this.brushRangeCells = this.getBrushRangeCells(); this.spreadsheet.interaction.changeState({ cells: map(this.brushRangeCells, (item) => getCellMeta(item)), stateName: InteractionStateName.PREPARE_SELECT, /* * 刷选首先会经过 hover => mousedown => mousemove, hover时会将当前行全部高亮 (row cell + data cell) * 如果是有效刷选, 更新时会重新渲染, hover 高亮的格子 会正常重置 * 如果是无效刷选(全部都是没数据的格子), brushRangeDataCells = [], 更新时会跳过, 需要强制重置 hover 高亮 */ force: true, onUpdateCells: this.onUpdateCells, }); }; this.renderPrepareSelected = (point) => { const { x, y } = point; const elements = this.spreadsheet.container.document.elementsFromPointSync(x, y); const cell = elements .map((element) => this.spreadsheet.getCell(element)) .find(Boolean); // 只有行头,列头,单元格可以刷选 const isBrushCellType = cell instanceof DataCell || cell instanceof RowCell || cell instanceof ColCell; if (!cell || !isBrushCellType) { return; } const { rowIndex, colIndex } = cell.getMeta(); this.endBrushPoint = { x, y, rowIndex, colIndex, }; const { interaction } = this.spreadsheet; interaction.addIntercepts([InterceptType.HOVER]); interaction.clearStyleIndependent(); if (this.isValidBrushSelection()) { this.showPrepareSelectedCells(); this.updatePrepareSelectMask(); } }; } bindEvents() { this.bindMouseDown(); this.bindMouseMove(); this.bindMouseUp(); } getPrepareSelectMaskTheme() { var _a; return (_a = this.spreadsheet.theme) === null || _a === void 0 ? void 0 : _a.prepareSelectMask; } initPrepareSelectMaskShape() { var _a; const { foregroundGroup } = this.spreadsheet.facet; if (!foregroundGroup) { return; } (_a = this.prepareSelectMaskShape) === null || _a === void 0 ? void 0 : _a.remove(); const prepareSelectMaskTheme = this.getPrepareSelectMaskTheme(); this.prepareSelectMaskShape = foregroundGroup.appendChild(new Rect({ style: { width: 0, height: 0, x: 0, y: 0, fill: prepareSelectMaskTheme === null || prepareSelectMaskTheme === void 0 ? void 0 : prepareSelectMaskTheme.backgroundColor, fillOpacity: prepareSelectMaskTheme === null || prepareSelectMaskTheme === void 0 ? void 0 : prepareSelectMaskTheme.backgroundOpacity, zIndex: FRONT_GROUND_GROUP_BRUSH_SELECTION_Z_INDEX, visibility: 'hidden', pointerEvents: 'none', }, })); } setBrushSelectionStage(stage) { this.brushSelectionStage = stage; } // 默认是 Data cell 的绘制区 isPointInCanvas(point) { const { height, width } = this.spreadsheet.facet.getCanvasSize(); const { minX, minY } = this.spreadsheet.facet.panelBBox; return ((point === null || point === void 0 ? void 0 : point.x) > minX && (point === null || point === void 0 ? void 0 : point.x) < width && (point === null || point === void 0 ? void 0 : point.y) > minY && (point === null || point === void 0 ? void 0 : point.y) < height); } resetDrag() { this.hidePrepareSelectMaskShape(); this.setBrushSelectionStage(InteractionBrushSelectionStage.UN_DRAGGED); } isValidBrushSelection() { const { start, end } = this.getBrushRange(); const isMovedEnoughDistance = end.x - start.x > this.brushSelectionMinimumMoveDistance || end.y - start.y > this.brushSelectionMinimumMoveDistance; return isMovedEnoughDistance; } setDisplayedCells() { this.displayedCells = this.spreadsheet.facet.getDataCells(); } updatePrepareSelectMask() { const brushRange = this.getBrushRange(); const { x, y } = this.getPrepareSelectMaskPosition(brushRange); this.prepareSelectMaskShape.attr({ x, y, width: brushRange.width, height: brushRange.height, }); this.prepareSelectMaskShape.setAttribute('visibility', 'visible'); } hidePrepareSelectMaskShape() { var _a; (_a = this.prepareSelectMaskShape) === null || _a === void 0 ? void 0 : _a.setAttribute('visibility', 'hidden'); } resetScrollDelta() { this.autoScrollConfig = cloneDeep(BRUSH_AUTO_SCROLL_INITIAL_CONFIG); } getBrushPoint(event) { const { scrollY, scrollX } = this.spreadsheet.facet.getScrollOffset(); const point = { x: event === null || event === void 0 ? void 0 : event.x, y: event === null || event === void 0 ? void 0 : event.y, }; const cell = this.spreadsheet.getCell(event.target); const { colIndex, rowIndex } = cell.getMeta(); return Object.assign(Object.assign({}, point), { rowIndex, colIndex, scrollY, scrollX }); } // 四个刷选方向: 左 => 右, 右 => 左, 上 => 下, 下 => 上, 将最终结果进行重新排序, 获取真实的 row, col index getBrushRange() { var _a, _b, _c, _d, _e, _f, _g, _h; const { scrollX = 0, scrollY = 0 } = this.spreadsheet.facet.getScrollOffset(); const minRowIndex = Math.min(this.startBrushPoint.rowIndex, (_a = this.endBrushPoint) === null || _a === void 0 ? void 0 : _a.rowIndex); const maxRowIndex = Math.max(this.startBrushPoint.rowIndex, (_b = this.endBrushPoint) === null || _b === void 0 ? void 0 : _b.rowIndex); const minColIndex = Math.min(this.startBrushPoint.colIndex, (_c = this.endBrushPoint) === null || _c === void 0 ? void 0 : _c.colIndex); const maxColIndex = Math.max(this.startBrushPoint.colIndex, (_d = this.endBrushPoint) === null || _d === void 0 ? void 0 : _d.colIndex); const startXInView = this.startBrushPoint.x + this.startBrushPoint.scrollX - scrollX; const startYInView = this.startBrushPoint.y + this.startBrushPoint.scrollY - scrollY; // startBrushPoint 和 endBrushPoint 加上当前 offset const minX = Math.min(startXInView, (_e = this.endBrushPoint) === null || _e === void 0 ? void 0 : _e.x); const maxX = Math.max(startXInView, (_f = this.endBrushPoint) === null || _f === void 0 ? void 0 : _f.x); const minY = Math.min(startYInView, (_g = this.endBrushPoint) === null || _g === void 0 ? void 0 : _g.y); const maxY = Math.max(startYInView, (_h = this.endBrushPoint) === null || _h === void 0 ? void 0 : _h.y); /* * x, y: 表示从整个表格(包含表头)从左上角作为 (0, 0) 的画布区域。 * 这个 x, y 只有在绘制虚拟画布 和 是否有效移动时有效。 */ return { start: { rowIndex: minRowIndex, colIndex: minColIndex, x: minX, y: minY, }, end: { rowIndex: maxRowIndex, colIndex: maxColIndex, x: maxX, y: maxY, }, width: maxX - minX, height: maxY - minY, }; } // 获取对角线的两个坐标, 得到对应矩阵并且有数据的单元格 getBrushRangeCells() { this.setDisplayedCells(); return this.displayedCells.filter((cell) => { const meta = cell.getMeta(); return this.isInBrushRange(meta); }); } mouseDown(event) { var _a; (_a = event === null || event === void 0 ? void 0 : event.preventDefault) === null || _a === void 0 ? void 0 : _a.call(event); if (this.spreadsheet.interaction.hasIntercepts([InterceptType.CLICK])) { return; } this.setBrushSelectionStage(InteractionBrushSelectionStage.CLICK); this.initPrepareSelectMaskShape(); this.setDisplayedCells(); this.startBrushPoint = this.getBrushPoint(event); } addBrushIntercepts() { this.spreadsheet.interaction.addIntercepts([ InterceptType.DATA_CELL_BRUSH_SELECTION, ]); } bindMouseUp(enableScroll = false) { // 使用全局的 mouseup, 而不是 canvas 的 mouse up 防止刷选过程中移出表格区域时无法响应事件 this.spreadsheet.on(S2Event.GLOBAL_MOUSE_UP, (event) => { if (this.brushSelectionStage !== InteractionBrushSelectionStage.DRAGGED) { this.resetDrag(); return; } if (enableScroll) { this.clearAutoScroll(); } if (this.isValidBrushSelection()) { this.addBrushIntercepts(); this.updateSelectedCells(event); const tooltipData = getCellsTooltipData(this.spreadsheet); this.spreadsheet.showTooltipWithInfo(event, tooltipData); } if (this.spreadsheet.interaction.getCurrentStateName() === InteractionStateName.PREPARE_SELECT) { this.spreadsheet.interaction.reset(); } this.resetDrag(); }); // 刷选过程中右键弹出系统菜单时, 应该重置刷选, 防止系统菜单关闭后 mouse up 未相应依然是刷选状态 this.spreadsheet.on(S2Event.GLOBAL_CONTEXT_MENU, () => { if (this.brushSelectionStage === InteractionBrushSelectionStage.UN_DRAGGED) { return; } this.spreadsheet.interaction.removeIntercepts([InterceptType.HOVER]); this.resetDrag(); }); } autoBrushScroll(point, isRowHeader = false) { var _a, _b; this.clearAutoScroll(); if (!this.isPointInCanvas(point)) { const deltaX = (point === null || point === void 0 ? void 0 : point.x) - ((_a = this.endBrushPoint) === null || _a === void 0 ? void 0 : _a.x); const deltaY = (point === null || point === void 0 ? void 0 : point.y) - ((_b = this.endBrushPoint) === null || _b === void 0 ? void 0 : _b.y); this.handleScroll(deltaX, deltaY, isRowHeader); return true; } return false; } emitBrushSelectionEvent(event, scrollBrushRangeCells, detail) { this.spreadsheet.emit(event, scrollBrushRangeCells, detail); this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, scrollBrushRangeCells, detail); // 未刷选到有效单元格, 允许 hover if (isEmpty(scrollBrushRangeCells)) { this.spreadsheet.interaction.removeIntercepts([InterceptType.HOVER]); } } getVisibleBrushRangeCells(nodeId) { return this.brushRangeCells.find((cell) => { const visibleCellMeta = cell.getMeta(); return (visibleCellMeta === null || visibleCellMeta === void 0 ? void 0 : visibleCellMeta.id) === nodeId; }); } // 需要查看继承他的父类是如何定义的 // eslint-disable-next-line @typescript-eslint/no-unused-vars isInBrushRange(node) { return false; } bindMouseDown() { } bindMouseMove() { } // eslint-disable-next-line @typescript-eslint/no-unused-vars updateSelectedCells(event) { } getPrepareSelectMaskPosition(brushRange) { return { x: brushRange.start.x, y: brushRange.start.y, }; } } //# sourceMappingURL=base-brush-selection.js.map