@antv/s2
Version:
effective spreadsheet render core lib
562 lines • 25.6 kB
JavaScript
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