@antv/s2
Version:
effective spreadsheet render core lib
519 lines • 22.7 kB
JavaScript
import { Group } from '@antv/g';
import { each, get, includes, isArray, isBoolean, isFunction, isNumber, keys, pickBy, sumBy, } from 'lodash';
import { DEFAULT_FONT_COLOR, DEFAULT_TEXT_LINE_HEIGHT, REVERSE_FONT_COLOR, SHAPE_ATTRS_MAP, SHAPE_STYLE_MAP, } from '../common/constant';
import { CellClipBox, } from '../common/interface';
import { SingletonRenderer } from '../renderer';
import { getBorderPositionAndStyle, getCellBoxByType, } from '../utils/cell/cell';
import { getIconTotalWidth, } from '../utils/cell/header-cell';
import { isReadableText, shouldReverseFontColor } from '../utils/color';
import { getIconPosition } from '../utils/condition/condition';
import { renderIcon, renderLine, renderRect, renderText, updateShapeAttr, } from '../utils/g-renders';
import { isLinkFieldNode } from '../utils/interaction/link-field';
import { isMobile } from '../utils/is-mobile';
import { getDisplayText, getEmptyPlaceholder as getEmptyPlaceholderInner, } from '../utils/text';
export class BaseCell extends Group {
get actualText() {
return this.getMultiLineActualTexts().join('');
}
constructor(meta, spreadsheet, ...restOptions) {
super({});
this.textShapes = [];
this.conditionIconShapes = [];
// interactive control shapes, unify read and manipulate operations
this.stateShapes = new Map();
this.meta = meta;
this.spreadsheet = spreadsheet;
this.theme = spreadsheet.theme;
this.conditions = this.spreadsheet.options.conditions;
this.groupedIcons = { left: [], right: [] };
this.handleRestOptions(...restOptions);
if (this.shouldInit()) {
this.initCell();
}
}
/**
* in case there are more params to be handled
* @param options any type's rest params
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleRestOptions(...options) { }
getResizedTextMaxLines() { }
/* -------------------------------------------------------------------------- */
/* common functions that will be used in subtype */
/* -------------------------------------------------------------------------- */
getMeta() {
return this.meta;
}
setMeta(viewMeta) {
this.meta = viewMeta;
}
getIconStyle() {
var _a;
return (_a = this.theme[this.cellType]) === null || _a === void 0 ? void 0 : _a.icon;
}
isShallowRender() {
return false;
}
getCellTextWordWrapStyle(cellType) {
var _a, _b;
const { wordWrap, maxLines, textOverflow } = (((_b = (_a = this.spreadsheet.options) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b[cellType || this.cellType]) || {});
return {
wordWrap,
maxLines,
textOverflow,
};
}
/**
* 获取实际渲染的文本 (含省略号)
*/
getActualText() {
return this.getMultiLineActualTexts().join('');
}
/**
* 实际渲染的文本宽度, 如果是多行文本, 取最大的一行宽度
*/
getActualTextWidth() {
var _a;
return ((_a = this.textShape) === null || _a === void 0 ? void 0 : _a.getComputedTextLength()) || 0;
}
/**
* 实际渲染的文本宽度, 如果是多行文本, 取每一行文本高度的总和
* @alias getMultiLineActualTextHeight
*/
getActualTextHeight() {
return this.getMultiLineActualTextHeight();
}
/**
* 获取实际渲染的多行文本 (含省略号)
*/
getMultiLineActualTexts() {
var _a, _b, _c;
// G6.0 优化延迟了包围盒计算的逻辑,先调用一下 getGeometryBounds 触发包围盒计算(内部有 cache 的不用担心多次调用)
(_a = this.textShape) === null || _a === void 0 ? void 0 : _a.getGeometryBounds();
return ((_c = (_b = this.textShape) === null || _b === void 0 ? void 0 : _b.parsedStyle.metrics) === null || _c === void 0 ? void 0 : _c.lines) || [];
}
/**
* 实际渲染的多行文本宽度 (每一行文本宽度的总和)
*/
getMultiLineActualTextWidth() {
return sumBy(this.getTextLineBoundingRects(), 'width') || 0;
}
/**
* 实际渲染的多行文本高度 (每一行文本高度的总和)
* @alias getActualTextHeight
*/
getMultiLineActualTextHeight() {
return sumBy(this.getTextLineBoundingRects(), 'height') || 0;
}
/**
* 获取原始的文本 (不含省略号)
*/
getOriginalText() {
return this.originalText;
}
/**
* 文本是否溢出 (有省略号)
*/
isTextOverflowing() {
var _a;
return (_a = this.textShape) === null || _a === void 0 ? void 0 : _a.isOverflowing();
}
/**
* 是否是多行文本
*/
isMultiLineText() {
const { parsedStyle } = this.getTextShape();
return ((parsedStyle === null || parsedStyle === void 0 ? void 0 : parsedStyle.maxLines) > 1 && this.getTextLineBoundingRects().length > 1);
}
/**
* 获取文本包围盒
*/
getTextLineBoundingRects() {
var _a;
return ((_a = this.textShape) === null || _a === void 0 ? void 0 : _a.getLineBoundingRects()) || [];
}
/**
* 获取文本行高
*/
getTextLineHeight() {
var _a, _b, _c;
return (((_c = (_b = (_a = this.textShape) === null || _a === void 0 ? void 0 : _a.parsedStyle) === null || _b === void 0 ? void 0 : _b.metrics) === null || _c === void 0 ? void 0 : _c.lineHeight) ||
DEFAULT_TEXT_LINE_HEIGHT);
}
/**
* 获取单元格空值占位符
*/
getEmptyPlaceholder() {
const { options: { placeholder }, } = this.spreadsheet;
return getEmptyPlaceholderInner(this, placeholder);
}
/**
* 获取单元格展示的数值
*/
getFieldValue() {
return this.getFormattedFieldValue().formattedValue;
}
shouldInit() {
const { width, height } = this.meta;
return width > 0 && height > 0;
}
getStyle(name) {
return get(this.theme, name || this.cellType);
}
getLinkFieldShape() {
return this.linkFieldShape;
}
getBackgroundShape() {
return this.backgroundShape;
}
getStateShapes() {
return this.stateShapes;
}
getResizeAreaStyle() {
return this.getStyle('resizeArea');
}
shouldDrawResizeAreaByType(type, cell) {
const { resize } = this.spreadsheet.options.interaction;
if (isBoolean(resize)) {
return resize;
}
if (isFunction(resize === null || resize === void 0 ? void 0 : resize.visible)) {
return resize === null || resize === void 0 ? void 0 : resize.visible(cell);
}
return resize === null || resize === void 0 ? void 0 : resize[type];
}
getBBoxByType(type = CellClipBox.BORDER_BOX) {
const bbox = {
x: this.meta.x,
y: this.meta.y,
height: this.meta.height,
width: this.meta.width,
};
const cellStyle = (this.getStyle() ||
this.theme.dataCell);
return getCellBoxByType(bbox, this.getBorderPositions(), cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.cell, type);
}
drawBorders() {
this.getBorderPositions().forEach((type) => {
var _a;
const { position, style } = getBorderPositionAndStyle(type, this.getBBoxByType(), (_a = this.getStyle()) === null || _a === void 0 ? void 0 : _a.cell);
renderLine(this, Object.assign(Object.assign({}, position), style));
});
}
/**
* 绘制 hover 悬停,刷选的外框
*/
drawInteractiveBorderShape() {
this.stateShapes.set('interactiveBorderShape', renderRect(this, Object.assign(Object.assign({}, this.getBBoxByType(CellClipBox.PADDING_BOX)), { visibility: 'hidden', pointerEvents: 'none' })));
}
/**
* 交互使用的背景色
*/
drawInteractiveBgShape() {
this.stateShapes.set('interactiveBgShape', renderRect(this, Object.assign(Object.assign({}, this.getBBoxByType()), { visibility: 'hidden', pointerEvents: 'none' })));
}
drawBackgroundShape() {
const { backgroundColor, backgroundColorOpacity } = this.getBackgroundColor();
this.backgroundShape = renderRect(this, Object.assign(Object.assign({}, this.getBBoxByType()), { fill: backgroundColor, fillOpacity: backgroundColorOpacity }));
}
renderTextShape(style, options) {
const text = getDisplayText(style.text, this.getEmptyPlaceholder());
const shallowRender = (options === null || options === void 0 ? void 0 : options.shallowRender) || this.isShallowRender();
this.textShape = renderText({
group: this,
textShape: shallowRender ? undefined : this.textShape,
style: Object.assign(Object.assign({}, style), {
// 文本必须为字符串
text: `${text}` }),
});
this.addTextShape(this.textShape);
if (shallowRender) {
return this.textShape;
}
this.originalText = text;
return this.textShape;
}
updateTextPosition(position) {
var _a, _b, _c, _d;
const defaultPosition = this.getTextPosition();
(_a = this.textShape) === null || _a === void 0 ? void 0 : _a.attr('x', (_b = position === null || position === void 0 ? void 0 : position.x) !== null && _b !== void 0 ? _b : defaultPosition === null || defaultPosition === void 0 ? void 0 : defaultPosition.x);
(_c = this.textShape) === null || _c === void 0 ? void 0 : _c.attr('y', (_d = position === null || position === void 0 ? void 0 : position.y) !== null && _d !== void 0 ? _d : defaultPosition === null || defaultPosition === void 0 ? void 0 : defaultPosition.y);
}
drawTextOrCustomRenderer() {
const renderer = this.getRenderer();
if (renderer) {
SingletonRenderer.render(renderer, this).then(() => {
this.afterDrawText();
});
return;
}
this.drawTextShape();
this.afterDrawText();
}
drawTextShape() {
// 额外添加一像素余量,防止出现省略号 (文本和省略后的宽度一致): https://github.com/antvis/S2/issues/2726
const EXTRA_PIXEL = 1;
// G 遵循浏览器的规范, 空间不足以展示省略号时, 会裁剪文字, 而不是展示省略号: https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow#ellipsis
const maxTextWidth = Math.max(this.getMaxTextWidth(), 0) + EXTRA_PIXEL;
const textStyle = this.getTextStyle();
const maxLines = this.getResizedTextMaxLines() || (textStyle === null || textStyle === void 0 ? void 0 : textStyle.maxLines);
const text = this.getFieldValue();
// 在坐标计算 (getTextPosition) 之前, 预渲染一次, 提前生成 textShape, 获得文字宽度, 用于计算 icon 绘制坐标
this.renderTextShape(Object.assign(Object.assign({}, textStyle), { x: 0, y: 0, text, wordWrapWidth: maxTextWidth, maxLines }));
if (this.isShallowRender()) {
return;
}
this.updateTextPosition();
this.drawLinkField(this.meta);
}
drawLinkFieldShape(showLinkFieldShape, linkFillColor) {
if (!showLinkFieldShape) {
return;
}
const { device } = this.spreadsheet.options;
// 配置了链接跳转
if (!isMobile(device)) {
const textStyle = this.getTextStyle();
const position = this.getTextPosition();
const actualTextWidth = this.getActualTextWidth();
// 默认居左,其他 align 方式需要调整
let startX = position.x;
if (textStyle.textAlign === 'center') {
startX -= actualTextWidth / 2;
}
else if (textStyle.textAlign === 'right') {
startX -= actualTextWidth;
}
const { bottom: maxY } = this.textShape.getBBox();
this.linkFieldShape = renderLine(this, {
x1: startX,
y1: maxY + 1,
// 不用 bbox 的 maxX,因为 g-base 文字宽度预估偏差较大
x2: startX + actualTextWidth,
y2: maxY + 1,
stroke: linkFillColor,
lineWidth: 1,
});
}
this.textShape.style.fill = linkFillColor;
this.textShape.style.cursor = 'pointer';
this.textShape.appendInfo = {
// 标记为字段标记文本,方便做链接跳转直接识别
isLinkFieldText: true,
meta: this.meta,
};
}
// 要被子类覆写,返回颜色字符串
getLinkFieldStyle() {
return this.getTextStyle().linkTextFill;
}
drawLinkField(meta) {
const { linkFields = [] } = this.spreadsheet.options.interaction;
const linkTextFill = this.getLinkFieldStyle();
const isLinkField = isLinkFieldNode(linkFields, meta);
this.drawLinkFieldShape(isLinkField, linkTextFill);
}
// 根据当前 state 来更新 cell 的样式
updateByState(stateName, cell) {
this.spreadsheet.interaction.setInteractedCells(cell);
const stateStyles = get(this.theme, `${this.cellType}.cell.interactionState.${stateName}`);
each(stateStyles, (style, styleKey) => {
const targetShapeNames = keys(pickBy(SHAPE_ATTRS_MAP, (attrs) => includes(attrs, styleKey)));
targetShapeNames.forEach((shapeName) => {
const isStateShape = this.stateShapes.has(shapeName);
const shapeGroup = isStateShape
? this.stateShapes.get(shapeName)
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this[shapeName];
// 兼容多列文本 (MultiData)
const shapes = (!isArray(shapeGroup) ? [shapeGroup] : shapeGroup);
// stateShape 默认 visible 为 false
if (isStateShape) {
shapes.forEach((shape) => {
shape.setAttribute('visibility', 'visible');
});
}
// 根据 borderWidth 更新 borderShape 大小 https://github.com/antvis/S2/pull/705
if (shapeName === 'interactiveBorderShape' &&
styleKey === 'borderWidth') {
if (isNumber(style)) {
const marginStyle = this.getInteractiveBorderShapeStyle(style);
each(marginStyle, (currentStyle, currentStyleKey) => {
updateShapeAttr(shapes, currentStyleKey, currentStyle);
});
}
}
// @ts-ignore
updateShapeAttr(shapes, SHAPE_STYLE_MAP[styleKey], style);
});
});
}
getInteractiveBorderShapeStyle(borderSize) {
const { x, y, height, width } = this.getBBoxByType(CellClipBox.PADDING_BOX);
const halfSize = borderSize / 2;
return {
x: x + halfSize,
y: y + halfSize,
width: width - borderSize,
height: height - borderSize,
};
}
hideInteractionShape() {
this.stateShapes.forEach((shape) => {
updateShapeAttr(shape, SHAPE_STYLE_MAP.backgroundOpacity, 0);
updateShapeAttr(shape, SHAPE_STYLE_MAP.backgroundColor, 'transparent');
updateShapeAttr(shape, SHAPE_STYLE_MAP.borderOpacity, 0);
updateShapeAttr(shape, SHAPE_STYLE_MAP.borderWidth, 1);
updateShapeAttr(shape, SHAPE_STYLE_MAP.borderColor, 'transparent');
});
}
clearUnselectedState() {
updateShapeAttr(this.backgroundShape, SHAPE_STYLE_MAP.backgroundOpacity, 1);
updateShapeAttr(this.textShapes, SHAPE_STYLE_MAP.textOpacity, 1);
updateShapeAttr(this.linkFieldShape, SHAPE_STYLE_MAP.opacity, 1);
}
getTextShape() {
return this.textShape;
}
getTextShapes() {
return this.textShapes || [this.textShape];
}
addTextShape(textShape) {
if (!textShape) {
return;
}
this.textShapes.push(textShape);
}
getConditionIconShape() {
return this.conditionIconShape;
}
getConditionIconShapes() {
return this.conditionIconShapes || [this.conditionIconShape];
}
addConditionIconShape(iconShape) {
if (!iconShape) {
return;
}
this.conditionIconShapes.push(iconShape);
}
resetTextAndConditionIconShapes() {
this.textShapes = [];
this.conditionIconShapes = [];
}
get cellConditions() {
return this.conditions;
}
drawConditionIconShapes() {
const attrs = this.getIconConditionResult();
if (attrs) {
const position = this.getIconPosition();
const { size } = this.getStyle().icon;
this.conditionIconShape = renderIcon(this, Object.assign(Object.assign({}, position), { name: attrs === null || attrs === void 0 ? void 0 : attrs.name, width: size, height: size, fill: attrs === null || attrs === void 0 ? void 0 : attrs.fill }));
this.addConditionIconShape(this.conditionIconShape);
}
}
getTextConditionMappingResult() {
var _a;
const textCondition = this.findFieldCondition((_a = this.conditions) === null || _a === void 0 ? void 0 : _a.text);
if (textCondition === null || textCondition === void 0 ? void 0 : textCondition.mapping) {
return this.mappingValue(textCondition);
}
return null;
}
getContainConditionMappingResultTextStyle(style) {
// 优先级:默认字体颜色(已经根据背景反色后的) < 主题配置文字样式 < 条件格式文字样式
const defaultTextFill = this.getDefaultTextFill(style.fill);
const conditionStyle = this.getTextConditionMappingResult();
return Object.assign(Object.assign(Object.assign({}, style), conditionStyle), { fill: (conditionStyle === null || conditionStyle === void 0 ? void 0 : conditionStyle.fill) || defaultTextFill });
}
/**
* 获取默认字体颜色:根据字段标记背景颜色,设置字体颜色
* @param textStyle
* @private
*/
getDefaultTextFill(textFill) {
const { backgroundColor, intelligentReverseTextColor } = this.getBackgroundColor();
// text 默认为黑色,当背景颜色亮度过低时,修改 text 为白色
if (shouldReverseFontColor(backgroundColor) &&
(textFill === DEFAULT_FONT_COLOR ||
!isReadableText(backgroundColor, textFill)) &&
intelligentReverseTextColor) {
textFill = REVERSE_FONT_COLOR;
}
return textFill || '';
}
getBackgroundConditionFill() {
var _a;
// get background condition fill color
const bgCondition = this.findFieldCondition((_a = this.conditions) === null || _a === void 0 ? void 0 : _a.background);
if (bgCondition === null || bgCondition === void 0 ? void 0 : bgCondition.mapping) {
const attrs = this.mappingValue(bgCondition);
if (attrs) {
return {
backgroundColor: attrs.fill,
backgroundColorOpacity: 1,
intelligentReverseTextColor: attrs.intelligentReverseTextColor || false,
};
}
}
return {
intelligentReverseTextColor: false,
};
}
getIconConditionResult() {
var _a;
const iconCondition = this.findFieldCondition((_a = this.conditions) === null || _a === void 0 ? void 0 : _a.icon);
if (iconCondition === null || iconCondition === void 0 ? void 0 : iconCondition.mapping) {
const attrs = this.mappingValue(iconCondition);
if (attrs && attrs.icon) {
return {
name: attrs.icon,
position: getIconPosition(iconCondition),
fill: attrs.fill,
isConditionIcon: true,
};
}
}
}
getActionAndConditionIconWidth(position) {
const { left, right } = this.groupedIcons;
const iconStyle = this.getStyle().icon;
if (!position) {
return (getIconTotalWidth(left, iconStyle) + getIconTotalWidth(right, iconStyle));
}
return getIconTotalWidth(this.groupedIcons[position], iconStyle);
}
getCrossBackgroundColor(rowIndex) {
const { crossBackgroundColor, backgroundColorOpacity } = this.getStyle().cell;
if (crossBackgroundColor && rowIndex % 2 === 0) {
// 隔行颜色的配置
// 偶数行展示灰色背景,因为index是从0开始的
return { backgroundColorOpacity, backgroundColor: crossBackgroundColor };
}
return {
backgroundColorOpacity,
backgroundColor: this.getStyle().cell.backgroundColor,
};
}
getMaxLinesByCustomHeight(options) {
var _a;
const { targetCell = this, displayHeight = this.meta.height, isCustomHeight = false, } = options;
const cell = targetCell || this;
const cellStyle = (_a = this.spreadsheet.options.style) === null || _a === void 0 ? void 0 : _a[cell.cellType];
const isEnableHeightAdaptive = (cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.maxLines) > 1 && (cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.wordWrap);
if (!isEnableHeightAdaptive || !isCustomHeight) {
return;
}
const { cell: cellTheme } = cell === null || cell === void 0 ? void 0 : cell.getStyle();
const padding = cellTheme.padding.top + cellTheme.padding.bottom;
const lineHeight = cell === null || cell === void 0 ? void 0 : cell.getTextLineHeight();
const maxLines = Math.max(1, Math.round((displayHeight - padding) / lineHeight));
return maxLines;
}
getRenderer() {
var _a, _b;
return (_b = (_a = this.spreadsheet.dataCfg.meta) === null || _a === void 0 ? void 0 : _a.find((m) => m.field === this.getMetaField())) === null || _b === void 0 ? void 0 : _b.renderer;
}
}
//# sourceMappingURL=base-cell.js.map