UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

519 lines 22.7 kB
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