feat:optimize event code structure

pr675
Hufe921 3 years ago
parent 5e9c1ce577
commit f63affc1c4

@ -1,81 +1,70 @@
import { ElementType, IEditorOption } from '../..'
import { ZERO } from '../../dataset/constant/Common'
import { EDITOR_ELEMENT_COPY_ATTR } from '../../dataset/constant/Element'
import { ElementStyleKey } from '../../dataset/enum/ElementStyle'
import { MouseEventButton } from '../../dataset/enum/Event'
import { KeyMap } from '../../dataset/enum/KeyMap'
import { IElement } from '../../interface/Element'
import { IElement, IElementPosition } from '../../interface/Element'
import { ICurrentPosition } from '../../interface/Position'
import { writeElementList } from '../../utils/clipboard'
import { Cursor } from '../cursor/Cursor'
import { Draw } from '../draw/Draw'
import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle'
import { TableTool } from '../draw/particle/table/TableTool'
import { HistoryManager } from '../history/HistoryManager'
import { Listener } from '../listener/Listener'
import { Position } from '../position/Position'
import { RangeManager } from '../range/RangeManager'
import { LETTER_REG, NUMBER_LIKE_REG } from '../../dataset/constant/Regular'
import { Control } from '../draw/control/Control'
import { CheckboxControl } from '../draw/control/checkbox/CheckboxControl'
import { findParent, splitText, threeClick } from '../../utils'
import { Previewer } from '../draw/particle/previewer/Previewer'
import { DeepRequired } from '../../interface/Common'
import { DateParticle } from '../draw/particle/date/DateParticle'
import { threeClick } from '../../utils'
import { IRange } from '../../interface/Range'
import { mousedown } from './handlers/mousedown'
import { mouseup } from './handlers/mouseup'
import { mouseleave } from './handlers/mouseleave'
import { mousemove } from './handlers/mousemove'
import { keydown } from './handlers/keydown'
import { input } from './handlers/input'
import { cut } from './handlers/cut'
import { copy } from './handlers/copy'
import { drop } from './handlers/drop'
import click from './handlers/click'
import composition from './handlers/composition'
import drag from './handlers/drag'
export class CanvasEvent {
private isAllowSelection: boolean
private isCompositing: boolean
private mouseDownStartPosition: ICurrentPosition | null
public isAllowSelection: boolean
public isCompositing: boolean
public isAllowDrag: boolean
public isAllowDrop: boolean
public cacheRange: IRange | null
public cacheElementList: IElement[] | null
public cachePositionList: IElementPosition[] | null
public mouseDownStartPosition: ICurrentPosition | null
private draw: Draw
private options: DeepRequired<IEditorOption>
private pageContainer: HTMLDivElement
private pageList: HTMLCanvasElement[]
private position: Position
private range: RangeManager
private cursor: Cursor | null
private historyManager: HistoryManager
private previewer: Previewer
private tableTool: TableTool
private hyperlinkParticle: HyperlinkParticle
private dateParticle: DateParticle
private listener: Listener
private control: Control
private position: Position
constructor(draw: Draw) {
this.draw = draw
this.pageContainer = draw.getPageContainer()
this.pageList = draw.getPageList()
this.range = this.draw.getRange()
this.position = this.draw.getPosition()
this.isAllowSelection = false
this.isCompositing = false
this.isAllowDrag = false
this.isAllowDrop = false
this.cacheRange = null
this.cacheElementList = null
this.cachePositionList = null
this.mouseDownStartPosition = null
}
this.pageContainer = draw.getPageContainer()
this.pageList = draw.getPageList()
this.draw = draw
this.options = draw.getOptions()
this.cursor = null
this.position = this.draw.getPosition()
this.range = this.draw.getRange()
this.historyManager = this.draw.getHistoryManager()
this.previewer = this.draw.getPreviewer()
this.tableTool = this.draw.getTableTool()
this.hyperlinkParticle = this.draw.getHyperlinkParticle()
this.dateParticle = this.draw.getDateParticle()
this.listener = this.draw.getListener()
this.control = this.draw.getControl()
public getDraw(): Draw {
return this.draw
}
public register() {
// 延迟加载
this.cursor = this.draw.getCursor()
this.pageContainer.addEventListener('mousedown', this.mousedown.bind(this))
this.pageContainer.addEventListener('mouseup', this.mouseup.bind(this))
this.pageContainer.addEventListener('mouseleave', this.mouseleave.bind(this))
this.pageContainer.addEventListener('mousemove', this.mousemove.bind(this))
this.pageContainer.addEventListener('dblclick', this.dblclick.bind(this))
this.pageContainer.addEventListener('dragover', this.dragover.bind(this))
this.pageContainer.addEventListener('drop', this.drop.bind(this))
threeClick(this.pageContainer, this.threeClick.bind(this))
}
@ -86,6 +75,11 @@ export class CanvasEvent {
}
}
public setIsAllowDrag(payload: boolean) {
this.isAllowDrag = payload
this.isAllowDrop = payload
}
public clearPainterStyle() {
this.pageList.forEach(p => {
p.style.cursor = 'text'
@ -113,56 +107,9 @@ export class CanvasEvent {
}
}
public mousemove(evt: MouseEvent) {
if (!this.isAllowSelection || !this.mouseDownStartPosition) return
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
this.draw.setPageNo(Number(pageIndex))
}
// 结束位置
const positionResult = this.position.getPositionByXY({
x: evt.offsetX,
y: evt.offsetY
})
const {
index,
isTable,
tdValueIndex,
tdIndex,
trIndex,
tableId
} = positionResult
const {
index: startIndex,
isTable: startIsTable,
tdIndex: startTdIndex,
trIndex: startTrIndex
} = this.mouseDownStartPosition
const endIndex = isTable ? tdValueIndex! : index
// 判断是否是表格跨行/列
if (isTable && startIsTable && (tdIndex !== startTdIndex || trIndex !== startTrIndex)) {
this.range.setRange(
endIndex,
endIndex,
tableId,
startTdIndex,
tdIndex,
startTrIndex,
trIndex
)
} else {
let end = ~endIndex ? endIndex : 0
// 开始位置
let start = startIndex
if (start > end) {
[start, end] = [end, start]
}
if (start === end) return
this.range.setRange(start, end)
}
// 绘制
public selectAll() {
const position = this.position.getPositionList()
this.range.setRange(0, position.length - 1)
this.draw.render({
isSubmitHistory: false,
isSetCursor: false,
@ -170,536 +117,60 @@ export class CanvasEvent {
})
}
public mousemove(evt: MouseEvent) {
mousemove(evt, this)
}
public mousedown(evt: MouseEvent) {
if (evt.button === MouseEventButton.RIGHT) return
const isReadonly = this.draw.isReadonly()
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
this.draw.setPageNo(Number(pageIndex))
}
this.isAllowSelection = true
const positionResult = this.position.adjustPositionContext({
x: evt.offsetX,
y: evt.offsetY
})
const {
index,
isDirectHit,
isCheckbox,
isImage,
isTable,
tdValueIndex,
} = positionResult
// 记录选区开始位置
this.mouseDownStartPosition = {
...positionResult,
index: isTable ? tdValueIndex! : index
}
const elementList = this.draw.getElementList()
const positionList = this.position.getPositionList()
const curIndex = isTable ? tdValueIndex! : index
const curElement = elementList[curIndex]
// 绘制
const isDirectHitImage = !!(isDirectHit && isImage)
const isDirectHitCheckbox = !!(isDirectHit && isCheckbox)
if (~index) {
this.range.setRange(curIndex, curIndex)
this.position.setCursorPosition(positionList[curIndex])
// 复选框
const isSetCheckbox = isDirectHitCheckbox && !isReadonly
if (isSetCheckbox) {
const { checkbox } = curElement
if (checkbox) {
checkbox.value = !checkbox.value
} else {
curElement.checkbox = {
value: true
}
}
const activeControl = this.control.getActiveControl()
if (activeControl instanceof CheckboxControl) {
activeControl.setSelect()
}
}
this.draw.render({
curIndex,
isSubmitHistory: isSetCheckbox,
isSetCursor: !isDirectHitImage && !isDirectHitCheckbox,
isComputeRowList: false
})
}
// 预览工具组件
this.previewer.clearResizer()
if (isDirectHitImage && !isReadonly) {
this.previewer.drawResizer(curElement, positionList[curIndex],
curElement.type === ElementType.LATEX
? {
mime: 'svg',
srcKey: 'laTexSVG'
}
: {})
}
// 表格工具组件
this.tableTool.dispose()
if (isTable && !isReadonly) {
const originalElementList = this.draw.getOriginalElementList()
const originalPositionList = this.position.getOriginalPositionList()
this.tableTool.render(originalElementList[index], originalPositionList[index])
}
// 超链接
this.hyperlinkParticle.clearHyperlinkPopup()
if (curElement.type === ElementType.HYPERLINK) {
this.hyperlinkParticle.drawHyperlinkPopup(curElement, positionList[curIndex])
}
// 日期控件
this.dateParticle.clearDatePicker()
if (curElement.type === ElementType.DATE && !isReadonly) {
this.dateParticle.renderDatePicker(curElement, positionList[curIndex])
}
mousedown(evt, this)
}
public mouseup(evt: MouseEvent) {
mouseup(evt, this)
}
public mouseleave(evt: MouseEvent) {
// 是否还在canvas内部
const { x, y, width, height } = this.pageContainer.getBoundingClientRect()
if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) return
this.setIsAllowSelection(false)
mouseleave(evt, this)
}
public keydown(evt: KeyboardEvent) {
const isReadonly = this.draw.isReadonly()
const cursorPosition = this.position.getCursorPosition()
if (!cursorPosition) return
const elementList = this.draw.getElementList()
const position = this.position.getPositionList()
const { index } = cursorPosition
const { startIndex, endIndex } = this.range.getRange()
const isCollapsed = startIndex === endIndex
// 当前激活控件
const isPartRangeInControlOutside = this.control.isPartRangeInControlOutside()
const activeControl = this.control.getActiveControl()
if (evt.key === KeyMap.Backspace) {
if (isReadonly || isPartRangeInControlOutside) return
let curIndex: number
if (activeControl) {
curIndex = this.control.keydown(evt)
} else {
// 判断是否允许删除
if (isCollapsed && elementList[index].value === ZERO && index === 0) {
evt.preventDefault()
return
}
if (!isCollapsed) {
elementList.splice(startIndex + 1, endIndex - startIndex)
} else {
elementList.splice(index, 1)
}
curIndex = isCollapsed ? index - 1 : startIndex
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
} else if (evt.key === KeyMap.Delete) {
if (isReadonly || isPartRangeInControlOutside) return
let curIndex: number
if (activeControl) {
curIndex = this.control.keydown(evt)
} else if (elementList[endIndex + 1]?.type === ElementType.CONTROL) {
curIndex = this.control.removeControl(endIndex + 1)
} else {
if (!isCollapsed) {
elementList.splice(startIndex + 1, endIndex - startIndex)
} else {
elementList.splice(index + 1, 1)
}
curIndex = isCollapsed ? index : startIndex
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
} else if (evt.key === KeyMap.Enter) {
if (isReadonly || isPartRangeInControlOutside) return
// 表格需要上下文信息
const positionContext = this.position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const enterText: IElement = {
value: ZERO,
...restArg
}
let curIndex: number
if (activeControl) {
curIndex = this.control.setValue([enterText])
} else {
if (isCollapsed) {
elementList.splice(index + 1, 0, enterText)
} else {
elementList.splice(startIndex + 1, endIndex - startIndex, enterText)
}
curIndex = index + 1
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
evt.preventDefault()
} else if (evt.key === KeyMap.Left) {
if (isReadonly) return
if (index > 0) {
const curIndex = index - 1
this.range.setRange(curIndex, curIndex)
this.draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.key === KeyMap.Right) {
if (isReadonly) return
if (index < position.length - 1) {
const curIndex = index + 1
this.range.setRange(curIndex, curIndex)
this.draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
if (isReadonly) return
const { rowNo, index, coordinate: { leftTop, rightTop } } = cursorPosition
if ((evt.key === KeyMap.Up && rowNo !== 0) || (evt.key === KeyMap.Down && rowNo !== this.draw.getRowCount())) {
// 下一个光标点所在行位置集合
const probablePosition = evt.key === KeyMap.Up
? position.slice(0, index).filter(p => p.rowNo === rowNo - 1)
: position.slice(index, position.length - 1).filter(p => p.rowNo === rowNo + 1)
// 查找与当前位置元素点交叉最多的位置
let maxIndex = 0
let maxDistance = 0
for (let p = 0; p < probablePosition.length; p++) {
const position = probablePosition[p]
// 当前光标在前
if (position.coordinate.leftTop[0] >= leftTop[0] && position.coordinate.leftTop[0] <= rightTop[0]) {
const curDistance = rightTop[0] - position.coordinate.leftTop[0]
if (curDistance > maxDistance) {
maxIndex = position.index
maxDistance = curDistance
}
}
// 当前光标在后
else if (position.coordinate.leftTop[0] <= leftTop[0] && position.coordinate.rightTop[0] >= leftTop[0]) {
const curDistance = position.coordinate.rightTop[0] - leftTop[0]
if (curDistance > maxDistance) {
maxIndex = position.index
maxDistance = curDistance
}
}
// 匹配不到
if (p === probablePosition.length - 1 && maxIndex === 0) {
maxIndex = position.index
}
}
const curIndex = maxIndex
this.range.setRange(curIndex, curIndex)
this.draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.ctrlKey && evt.key === KeyMap.Z) {
if (isReadonly) return
this.historyManager.undo()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.Y) {
if (isReadonly) return
this.historyManager.redo()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.C) {
this.copy()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.X) {
this.cut()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.A) {
this.selectAll()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.S) {
if (isReadonly) return
if (this.listener.saved) {
this.listener.saved(this.draw.getValue())
}
evt.preventDefault()
} else if (evt.key === KeyMap.ESC) {
this.clearPainterStyle()
evt.preventDefault()
} else if (evt.key === KeyMap.TAB) {
this.draw.insertElementList([{
type: ElementType.TAB,
value: ''
}])
evt.preventDefault()
}
keydown(evt, this)
}
public dblclick() {
const cursorPosition = this.position.getCursorPosition()
if (!cursorPosition) return
const { value, index } = cursorPosition
const elementList = this.draw.getElementList()
// 判断是否是数字或英文
let upCount = 0
let downCount = 0
const isNumber = NUMBER_LIKE_REG.test(value)
if (isNumber || LETTER_REG.test(value)) {
// 向上查询
let upStartIndex = index - 1
while (upStartIndex > 0) {
const value = elementList[upStartIndex].value
if ((isNumber && NUMBER_LIKE_REG.test(value)) || (!isNumber && LETTER_REG.test(value))) {
upCount++
upStartIndex--
} else {
break
}
}
// 向下查询
let downStartIndex = index + 1
while (downStartIndex < elementList.length) {
const value = elementList[downStartIndex].value
if ((isNumber && NUMBER_LIKE_REG.test(value)) || (!isNumber && LETTER_REG.test(value))) {
downCount++
downStartIndex++
} else {
break
}
}
}
// 设置选中区域
this.range.setRange(index - upCount - 1, index + downCount)
// 刷新文档
this.draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
click.dblclick(this)
}
public threeClick() {
const cursorPosition = this.position.getCursorPosition()
if (!cursorPosition) return
const { index } = cursorPosition
const elementList = this.draw.getElementList()
// 判断是否是零宽字符
let upCount = 0
let downCount = 0
// 向上查询
let upStartIndex = index - 1
while (upStartIndex > 0) {
const value = elementList[upStartIndex].value
if (value !== ZERO) {
upCount++
upStartIndex--
} else {
break
}
}
// 向下查询
let downStartIndex = index + 1
while (downStartIndex < elementList.length) {
const value = elementList[downStartIndex].value
if (value !== ZERO) {
downCount++
downStartIndex++
} else {
break
}
}
// 设置选中区域
this.range.setRange(index - upCount - 1, index + downCount)
// 刷新文档
this.draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
click.threeClick(this)
}
public input(data: string) {
const isReadonly = this.draw.isReadonly()
if (isReadonly) return
if (!this.cursor) return
const cursorPosition = this.position.getCursorPosition()
if (!data || !cursorPosition || this.isCompositing) return
if (this.control.isPartRangeInControlOutside()) {
// 忽略选区部分在控件的输入
return
}
const activeControl = this.control.getActiveControl()
const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT, DATE } = ElementType
const text = data.replaceAll(`\n`, ZERO)
const elementList = this.draw.getElementList()
const agentDom = this.cursor.getAgentDom()
agentDom.value = ''
const { index } = cursorPosition
const { startIndex, endIndex } = this.range.getRange()
const isCollapsed = startIndex === endIndex
// 表格需要上下文信息
const positionContext = this.position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const element = elementList[endIndex]
const inputData: IElement[] = splitText(text).map(value => {
const newElement: IElement = {
value,
...restArg
}
const nextElement = elementList[endIndex + 1]
if (
element.type === TEXT
|| (!element.type && element.value !== ZERO)
|| (element.type === HYPERLINK && nextElement?.type === HYPERLINK)
|| (element.type === DATE && nextElement?.type === DATE)
|| (element.type === SUBSCRIPT && nextElement?.type === SUBSCRIPT)
|| (element.type === SUPERSCRIPT && nextElement?.type === SUPERSCRIPT)
) {
EDITOR_ELEMENT_COPY_ATTR.forEach(attr => {
const value = element[attr] as never
if (value !== undefined) {
newElement[attr] = value
}
})
}
return newElement
})
// 控件-移除placeholder
let curIndex: number
if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) {
curIndex = this.control.setValue(inputData)
} else {
let start = 0
if (isCollapsed) {
start = index + 1
} else {
start = startIndex + 1
elementList.splice(startIndex + 1, endIndex - startIndex)
}
// 禁止直接使用解构存在性能问题
for (let i = 0; i < inputData.length; i++) {
elementList.splice(start + i, 0, inputData[i])
}
curIndex = (isCollapsed ? index : startIndex) + inputData.length
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
input(data, this)
}
public cut() {
const { startIndex, endIndex } = this.range.getRange()
if (!~startIndex && !~startIndex) return
const isReadonly = this.draw.isReadonly()
if (isReadonly) return
const isPartRangeInControlOutside = this.control.isPartRangeInControlOutside()
if (isPartRangeInControlOutside) return
const activeControl = this.control.getActiveControl()
const elementList = this.draw.getElementList()
let start = startIndex
let end = endIndex
// 无选区则剪切一行
if (startIndex === endIndex) {
const positionList = this.position.getPositionList()
const curRowNo = positionList[startIndex].rowNo
const cutElementIndexList: number[] = []
for (let p = 0; p < positionList.length; p++) {
const position = positionList[p]
if (position.rowNo > curRowNo) break
if (position.rowNo === curRowNo) {
cutElementIndexList.push(p)
}
}
const firstElementIndex = cutElementIndexList[0] - 1
start = firstElementIndex < 0 ? 0 : firstElementIndex
end = cutElementIndexList[cutElementIndexList.length - 1]
}
// 写入粘贴板
writeElementList(elementList.slice(start + 1, end + 1), this.options)
let curIndex: number
if (activeControl) {
curIndex = this.control.cut()
} else {
elementList.splice(start + 1, end - start)
curIndex = start
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
cut(this)
}
public copy() {
const { startIndex, endIndex } = this.range.getRange()
const elementList = this.draw.getElementList()
if (startIndex !== endIndex) {
writeElementList(elementList.slice(startIndex + 1, endIndex + 1), this.options)
}
}
public selectAll() {
const position = this.position.getPositionList()
this.range.setRange(0, position.length - 1)
this.draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
copy(this)
}
public compositionstart() {
this.isCompositing = true
composition.compositionstart(this)
}
public compositionend() {
this.isCompositing = false
composition.compositionend(this)
}
public drop(evt: DragEvent) {
evt.preventDefault()
const data = evt.dataTransfer?.getData('text')
if (data) {
this.input(data)
}
drop(evt, this)
}
public dragover(evt: DragEvent) {
const isReadonly = this.draw.isReadonly()
if (isReadonly) return
evt.preventDefault()
// 非编辑器区禁止拖放
const pageContainer = findParent(
evt.target as Element,
(node: Element) => node === this.pageContainer,
true
)
if (!pageContainer) return
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
this.draw.setPageNo(Number(pageIndex))
}
const { isTable, tdValueIndex, index } = this.position.adjustPositionContext({
x: evt.offsetX,
y: evt.offsetY
})
// 设置选区及光标位置
const positionList = this.position.getPositionList()
const curIndex = isTable ? tdValueIndex! : index
if (~index) {
this.range.setRange(curIndex, curIndex)
this.position.setCursorPosition(positionList[curIndex])
}
this.cursor?.drawCursor()
public dragover(evt: DragEvent | MouseEvent) {
drag.dragover(evt, this)
}
}

@ -48,7 +48,7 @@ export class GlobalEvent {
window.addEventListener('blur', this.recoverEffect)
document.addEventListener('keyup', this.setRangeStyle)
document.addEventListener('click', this.recoverEffect)
document.addEventListener('mouseup', this.setSelectionAbility)
document.addEventListener('mouseup', this.setCanvasEventAbility)
document.addEventListener('wheel', this.setPageScale, { passive: false })
document.addEventListener('visibilitychange', this._handleVisibilityChange)
}
@ -57,7 +57,7 @@ export class GlobalEvent {
window.removeEventListener('blur', this.recoverEffect)
document.removeEventListener('keyup', this.setRangeStyle)
document.removeEventListener('click', this.recoverEffect)
document.removeEventListener('mouseup', this.setSelectionAbility)
document.removeEventListener('mouseup', this.setCanvasEventAbility)
document.removeEventListener('wheel', this.setPageScale)
document.removeEventListener('visibilitychange', this._handleVisibilityChange)
}
@ -90,7 +90,8 @@ export class GlobalEvent {
this.dateParticle.clearDatePicker()
}
public setSelectionAbility = () => {
public setCanvasEventAbility = () => {
this.canvasEvent.setIsAllowDrag(false)
this.canvasEvent.setIsAllowSelection(false)
}

@ -0,0 +1,97 @@
import { ZERO } from '../../../dataset/constant/Common'
import { LETTER_REG, NUMBER_LIKE_REG } from '../../../dataset/constant/Regular'
import { CanvasEvent } from '../CanvasEvent'
function dblclick(host: CanvasEvent) {
const draw = host.getDraw()
const position = draw.getPosition()
const cursorPosition = position.getCursorPosition()
if (!cursorPosition) return
const { value, index } = cursorPosition
// 判断是否是数字或英文
let upCount = 0
let downCount = 0
const isNumber = NUMBER_LIKE_REG.test(value)
if (isNumber || LETTER_REG.test(value)) {
const elementList = draw.getElementList()
// 向上查询
let upStartIndex = index - 1
while (upStartIndex > 0) {
const value = elementList[upStartIndex].value
if ((isNumber && NUMBER_LIKE_REG.test(value)) || (!isNumber && LETTER_REG.test(value))) {
upCount++
upStartIndex--
} else {
break
}
}
// 向下查询
let downStartIndex = index + 1
while (downStartIndex < elementList.length) {
const value = elementList[downStartIndex].value
if ((isNumber && NUMBER_LIKE_REG.test(value)) || (!isNumber && LETTER_REG.test(value))) {
downCount++
downStartIndex++
} else {
break
}
}
}
// 设置选中区域
const rangeManager = draw.getRange()
rangeManager.setRange(index - upCount - 1, index + downCount)
// 刷新文档
draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
}
function threeClick(host: CanvasEvent) {
const draw = host.getDraw()
const position = draw.getPosition()
const cursorPosition = position.getCursorPosition()
if (!cursorPosition) return
const { index } = cursorPosition
const elementList = draw.getElementList()
// 判断是否是零宽字符
let upCount = 0
let downCount = 0
// 向上查询
let upStartIndex = index - 1
while (upStartIndex > 0) {
const value = elementList[upStartIndex].value
if (value !== ZERO) {
upCount++
upStartIndex--
} else {
break
}
}
// 向下查询
let downStartIndex = index + 1
while (downStartIndex < elementList.length) {
const value = elementList[downStartIndex].value
if (value !== ZERO) {
downCount++
downStartIndex++
} else {
break
}
}
// 设置选中区域
const rangeManager = draw.getRange()
rangeManager.setRange(index - upCount - 1, index + downCount)
// 刷新文档
draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
}
export default {
dblclick,
threeClick
}

@ -0,0 +1,14 @@
import { CanvasEvent } from '../CanvasEvent'
function compositionstart(host: CanvasEvent) {
host.isCompositing = true
}
function compositionend(host: CanvasEvent) {
host.isCompositing = false
}
export default {
compositionstart,
compositionend
}

@ -0,0 +1,13 @@
import { writeElementList } from '../../../utils/clipboard'
import { CanvasEvent } from '../CanvasEvent'
export function copy(host: CanvasEvent) {
const draw = host.getDraw()
const rangeManager = draw.getRange()
const { startIndex, endIndex } = rangeManager.getRange()
if (startIndex !== endIndex) {
const options = draw.getOptions()
const elementList = draw.getElementList()
writeElementList(elementList.slice(startIndex + 1, endIndex + 1), options)
}
}

@ -0,0 +1,47 @@
import { writeElementList } from '../../../utils/clipboard'
import { CanvasEvent } from '../CanvasEvent'
export function cut(host: CanvasEvent) {
const draw = host.getDraw()
const rangeManager = draw.getRange()
const { startIndex, endIndex } = rangeManager.getRange()
if (!~startIndex && !~startIndex) return
const isReadonly = draw.isReadonly()
if (isReadonly) return
const control = draw.getControl()
const isPartRangeInControlOutside = control.isPartRangeInControlOutside()
if (isPartRangeInControlOutside) return
const activeControl = control.getActiveControl()
const elementList = draw.getElementList()
let start = startIndex
let end = endIndex
// 无选区则剪切一行
if (startIndex === endIndex) {
const position = draw.getPosition()
const positionList = position.getPositionList()
const curRowNo = positionList[startIndex].rowNo
const cutElementIndexList: number[] = []
for (let p = 0; p < positionList.length; p++) {
const position = positionList[p]
if (position.rowNo > curRowNo) break
if (position.rowNo === curRowNo) {
cutElementIndexList.push(p)
}
}
const firstElementIndex = cutElementIndexList[0] - 1
start = firstElementIndex < 0 ? 0 : firstElementIndex
end = cutElementIndexList[cutElementIndexList.length - 1]
}
const options = draw.getOptions()
// 写入粘贴板
writeElementList(elementList.slice(start + 1, end + 1), options)
let curIndex: number
if (activeControl) {
curIndex = control.cut()
} else {
elementList.splice(start + 1, end - start)
curIndex = start
}
rangeManager.setRange(curIndex, curIndex)
draw.render({ curIndex })
}

@ -0,0 +1,42 @@
import { findParent } from '../../../utils'
import { CanvasEvent } from '../CanvasEvent'
function dragover(evt: DragEvent | MouseEvent, host: CanvasEvent) {
const draw = host.getDraw()
const isReadonly = draw.isReadonly()
if (isReadonly) return
evt.preventDefault()
// 非编辑器区禁止拖放
const pageContainer = draw.getPageContainer()
const editorRegion = findParent(
evt.target as Element,
(node: Element) => node === pageContainer,
true
)
if (!editorRegion) return
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
draw.setPageNo(Number(pageIndex))
}
const position = draw.getPosition()
const { isTable, tdValueIndex, index } = position.adjustPositionContext({
x: evt.offsetX,
y: evt.offsetY
})
// 设置选区及光标位置
const positionList = position.getPositionList()
const curIndex = isTable ? tdValueIndex! : index
if (~index) {
const rangeManager = draw.getRange()
rangeManager.setRange(curIndex, curIndex)
position.setCursorPosition(positionList[curIndex])
}
const cursor = draw.getCursor()
cursor.drawCursor()
}
export default {
dragover
}

@ -0,0 +1,9 @@
import { CanvasEvent } from '../CanvasEvent'
export function drop(evt: DragEvent, host: CanvasEvent) {
evt.preventDefault()
const data = evt.dataTransfer?.getData('text')
if (data) {
host.input(data)
}
}

@ -0,0 +1,82 @@
import { ZERO } from '../../../dataset/constant/Common'
import { EDITOR_ELEMENT_COPY_ATTR } from '../../../dataset/constant/Element'
import { ElementType } from '../../../dataset/enum/Element'
import { IElement } from '../../../interface/Element'
import { splitText } from '../../../utils'
import { CanvasEvent } from '../CanvasEvent'
export function input(data: string, host: CanvasEvent) {
const draw = host.getDraw()
const isReadonly = draw.isReadonly()
if (isReadonly) return
const position = draw.getPosition()
const cursorPosition = position.getCursorPosition()
if (!data || !cursorPosition || host.isCompositing) return
const control = draw.getControl()
if (control.isPartRangeInControlOutside()) {
// 忽略选区部分在控件的输入
return
}
const activeControl = control.getActiveControl()
const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT, DATE } = ElementType
const text = data.replaceAll(`\n`, ZERO)
const cursor = draw.getCursor()
const agentDom = cursor.getAgentDom()
agentDom.value = ''
const { index } = cursorPosition
const rangeManager = draw.getRange()
const { startIndex, endIndex } = rangeManager.getRange()
const isCollapsed = startIndex === endIndex
// 表格需要上下文信息
const positionContext = position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const elementList = draw.getElementList()
const element = elementList[endIndex]
const inputData: IElement[] = splitText(text).map(value => {
const newElement: IElement = {
value,
...restArg
}
const nextElement = elementList[endIndex + 1]
if (
element.type === TEXT
|| (!element.type && element.value !== ZERO)
|| (element.type === HYPERLINK && nextElement?.type === HYPERLINK)
|| (element.type === DATE && nextElement?.type === DATE)
|| (element.type === SUBSCRIPT && nextElement?.type === SUBSCRIPT)
|| (element.type === SUPERSCRIPT && nextElement?.type === SUPERSCRIPT)
) {
EDITOR_ELEMENT_COPY_ATTR.forEach(attr => {
const value = element[attr] as never
if (value !== undefined) {
newElement[attr] = value
}
})
}
return newElement
})
// 控件-移除placeholder
let curIndex: number
if (activeControl && elementList[endIndex + 1]?.controlId === element.controlId) {
curIndex = control.setValue(inputData)
} else {
let start = 0
if (isCollapsed) {
start = index + 1
} else {
start = startIndex + 1
elementList.splice(startIndex + 1, endIndex - startIndex)
}
// 禁止直接使用解构存在性能问题
for (let i = 0; i < inputData.length; i++) {
elementList.splice(start + i, 0, inputData[i])
}
curIndex = (isCollapsed ? index : startIndex) + inputData.length
}
rangeManager.setRange(curIndex, curIndex)
draw.render({ curIndex })
}

@ -0,0 +1,186 @@
import { ZERO } from '../../../dataset/constant/Common'
import { ElementType } from '../../../dataset/enum/Element'
import { KeyMap } from '../../../dataset/enum/KeyMap'
import { IElement } from '../../../interface/Element'
import { CanvasEvent } from '../CanvasEvent'
export function keydown(evt: KeyboardEvent, host: CanvasEvent) {
const draw = host.getDraw()
const position = draw.getPosition()
const cursorPosition = position.getCursorPosition()
if (!cursorPosition) return
const isReadonly = draw.isReadonly()
const historyManager = draw.getHistoryManager()
const elementList = draw.getElementList()
const positionList = position.getPositionList()
const { index } = cursorPosition
const rangeManager = draw.getRange()
const { startIndex, endIndex } = rangeManager.getRange()
const isCollapsed = startIndex === endIndex
// 当前激活控件
const control = draw.getControl()
const isPartRangeInControlOutside = control.isPartRangeInControlOutside()
const activeControl = control.getActiveControl()
if (evt.key === KeyMap.Backspace) {
if (isReadonly || isPartRangeInControlOutside) return
let curIndex: number
if (activeControl) {
curIndex = control.keydown(evt)
} else {
// 判断是否允许删除
if (isCollapsed && elementList[index].value === ZERO && index === 0) {
evt.preventDefault()
return
}
if (!isCollapsed) {
elementList.splice(startIndex + 1, endIndex - startIndex)
} else {
elementList.splice(index, 1)
}
curIndex = isCollapsed ? index - 1 : startIndex
}
rangeManager.setRange(curIndex, curIndex)
draw.render({ curIndex })
} else if (evt.key === KeyMap.Delete) {
if (isReadonly || isPartRangeInControlOutside) return
let curIndex: number
if (activeControl) {
curIndex = control.keydown(evt)
} else if (elementList[endIndex + 1]?.type === ElementType.CONTROL) {
curIndex = control.removeControl(endIndex + 1)
} else {
if (!isCollapsed) {
elementList.splice(startIndex + 1, endIndex - startIndex)
} else {
elementList.splice(index + 1, 1)
}
curIndex = isCollapsed ? index : startIndex
}
rangeManager.setRange(curIndex, curIndex)
draw.render({ curIndex })
} else if (evt.key === KeyMap.Enter) {
if (isReadonly || isPartRangeInControlOutside) return
// 表格需要上下文信息
const positionContext = position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const enterText: IElement = {
value: ZERO,
...restArg
}
let curIndex: number
if (activeControl) {
curIndex = control.setValue([enterText])
} else {
if (isCollapsed) {
elementList.splice(index + 1, 0, enterText)
} else {
elementList.splice(startIndex + 1, endIndex - startIndex, enterText)
}
curIndex = index + 1
}
rangeManager.setRange(curIndex, curIndex)
draw.render({ curIndex })
evt.preventDefault()
} else if (evt.key === KeyMap.Left) {
if (isReadonly) return
if (index > 0) {
const curIndex = index - 1
rangeManager.setRange(curIndex, curIndex)
draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.key === KeyMap.Right) {
if (isReadonly) return
if (index < positionList.length - 1) {
const curIndex = index + 1
rangeManager.setRange(curIndex, curIndex)
draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
if (isReadonly) return
const { rowNo, index, coordinate: { leftTop, rightTop } } = cursorPosition
if ((evt.key === KeyMap.Up && rowNo !== 0) || (evt.key === KeyMap.Down && rowNo !== draw.getRowCount())) {
// 下一个光标点所在行位置集合
const probablePosition = evt.key === KeyMap.Up
? positionList.slice(0, index).filter(p => p.rowNo === rowNo - 1)
: positionList.slice(index, positionList.length - 1).filter(p => p.rowNo === rowNo + 1)
// 查找与当前位置元素点交叉最多的位置
let maxIndex = 0
let maxDistance = 0
for (let p = 0; p < probablePosition.length; p++) {
const position = probablePosition[p]
// 当前光标在前
if (position.coordinate.leftTop[0] >= leftTop[0] && position.coordinate.leftTop[0] <= rightTop[0]) {
const curDistance = rightTop[0] - position.coordinate.leftTop[0]
if (curDistance > maxDistance) {
maxIndex = position.index
maxDistance = curDistance
}
}
// 当前光标在后
else if (position.coordinate.leftTop[0] <= leftTop[0] && position.coordinate.rightTop[0] >= leftTop[0]) {
const curDistance = position.coordinate.rightTop[0] - leftTop[0]
if (curDistance > maxDistance) {
maxIndex = position.index
maxDistance = curDistance
}
}
// 匹配不到
if (p === probablePosition.length - 1 && maxIndex === 0) {
maxIndex = position.index
}
}
const curIndex = maxIndex
rangeManager.setRange(curIndex, curIndex)
draw.render({
curIndex,
isSubmitHistory: false,
isComputeRowList: false
})
}
} else if (evt.ctrlKey && evt.key === KeyMap.Z) {
if (isReadonly) return
historyManager.undo()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.Y) {
if (isReadonly) return
historyManager.redo()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.C) {
host.copy()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.X) {
host.cut()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.A) {
host.selectAll()
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.S) {
if (isReadonly) return
const listener = draw.getListener()
if (listener.saved) {
listener.saved(draw.getValue())
}
evt.preventDefault()
} else if (evt.key === KeyMap.ESC) {
host.clearPainterStyle()
evt.preventDefault()
} else if (evt.key === KeyMap.TAB) {
draw.insertElementList([{
type: ElementType.TAB,
value: ''
}])
evt.preventDefault()
}
}

@ -0,0 +1,117 @@
import { ElementType } from '../../../dataset/enum/Element'
import { MouseEventButton } from '../../../dataset/enum/Event'
import { deepClone } from '../../../utils'
import { CheckboxControl } from '../../draw/control/checkbox/CheckboxControl'
import { CanvasEvent } from '../CanvasEvent'
export function mousedown(evt: MouseEvent, host: CanvasEvent) {
if (evt.button === MouseEventButton.RIGHT) return
const draw = host.getDraw()
const isReadonly = draw.isReadonly()
const rangeManager = draw.getRange()
const position = draw.getPosition()
// 是否是选区拖拽
if (!host.isAllowDrag) {
const range = rangeManager.getRange()
if (!isReadonly && range.startIndex !== range.endIndex) {
const isPointInRange = rangeManager.getIsPointInRange(evt.offsetX, evt.offsetY)
if (isPointInRange) {
host.isAllowDrag = true
host.cacheRange = deepClone(range)
host.cacheElementList = draw.getElementList()
host.cachePositionList = position.getPositionList()
return
}
}
}
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
draw.setPageNo(Number(pageIndex))
}
host.isAllowSelection = true
const positionResult = position.adjustPositionContext({
x: evt.offsetX,
y: evt.offsetY
})
const {
index,
isDirectHit,
isCheckbox,
isImage,
isTable,
tdValueIndex,
} = positionResult
// 记录选区开始位置
host.mouseDownStartPosition = {
...positionResult,
index: isTable ? tdValueIndex! : index
}
const elementList = draw.getElementList()
const positionList = position.getPositionList()
const curIndex = isTable ? tdValueIndex! : index
const curElement = elementList[curIndex]
// 绘制
const isDirectHitImage = !!(isDirectHit && isImage)
const isDirectHitCheckbox = !!(isDirectHit && isCheckbox)
if (~index) {
rangeManager.setRange(curIndex, curIndex)
position.setCursorPosition(positionList[curIndex])
// 复选框
const isSetCheckbox = isDirectHitCheckbox && !isReadonly
if (isSetCheckbox) {
const { checkbox } = curElement
if (checkbox) {
checkbox.value = !checkbox.value
} else {
curElement.checkbox = {
value: true
}
}
const control = draw.getControl()
const activeControl = control.getActiveControl()
if (activeControl instanceof CheckboxControl) {
activeControl.setSelect()
}
}
draw.render({
curIndex,
isSubmitHistory: isSetCheckbox,
isSetCursor: !isDirectHitImage && !isDirectHitCheckbox,
isComputeRowList: false
})
}
// 预览工具组件
const previewer = draw.getPreviewer()
previewer.clearResizer()
if (isDirectHitImage && !isReadonly) {
previewer.drawResizer(curElement, positionList[curIndex],
curElement.type === ElementType.LATEX
? {
mime: 'svg',
srcKey: 'laTexSVG'
}
: {})
}
// 表格工具组件
const tableTool = draw.getTableTool()
tableTool.dispose()
if (isTable && !isReadonly) {
const originalElementList = draw.getOriginalElementList()
const originalPositionList = position.getOriginalPositionList()
tableTool.render(originalElementList[index], originalPositionList[index])
}
// 超链接
const hyperlinkParticle = draw.getHyperlinkParticle()
hyperlinkParticle.clearHyperlinkPopup()
if (curElement.type === ElementType.HYPERLINK) {
hyperlinkParticle.drawHyperlinkPopup(curElement, positionList[curIndex])
}
// 日期控件
const dateParticle = draw.getDateParticle()
dateParticle.clearDatePicker()
if (curElement.type === ElementType.DATE && !isReadonly) {
dateParticle.renderDatePicker(curElement, positionList[curIndex])
}
}

@ -0,0 +1,10 @@
import { CanvasEvent } from '../CanvasEvent'
export function mouseleave(evt: MouseEvent, host: CanvasEvent) {
// 是否还在canvas内部
const draw = host.getDraw()
const pageContainer = draw.getPageContainer()
const { x, y, width, height } = pageContainer.getBoundingClientRect()
if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) return
host.setIsAllowSelection(false)
}

@ -0,0 +1,78 @@
import { CanvasEvent } from '../CanvasEvent'
export function mousemove(evt: MouseEvent, host: CanvasEvent) {
const draw = host.getDraw()
// 是否是拖拽文字
if (host.isAllowDrag) {
// 是否允许拖拽到选区
const x = evt.offsetX
const y = evt.offsetY
const { startIndex, endIndex } = host.cacheRange!
const positionList = host.cachePositionList!
for (let p = startIndex + 1; p <= endIndex; p++) {
const { coordinate: { leftTop, rightBottom } } = positionList[p]
if (x >= leftTop[0] && x <= rightBottom[0] && y >= leftTop[1] && y <= rightBottom[1]) {
return
}
}
host.dragover(evt)
host.isAllowDrop = true
return
}
if (!host.isAllowSelection || !host.mouseDownStartPosition) return
const target = evt.target as HTMLDivElement
const pageIndex = target.dataset.index
// 设置pageNo
if (pageIndex) {
draw.setPageNo(Number(pageIndex))
}
// 结束位置
const position = draw.getPosition()
const positionResult = position.getPositionByXY({
x: evt.offsetX,
y: evt.offsetY
})
const {
index,
isTable,
tdValueIndex,
tdIndex,
trIndex,
tableId
} = positionResult
const {
index: startIndex,
isTable: startIsTable,
tdIndex: startTdIndex,
trIndex: startTrIndex
} = host.mouseDownStartPosition
const endIndex = isTable ? tdValueIndex! : index
// 判断是否是表格跨行/列
const rangeManager = draw.getRange()
if (isTable && startIsTable && (tdIndex !== startTdIndex || trIndex !== startTrIndex)) {
rangeManager.setRange(
endIndex,
endIndex,
tableId,
startTdIndex,
tdIndex,
startTrIndex,
trIndex
)
} else {
let end = ~endIndex ? endIndex : 0
// 开始位置
let start = startIndex
if (start > end) {
[start, end] = [end, start]
}
if (start === end) return
rangeManager.setRange(start, end)
}
// 绘制
draw.render({
isSubmitHistory: false,
isSetCursor: false,
isComputeRowList: false
})
}

@ -0,0 +1,89 @@
import { EDITOR_ELEMENT_STYLE_ATTR } from '../../../dataset/constant/Element'
import { IElement } from '../../../interface/Element'
import { CanvasEvent } from '../CanvasEvent'
export function mouseup(evt: MouseEvent, host: CanvasEvent) {
// 判断是否允许拖放
if (host.isAllowDrop) {
const draw = host.getDraw()
const position = draw.getPosition()
const rangeManager = draw.getRange()
const cacheRange = host.cacheRange!
const cacheElementList = host.cacheElementList!
const cachePositionList = host.cachePositionList!
// 如果同一上下文拖拽位置向后移动,则重置光标位置
const range = rangeManager.getRange()
const elementList = draw.getElementList()
const positionList = position.getPositionList()
const startPosition = positionList[range.startIndex]
const cacheStartElement = cacheElementList[cacheRange.startIndex]
const startElement = elementList[range.startIndex]
let curIndex = range.startIndex
if (cacheStartElement.tdId === startElement.tdId && range.startIndex > cacheRange.endIndex) {
curIndex -= (cacheRange.endIndex - cacheRange.startIndex)
}
// 删除原有拖拽元素
const deleteElementList = cacheElementList.splice(cacheRange.startIndex + 1, cacheRange.endIndex - cacheRange.startIndex)
// 格式化元素
let restArg = {}
if (startElement.tableId) {
const { tdId, trId, tableId } = startElement
restArg = { tdId, trId, tableId }
}
const replaceElementList = deleteElementList.map(el => {
const newElement: IElement = {
value: el.value,
...restArg
}
EDITOR_ELEMENT_STYLE_ATTR.forEach(attr => {
const value = el[attr] as never
if (value !== undefined) {
newElement[attr] = value
}
})
return newElement
})
elementList.splice(curIndex + 1, 0, ...replaceElementList)
// 重设上下文
const cacheStartPosition = cachePositionList[cacheRange.startIndex]
const positionContext = position.getPositionContext()
let positionContextIndex = positionContext.index
if (positionContextIndex) {
if (startElement.tableId && !cacheStartElement.tableId) {
// 表格外移动到表格内&&表格之前
if (cacheStartPosition.index < positionContextIndex) {
positionContextIndex -= deleteElementList.length
}
} else if (!startElement.tableId && cacheStartElement.tableId) {
// 表格内移到表格外&&表格之前
if (startPosition.index < positionContextIndex) {
positionContextIndex += deleteElementList.length
}
}
position.setPositionContext({
...positionContext,
index: positionContextIndex
})
}
// 设置选区
rangeManager.setRange(
curIndex,
curIndex + deleteElementList.length,
range.tableId,
range.startTdIndex,
range.endTdIndex,
range.startTrIndex,
range.endTrIndex
)
// 重新渲染&重设状态
draw.render({
isSetCursor: false
})
host.isAllowDrag = false
host.isAllowDrop = false
} else if (host.isAllowDrag) {
// 如果是允许拖拽不允许拖放则光标重置
host.mousedown(evt)
host.isAllowDrag = false
}
}

@ -1,7 +1,7 @@
import { ElementType } from '../enum/Element'
import { IElement } from '../../interface/Element'
export const EDITOR_ELEMENT_STYLE_ATTR = [
export const EDITOR_ELEMENT_STYLE_ATTR: Array<keyof IElement> = [
'bold',
'color',
'highlight',

Loading…
Cancel
Save