feat: add text decoration property

pr675
Hufe921 2 years ago
parent dc2804c492
commit f1570f2180

@ -244,7 +244,7 @@ Feature: Underline
Usage: Usage:
```javascript ```javascript
instance.command.executeUnderline() instance.command.executeUnderline(textDecoration?: ITextDecoration)
``` ```
## executeStrikeout ## executeStrikeout

@ -41,6 +41,9 @@ interface IElement {
}; };
rowMargin?: number; rowMargin?: number;
letterSpacing?: number; letterSpacing?: number;
textDecoration?: {
style?: TextDecorationStyle;
};
// groupIds // groupIds
groupIds?: string[]; groupIds?: string[];
// table // table

@ -244,7 +244,7 @@ instance.command.executeItalic()
用法: 用法:
```javascript ```javascript
instance.command.executeUnderline() instance.command.executeUnderline(textDecoration?: ITextDecoration)
``` ```
## executeStrikeout ## executeStrikeout

@ -41,6 +41,9 @@ interface IElement {
}; };
rowMargin?: number; rowMargin?: number;
letterSpacing?: number; letterSpacing?: number;
textDecoration?: {
style?: TextDecorationStyle;
};
// 组信息-可用于批注等其他成组使用场景 // 组信息-可用于批注等其他成组使用场景
groupIds?: string[]; groupIds?: string[];
// 表格 // 表格

