From f1570f2180086c1d4f9bf92e06edf5baecbd436c Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Tue, 27 Feb 2024 22:27:33 +0800 Subject: [PATCH] feat: add text decoration property --- docs/en/guide/command-execute.md | 2 +- docs/en/guide/schema.md | 3 + docs/guide/command-execute.md | 2 +- docs/guide/schema.md | 3 + src/editor/core/command/CommandAdapt.ts | 23 ++++- src/editor/core/draw/Draw.ts | 3 +- .../core/draw/richtext/AbstractRichText.ts | 18 ++-- src/editor/core/draw/richtext/Underline.ts | 89 +++++++++++++++++-- src/editor/dataset/constant/Element.ts | 9 +- src/editor/dataset/enum/Text.ts | 13 +++ src/editor/index.ts | 4 +- src/editor/interface/Element.ts | 2 + src/editor/interface/Text.ts | 5 ++ src/editor/utils/index.ts | 10 +++ 14 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 src/editor/dataset/enum/Text.ts create mode 100644 src/editor/interface/Text.ts diff --git a/docs/en/guide/command-execute.md b/docs/en/guide/command-execute.md index 4d6148a..67e522e 100644 --- a/docs/en/guide/command-execute.md +++ b/docs/en/guide/command-execute.md @@ -244,7 +244,7 @@ Feature: Underline Usage: ```javascript -instance.command.executeUnderline() +instance.command.executeUnderline(textDecoration?: ITextDecoration) ``` ## executeStrikeout diff --git a/docs/en/guide/schema.md b/docs/en/guide/schema.md index e973c8d..3871784 100644 --- a/docs/en/guide/schema.md +++ b/docs/en/guide/schema.md @@ -41,6 +41,9 @@ interface IElement { }; rowMargin?: number; letterSpacing?: number; + textDecoration?: { + style?: TextDecorationStyle; + }; // groupIds groupIds?: string[]; // table diff --git a/docs/guide/command-execute.md b/docs/guide/command-execute.md index 20547dc..307268f 100644 --- a/docs/guide/command-execute.md +++ b/docs/guide/command-execute.md @@ -244,7 +244,7 @@ instance.command.executeItalic() 用法: ```javascript -instance.command.executeUnderline() +instance.command.executeUnderline(textDecoration?: ITextDecoration) ``` ## executeStrikeout diff --git a/docs/guide/schema.md b/docs/guide/schema.md index b0c6a37..88cfe3b 100644 --- a/docs/guide/schema.md +++ b/docs/guide/schema.md @@ -41,6 +41,9 @@ interface IElement { }; rowMargin?: number; letterSpacing?: number; + textDecoration?: { + style?: TextDecorationStyle; + }; // 组信息-可用于批注等其他成组使用场景 groupIds?: string[]; // 表格 diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 6d1cada..445b87f 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -51,8 +51,9 @@ import { IRange, RangeContext, RangeRect } from '../../interface/Range' import { IColgroup } from '../../interface/table/Colgroup' import { ITd } from '../../interface/table/Td' import { ITr } from '../../interface/table/Tr' +import { ITextDecoration } from '../../interface/Text' import { IWatermark } from '../../interface/Watermark' -import { deepClone, downloadFile, getUUID } from '../../utils' +import { deepClone, downloadFile, getUUID, isObjectEqual } from '../../utils' import { createDomFromElementList, formatElementContext, @@ -486,15 +487,29 @@ export class CommandAdapt { } } - public underline() { + public underline(textDecoration?: ITextDecoration) { const isDisabled = this.draw.isReadonly() || this.control.isDisabledControl() if (isDisabled) return const selection = this.range.getSelectionElementList() if (selection?.length) { - const noUnderlineIndex = selection.findIndex(s => !s.underline) + // 没有设置下划线、当前与之前有一个设置不存在、文本装饰不一致时重设下划线 + const isSetUnderline = selection.some( + s => + !s.underline || + (!textDecoration && s.textDecoration) || + (textDecoration && !s.textDecoration) || + (textDecoration && + s.textDecoration && + !isObjectEqual(s.textDecoration, textDecoration)) + ) selection.forEach(el => { - el.underline = !!~noUnderlineIndex + el.underline = isSetUnderline + if (isSetUnderline && textDecoration) { + el.textDecoration = textDecoration + } else { + delete el.textDecoration + } }) this.draw.render({ isSetCursor: false, diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 7d57338..9f5ab3e 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -1716,7 +1716,8 @@ export class Draw { y + curRow.height - rowMargin + offsetY, metrics.width + offsetX, 0, - color + color, + element.textDecoration?.style ) } else if (preElement?.underline || preElement?.control?.underline) { this.underline.render(ctx) diff --git a/src/editor/core/draw/richtext/AbstractRichText.ts b/src/editor/core/draw/richtext/AbstractRichText.ts index d5c5ae6..5eeca73 100644 --- a/src/editor/core/draw/richtext/AbstractRichText.ts +++ b/src/editor/core/draw/richtext/AbstractRichText.ts @@ -1,8 +1,10 @@ +import { TextDecorationStyle } from '../../../dataset/enum/Text' import { IElementFillRect } from '../../../interface/Element' export abstract class AbstractRichText { - public fillRect: IElementFillRect - public fillColor?: string + protected fillRect: IElementFillRect + protected fillColor?: string + protected fillDecorationStyle?: TextDecorationStyle constructor() { this.fillRect = this.clearFillInfo() @@ -10,6 +12,7 @@ export abstract class AbstractRichText { public clearFillInfo() { this.fillColor = undefined + this.fillDecorationStyle = undefined this.fillRect = { x: 0, y: 0, @@ -25,15 +28,19 @@ export abstract class AbstractRichText { y: number, width: number, height?: number, - color?: string + color?: string, + decorationStyle?: TextDecorationStyle ) { const isFirstRecord = !this.fillRect.width // 颜色不同时立即绘制 - if (!isFirstRecord && this.fillColor !== color) { + if ( + !isFirstRecord && + (this.fillColor !== color || this.fillDecorationStyle !== decorationStyle) + ) { this.render(ctx) this.clearFillInfo() // 重新记录 - this.recordFillInfo(ctx, x, y, width, height, color) + this.recordFillInfo(ctx, x, y, width, height, color, decorationStyle) return } if (isFirstRecord) { @@ -45,6 +52,7 @@ export abstract class AbstractRichText { } this.fillRect.width += width this.fillColor = color + this.fillDecorationStyle = decorationStyle } public abstract render(ctx: CanvasRenderingContext2D): void diff --git a/src/editor/core/draw/richtext/Underline.ts b/src/editor/core/draw/richtext/Underline.ts index 33d1ac3..ff73f7b 100644 --- a/src/editor/core/draw/richtext/Underline.ts +++ b/src/editor/core/draw/richtext/Underline.ts @@ -1,6 +1,7 @@ import { AbstractRichText } from './AbstractRichText' import { IEditorOption } from '../../../interface/Editor' import { Draw } from '../Draw' +import { DashType, TextDecorationStyle } from '../../../dataset/enum/Text' export class Underline extends AbstractRichText { private options: Required @@ -10,17 +11,95 @@ export class Underline extends AbstractRichText { this.options = draw.getOptions() } + // 下划线 + private _drawLine( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number, + dashType?: DashType + ) { + const endX = startX + width + ctx.beginPath() + switch (dashType) { + case DashType.DASHED: + // 长虚线- - - - - - + ctx.setLineDash([3, 1]) + break + case DashType.DOTTED: + // 点虚线 . . . . . . + ctx.setLineDash([1, 1]) + break + } + ctx.moveTo(startX, startY) + ctx.lineTo(endX, startY) + ctx.stroke() + } + + // 双实线 + private _drawDouble( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number + ) { + const SPACING = 3 // 双实线间距 + const endX = startX + width + const endY = startY + SPACING * this.options.scale + ctx.beginPath() + ctx.moveTo(startX, startY) + ctx.lineTo(endX, startY) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(startX, endY) + ctx.lineTo(endX, endY) + ctx.stroke() + } + + // 波浪线 + private _drawWave( + ctx: CanvasRenderingContext2D, + startX: number, + startY: number, + width: number + ) { + const { scale } = this.options + const AMPLITUDE = 1.2 * scale // 振幅 + const FREQUENCY = 1 / scale // 频率 + const adjustY = startY + 2 * AMPLITUDE // 增加2倍振幅 + ctx.beginPath() + for (let x = 0; x < width; x++) { + const y = AMPLITUDE * Math.sin(FREQUENCY * x) + ctx.lineTo(startX + x, adjustY + y) + } + ctx.stroke() + } + public render(ctx: CanvasRenderingContext2D) { if (!this.fillRect.width) return - const { underlineColor } = this.options + const { underlineColor, scale } = this.options const { x, y, width } = this.fillRect ctx.save() ctx.strokeStyle = this.fillColor || underlineColor + ctx.lineWidth = scale const adjustY = Math.floor(y + 2 * ctx.lineWidth) + 0.5 // +0.5从1处渲染,避免线宽度等于3 - ctx.beginPath() - ctx.moveTo(x, adjustY) - ctx.lineTo(x + width, adjustY) - ctx.stroke() + switch (this.fillDecorationStyle) { + case TextDecorationStyle.WAVY: + this._drawWave(ctx, x, adjustY, width) + break + case TextDecorationStyle.DOUBLE: + this._drawDouble(ctx, x, adjustY, width) + break + case TextDecorationStyle.DASHED: + this._drawLine(ctx, x, adjustY, width, DashType.DASHED) + break + case TextDecorationStyle.DOTTED: + this._drawLine(ctx, x, adjustY, width, DashType.DOTTED) + break + default: + this._drawLine(ctx, x, adjustY, width) + break + } ctx.restore() this.clearFillInfo() } diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index cd4f070..292b991 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -10,7 +10,8 @@ export const EDITOR_ELEMENT_STYLE_ATTR: Array = [ 'size', 'italic', 'underline', - 'strikeout' + 'strikeout', + 'textDecoration' ] export const EDITOR_ROW_ATTR: Array = ['rowFlex', 'rowMargin'] @@ -32,7 +33,8 @@ export const EDITOR_ELEMENT_COPY_ATTR: Array = [ 'dateFormat', 'groupIds', 'rowFlex', - 'rowMargin' + 'rowMargin', + 'textDecoration' ] export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ @@ -66,7 +68,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'groupIds', 'conceptId', 'imgDisplay', - 'imgFloatPosition' + 'imgFloatPosition', + 'textDecoration' ] export const TABLE_TD_ZIP_ATTR: Array = [ diff --git a/src/editor/dataset/enum/Text.ts b/src/editor/dataset/enum/Text.ts new file mode 100644 index 0000000..9a023de --- /dev/null +++ b/src/editor/dataset/enum/Text.ts @@ -0,0 +1,13 @@ +export enum TextDecorationStyle { + SOLID = 'solid', + DOUBLE = 'double', + DASHED = 'dashed', + DOTTED = 'dotted', + WAVY = 'wavy' +} + +export enum DashType { + SOLID = 'solid', + DASHED = 'dashed', + DOTTED = 'dotted' +} diff --git a/src/editor/index.ts b/src/editor/index.ts index 40f83c2..6acc307 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -75,6 +75,7 @@ import { defaultZoneOption } from './dataset/constant/Zone' import { IBackgroundOption } from './interface/Background' import { defaultBackground } from './dataset/constant/Background' import { BackgroundRepeat, BackgroundSize } from './dataset/enum/Background' +import { TextDecorationStyle } from './dataset/enum/Text' export default class Editor { public command: Command @@ -299,7 +300,8 @@ export { WordBreak, ControlIndentation, BackgroundRepeat, - BackgroundSize + BackgroundSize, + TextDecorationStyle } // 对外类型 diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index 1992d08..a52e50a 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -8,6 +8,7 @@ import { TableBorder } from '../dataset/enum/table/Table' import { IBlock } from './Block' import { ICheckbox } from './Checkbox' import { IControl } from './Control' +import { ITextDecoration } from './Text' import { IColgroup } from './table/Colgroup' import { ITr } from './table/Tr' @@ -31,6 +32,7 @@ export interface IElementStyle { rowFlex?: RowFlex rowMargin?: number letterSpacing?: number + textDecoration?: ITextDecoration } export interface IElementGroup { diff --git a/src/editor/interface/Text.ts b/src/editor/interface/Text.ts new file mode 100644 index 0000000..d04e6ac --- /dev/null +++ b/src/editor/interface/Text.ts @@ -0,0 +1,5 @@ +import { TextDecorationStyle } from '../dataset/enum/Text' + +export interface ITextDecoration { + style?: TextDecorationStyle +} diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts index 8f2242f..ad2b4fd 100644 --- a/src/editor/utils/index.ts +++ b/src/editor/utils/index.ts @@ -285,3 +285,13 @@ export function isArrayEqual(arr1: unknown[], arr2: unknown[]): boolean { } return !arr1.some(item => !arr2.includes(item)) } + +export function isObjectEqual(obj1: unknown, obj2: unknown): boolean { + if (!isObject(obj1) || !isObject(obj2)) return false + const obj1Keys = Object.keys(obj1) + const obj2Keys = Object.keys(obj2) + if (obj1Keys.length !== obj2Keys.length) { + return false + } + return !obj1Keys.some(key => obj2[key] !== obj1[key]) +}