@antv/s2
Version:
effective spreadsheet render core lib
967 lines • 80.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseFacet = void 0;
const tslib_1 = require("tslib");
const g_1 = require("@antv/g");
const d3_interpolate_1 = require("@antv/vendor/d3-interpolate");
const d3_timer_1 = require("@antv/vendor/d3-timer");
const flru_1 = tslib_1.__importDefault(require("flru"));
const lodash_1 = require("lodash");
const cell_1 = require("../cell");
const constant_1 = require("../common/constant");
const pagination_1 = require("../common/constant/pagination");
const debug_1 = require("../common/debug");
const interface_1 = require("../common/interface");
const panel_scroll_group_1 = require("../group/panel-scroll-group");
const scrollbar_1 = require("../ui/scrollbar");
const facet_1 = require("../utils/facet");
const get_all_child_cells_1 = require("../utils/get-all-child-cells");
const grid_1 = require("../utils/grid");
const indexes_1 = require("../utils/indexes");
const is_mobile_1 = require("../utils/is-mobile");
const math_1 = require("../utils/math");
const corner_bbox_1 = require("./bbox/corner-bbox");
const panel_bbox_1 = require("./bbox/panel-bbox");
const header_1 = require("./header");
const node_1 = require("./layout/node");
const wheelEvent_1 = require("./mobile/wheelEvent");
const utils_1 = require("./utils");
class BaseFacet {
get scrollBarTheme() {
return this.spreadsheet.theme.scrollBar;
}
get scrollBarSize() {
var _a;
return (_a = this.scrollBarTheme) === null || _a === void 0 ? void 0 : _a.size;
}
constructor(spreadsheet) {
this.scrollFrameId = null;
this.getLayoutResult = () => {
return Object.assign(Object.assign({}, this.layoutResult), { cornerNodes: this.getCornerNodes(), seriesNumberNodes: this.getSeriesNumberNodes() });
};
this.hideScrollBar = () => {
var _a, _b, _c;
(_a = this.hRowScrollBar) === null || _a === void 0 ? void 0 : _a.setAttribute('visibility', 'hidden');
(_b = this.hScrollBar) === null || _b === void 0 ? void 0 : _b.setAttribute('visibility', 'hidden');
(_c = this.vScrollBar) === null || _c === void 0 ? void 0 : _c.setAttribute('visibility', 'hidden');
};
this.delayHideScrollBar = (0, lodash_1.debounce)(this.hideScrollBar, 1000);
this.delayHideScrollbarOnMobile = () => {
if ((0, is_mobile_1.isMobile)()) {
this.delayHideScrollBar();
}
};
this.showVerticalScrollBar = () => {
var _a;
(_a = this.vScrollBar) === null || _a === void 0 ? void 0 : _a.setAttribute('visibility', 'visible');
};
this.showHorizontalScrollBar = () => {
var _a, _b;
(_a = this.hRowScrollBar) === null || _a === void 0 ? void 0 : _a.setAttribute('visibility', 'visible');
(_b = this.hScrollBar) === null || _b === void 0 ? void 0 : _b.setAttribute('visibility', 'visible');
};
this.onContainerWheelForMobileCompatibility = () => {
const canvas = this.spreadsheet.getCanvasElement();
let startY;
let endY;
canvas.addEventListener('touchstart', (event) => {
startY = event.touches[0].clientY;
});
canvas.addEventListener('touchend', (event) => {
endY = event.changedTouches[0].clientY;
if (endY < startY) {
this.scrollDirection = constant_1.ScrollDirection.SCROLL_UP;
}
else if (endY > startY) {
this.scrollDirection = constant_1.ScrollDirection.SCROLL_DOWN;
}
});
};
this.onContainerWheel = () => {
if ((0, is_mobile_1.isMobile)()) {
this.onContainerWheelForMobile();
}
else {
this.onContainerWheelForPc();
}
};
// g-gesture@1.0.1 手指快速往上滚动时, deltaY 有时会为负数, 导致向下滚动时然后回弹, 看起来就像表格在抖动, 需要判断滚动方向, 修正一下.
this.getMobileWheelDeltaY = (deltaY) => {
if (this.scrollDirection === constant_1.ScrollDirection.SCROLL_UP) {
return Math.max(0, deltaY);
}
if (this.scrollDirection === constant_1.ScrollDirection.SCROLL_DOWN) {
return Math.min(0, deltaY);
}
return deltaY;
};
this.onContainerWheelForPc = () => {
const canvas = this.spreadsheet.getCanvasElement();
canvas === null || canvas === void 0 ? void 0 : canvas.addEventListener('wheel', this.onWheel);
};
this.onContainerWheelForMobile = () => {
this.mobileWheel = new wheelEvent_1.WheelEvent(this.spreadsheet.container);
this.mobileWheel.on('wheel', (ev) => {
this.spreadsheet.hideTooltip();
const originEvent = ev.originalEvent;
const { deltaX, deltaY: defaultDeltaY, x, y } = ev;
const deltaY = this.getMobileWheelDeltaY(defaultDeltaY);
this.onWheel(Object.assign(Object.assign({}, originEvent), { deltaX,
deltaY, offsetX: x, offsetY: y }));
});
this.onContainerWheelForMobileCompatibility();
};
this.bindEvents = () => {
this.onContainerWheel();
this.emitPaginationEvent();
};
this.setScrollOffset = (scrollOffset) => {
Object.keys(scrollOffset || {}).forEach((key) => {
const offset = (0, lodash_1.get)(scrollOffset, key);
if (!(0, lodash_1.isUndefined)(offset)) {
this.spreadsheet.store.set(key, (0, math_1.floor)(offset));
}
});
};
this.getScrollOffset = () => {
const { store } = this.spreadsheet;
return {
scrollX: store.get('scrollX', 0),
scrollY: store.get('scrollY', 0),
rowHeaderScrollX: store.get('rowHeaderScrollX', 0),
};
};
this.resetScrollX = () => {
this.setScrollOffset({ scrollX: 0 });
};
this.resetRowScrollX = () => {
this.setScrollOffset({ rowHeaderScrollX: 0 });
};
this.resetScrollY = () => {
this.setScrollOffset({ scrollY: 0 });
};
this.resetScrollOffset = () => {
this.setScrollOffset({ scrollX: 0, scrollY: 0, rowHeaderScrollX: 0 });
};
this.emitPaginationEvent = () => {
const { pagination } = this.spreadsheet.options;
if (pagination) {
const { current = pagination_1.DEFAULT_PAGE_INDEX, pageSize } = pagination;
const total = this.viewCellHeights.getTotalLength();
const pageCount = (0, math_1.floor)((total - 1) / pageSize) + 1;
this.spreadsheet.emit(constant_1.S2Event.LAYOUT_PAGINATION, {
pageSize,
pageCount,
total,
current,
});
}
};
this.unbindEvents = () => {
var _a;
const canvas = this.spreadsheet.getCanvasElement();
canvas === null || canvas === void 0 ? void 0 : canvas.removeEventListener('wheel', this.onWheel);
(_a = this.mobileWheel) === null || _a === void 0 ? void 0 : _a.destroy();
};
this.calculateCellWidthHeight = () => {
const { colLeafNodes } = this.layoutResult;
const widths = (0, lodash_1.reduce)(colLeafNodes, (result, node) => {
const width = (0, lodash_1.last)(result) || 0;
result.push(width + node.width);
return result;
}, [0]);
this.viewCellWidths = widths;
this.viewCellHeights = this.getViewCellHeights();
};
this.getRealScrollX = (scrollX, hRowScroll = 0) => this.spreadsheet.isFrozenRowHeader() ? hRowScroll : scrollX;
this.getRealWidth = () => (0, lodash_1.last)(this.viewCellWidths) || 0;
this.getRealHeight = () => {
const { pagination } = this.spreadsheet.options;
const heights = this.viewCellHeights;
if (pagination) {
const { start, end } = this.getCellRange();
return heights.getCellOffsetY(end + 1) - heights.getCellOffsetY(start);
}
return heights.getTotalHeight();
};
this.scrollWithAnimation = (offsetConfig = {}, duration = 200, cb) => {
var _a, _b, _c, _d;
const { scrollX: adjustedScrollX, scrollY: adjustedScrollY, rowHeaderScrollX: adjustedRowScrollX, } = this.getAdjustedScrollOffset({
scrollX: ((_a = offsetConfig.offsetX) === null || _a === void 0 ? void 0 : _a.value) || 0,
scrollY: ((_b = offsetConfig.offsetY) === null || _b === void 0 ? void 0 : _b.value) || 0,
rowHeaderScrollX: ((_c = offsetConfig.rowHeaderOffsetX) === null || _c === void 0 ? void 0 : _c.value) || 0,
});
(_d = this.timer) === null || _d === void 0 ? void 0 : _d.stop();
const scrollOffset = this.getScrollOffset();
const newOffset = [
adjustedScrollX !== null && adjustedScrollX !== void 0 ? adjustedScrollX : scrollOffset.scrollX,
adjustedScrollY !== null && adjustedScrollY !== void 0 ? adjustedScrollY : scrollOffset.scrollY,
adjustedRowScrollX !== null && adjustedRowScrollX !== void 0 ? adjustedRowScrollX : scrollOffset.rowHeaderScrollX,
];
const interpolate = (0, d3_interpolate_1.interpolateArray)(Object.values(scrollOffset), newOffset);
this.timer = (0, d3_timer_1.timer)((elapsed) => {
try {
const ratio = Math.min(elapsed / duration, 1);
const [scrollX, scrollY, rowHeaderScrollX] = interpolate(ratio);
this.setScrollOffset({
rowHeaderScrollX,
scrollX,
scrollY,
});
this.startScroll(offsetConfig === null || offsetConfig === void 0 ? void 0 : offsetConfig.skipScrollEvent);
if (elapsed > duration) {
this.timer.stop();
cb === null || cb === void 0 ? void 0 : cb();
}
}
catch (e) {
// eslint-disable-next-line no-console
console.error(e);
this.timer.stop();
}
});
};
this.scrollImmediately = (offsetConfig = {}) => {
var _a, _b, _c;
const { scrollX, scrollY, rowHeaderScrollX } = this.getAdjustedScrollOffset({
scrollX: ((_a = offsetConfig.offsetX) === null || _a === void 0 ? void 0 : _a.value) || 0,
scrollY: ((_b = offsetConfig.offsetY) === null || _b === void 0 ? void 0 : _b.value) || 0,
rowHeaderScrollX: ((_c = offsetConfig.rowHeaderOffsetX) === null || _c === void 0 ? void 0 : _c.value) || 0,
});
this.setScrollOffset({ scrollX, scrollY, rowHeaderScrollX });
this.startScroll(offsetConfig === null || offsetConfig === void 0 ? void 0 : offsetConfig.skipScrollEvent);
};
/**
* @param skipScrollEvent 不触发 S2Event.GLOBAL_SCROLL
*/
this.startScroll = (skipScrollEvent = false) => {
var _a, _b, _c;
const { rowHeaderScrollX, scrollX, scrollY } = this.getScrollOffset();
(_a = this.hRowScrollBar) === null || _a === void 0 ? void 0 : _a.onlyUpdateThumbOffset(this.getScrollBarOffset(rowHeaderScrollX, this.hRowScrollBar));
(_b = this.hScrollBar) === null || _b === void 0 ? void 0 : _b.onlyUpdateThumbOffset(this.getScrollBarOffset(scrollX, this.hScrollBar));
(_c = this.vScrollBar) === null || _c === void 0 ? void 0 : _c.onlyUpdateThumbOffset(this.getScrollBarOffset(scrollY, this.vScrollBar));
this.dynamicRenderCell(skipScrollEvent);
};
this.getRendererHeight = () => {
const { start, end } = this.getCellRange();
return (this.viewCellHeights.getCellOffsetY(end + 1) -
this.viewCellHeights.getCellOffsetY(start));
};
this.getAdjustedScrollOffset = ({ scrollX, scrollY, rowHeaderScrollX, }) => {
return {
scrollX: (0, facet_1.getAdjustedScrollOffset)(scrollX, this.layoutResult.colsHierarchy.width, this.panelBBox.width),
scrollY: (0, facet_1.getAdjustedScrollOffset)(scrollY, this.getRendererHeight(), this.panelBBox.height),
rowHeaderScrollX: (0, facet_1.getAdjustedRowScrollX)(rowHeaderScrollX, this.cornerBBox),
};
};
// (滑动 offset / 最大 offset(滚动对象真正长度 - 轨道长)) = (滑块 offset / 最大滑动距离(轨道长 - 滑块长))
this.getScrollBarOffset = (offset, scrollbar) => {
const { trackLen, thumbLen, scrollTargetMaxOffset } = scrollbar;
return (offset * (trackLen - thumbLen)) / scrollTargetMaxOffset;
};
this.isScrollOverThePanelArea = ({ offsetX, offsetY }) => offsetX > this.panelBBox.minX &&
offsetX < this.panelBBox.maxX &&
offsetY > this.panelBBox.minY &&
offsetY < this.panelBBox.maxY;
this.isScrollOverTheCornerArea = ({ offsetX, offsetY }) => offsetX > this.cornerBBox.minX &&
offsetX < this.cornerBBox.maxX &&
offsetY > this.cornerBBox.minY &&
offsetY < this.cornerBBox.maxY + this.panelBBox.height;
this.updateHorizontalRowScrollOffset = ({ offset, offsetX, offsetY, }) => {
var _a;
// 在行头区域滚动时 才更新行头水平滚动条
if (this.isScrollOverTheCornerArea({ offsetX, offsetY })) {
(_a = this.hRowScrollBar) === null || _a === void 0 ? void 0 : _a.emitScrollChange(offset);
}
};
this.updateHorizontalScrollOffset = ({ offset, offsetX, offsetY, }) => {
var _a;
// 1.行头没有滚动条 2.在数值区域滚动时 才更新数值区域水平滚动条
if (!this.hRowScrollBar ||
this.isScrollOverThePanelArea({ offsetX, offsetY })) {
(_a = this.hScrollBar) === null || _a === void 0 ? void 0 : _a.emitScrollChange(offset);
}
};
this.isScrollToLeft = ({ deltaX, offsetX, offsetY }) => {
if (!this.hScrollBar && !this.hRowScrollBar) {
return true;
}
const isScrollRowHeaderToLeft = !this.hRowScrollBar ||
this.isScrollOverThePanelArea({ offsetY, offsetX }) ||
this.hRowScrollBar.thumbOffset <= 0;
const isScrollPanelToLeft = !this.hScrollBar || this.hScrollBar.thumbOffset <= 0;
return deltaX <= 0 && isScrollPanelToLeft && isScrollRowHeaderToLeft;
};
this.isScrollToRight = ({ deltaX, offsetX, offsetY }) => {
var _a, _b, _c, _d, _e, _f;
if (!this.hScrollBar && !this.hRowScrollBar) {
return true;
}
const viewportWidth = this.spreadsheet.isFrozenRowHeader()
? (_a = this.panelBBox) === null || _a === void 0 ? void 0 : _a.width
: (_b = this.panelBBox) === null || _b === void 0 ? void 0 : _b.maxX;
const isScrollRowHeaderToRight = !this.hRowScrollBar ||
this.isScrollOverThePanelArea({ offsetY, offsetX }) ||
((_c = this.hRowScrollBar) === null || _c === void 0 ? void 0 : _c.thumbOffset) + ((_d = this.hRowScrollBar) === null || _d === void 0 ? void 0 : _d.thumbLen) >=
this.cornerBBox.width;
const isScrollPanelToRight = (this.hRowScrollBar &&
this.isScrollOverTheCornerArea({ offsetX, offsetY })) ||
((_e = this.hScrollBar) === null || _e === void 0 ? void 0 : _e.thumbOffset) + ((_f = this.hScrollBar) === null || _f === void 0 ? void 0 : _f.thumbLen) >= viewportWidth;
return deltaX >= 0 && isScrollPanelToRight && isScrollRowHeaderToRight;
};
this.isScrollToTop = (deltaY) => {
var _a;
if (!this.vScrollBar) {
return true;
}
return deltaY <= 0 && ((_a = this.vScrollBar) === null || _a === void 0 ? void 0 : _a.thumbOffset) <= 0;
};
this.isScrollToBottom = (deltaY) => {
var _a, _b, _c;
if (!this.vScrollBar) {
return true;
}
return (deltaY >= 0 &&
((_a = this.vScrollBar) === null || _a === void 0 ? void 0 : _a.thumbOffset) + ((_b = this.vScrollBar) === null || _b === void 0 ? void 0 : _b.thumbLen) >=
((_c = this.panelBBox) === null || _c === void 0 ? void 0 : _c.height));
};
this.isVerticalScrollOverTheViewport = (deltaY) => !this.isScrollToTop(deltaY) && !this.isScrollToBottom(deltaY);
this.isHorizontalScrollOverTheViewport = (scrollOffset) => !this.isScrollToLeft(scrollOffset) && !this.isScrollToRight(scrollOffset);
/**
* 在当前表格滚动分两种情况:
* 1. 当前表格无滚动条: 无需阻止外部容器滚动
* 2. 当前表格有滚动条:
* - 未滚动到顶部或底部: 当前表格滚动, 阻止外部容器滚动
* - 滚动到顶部或底部: 恢复外部容器滚动
*/
this.isScrollOverTheViewport = (scrollOffset) => {
const { deltaY, deltaX, offsetY } = scrollOffset;
const isScrollOverTheHeader = offsetY <= this.cornerBBox.maxY;
// 光标在角头或列头时, 不触发表格自身滚动
if (isScrollOverTheHeader) {
return false;
}
if (deltaY !== 0) {
return this.isVerticalScrollOverTheViewport(deltaY);
}
if (deltaX !== 0) {
return this.isHorizontalScrollOverTheViewport(scrollOffset);
}
return false;
};
this.cancelScrollFrame = () => {
if ((0, is_mobile_1.isMobile)() && this.scrollFrameId) {
return false;
}
cancelAnimationFrame(this.scrollFrameId);
return true;
};
this.clearScrollFrameIdOnMobile = () => {
if ((0, is_mobile_1.isMobile)()) {
this.scrollFrameId = null;
}
};
/**
* https://developer.mozilla.org/zh-CN/docs/Web/CSS/overscroll-behavior
* 阻止外部容器滚动: 表格是虚拟滚动, 这里按照标准模拟浏览器的 [overscroll-behavior] 实现
* 1. auto => 只有在滚动到表格顶部或底部时才触发外部容器滚动
* 1. contain => 默认的滚动边界行为不变(“触底”效果或者刷新),但是临近的滚动区域不会被滚动链影响到
* 2. none => 临近滚动区域不受到滚动链影响,而且默认的滚动到边界的表现也被阻止
* 所以只要不为 `auto`, 或者表格内, 都需要阻止外部容器滚动
*/
this.stopScrollChainingIfNeeded = (event) => {
const { interaction } = this.spreadsheet.options;
if ((interaction === null || interaction === void 0 ? void 0 : interaction.overscrollBehavior) !== 'auto') {
this.cancelScrollFrame();
this.stopScrollChaining(event);
}
};
this.stopScrollChaining = (event) => {
var _a, _b;
if (event === null || event === void 0 ? void 0 : event.cancelable) {
(_a = event === null || event === void 0 ? void 0 : event.preventDefault) === null || _a === void 0 ? void 0 : _a.call(event);
}
// 使用 G 对应的原生 TouchEvent,以达到移动端禁用外部容器滚动的效果
const mobileEvent = (event === null || event === void 0 ? void 0 : event.nativeEvent) || event;
if (mobileEvent === null || mobileEvent === void 0 ? void 0 : mobileEvent.cancelable) {
(_b = mobileEvent === null || mobileEvent === void 0 ? void 0 : mobileEvent.preventDefault) === null || _b === void 0 ? void 0 : _b.call(mobileEvent);
}
};
this.onWheel = (event) => {
const { interaction } = this.spreadsheet.options;
let { deltaX, deltaY, offsetX, offsetY } = event;
const { scrollX: currentScrollX, rowHeaderScrollX } = this.getScrollOffset();
const { shiftKey } = event;
// Windows 环境,按住 shift 时,固定为水平方向滚动,macOS 环境默认有该行为
// see https://github.com/antvis/S2/issues/2198
if (shiftKey && (0, is_mobile_1.isWindows)()) {
offsetX = offsetX - deltaX + deltaY;
deltaX = deltaY;
offsetY -= deltaY;
deltaY = 0;
}
const [optimizedDeltaX, optimizedDeltaY] = (0, utils_1.optimizeScrollXY)(deltaX, deltaY, interaction === null || interaction === void 0 ? void 0 : interaction.scrollSpeedRatio);
this.spreadsheet.hideTooltip();
this.spreadsheet.interaction.clearHoverTimer();
if (!this.isScrollOverTheViewport({
deltaX: optimizedDeltaX,
deltaY: optimizedDeltaY,
offsetX,
offsetY,
})) {
this.stopScrollChainingIfNeeded(event);
return;
}
this.stopScrollChaining(event);
this.spreadsheet.interaction.addIntercepts([constant_1.InterceptType.HOVER]);
if (!this.cancelScrollFrame()) {
return;
}
if (this.scrollDirection !== undefined &&
this.scrollDirection !==
(optimizedDeltaX > 0
? constant_1.ScrollDirection.SCROLL_LEFT
: constant_1.ScrollDirection.SCROLL_RIGHT)) {
this.scrollDirection =
optimizedDeltaX > 0
? constant_1.ScrollDirection.SCROLL_LEFT
: constant_1.ScrollDirection.SCROLL_RIGHT;
this.updateHorizontalRowScrollOffset({
offsetX,
offsetY,
offset: rowHeaderScrollX,
});
this.updateHorizontalScrollOffset({
offsetX,
offsetY,
offset: currentScrollX,
});
return;
}
this.scrollDirection =
deltaX > 0 ? constant_1.ScrollDirection.SCROLL_LEFT : constant_1.ScrollDirection.SCROLL_RIGHT;
this.scrollFrameId = requestAnimationFrame(() => {
var _a;
const { scrollX: currentScrollX, scrollY: currentScrollY, rowHeaderScrollX, } = this.getScrollOffset();
if (optimizedDeltaX !== 0) {
this.showHorizontalScrollBar();
this.updateHorizontalRowScrollOffset({
offsetX,
offsetY,
offset: optimizedDeltaX + rowHeaderScrollX,
});
this.updateHorizontalScrollOffset({
offsetX,
offsetY,
offset: optimizedDeltaX + currentScrollX,
});
}
if (optimizedDeltaY !== 0) {
this.showVerticalScrollBar();
(_a = this.vScrollBar) === null || _a === void 0 ? void 0 : _a.emitScrollChange(optimizedDeltaY + currentScrollY);
}
this.delayHideScrollbarOnMobile();
this.clearScrollFrameIdOnMobile();
});
};
this.realDataCellRender = (scrollX, scrollY) => {
const indexes = this.calculateXYIndexes(scrollX, scrollY);
debug_1.DebuggerUtil.getInstance().logger('realDataCellRender:', this.preCellIndexes, indexes);
const { add: willAddDataCells, remove: willRemoveDataCells } = (0, indexes_1.diffPanelIndexes)(this.preCellIndexes, indexes);
debug_1.DebuggerUtil.getInstance().debugCallback(debug_1.DEBUG_VIEW_RENDER, () => {
// add new cell in panelCell
(0, lodash_1.each)(willAddDataCells, ([colIndex, rowIndex]) => {
const viewMeta = this.getCellMeta(rowIndex, colIndex);
const cell = this.createDataCell(viewMeta);
if (!cell) {
return;
}
this.addDataCell(cell);
});
const allDataCells = this.getDataCells();
// remove cell from panelCell
(0, lodash_1.each)(willRemoveDataCells, ([colIndex, rowIndex]) => {
const mountedDataCell = (0, lodash_1.find)(allDataCells, (cell) => cell.name === `${rowIndex}-${colIndex}`);
mountedDataCell === null || mountedDataCell === void 0 ? void 0 : mountedDataCell.destroy();
});
debug_1.DebuggerUtil.getInstance().logger(`Render Cell Panel: ${allDataCells === null || allDataCells === void 0 ? void 0 : allDataCells.length}, Add: ${willAddDataCells === null || willAddDataCells === void 0 ? void 0 : willAddDataCells.length}, Remove: ${willRemoveDataCells === null || willRemoveDataCells === void 0 ? void 0 : willRemoveDataCells.length}`);
});
this.preCellIndexes = indexes;
this.spreadsheet.emit(constant_1.S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER, {
add: willAddDataCells,
remove: willRemoveDataCells,
spreadsheet: this.spreadsheet,
});
};
this.getGridInfo = () => {
const [colMin, colMax, rowMin, rowMax] = this.preCellIndexes.center;
const cols = (0, grid_1.getColsForGrid)(colMin, colMax, this.layoutResult.colLeafNodes);
const rows = (0, grid_1.getRowsForGrid)(rowMin, rowMax, this.viewCellHeights);
return {
cols,
rows,
};
};
this.onAfterScroll = (0, lodash_1.debounce)(() => {
const { interaction, container } = this.spreadsheet;
// 如果是选中单元格状态, 则继续保留 hover 拦截, 避免滚动后 hover 清空已选单元格
if (!interaction.isSelectedState()) {
interaction.removeIntercepts([constant_1.InterceptType.HOVER]);
if (interaction.getHoverAfterScroll()) {
// https://github.com/antvis/S2/issues/2222
const canvasMousemoveEvent = interaction.eventController.canvasMousemoveEvent;
if (canvasMousemoveEvent) {
const { x, y } = canvasMousemoveEvent;
const shape = container.document.elementFromPointSync(x, y);
if (shape) {
container.emit(constant_1.OriginEventType.POINTER_MOVE, Object.assign(Object.assign({}, canvasMousemoveEvent), { shape, target: shape, timestamp: performance.now() }));
}
}
}
}
}, 300);
/**
* 获取单元格的所有子节点 (含非可视区域)
* @example
* const rowCell = facet.getRowCells()[0]
* facet.getCellChildrenNodes(rowCell)
*/
this.getCellChildrenNodes = (cell) => {
var _a;
const selectNode = (_a = cell === null || cell === void 0 ? void 0 : cell.getMeta) === null || _a === void 0 ? void 0 : _a.call(cell);
const isRowCell = (cell === null || cell === void 0 ? void 0 : cell.cellType) === constant_1.CellType.ROW_CELL;
const isHierarchyTree = this.spreadsheet.isHierarchyTreeType();
// 树状模式的行头点击不需要遍历当前行头的所有子节点,因为只会有一级
if (isHierarchyTree && isRowCell) {
return node_1.Node.getAllLeaveNodes(selectNode).filter((node) => node.rowIndex === selectNode.rowIndex);
}
// 平铺模式 或 树状模式的列头点击遍历所有子节点
return node_1.Node.getAllChildrenNodes(selectNode);
};
this.spreadsheet = spreadsheet;
this.init();
}
shouldRender() {
return !(0, utils_1.areAllFieldsEmpty)(this.spreadsheet.dataCfg.fields);
}
initTextWrapTemp() {
var _a;
const node = {};
const args = [
node,
this.spreadsheet,
{ shallowRender: true },
];
this.textWrapTempRowCell = this.getRowCellInstance(...args);
this.textWrapTempColCell = this.getColCellInstance(...args);
this.textWrapTempCornerCell = (_a = this.getCornerCellInstance) === null || _a === void 0 ? void 0 : _a.call(this, ...args);
this.textWrapNodeHeightCache = (0, flru_1.default)(500);
this.customRowHeightStatusMap = {};
}
initGroups() {
this.initBackgroundGroup();
this.initPanelGroups();
this.initForegroundGroup();
}
initForegroundGroup() {
this.foregroundGroup = this.spreadsheet.container.appendChild(new g_1.Group({
name: constant_1.KEY_GROUP_FORE_GROUND,
style: { zIndex: constant_1.FRONT_GROUND_GROUP_CONTAINER_Z_INDEX },
}));
}
initBackgroundGroup() {
this.backgroundGroup = this.spreadsheet.container.appendChild(new g_1.Group({
name: constant_1.KEY_GROUP_BACK_GROUND,
style: { zIndex: constant_1.BACK_GROUND_GROUP_CONTAINER_Z_INDEX },
}));
}
initPanelGroups() {
this.panelGroup = this.spreadsheet.container.appendChild(new g_1.Group({
name: constant_1.KEY_GROUP_PANEL_GROUND,
style: { zIndex: constant_1.PANEL_GROUP_GROUP_CONTAINER_Z_INDEX },
}));
this.panelScrollGroup = new panel_scroll_group_1.PanelScrollGroup({
name: constant_1.KEY_GROUP_PANEL_SCROLL,
zIndex: constant_1.PANEL_GROUP_SCROLL_GROUP_Z_INDEX,
s2: this.spreadsheet,
});
this.panelGroup.appendChild(this.panelScrollGroup);
}
getCellCustomSize(node, customSize) {
return (0, lodash_1.isFunction)(customSize) ? customSize(node) : customSize;
}
getRowCellDraggedWidth(node) {
var _a, _b, _c;
const { rowCell } = this.spreadsheet.options.style;
return ((_b = (_a = rowCell === null || rowCell === void 0 ? void 0 : rowCell.widthByField) === null || _a === void 0 ? void 0 : _a[node === null || node === void 0 ? void 0 : node.id]) !== null && _b !== void 0 ? _b : (_c = rowCell === null || rowCell === void 0 ? void 0 : rowCell.widthByField) === null || _c === void 0 ? void 0 : _c[node === null || node === void 0 ? void 0 : node.field]);
}
getRowCellDraggedHeight(node) {
var _a, _b, _c;
const { rowCell } = this.spreadsheet.options.style;
return ((_b = (_a = rowCell === null || rowCell === void 0 ? void 0 : rowCell.heightByField) === null || _a === void 0 ? void 0 : _a[node === null || node === void 0 ? void 0 : node.id]) !== null && _b !== void 0 ? _b : (_c = rowCell === null || rowCell === void 0 ? void 0 : rowCell.heightByField) === null || _c === void 0 ? void 0 : _c[node === null || node === void 0 ? void 0 : node.field]);
}
isCustomRowCellHeight(node) {
var _a;
const { dataCell } = this.spreadsheet.options.style;
const defaultDataCellHeight = (_a = constant_1.DEFAULT_STYLE.dataCell) === null || _a === void 0 ? void 0 : _a.height;
return ((0, lodash_1.isNumber)(this.getCustomRowCellHeight(node)) ||
(dataCell === null || dataCell === void 0 ? void 0 : dataCell.height) !== defaultDataCellHeight);
}
getCustomRowCellHeight(node) {
var _a;
const { rowCell } = this.spreadsheet.options.style;
return ((_a = this.getRowCellDraggedHeight(node)) !== null && _a !== void 0 ? _a : this.getCellCustomSize(node, rowCell === null || rowCell === void 0 ? void 0 : rowCell.height));
}
getRowCellHeight(node) {
var _a;
const { dataCell } = this.spreadsheet.options.style;
// 优先级: 行头拖拽 > 行头自定义高度 > 通用单元格高度
return (_a = this.getCustomRowCellHeight(node)) !== null && _a !== void 0 ? _a : dataCell === null || dataCell === void 0 ? void 0 : dataCell.height;
}
getColCellDraggedWidth(node) {
var _a, _b, _c, _d, _e, _f;
const { colCell } = this.spreadsheet.options.style;
// 指标的 field 是 $$extra$$, 对用户来说其实是 s2DataConfig.fields.values 里面的 field
// 此时应该按 $$extra$$ 对应的 value field 匹配
return ((_d = (_b = (_a = colCell === null || colCell === void 0 ? void 0 : colCell.widthByField) === null || _a === void 0 ? void 0 : _a[node === null || node === void 0 ? void 0 : node.id]) !== null && _b !== void 0 ? _b : (_c = colCell === null || colCell === void 0 ? void 0 : colCell.widthByField) === null || _c === void 0 ? void 0 : _c[node === null || node === void 0 ? void 0 : node.field]) !== null && _d !== void 0 ? _d : (_e = colCell === null || colCell === void 0 ? void 0 : colCell.widthByField) === null || _e === void 0 ? void 0 : _e[(_f = node === null || node === void 0 ? void 0 : node.query) === null || _f === void 0 ? void 0 : _f[constant_1.EXTRA_FIELD]]);
}
getColCellDraggedHeight(node) {
var _a, _b, _c, _d, _e, _f;
const { colCell } = this.spreadsheet.options.style;
// 高度同理
return ((_d = (_b = (_a = colCell === null || colCell === void 0 ? void 0 : colCell.heightByField) === null || _a === void 0 ? void 0 : _a[node === null || node === void 0 ? void 0 : node.id]) !== null && _b !== void 0 ? _b : (_c = colCell === null || colCell === void 0 ? void 0 : colCell.heightByField) === null || _c === void 0 ? void 0 : _c[node === null || node === void 0 ? void 0 : node.field]) !== null && _d !== void 0 ? _d : (_e = colCell === null || colCell === void 0 ? void 0 : colCell.heightByField) === null || _e === void 0 ? void 0 : _e[(_f = node === null || node === void 0 ? void 0 : node.query) === null || _f === void 0 ? void 0 : _f[constant_1.EXTRA_FIELD]]);
}
getColNodeHeight(options) {
var _a, _b;
const { colNode, colsHierarchy, useCache = true, cornerNodes = [], } = options;
if (!colNode) {
return 0;
}
const { colCell: colCellStyle, cornerCell: cornerCellStyle } = this.spreadsheet.options.style;
// 优先级: 列头拖拽 > 列头自定义高度 > 多行文本自适应高度 > 通用单元格高度
const height = (_a = this.getColCellDraggedHeight(colNode)) !== null && _a !== void 0 ? _a : this.getCellCustomSize(colNode, colCellStyle === null || colCellStyle === void 0 ? void 0 : colCellStyle.height);
if ((0, lodash_1.isNumber)(height) && height !== ((_b = constant_1.DEFAULT_STYLE.colCell) === null || _b === void 0 ? void 0 : _b.height)) {
// 标记为自定义高度, 方便计算文本 maxLines
colNode.extra.isCustomHeight = true;
return height;
}
const isEnableColNodeHeightAdaptive = (colCellStyle === null || colCellStyle === void 0 ? void 0 : colCellStyle.maxLines) > 1 && (colCellStyle === null || colCellStyle === void 0 ? void 0 : colCellStyle.wordWrap);
const isEnableCornerNodeHeightAdaptive = (cornerCellStyle === null || cornerCellStyle === void 0 ? void 0 : cornerCellStyle.maxLines) > 1 && (cornerCellStyle === null || cornerCellStyle === void 0 ? void 0 : cornerCellStyle.wordWrap);
const defaultHeight = this.getDefaultColNodeHeight(colNode, colsHierarchy);
let colAdaptiveHeight = defaultHeight;
let cornerAdaptiveHeight = defaultHeight;
// 1. 列头开启自动换行, 计算列头自适应高度
if (isEnableColNodeHeightAdaptive) {
colAdaptiveHeight = this.getNodeAdaptiveHeight({
meta: colNode,
cell: this.textWrapTempColCell,
defaultHeight,
useCache,
});
}
/**
* 2. 角头开启自动换行, 列头的高度除了自身以外, 还需要考虑当前整行对应的角头
* 存在角头/列头同时换行, 只有角头换行, 只有列头换行等多种场景
*/
if (isEnableCornerNodeHeightAdaptive) {
const currentCornerNodes = cornerNodes.filter((node) => {
// 兼容数值置于行/列的不同场景
if (colNode.isLeaf) {
return node.cornerType === interface_1.CornerNodeType.Row;
}
return node.field === colNode.field;
});
if (!(0, lodash_1.isEmpty)(currentCornerNodes)) {
cornerAdaptiveHeight = (0, lodash_1.max)(currentCornerNodes.map((cornerNode) => this.getNodeAdaptiveHeight({
meta: cornerNode,
cell: this.textWrapTempCornerCell,
defaultHeight,
useCache: false,
})));
}
}
// 两者要取最大, 保证高度自动撑高的合理性
return (0, math_1.round)(Math.max(cornerAdaptiveHeight, colAdaptiveHeight, defaultHeight));
}
getDefaultColNodeHeight(colNode, colsHierarchy) {
var _a, _b, _c, _d;
if (!colNode) {
return 0;
}
const { colCell } = this.spreadsheet.options.style;
// 当前层级高度最大的单元格
const sampleMaxHeight = ((_b = (_a = colsHierarchy === null || colsHierarchy === void 0 ? void 0 : colsHierarchy.sampleNodesForAllLevels) === null || _a === void 0 ? void 0 : _a.find((node) => node.level === colNode.level)) === null || _b === void 0 ? void 0 : _b.height) || 0;
// 优先级: 列头拖拽 > 列头自定义高度 > 通用单元格高度
const defaultHeight = (_d = (_c = this.getColCellDraggedHeight(colNode)) !== null && _c !== void 0 ? _c : this.getCellCustomSize(colNode, colCell === null || colCell === void 0 ? void 0 : colCell.height)) !== null && _d !== void 0 ? _d : 0;
return (0, math_1.round)(Math.max(defaultHeight, sampleMaxHeight));
}
getNodeAdaptiveHeight(options) {
var _a, _b;
const { meta, cell, defaultHeight = 0, useCache = true } = options;
if (!meta || !cell) {
return defaultHeight;
}
// 共用一个单元格用于测量, 通过动态更新 meta 的方式, 避免数据量大时频繁实例化触发 GC
cell.setMeta(Object.assign(Object.assign({}, meta), { shallowRender: true }));
const fieldValue = String(cell.getFieldValue());
if (!fieldValue) {
return defaultHeight;
}
const maxTextWidth = Math.ceil(cell.getMaxTextWidth());
if (maxTextWidth <= 0 && cell.cellType === constant_1.CellType.COL_CELL) {
return defaultHeight;
}
/**
* [Bug Fix] 使用完整的 fieldValue 作为缓存键,确保准确性
* 之前的 `size(fieldValue)` (即 fieldValue.length) 是不准确的
* 相同长度的字符串,其渲染后的实际宽度可能完全不同
* * */
const cacheKey = `${fieldValue}${constant_1.NODE_ID_SEPARATOR}${maxTextWidth}`;
const cacheHeight = this.textWrapNodeHeightCache.get(cacheKey);
if (useCache && (0, lodash_1.isNumber)(cacheHeight)) {
return cacheHeight || defaultHeight;
}
// 预生成 icon 配置, 用于计算文本正确的最大可用宽度
(_b = (_a = cell).generateIconConfig) === null || _b === void 0 ? void 0 : _b.call(_a);
cell.drawTextShape();
const { padding } = cell.getStyle().cell;
const textHeight = cell.getActualTextHeight();
const adaptiveHeight = textHeight + padding.top + padding.bottom;
const height = cell.isMultiLineText() && textHeight >= defaultHeight
? adaptiveHeight
: defaultHeight;
this.textWrapNodeHeightCache.set(cacheKey, height);
return height;
}
/**
* 根据叶子节点宽度计算所有父级节点宽度和 x 坐标
*/
calculateColParentNodeWidthAndX(colLeafNodes) {
var _a;
let prevColParent = null;
let i = 0;
const leafNodes = colLeafNodes.slice(0);
while (i < leafNodes.length) {
const node = leafNodes[i++];
const parentNode = node === null || node === void 0 ? void 0 : node.parent;
if (prevColParent !== parentNode && parentNode) {
leafNodes.push(parentNode);
const firstVisibleChildNode = (_a = parentNode.children) === null || _a === void 0 ? void 0 : _a.find((childNode) => childNode.width);
// 父节点 x 坐标 = 第一个未隐藏的子节点的 x 坐标
const parentNodeX = (firstVisibleChildNode === null || firstVisibleChildNode === void 0 ? void 0 : firstVisibleChildNode.x) || 0;
// 父节点宽度 = 所有子节点宽度之和
const parentNodeWidth = (0, lodash_1.sumBy)(parentNode.children, 'width');
parentNode.x = parentNodeX;
parentNode.width = parentNodeWidth;
prevColParent = parentNode;
}
}
}
/**
* 将每一层级的采样节点更新为高度最大的节点 (未隐藏, 非汇总节点)
*/
updateColsHierarchySampleMaxHeightNodes(colsHierarchy, rowsHierarchy) {
var _a;
const hasNotSample = (0, lodash_1.isEmpty)(colsHierarchy.sampleNodesForAllLevels);
const sampleNodes = hasNotSample
? colsHierarchy.allNodesWithoutRoot
: colsHierarchy.sampleNodesForAllLevels;
const sampleMaxHeightNodesForAllLevels = sampleNodes.map((sampleNode) => {
const maxHeightNode = (0, lodash_1.maxBy)(colsHierarchy
.getNodes(sampleNode.level)
.filter((node) => !node.isTotals || hasNotSample), (levelSampleNode) => {
return this.getColNodeHeight({
colNode: levelSampleNode,
colsHierarchy,
});
});
return maxHeightNode;
});
if (hasNotSample) {
colsHierarchy.sampleNodeForLastLevel =
(_a = sampleMaxHeightNodesForAllLevels[0]) !== null && _a !== void 0 ? _a : null;
colsHierarchy.maxLevel = 0;
}
colsHierarchy.sampleNodesForAllLevels = (0, lodash_1.compact)(sampleMaxHeightNodesForAllLevels);
const cornerNodes = rowsHierarchy
? header_1.CornerHeader.getCornerNodes({
position: { x: 0, y: 0 },
width: rowsHierarchy.width,
height: colsHierarchy.height,
layoutResult: {
rowsHierarchy,
colsHierarchy,
},
seriesNumberWidth: this.getSeriesNumberWidth(),
spreadsheet: this.spreadsheet,
})
: [];
colsHierarchy.sampleNodesForAllLevels.forEach((levelSampleNode) => {
var _a;
levelSampleNode.height = this.getColNodeHeight({
colNode: levelSampleNode,
colsHierarchy,
cornerNodes,
});
if (levelSampleNode.level === 0) {
levelSampleNode.y = 0;
}
else {
const preLevelSample = (_a = colsHierarchy.sampleNodesForAllLevels[levelSampleNode.level - 1]) !== null && _a !== void 0 ? _a : {
y: 0,
height: 0,
};
levelSampleNode.y = preLevelSample.y + preLevelSample.height;
}
colsHierarchy.height += levelSampleNode.height;
});
colsHierarchy.rootNode.height = colsHierarchy.height;
}
render() {
if (!this.shouldRender()) {
return;
}
this.adjustScrollOffset();
this.renderHeaders();
this.renderScrollBars();
this.renderBackground();
this.dynamicRenderCell(true);
}
/**
* 在每次render, 校验scroll offset是否在合法范围中
* 比如在滚动条已经滚动到100%的状态的前提下:( maxAvailableScrollOffsetX = colsHierarchy.width - viewportBBox.width )
* 此时changeSheetSize,sheet从 small width 变为 big width
* 导致后者 viewport 区域更大,其结果就是后者的 maxAvailableScrollOffsetX 更小
* 此时就需要重置 scrollOffsetX,否则就会导致滚动过多,出现空白区域
*/
adjustScrollOffset() {
const offset = this.getAdjustedScrollOffset(this.getScrollOffset());
this.setScrollOffset(offset);
}
getSeriesNumberWidth() {
var _a, _b;
const { seriesNumber } = this.spreadsheet.options;
return (0, math_1.round)((seriesNumber === null || seriesNumber === void 0 ? void 0 : seriesNumber.enable)
? (_b = (_a = this.spreadsheet.theme.rowCell) === null || _a === void 0 ? void 0 : _a.seriesNumberWidth) !== null && _b !== void 0 ? _b : 0
: 0);
}
getCanvasSize() {
const { width = 0, height = 0 } = this.spreadsheet.options;
return {
width,
height,
};
}
/**
* @alias s2.interaction.scrollTo(offsetConfig)
*/
updateScrollOffset(offsetConfig) {
var _a, _b, _c;
if (((_a = offsetConfig.rowHeaderOffsetX) === null || _a === void 0 ? void 0 : _a.animate) ||
((_b = offsetConfig.offsetX) === null || _b === void 0 ? void 0 : _b.animate) ||
((_c = offsetConfig.offsetY) === null || _c === void 0 ? void 0 : _c.animate)) {
this.scrollWithAnimation(offsetConfig);
}
else {
this.scrollImmediately(offsetConfig);
}
}
getPaginationScrollY() {
const { pagination } = this.spreadsheet.options;
if (pagination) {
const { current = pagination_1.DEFAULT_PAGE_INDEX, pageSize } = pagination;
const heights = this.viewCellHeights;
const offset = Math.max((current - 1) * pageSize, 0);
return heights.getCellOffsetY(offset);
}
return 0;
}
destroy() {
this.unbindEvents();
this.clearAllGroup();
this.preCellIndexes = null;
this.customRowHeightStatusMap = {};
this.textWrapNodeHeightCache.clear(false);
cancelAnimationFrame(this.scrollFrameId);
}
calculateCornerBBox() {
this.cornerBBox = new corner_bbox_1.CornerBBox(this, true);
}
calculatePanelBBox() {
this.panelBBox = new panel_bbox_1.PanelBBox(this, true);
}
getCellRange() {
const { pagination } = this.spreadsheet.options;
return (0, utils_1.getCellRange)(this.viewCellHeights, pagination);
}
clearAllGroup() {
this.panelGroup.remove();
this.foregroundGroup.remove();
this.backgroundGroup.remove();
}
renderRowScrollBar(rowHeaderScrollX) {
if (this.spreadsheet.isFrozenRowHeader() &&
this.cornerBBox.width < this.cornerBBox.originalWidth) {
const maxOffset = this.cornerBBox.originalWidth - this.cornerBBox.width;
const { maxY } = this.getScrollbarPosition();
const { splitLine, scrollBar } = this.spreadsheet.theme;
const thumbSize = Math.max((this.cornerBBox.width * this.cornerBBox.width) /
this.cornerBBox.originalWidth, scrollBar === null || scrollBar === void 0 ? void 0 : scrollBar.thumbHorizontalMinSize);
// 行头有分割线, 滚动条应该预留分割线的宽度
const displayThumbSize = thumbSize - (splitLine === null || splitLine === void 0 ? void 0 : splitLine.verticalBorderWidth);
this.hRowScrollBar = new scrollbar_1.ScrollBar({
isHorizontal: true,
trackLen: this.cornerBBox.width,
thumbLen: displayThumbSize,
position: {
x: this.cornerBBox.minX + this.scrollBarSize / 2,
y: maxY,
},
thumbOffset: (rowHeaderScrollX * (this.cornerBBox.width - thumbSize)) / maxOffset,
theme: this.scrollBarTheme,
scrollTargetMaxOffset: maxOffset,
});
this.hRowScrollBar.addEventListener(scrollbar_1.ScrollType.ScrollChange, ({ offset }) => {
var _a, _b;
const newOffset = this.getValidScrollBarOffset(offset, maxOffset);
const newRowHeaderScrollX = (0, math_1.floor)(newOffset);
this.setScrollOffset({ rowHeaderScrollX: newRowHeaderScrollX });
(_a = this.rowHeader) === null || _a === void 0 ? void 0 : _a.onRowScrollX(newRowHeaderScrollX, constant_1.KEY_GROUP_ROW_RESIZE_AREA);
(_b = this.seriesNumberHeader) === null || _b === void 0 ? void 0 : _b.onRowScrollX(newRowHeaderScrollX, constant_1.KEY_GROUP_ROW_INDEX_RESIZE_AREA);
this.cornerHeader.onRowScrollX(newRowHeaderScrollX, constant_1.KEY_GROUP_CORNER_RESIZE_AREA);
const scrollBarOffsetX = this.getScrollBarOffset(newRowHeaderScrollX, this.hRowScrollBar);
const { scrollX, scrollY } = this.getScrollOffset();
const position = {
scrollX,
scrollY,
rowHeaderScrollX: scrollBarOffsetX,
};
this.hRowScrollBar.updateThumbOffset(scrollBarOffsetX, false);
this.spreadsheet.emit(constant_1.S2Event.ROW_CELL_SCROLL, position);
this.spreadsheet.emit(constant_1.S2Event.GLOBAL_SCROLL, position);
});
this.foregroundGroup.appendChild(this.hRowScrollBar);
}
}
getValidScrollBarOffset(offset, maxOffset) {
return (0, lodash_1.clamp)(offset, 0, maxOffset);
}
renderHScrollBar(width, realWidth, scrollX) {
if ((0, math_1.floor)(width) < (0, math_1.floor)(realWidth)) {
const halfScrollSize = this.scrollBarSize / 2;
const { maxY } = this.getScrollbarPosition();
const isScrollContainsRowHeader = !this.spreadsheet.isFrozenRowHeader();
const finalWidth = width +
(isScrollContainsRowHeader
? Math.min(this.cornerBBox.width, this.getCanvasSize().width)
: 0);
const finalPosition = {
x: this.panelBBox.minX +
(isScrollContainsRowHeader
? -this.cornerBBox.width + halfScrollSize
: halfScrollSize),
y: maxY,
};
const finaleRealWidth = realWidth + (isScrollContainsRowHeader ? this.cornerBBox.width : 0);
const { scrollBar } = this.spreadsheet.theme;
const maxOffset = finaleRea