feat: add text decoration property
This commit is contained in:
@@ -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'
|
||||||
|
}
|
||||||
+3
-1
@@ -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])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user