@ -51,8 +51,9 @@ import { IRange, RangeContext, RangeRect } from '../../interface/Range'
import { IColgroup } from '../../interface/table/Colgroup' import { IColgroup } from '../../interface/table/Colgroup'
import { ITd } from '../../interface/table/Td' import { ITd } from '../../interface/table/Td'
import { ITr } from '../../interface/table/Tr' import { ITr } from '../../interface/table/Tr'
import { ITextDecoration } from '../../interface/Text'
import { IWatermark } from '../../interface/Watermark' import { IWatermark } from '../../interface/Watermark'
import { deepClone, downloadFile, getUUID } from '../../utils' import { deepClone, downloadFile, getUUID, isObjectEqual } from '../../utils'
import { import {
createDomFromElementList, createDomFromElementList,
formatElementContext, formatElementContext,
@ -486,15 +487,29 @@ export class CommandAdapt {
} }
} }
public underline() { public underline(textDecoration?: ITextDecoration) {
const isDisabled = const isDisabled =
this.draw.isReadonly() || this.control.isDisabledControl() this.draw.isReadonly() || this.control.isDisabledControl()
if (isDisabled) return if (isDisabled) return
const selection = this.range.getSelectionElementList() const selection = this.range.getSelectionElementList()
if (selection?.length) { 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 => { selection.forEach(el => {
el.underline = !!~noUnderlineIndex el.underline = isSetUnderline
if (isSetUnderline && textDecoration) {
el.textDecoration = textDecoration
} else {
delete el.textDecoration
}
}) })
this.draw.render({ this.draw.render({
isSetCursor: false, isSetCursor: false,

@ -1716,7 +1716,8 @@ export class Draw {
y + curRow.height - rowMargin + offsetY, y + curRow.height - rowMargin + offsetY,
metrics.width + offsetX, metrics.width + offsetX,
0, 0,
color color,
element.textDecoration?.style
) )
} else if (preElement?.underline || preElement?.control?.underline) { } else if (preElement?.underline || preElement?.control?.underline) {
this.underline.render(ctx) this.underline.render(ctx)

@ -1,8 +1,10 @@
import { TextDecorationStyle } from '../../../dataset/enum/Text'
import { IElementFillRect } from '../../../interface/Element' import { IElementFillRect } from '../../../interface/Element'
export abstract class AbstractRichText { export abstract class AbstractRichText {
public fillRect: IElementFillRect protected fillRect: IElementFillRect
public fillColor?: string protected fillColor?: string
protected fillDecorationStyle?: TextDecorationStyle
constructor() { constructor() {
this.fillRect = this.clearFillInfo() this.fillRect = this.clearFillInfo()
@ -10,6 +12,7 @@ export abstract class AbstractRichText {
public clearFillInfo() { public clearFillInfo() {
this.fillColor = undefined this.fillColor = undefined
this.fillDecorationStyle = undefined
this.fillRect = { this.fillRect = {
x: 0, x: 0,
y: 0, y: 0,
@ -25,15 +28,19 @@ export abstract class AbstractRichText {
y: number, y: number,
width: number, width: number,
height?: number, height?: number,
color?: string color?: string,
decorationStyle?: TextDecorationStyle
) { ) {
const isFirstRecord = !this.fillRect.width const isFirstRecord = !this.fillRect.width
// 颜色不同时立即绘制 // 颜色不同时立即绘制
if (!isFirstRecord && this.fillColor !== color) { if (
!isFirstRecord &&
(this.fillColor !== color || this.fillDecorationStyle !== decorationStyle)
) {
this.render(ctx) this.render(ctx)
this.clearFillInfo() this.clearFillInfo()
// 重新记录 // 重新记录
this.recordFillInfo(ctx, x, y, width, height, color) this.recordFillInfo(ctx, x, y, width, height, color, decorationStyle)
return return
} }
if (isFirstRecord) { if (isFirstRecord) {
@ -45,6 +52,7 @@ export abstract class AbstractRichText {
} }
this.fillRect.width += width this.fillRect.width += width
this.fillColor = color this.fillColor = color
this.fillDecorationStyle = decorationStyle
} }
public abstract render(ctx: CanvasRenderingContext2D): void public abstract render(ctx: CanvasRenderingContext2D): void

@ -1,6 +1,7 @@
import { AbstractRichText } from './AbstractRichText' import { AbstractRichText } from './AbstractRichText'
import { IEditorOption } from '../../../interface/Editor' import { IEditorOption } from '../../../interface/Editor'
import { Draw } from '../Draw' import { Draw } from '../Draw'
import { DashType, TextDecorationStyle } from '../../../dataset/enum/Text'
export class Underline extends AbstractRichText { export class Underline extends AbstractRichText {
private options: Required<IEditorOption> private options: Required<IEditorOption>
@ -10,17 +11,95 @@ export class Underline extends AbstractRichText {
this.options = draw.getOptions() 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) { public render(ctx: CanvasRenderingContext2D) {
if (!this.fillRect.width) return if (!this.fillRect.width) return
const { underlineColor } = this.options const { underlineColor, scale } = this.options
const { x, y, width } = this.fillRect const { x, y, width } = this.fillRect
ctx.save() ctx.save()
ctx.strokeStyle = this.fillColor || underlineColor ctx.strokeStyle = this.fillColor || underlineColor
ctx.lineWidth = scale
const adjustY = Math.floor(y + 2 * ctx.lineWidth) + 0.5 // +0.5从1处渲染避免线宽度等于3 const adjustY = Math.floor(y + 2 * ctx.lineWidth) + 0.5 // +0.5从1处渲染避免线宽度等于3
ctx.beginPath() switch (this.fillDecorationStyle) {
ctx.moveTo(x, adjustY) case TextDecorationStyle.WAVY:
ctx.lineTo(x + width, adjustY) this._drawWave(ctx, x, adjustY, width)
ctx.stroke() 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() ctx.restore()
this.clearFillInfo() this.clearFillInfo()
} }

@ -10,7 +10,8 @@ export const EDITOR_ELEMENT_STYLE_ATTR: Array<keyof IElement> = [
'size', 'size',
'italic', 'italic',
'underline', 'underline',
'strikeout' 'strikeout',
'textDecoration'
] ]
export const EDITOR_ROW_ATTR: Array<keyof IElement> = ['rowFlex', 'rowMargin'] export const EDITOR_ROW_ATTR: Array<keyof IElement> = ['rowFlex', 'rowMargin']
@ -32,7 +33,8 @@ export const EDITOR_ELEMENT_COPY_ATTR: Array<keyof IElement> = [
'dateFormat', 'dateFormat',
'groupIds', 'groupIds',
'rowFlex', 'rowFlex',
'rowMargin' 'rowMargin',
'textDecoration'
] ]
export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
@ -66,7 +68,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
'groupIds', 'groupIds',
'conceptId', 'conceptId',
'imgDisplay', 'imgDisplay',
'imgFloatPosition' 'imgFloatPosition',
'textDecoration'
] ]
export const TABLE_TD_ZIP_ATTR: Array<keyof ITd> = [ 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'
}

@ -75,6 +75,7 @@ import { defaultZoneOption } from './dataset/constant/Zone'
import { IBackgroundOption } from './interface/Background' import { IBackgroundOption } from './interface/Background'
import { defaultBackground } from './dataset/constant/Background' import { defaultBackground } from './dataset/constant/Background'
import { BackgroundRepeat, BackgroundSize } from './dataset/enum/Background' import { BackgroundRepeat, BackgroundSize } from './dataset/enum/Background'
import { TextDecorationStyle } from './dataset/enum/Text'
export default class Editor { export default class Editor {
public command: Command public command: Command
@ -299,7 +300,8 @@ export {
WordBreak, WordBreak,
ControlIndentation, ControlIndentation,
BackgroundRepeat, BackgroundRepeat,
BackgroundSize BackgroundSize,
TextDecorationStyle
} }
// 对外类型 // 对外类型

@ -8,6 +8,7 @@ import { TableBorder } from '../dataset/enum/table/Table'
import { IBlock } from './Block' import { IBlock } from './Block'
import { ICheckbox } from './Checkbox' import { ICheckbox } from './Checkbox'
import { IControl } from './Control' import { IControl } from './Control'
import { ITextDecoration } from './Text'
import { IColgroup } from './table/Colgroup' import { IColgroup } from './table/Colgroup'
import { ITr } from './table/Tr' import { ITr } from './table/Tr'
@ -31,6 +32,7 @@ export interface IElementStyle {
rowFlex?: RowFlex rowFlex?: RowFlex
rowMargin?: number rowMargin?: number
letterSpacing?: number letterSpacing?: number
textDecoration?: ITextDecoration
} }
export interface IElementGroup { 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)) 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])
}

Loading…
Cancel
Save