UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

609 lines 23.3 kB
import { __awaiter } from "tslib"; import EE from '@antv/event-emitter'; import { Canvas, } from '@antv/g'; import { Renderer } from '@antv/g-canvas'; import { forEach, forIn, get, includes, isEmpty, isFunction, isString, last, memoize, some, values, } from 'lodash'; import { BaseCell } from '../cell'; import { InterceptType, S2Event, getDefaultSeriesNumberText, getTooltipOperatorSortMenus, getTooltipOperatorTableSortMenus, } from '../common/constant'; import { DebuggerUtil } from '../common/debug'; import { i18n } from '../common/i18n'; import { registerIcon } from '../common/icons/factory'; import { Store } from '../common/store'; import { RootInteraction } from '../interaction/root'; import { getTheme } from '../theme'; import { HdAdapter } from '../ui/hd-adapter'; import { BaseTooltip } from '../ui/tooltip'; import { getOffscreenCanvas, removeOffscreenCanvas } from '../utils/canvas'; import { clearValueRangeState } from '../utils/condition/state-controller'; import { hideColumnsByThunkGroup } from '../utils/hide-columns'; import { isMobile } from '../utils/is-mobile'; import { customMerge, setupDataConfig, setupOptions } from '../utils/merge'; import { getTooltipData, getTooltipOptions } from '../utils/tooltip'; export class SpreadSheet extends EE { constructor(dom, dataCfg, options) { super(); this.store = new Store(); /** * 表格是否已销毁 */ this.destroyed = false; /** * 获取文本在画布中的测量信息 * @param text 待计算的文本 * @param font 文本 css 样式 * @returns 文本测量信息 TextMetrics */ this.measureText = memoize((text, font) => { if (!font) { return null; } const canvas = getOffscreenCanvas() || this.getCanvasElement(); const ctx = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d'); const { fontSize, fontFamily, fontWeight, fontStyle, fontVariant } = font; ctx.font = [ fontStyle, fontVariant, fontWeight, `${fontSize}px`, fontFamily, ] .join(' ') .trim(); return ctx.measureText(String(text)); }, (text, font) => [text, ...values(font)].join('')); /** * 计算文本在画布中的宽度 * @param text 待计算的文本 * @param font 文本 css 样式 * @returns 文本宽度 */ this.measureTextWidth = (text, font) => { const textMetrics = this.measureText(text, font); return (textMetrics === null || textMetrics === void 0 ? void 0 : textMetrics.width) || 0; }; /** * 计算文本在画布中的宽度 https://developer.mozilla.org/zh-CN/docs/Web/API/TextMetrics * @param text 待计算的文本 * @param font 文本 css 样式 * @returns 文本高度 */ this.measureTextHeight = (text, font) => { const textMetrics = this.measureText(text, font); if (!textMetrics) { return 0; } return (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent); }; /** * 粗略计算文本在画布中的宽度 * @param text 待计算的文本 * @param font 文本 css 样式 * @returns 文本宽度 */ this.measureTextWidthRoughly = (text, font) => { const alphaWidth = this.measureTextWidth('a', font); const chineseWidth = this.measureTextWidth('蚂', font); let w = 0; if (!text) { return w; } // eslint-disable-next-line no-restricted-syntax for (const char of String(text)) { const code = char.charCodeAt(0); // /[\u0000-\u00ff]/ w += code >= 0 && code <= 255 ? alphaWidth : chineseWidth; } return w; }; this.setupDataConfig(dataCfg); this.setupOptions(options); this.dataSet = this.getDataSet(); this.setDebug(); this.initTooltip(); this.initContainer(dom); this.bindEvents(); this.initInteraction(); this.initTheme(); this.initHdAdapter(); this.registerIcons(); this.setOverscrollBehavior(); this.mountSheetInstance(); } setupDataConfig(dataCfg) { this.dataCfg = setupDataConfig(dataCfg); } setupOptions(options) { this.options = setupOptions(options); } isCustomHeaderFields(fieldType) { const { fields } = this.dataCfg; if (!fieldType) { return some([...fields === null || fields === void 0 ? void 0 : fields.rows, ...fields === null || fields === void 0 ? void 0 : fields.columns], (field) => !isString(field)); } return some(fields === null || fields === void 0 ? void 0 : fields[fieldType], (field) => !isString(field)); } isCustomColumnFields() { return this.isCustomHeaderFields('columns'); } setOverscrollBehavior() { const { overscrollBehavior } = this.options.interaction; // 行内样式 + css 样式 const initOverscrollBehavior = window .getComputedStyle(document.body) .getPropertyValue('overscroll-behavior'); // 用户没有在 body 上主动设置过 overscrollBehavior,才进行更新 const hasInitOverscrollBehavior = initOverscrollBehavior && initOverscrollBehavior !== 'auto'; if (hasInitOverscrollBehavior) { this.store.set('initOverscrollBehavior', initOverscrollBehavior); } else if (overscrollBehavior) { document.body.style.overscrollBehavior = overscrollBehavior; } } restoreOverscrollBehavior() { document.body.style.overscrollBehavior = this.store.get('initOverscrollBehavior') || ''; } setDebug() { DebuggerUtil.getInstance().setDebug(this.options.debug); } initTheme() { // When calling spreadsheet directly, there is no theme and initialization is required this.setThemeCfg({ name: 'default', }); } getMountContainer(dom) { const mountContainer = isString(dom) ? document.querySelector(dom) : dom; if (!mountContainer) { throw new Error('Target mount container is not a DOM element'); } return mountContainer; } initHdAdapter() { if (this.options.hd) { this.hdAdapter = new HdAdapter(this); this.hdAdapter.init(); } } initInteraction() { var _a, _b; (_b = (_a = this.interaction) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a); this.interaction = new RootInteraction(this); } initTooltip() { var _a, _b, _c, _d; (_b = (_a = this.tooltip) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a); this.tooltip = this.renderTooltip(); if (!(this.tooltip instanceof BaseTooltip)) { // eslint-disable-next-line no-console console.warn(`[Custom Tooltip]: ${(_d = (_c = this.tooltip) === null || _c === void 0 ? void 0 : _c.constructor) === null || _d === void 0 ? void 0 : _d.toString()} should be extends from BaseTooltip`); } } renderTooltip() { var _a, _b; return ((_b = (_a = this.options.tooltip) === null || _a === void 0 ? void 0 : _a.render) === null || _b === void 0 ? void 0 : _b.call(_a, this)) || new BaseTooltip(this); } getTargetCell(target) { // 刷选等场景, 以最后一个发生交互的单元格为准 return this.getCell(target) || last(this.interaction.getInteractedCells()); } /** * 展示 Tooltip 提示 * @alias s2.tooltip.show() * @example s2.showTooltip({ position: { x: event.clientX, y: event.clientY, }, content: '<div>xxx</div>', options: {} }) */ showTooltip(showOptions) { const { content, event } = showOptions; const cell = this.getTargetCell(event === null || event === void 0 ? void 0 : event.target); const displayContent = isFunction(content) ? content(cell, showOptions) : content; return new Promise((resolve) => { var _a, _b; const options = Object.assign(Object.assign({}, showOptions), { content: displayContent, onMounted: resolve }); (_b = (_a = this.tooltip) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.call(_a, options); }); } showTooltipWithInfo(event, cellInfos, options) { var _a; const { enable: showTooltip, content } = getTooltipOptions(this, event); if (!showTooltip) { return; } const targetCell = this.getTargetCell(event === null || event === void 0 ? void 0 : event.target); const tooltipData = (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : getTooltipData({ spreadsheet: this, cellInfos, targetCell, options: Object.assign({ enableFormat: true }, options), }); return this.showTooltip({ data: tooltipData, position: { x: event.clientX, y: event.clientY, }, options, event, content, }); } hideTooltip() { var _a, _b; (_b = (_a = this.tooltip) === null || _a === void 0 ? void 0 : _a.hide) === null || _b === void 0 ? void 0 : _b.call(_a); } destroyTooltip() { var _a, _b; (_b = (_a = this.tooltip) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a); } registerIcons() { const customSVGIcons = this.options.customSVGIcons; if (isEmpty(customSVGIcons)) { return; } forEach(customSVGIcons, (customSVGIcon) => { registerIcon(customSVGIcon.name, customSVGIcon.src); }); } /** * 更新表格数据 * @param dataCfg 数据源配置 * @param reset 是否重置数据源配置, 直接使用传入的 dataCfg,不再与之前的配置进行合并 * @example s2.setDataCfg(dataCfg) * @example s2.setDataCfg(dataCfg, true) */ setDataCfg(dataCfg, reset) { this.store.set('originalDataCfg', dataCfg); if (reset) { this.dataCfg = setupDataConfig(dataCfg); } else { this.dataCfg = setupDataConfig(this.dataCfg, dataCfg); } // clear value ranger after each updated data cfg clearValueRangeState(this); } /** * 更新表格配置 * @param options 配置 * @param reset 是否重置配置, 直接使用传入的 options,不再与之前的配置进行合并 * @example s2.setOptions(dataCfg) * @example s2.setOptions(dataCfg, true) */ setOptions(options, reset) { var _a; this.hideTooltip(); if (reset) { this.setupOptions(options); } else { this.options = customMerge(this.options, options); } if (reset || ((_a = options === null || options === void 0 ? void 0 : options.tooltip) === null || _a === void 0 ? void 0 : _a.render)) { this.initTooltip(); } this.resetHiddenColumnsDetailInfoIfNeeded(); this.registerIcons(); } /** * 重置表格数据 * @example s2.resetDataCfg() */ resetDataCfg() { this.setDataCfg(null, true); } /** * 重置表格配置 * @example s2.resetOptions() */ resetOptions() { this.setOptions(null, true); } resetHiddenColumnsDetailInfoIfNeeded() { var _a; if (!isEmpty((_a = this.options.interaction) === null || _a === void 0 ? void 0 : _a.hiddenColumnFields)) { this.store.set('hiddenColumnsDetail', []); } } doRender(options) { return __awaiter(this, void 0, void 0, function* () { // 防止表格卸载后, 再次调用 render 函数的报错 const canvasElement = this.getCanvasElement(); // 使用 isConnected 替代 body.contains 检查 DOM 连接状态,兼容Shadow DOM if (!canvasElement || !canvasElement.isConnected) { return; } const { reloadData = true, rebuildDataSet = false, rebuildHiddenColumnsDetail = true, } = options || {}; this.emit(S2Event.LAYOUT_BEFORE_RENDER); if (rebuildDataSet) { this.dataSet = this.getDataSet(); } if (reloadData) { this.clearDrillDownData('', true); this.dataSet.setDataCfg(this.dataCfg); } this.buildFacet(); if (rebuildHiddenColumnsDetail) { yield this.initHiddenColumnsDetail(); } this.emit(S2Event.LAYOUT_AFTER_RENDER); }); } /** * 渲染表格 * @param reloadData * @param options * @example s2.render(true) s2.render(false) s2.render({ reloadData: true; rebuildDataSet: true; rebuildHiddenColumnsDetail: true; }) */ render(options) { return __awaiter(this, void 0, void 0, function* () { if (this.destroyed) { return; } const renderOptions = typeof options === 'boolean' ? { reloadData: options, } : options; yield this.container.ready; yield this.doRender(renderOptions); }); } mountSheetInstance() { const canvas = this.getCanvasElement(); if (canvas) { // eslint-disable-next-line no-underscore-dangle canvas.__s2_instance__ = this; } } unmountSheetInstance() { const canvas = this.getCanvasElement(); if (canvas) { // @ts-ignore // eslint-disable-next-line no-underscore-dangle delete canvas.__s2_instance__; } } /** * 卸载表格 * @example s2.destroy() */ destroy() { var _a, _b, _c, _d, _e; if (this.destroyed) { return; } this.destroyed = true; this.restoreOverscrollBehavior(); this.emit(S2Event.LAYOUT_DESTROY); (_a = this.facet) === null || _a === void 0 ? void 0 : _a.destroy(); (_b = this.hdAdapter) === null || _b === void 0 ? void 0 : _b.destroy(); (_c = this.interaction) === null || _c === void 0 ? void 0 : _c.destroy(); (_d = this.store) === null || _d === void 0 ? void 0 : _d.clear(); this.destroyTooltip(); this.clearCanvasEvent(); this.unmountSheetInstance(); (_e = this.container) === null || _e === void 0 ? void 0 : _e.destroy(); removeOffscreenCanvas(); } setThemeName(name) { this.themeName = name; } setThemeCfg(themeCfg = {}, getCustomTheme) { const theme = (themeCfg === null || themeCfg === void 0 ? void 0 : themeCfg.theme) || {}; const newTheme = getTheme(Object.assign(Object.assign({}, themeCfg), { spreadsheet: this, getCustomTheme })); this.theme = customMerge(newTheme, theme); this.setThemeName(themeCfg === null || themeCfg === void 0 ? void 0 : themeCfg.name); } setTheme(theme) { this.theme = customMerge(this.theme, theme); } getTheme() { return this.theme; } getThemeName() { return this.themeName; } /** * 更新分页配置 */ updatePagination(pagination) { this.options = customMerge(this.options, { pagination, }); // 清空滚动进度 this.facet.resetScrollOffset(); } /** * 修改表格画布大小,不用重新加载数据 * @param width * @param height */ changeSheetSize(width = this.options.width, height = this.options.height) { const canvas = this.getCanvasElement(); const { width: containerWidth, height: containerHeight } = this.container.getConfig(); const isSizeChanged = width !== containerWidth || height !== containerHeight; if (!isSizeChanged || !canvas) { return; } this.options = customMerge(this.options, { width, height }); // resize the canvas this.container.resize(width, height); } on(event, listener) { return super.on(event, listener); } emit(event, ...args) { super.emit(event, ...args); } /** * 获取 G Canvas 实例 * @see https://g.antv.antgroup.com/api/renderer/canvas */ getCanvas() { return this.container; } /** * 获取 G Canvas 配置 * @see https://g.antv.antgroup.com/api/canvas/options */ getCanvasConfig() { return this.getCanvas().getConfig(); } /** * 获取 <canvas/> HTML 元素 */ getCanvasElement() { return this.getCanvas() .getContextService() .getDomElement(); } getLayoutWidthType() { var _a; return (_a = this.options.style) === null || _a === void 0 ? void 0 : _a.layoutWidthType; } isCellType(cell) { return cell instanceof BaseCell; } // 获取当前 cell 实例 getCell(target) { let parent = target; // 一直索引到 g 顶层的 Canvas 来检查是否在指定的 cell 中 while (parent && !(parent instanceof Canvas)) { if (this.isCellType(parent)) { // 在单元格中则返回 return parent; } parent = parent === null || parent === void 0 ? void 0 : parent.parentNode; } return null; } // 获取当前 cell 类型 getCellType(target) { const cell = this.getCell(target); return cell === null || cell === void 0 ? void 0 : cell.cellType; } /** * 获取当前维度对应的汇总配置 */ getTotalsConfig(dimension) { const { totals } = this.options; const { rows } = this.dataSet.fields; const totalConfig = get(totals, includes(rows, dimension) ? 'row' : 'col', {}); const showSubTotals = totalConfig.showSubTotals && includes(totalConfig.subTotalsDimensions, dimension) ? totalConfig.showSubTotals : false; return Object.assign(Object.assign({ grandTotalsLabel: i18n('总计'), subTotalsLabel: i18n('小计'), grandTotalsGroupDimensions: [], subTotalsGroupDimensions: [] }, totalConfig), { showSubTotals }); } /** * Create all related groups, contains: * 1. container -- base canvas group * 2. backgroundGroup * 3. panelGroup -- main facet group belongs to * 4. foregroundGroup * @param dom * @private */ initContainer(dom) { const { width, height, device, transformCanvasConfig } = this.options; const renderer = new Renderer(); const canvasConfig = transformCanvasConfig === null || transformCanvasConfig === void 0 ? void 0 : transformCanvasConfig(renderer, this); /** * https://github.com/antvis/S2/issues/2857 * 开启 supportsPointerEvents 后, G Canvas 会禁用 `touchAction`: https://github.com/antvis/G/blob/910c58e9bcba48cfa7bb0585064d27d3ae0bff4c/packages/g-plugin-dom-interaction/src/DOMInteractionPlugin.ts#L135 */ const supportsPointerEvents = !isMobile(device); this.container = new Canvas(Object.assign({ container: this.getMountContainer(dom), width, height, renderer, supportsPointerEvents }, canvasConfig)); this.setupContainerStyle(); } setupContainerStyle() { const canvas = this.getCanvasElement(); if (canvas) { // canvas 需要设置为块级元素, 不然和父元素有 5px 的高度差 canvas.style.display = 'block'; // 避免双击 canvas 造成的外部文本选中 canvas.style.userSelect = 'none'; } } // 初次渲染时, 如果配置了隐藏列, 则生成一次相关配置信息 initHiddenColumnsDetail() { return __awaiter(this, void 0, void 0, function* () { const { hiddenColumnFields } = this.options.interaction; const lastHiddenColumnsDetail = this.store.get('hiddenColumnsDetail'); // 隐藏列为空, 并且没有操作的情况下, 则无需生成 if (isEmpty(hiddenColumnFields) && isEmpty(lastHiddenColumnsDetail)) { return; } yield hideColumnsByThunkGroup(this, hiddenColumnFields, true); }); } clearCanvasEvent() { const canvasEvents = this.getEvents(); forIn(canvasEvents, (_, event) => { this.off(event); }); } updateSortMethodMap(nodeId, sortMethod, replace = false) { const lastSortMethodMap = !replace ? this.store.get('sortMethodMap') : null; this.store.set('sortMethodMap', Object.assign(Object.assign({}, lastSortMethodMap), { [nodeId]: sortMethod })); } getMenuDefaultSelectedKeys(nodeId) { const sortMethodMap = this.store.get('sortMethodMap'); const selectedSortMethod = get(sortMethodMap, nodeId); return selectedSortMethod ? [selectedSortMethod] : []; } handleGroupSort(event, meta) { event.stopPropagation(); this.interaction.addIntercepts([InterceptType.HOVER]); const selectedKeys = this.getMenuDefaultSelectedKeys(meta === null || meta === void 0 ? void 0 : meta.id); const menuItems = this.isTableMode() ? getTooltipOperatorTableSortMenus() : getTooltipOperatorSortMenus(); const operator = { menu: { selectedKeys, items: menuItems, onClick: (_a) => __awaiter(this, [_a], void 0, function* ({ key: sortMethod }) { yield this.groupSortByMethod(sortMethod, meta); this.emit(S2Event.RANGE_SORTED, event); }), }, }; this.showTooltipWithInfo(event, [], { operator, onlyShowOperator: true, }); } getSeriesNumberText() { var _a; const { text, enable } = (_a = this.options.seriesNumber) !== null && _a !== void 0 ? _a : {}; if (!enable) { return ''; } return text !== null && text !== void 0 ? text : getDefaultSeriesNumberText(); } enableAsyncExport() { return true; } } //# sourceMappingURL=spread-sheet.js.map