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