feat: add text decoration property
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<IEditorOption>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export const EDITOR_ELEMENT_STYLE_ATTR: Array<keyof IElement> = [
|
||||
'size',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikeout'
|
||||
'strikeout',
|
||||
'textDecoration'
|
||||
]
|
||||
|
||||
export const EDITOR_ROW_ATTR: Array<keyof IElement> = ['rowFlex', 'rowMargin']
|
||||
@@ -32,7 +33,8 @@ export const EDITOR_ELEMENT_COPY_ATTR: Array<keyof IElement> = [
|
||||
'dateFormat',
|
||||
'groupIds',
|
||||
'rowFlex',
|
||||
'rowMargin'
|
||||
'rowMargin',
|
||||
'textDecoration'
|
||||
]
|
||||
|
||||
export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
|
||||
@@ -66,7 +68,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
|
||||
'groupIds',
|
||||
'conceptId',
|
||||
'imgDisplay',
|
||||
'imgFloatPosition'
|
||||
'imgFloatPosition',
|
||||
'textDecoration'
|
||||
]
|
||||
|
||||
export const TABLE_TD_ZIP_ATTR: Array<keyof ITd> = [
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
+3
-1
@@ -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
|
||||
}
|
||||
|
||||
// 对外类型
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TextDecorationStyle } from '../dataset/enum/Text'
|
||||
|
||||
export interface ITextDecoration {
|
||||
style?: TextDecorationStyle
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user