UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

966 lines 84.3 kB
import { Group, Rect, } from '@antv/g'; import { interpolateArray } from '@antv/vendor/d3-interpolate'; import { timer } from '@antv/vendor/d3-timer'; import flru from 'flru'; import { clamp, compact, concat, debounce, each, filter, find, get, includes, isArray, isEmpty, isFunction, isNil, isNumber, isUndefined, last, max, maxBy, reduce, sumBy, } from 'lodash'; import { ColCell, CornerCell, DataCell, MergedCell, RowCell, } from '../cell'; import { DataCellPool } from '../cell/pool'; import { BACK_GROUND_GROUP_CONTAINER_Z_INDEX, CellType, DEFAULT_STYLE, EXTRA_FIELD, FRONT_GROUND_GROUP_CONTAINER_Z_INDEX, InterceptType, KEY_GROUP_BACK_GROUND, KEY_GROUP_COL_RESIZE_AREA, KEY_GROUP_CORNER_RESIZE_AREA, KEY_GROUP_FORE_GROUND, KEY_GROUP_PANEL_GROUND, KEY_GROUP_PANEL_SCROLL, KEY_GROUP_ROW_INDEX_RESIZE_AREA, KEY_GROUP_ROW_RESIZE_AREA, NODE_ID_SEPARATOR, OriginEventType, PANEL_GROUP_GROUP_CONTAINER_Z_INDEX, PANEL_GROUP_SCROLL_GROUP_Z_INDEX, S2Event, ScrollDirection, ScrollbarPositionType, } from '../common/constant'; import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; import { DEBUG_HEADER_LAYOUT, DEBUG_VIEW_RENDER, DebuggerUtil, } from '../common/debug'; import { CornerNodeType, } from '../common/interface'; import { PanelScrollGroup } from '../group/panel-scroll-group'; import { DEFAULT_FONTSIZE } from '../theme'; import { ScrollBar, ScrollType } from '../ui/scrollbar'; import { getAdjustedRowScrollX, getAdjustedScrollOffset } from '../utils/facet'; import { getAllChildCells } from '../utils/get-all-child-cells'; import { getColsForGrid, getRowsForGrid } from '../utils/grid'; import { diffPanelIndexes } from '../utils/indexes'; import { isMobile, isWindows } from '../utils/is-mobile'; import { floor, round } from '../utils/math'; import { CornerBBox } from './bbox/corner-bbox'; import { PanelBBox } from './bbox/panel-bbox'; import { ColHeader, CornerHeader, Frame, RowHeader, SeriesNumberHeader, } from './header'; import { Node } from './layout/node'; import { WheelEvent as MobileWheel } from './mobile/wheelEvent'; import { areAllFieldsEmpty, getCellRange, optimizeScrollXY, translateGroup, } from './utils'; export 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 = debounce(this.hideScrollBar, 1000); this.delayHideScrollbarOnMobile = () => { if (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; // 重置滚动方向,让新的触摸手势可以向任意方向滚动 this.scrollDirection = undefined; }); canvas.addEventListener('touchend', (event) => { endY = event.changedTouches[0].clientY; if (endY < startY) { this.scrollDirection = ScrollDirection.SCROLL_UP; } else if (endY > startY) { this.scrollDirection = ScrollDirection.SCROLL_DOWN; } }); }; this.onContainerWheel = () => { if (isMobile()) { this.onContainerWheelForMobile(); } else { this.onContainerWheelForPc(); } }; // g-gesture@1.0.1 手指快速往上滚动时, deltaY 有时会为负数, 导致向下滚动时然后回弹, 看起来就像表格在抖动, 需要判断滚动方向, 修正一下. this.getMobileWheelDeltaY = (deltaY) => { if (this.scrollDirection === ScrollDirection.SCROLL_UP) { return Math.max(0, deltaY); } if (this.scrollDirection === 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 = () => { // https://github.com/antvis/S2/issues/3249 // 创建回调函数,根据 overscrollBehavior 和滚动边界判断是否阻止默认行为 const shouldPreventDefault = (deltaX, deltaY, offsetX, offsetY) => { const { interaction } = this.spreadsheet.options; const overscrollBehavior = interaction === null || interaction === void 0 ? void 0 : interaction.overscrollBehavior; // 对于 'contain' 和 'none' 模式,始终阻止默认行为 if (overscrollBehavior !== 'auto') { return true; } // 对于 'auto' 模式,只有在滚动区域内(未到边缘)时才阻止默认行为 // 到达边缘时允许事件冒泡到外层容器 const isScrollOverViewport = this.isScrollOverTheViewport({ deltaX, deltaY, offsetX, offsetY, }); return isScrollOverViewport; }; this.mobileWheel = new MobileWheel(this.spreadsheet.container, shouldPreventDefault); this.mobileWheel.on('wheel', (ev) => { this.spreadsheet.hideTooltip(); const originEvent = ev.originalEvent; const { deltaX, deltaY: defaultDeltaY, x, y, nativeEvent } = ev; const deltaY = this.getMobileWheelDeltaY(defaultDeltaY); this.onWheel(Object.assign(Object.assign({}, originEvent), { deltaX, deltaY, offsetX: x, offsetY: y, __nativeEvent__: nativeEvent })); }); this.onContainerWheelForMobileCompatibility(); }; this.bindEvents = () => { this.onContainerWheel(); this.emitPaginationEvent(); }; this.setScrollOffset = (scrollOffset) => { Object.keys(scrollOffset || {}).forEach((key) => { const offset = get(scrollOffset, key); if (!isUndefined(offset)) { this.spreadsheet.store.set(key, 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 = DEFAULT_PAGE_INDEX, pageSize } = pagination; const total = this.viewCellHeights.getTotalLength(); const pageCount = floor((total - 1) / pageSize) + 1; this.spreadsheet.emit(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 = reduce(colLeafNodes, (result, node) => { const width = 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 = () => 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 = interpolateArray(Object.values(scrollOffset), newOffset); this.timer = 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: getAdjustedScrollOffset(scrollX, this.layoutResult.colsHierarchy.width, this.panelBBox.width), scrollY: getAdjustedScrollOffset(scrollY, this.getRendererHeight(), this.panelBBox.height), rowHeaderScrollX: 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 (isMobile() && this.scrollFrameId) { return false; } cancelAnimationFrame(this.scrollFrameId); return true; }; this.clearScrollFrameIdOnMobile = () => { if (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; // https://github.com/antvis/S2/issues/3249 // 优先使用 __nativeEvent__ (移动端通过 wheelEvent.ts 传递的原生事件) // 需要在事件链早期调用 preventDefault,否则事件会变成 passive/non-cancelable const nativeEvent = // eslint-disable-next-line no-underscore-dangle (event === null || event === void 0 ? void 0 : event.__nativeEvent__) || (event === null || event === void 0 ? void 0 : event.nativeEvent); if (nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.cancelable) { (_a = nativeEvent === null || nativeEvent === void 0 ? void 0 : nativeEvent.preventDefault) === null || _a === void 0 ? void 0 : _a.call(nativeEvent); } if (event === null || event === void 0 ? void 0 : event.cancelable) { (_b = event === null || event === void 0 ? void 0 : event.preventDefault) === null || _b === void 0 ? void 0 : _b.call(event); } }; 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 && isWindows()) { offsetX = offsetX - deltaX + deltaY; deltaX = deltaY; offsetY -= deltaY; deltaY = 0; } const [optimizedDeltaX, optimizedDeltaY] = 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([InterceptType.HOVER]); if (!this.cancelScrollFrame()) { return; } // 水平滚动方向变化检测:只在有水平滚动时才检查 // 修复:添加 optimizedDeltaX !== 0 检查,避免垂直滚动时被误拦截 if (optimizedDeltaX !== 0 && this.scrollDirection !== undefined && this.scrollDirection !== (optimizedDeltaX > 0 ? ScrollDirection.SCROLL_LEFT : ScrollDirection.SCROLL_RIGHT)) { this.scrollDirection = optimizedDeltaX > 0 ? ScrollDirection.SCROLL_LEFT : ScrollDirection.SCROLL_RIGHT; this.updateHorizontalRowScrollOffset({ offsetX, offsetY, offset: rowHeaderScrollX, }); this.updateHorizontalScrollOffset({ offsetX, offsetY, offset: currentScrollX, }); return; } this.scrollDirection = deltaX > 0 ? ScrollDirection.SCROLL_LEFT : 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); DebuggerUtil.getInstance().logger('realDataCellRender:', this.preCellIndexes, indexes); const { add: willAddDataCells, remove: willRemoveDataCells } = diffPanelIndexes(this.preCellIndexes, indexes); DebuggerUtil.getInstance().debugCallback(DEBUG_VIEW_RENDER, () => { var _a; if ((_a = this.spreadsheet.options.future) === null || _a === void 0 ? void 0 : _a.experimentalReuseCell) { const allDataCells = this.getDataCells(); const maxLength = Math.max(willRemoveDataCells.length, willAddDataCells.length); // 交替执行删除和添加操作 for (let i = 0; i < maxLength; i++) { // 删除单元格 if (i < willRemoveDataCells.length) { const [colIndex, rowIndex] = willRemoveDataCells[i]; const mountedDataCell = find(allDataCells, (cell) => cell.name === `${rowIndex}-${colIndex}`); if (mountedDataCell) { this.dataCellPool.release(mountedDataCell); } } // 添加单元格 if (i < willAddDataCells.length) { const [colIndex, rowIndex] = willAddDataCells[i]; const viewMeta = this.getCellMeta(rowIndex, colIndex); const cell = this.createDataCell(viewMeta); if (cell) { this.addDataCell(cell); } } } 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}`); } else { // add new cell in panelCell 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 each(willRemoveDataCells, ([colIndex, rowIndex]) => { const mountedDataCell = find(allDataCells, (cell) => cell.name === `${rowIndex}-${colIndex}`); mountedDataCell === null || mountedDataCell === void 0 ? void 0 : mountedDataCell.destroy(); }); 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(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 = getColsForGrid(colMin, colMax, this.layoutResult.colLeafNodes); const rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); return { cols, rows, }; }; this.onAfterScroll = debounce(() => { const { interaction, container } = this.spreadsheet; // 如果是选中单元格状态, 则继续保留 hover 拦截, 避免滚动后 hover 清空已选单元格 if (!interaction.isSelectedState()) { interaction.removeIntercepts([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(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) === CellType.ROW_CELL; const isHierarchyTree = this.spreadsheet.isHierarchyTreeType(); // 树状模式的行头点击不需要遍历当前行头的所有子节点,因为只会有一级 if (isHierarchyTree && isRowCell) { return Node.getAllLeaveNodes(selectNode).filter((node) => node.rowIndex === selectNode.rowIndex); } // 平铺模式 或 树状模式的列头点击遍历所有子节点 return Node.getAllChildrenNodes(selectNode); }; this.spreadsheet = spreadsheet; this.init(); } shouldRender() { return !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 = flru(500); this.customRowHeightStatusMap = {}; } initGroups() { this.initBackgroundGroup(); this.initPanelGroups(); this.initForegroundGroup(); } initForegroundGroup() { this.foregroundGroup = this.spreadsheet.container.appendChild(new Group({ name: KEY_GROUP_FORE_GROUND, style: { zIndex: FRONT_GROUND_GROUP_CONTAINER_Z_INDEX }, })); } initBackgroundGroup() { this.backgroundGroup = this.spreadsheet.container.appendChild(new Group({ name: KEY_GROUP_BACK_GROUND, style: { zIndex: BACK_GROUND_GROUP_CONTAINER_Z_INDEX }, })); } initPanelGroups() { this.panelGroup = this.spreadsheet.container.appendChild(new Group({ name: KEY_GROUP_PANEL_GROUND, style: { zIndex: PANEL_GROUP_GROUP_CONTAINER_Z_INDEX }, })); this.panelScrollGroup = new PanelScrollGroup({ name: KEY_GROUP_PANEL_SCROLL, zIndex: PANEL_GROUP_SCROLL_GROUP_Z_INDEX, s2: this.spreadsheet, }); this.panelGroup.appendChild(this.panelScrollGroup); } getCellCustomSize(node, customSize) { return 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 = DEFAULT_STYLE.dataCell) === null || _a === void 0 ? void 0 : _a.height; return (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[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[EXTRA_FIELD]]); } getColNodeHeight(options) { var _a, _b, _c; 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 (isNumber(height) && height !== ((_b = 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)) || this.spreadsheet.theme.colCell.text.fontSize > DEFAULT_FONTSIZE || this.spreadsheet.theme.colCell.bolderText.fontSize > DEFAULT_FONTSIZE; const isEnableCornerNodeHeightAdaptive = ((cornerCellStyle === null || cornerCellStyle === void 0 ? void 0 : cornerCellStyle.maxLines) > 1 && (cornerCellStyle === null || cornerCellStyle === void 0 ? void 0 : cornerCellStyle.wordWrap)) || this.spreadsheet.theme.cornerCell.text.fontSize > DEFAULT_FONTSIZE || this.spreadsheet.theme.cornerCell.bolderText.fontSize > DEFAULT_FONTSIZE; 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 === CornerNodeType.Row; } return node.field === colNode.field; }); if (!isEmpty(currentCornerNodes)) { cornerAdaptiveHeight = (_c = max(currentCornerNodes.map((cornerNode) => this.getNodeAdaptiveHeight({ meta: cornerNode, cell: this.textWrapTempCornerCell, defaultHeight, useCache: false, })))) !== null && _c !== void 0 ? _c : defaultHeight; } } // 两者要取最大, 保证高度自动撑高的合理性 return 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 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 === CellType.COL_CELL) { return defaultHeight; } /** * [Bug Fix] 使用完整的 fieldValue 作为缓存键,确保准确性 * 之前的 `size(fieldValue)` (即 fieldValue.length) 是不准确的 * 相同长度的字符串,其渲染后的实际宽度可能完全不同 * * */ const cacheKey = `${fieldValue}${NODE_ID_SEPARATOR}${maxTextWidth}`; const cacheHeight = this.textWrapNodeHeightCache.get(cacheKey); if (useCache && 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; // Check if text actually uses multiple lines const singleLineHeight = cell.getTextLineHeight(); const hasWrappedText = textHeight > singleLineHeight * 1.5; // Use adaptive height when: // 1. Text actually wraps (uses multiple lines), OR // 2. Text height exceeds default height const needsAdaptiveHeight = hasWrappedText || textHeight >= defaultHeight; const height = needsAdaptiveHeight ? Math.max(adaptiveHeight, defaultHeight) : 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 = sumBy(parentNode.children, 'width'); parentNode.x = parentNodeX; parentNode.width = parentNodeWidth; prevColParent = parentNode; } } } /** * 将每一层级的采样节点更新为高度最大的节点 (未隐藏, 非汇总节点) */ updateColsHierarchySampleMaxHeightNodes(colsHierarchy, rowsHierarchy) { var _a; const hasNotSample = isEmpty(colsHierarchy.sampleNodesForAllLevels); const sampleNodes = hasNotSample ? colsHierarchy.allNodesWithoutRoot : colsHierarchy.sampleNodesForAllLevels; const sampleMaxHeightNodesForAllLevels = sampleNodes.map((sampleNode) => { const maxHeightNode = 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 = compact(sampleMaxHeightNodesForAllLevels); const cornerNodes = rowsHierarchy ? 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 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 = 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 CornerBBox(this, true); } calculatePanelBBox() { this.panelBBox = new PanelBBox(this, true); } getCellRange() { const { pagination } = this.spreadsheet.options; return getCellRange(this.viewCellHeights, pagination); } clearAllGroup() { this.panelGroup.remove(); th