parent
2739d4069f
commit
6dae6084df
@ -0,0 +1,22 @@
|
|||||||
|
import { Draw } from "../draw/Draw"
|
||||||
|
|
||||||
|
export class Command {
|
||||||
|
|
||||||
|
private undo: Function
|
||||||
|
private redo: Function
|
||||||
|
|
||||||
|
constructor(draw: Draw) {
|
||||||
|
const historyManager = draw.getHistoryManager()
|
||||||
|
this.undo = historyManager.undo
|
||||||
|
this.redo = historyManager.redo
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeUndo() {
|
||||||
|
return this.undo
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeRedo() {
|
||||||
|
return this.redo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { Draw } from "../draw/Draw"
|
||||||
|
import { CanvasEvent } from "../event/CanvasEvent"
|
||||||
|
import { Position } from "../position/Position"
|
||||||
|
import { RangeManager } from "../range/RangeManager"
|
||||||
|
import { CursorAgent } from "./CursorAgent"
|
||||||
|
|
||||||
|
export class Cursor {
|
||||||
|
|
||||||
|
private canvas: HTMLCanvasElement
|
||||||
|
private draw: Draw
|
||||||
|
private range: RangeManager
|
||||||
|
private position: Position
|
||||||
|
private cursorDom: HTMLDivElement
|
||||||
|
private cursorAgent: CursorAgent
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, draw: Draw, canvasEvent: CanvasEvent) {
|
||||||
|
this.canvas = canvas
|
||||||
|
this.draw = draw
|
||||||
|
this.range = this.draw.getRange()
|
||||||
|
this.position = this.draw.getPosition()
|
||||||
|
|
||||||
|
this.cursorDom = document.createElement('div')
|
||||||
|
this.cursorDom.classList.add('cursor')
|
||||||
|
this.canvas.parentNode?.append(this.cursorDom)
|
||||||
|
this.cursorAgent = new CursorAgent(canvas, canvasEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCursorDom(): HTMLDivElement {
|
||||||
|
return this.cursorDom
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAgentDom(): HTMLTextAreaElement {
|
||||||
|
return this.cursorAgent.getAgentCursorDom()
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCursorPosition(evt: MouseEvent) {
|
||||||
|
const positionIndex = this.position.getPositionByXY(evt.offsetX, evt.offsetY)
|
||||||
|
if (~positionIndex) {
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.draw.render({ curIndex: positionIndex, isSubmitHistory: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public drawCursor() {
|
||||||
|
const cursorPosition = this.draw.getPosition().getCursorPosition()
|
||||||
|
if (!cursorPosition) return
|
||||||
|
// 设置光标代理
|
||||||
|
const { lineHeight, metrics, coordinate: { rightTop } } = cursorPosition
|
||||||
|
const height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
|
||||||
|
const agentCursorDom = this.cursorAgent.getAgentCursorDom()
|
||||||
|
agentCursorDom.focus()
|
||||||
|
agentCursorDom.setSelectionRange(0, 0)
|
||||||
|
const lineBottom = rightTop[1] + lineHeight
|
||||||
|
const curosrleft = `${rightTop[0]}px`
|
||||||
|
agentCursorDom.style.left = curosrleft
|
||||||
|
agentCursorDom.style.top = `${lineBottom - 12}px`
|
||||||
|
// 模拟光标显示
|
||||||
|
this.cursorDom.style.left = curosrleft
|
||||||
|
this.cursorDom.style.top = `${lineBottom - height}px`
|
||||||
|
this.cursorDom.style.display = 'block'
|
||||||
|
this.cursorDom.style.height = `${height}px`
|
||||||
|
setTimeout(() => {
|
||||||
|
this.cursorDom.classList.add('cursor--animation')
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
public recoveryCursor() {
|
||||||
|
this.cursorDom.style.display = 'none'
|
||||||
|
this.cursorDom.classList.remove('cursor--animation')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { debounce } from "../../utils"
|
||||||
|
import { CanvasEvent } from "../event/CanvasEvent"
|
||||||
|
|
||||||
|
export class CursorAgent {
|
||||||
|
|
||||||
|
private canvas: HTMLCanvasElement
|
||||||
|
private agentCursorDom: HTMLTextAreaElement
|
||||||
|
private canvasEvent: CanvasEvent
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, canvasEvent: CanvasEvent) {
|
||||||
|
this.canvas = canvas
|
||||||
|
this.canvasEvent = canvasEvent
|
||||||
|
// 代理光标绘制
|
||||||
|
const agentCursorDom = document.createElement('textarea')
|
||||||
|
agentCursorDom.autocomplete = 'off'
|
||||||
|
agentCursorDom.classList.add('inputarea')
|
||||||
|
agentCursorDom.innerText = ''
|
||||||
|
this.canvas.parentNode?.append(agentCursorDom)
|
||||||
|
this.agentCursorDom = agentCursorDom
|
||||||
|
// 事件
|
||||||
|
agentCursorDom.onkeydown = (evt: KeyboardEvent) => this.keyDown(evt)
|
||||||
|
agentCursorDom.oninput = debounce(this.input.bind(this), 0)
|
||||||
|
agentCursorDom.onpaste = (evt: ClipboardEvent) => this.paste(evt)
|
||||||
|
agentCursorDom.addEventListener('compositionstart', this.compositionstart.bind(this))
|
||||||
|
agentCursorDom.addEventListener('compositionend', this.compositionend.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAgentCursorDom(): HTMLTextAreaElement {
|
||||||
|
return this.agentCursorDom
|
||||||
|
}
|
||||||
|
|
||||||
|
keyDown(evt: KeyboardEvent) {
|
||||||
|
this.canvasEvent.keydown(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
input(evt: InputEvent) {
|
||||||
|
if (!evt.data) return
|
||||||
|
this.canvasEvent.input(evt.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
paste(evt: ClipboardEvent) {
|
||||||
|
this.canvasEvent.paste(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
compositionstart() {
|
||||||
|
this.canvasEvent.compositionstart()
|
||||||
|
}
|
||||||
|
|
||||||
|
compositionend() {
|
||||||
|
this.canvasEvent.compositionend()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
import { ZERO } from "../../dataset/constant/Common"
|
||||||
|
import { IDrawOption } from "../../interface/Draw"
|
||||||
|
import { IEditorOption } from "../../interface/Editor"
|
||||||
|
import { IElement, IElementPosition } from "../../interface/Element"
|
||||||
|
import { IRow } from "../../interface/Row"
|
||||||
|
import { deepClone } from "../../utils"
|
||||||
|
import { Cursor } from "../cursor/Cursor"
|
||||||
|
import { CanvasEvent } from "../event/CanvasEvent"
|
||||||
|
import { GlobalEvent } from "../event/GlobalEvent"
|
||||||
|
import { HistoryManager } from "../history/HistoryManager"
|
||||||
|
import { Position } from "../position/Position"
|
||||||
|
import { RangeManager } from "../range/RangeManager"
|
||||||
|
import { Margin } from "./Margin"
|
||||||
|
|
||||||
|
export class Draw {
|
||||||
|
|
||||||
|
private canvas: HTMLCanvasElement
|
||||||
|
private ctx: CanvasRenderingContext2D
|
||||||
|
private options: Required<IEditorOption>
|
||||||
|
private position: Position
|
||||||
|
private elementList: IElement[]
|
||||||
|
|
||||||
|
private cursor: Cursor
|
||||||
|
private range: RangeManager
|
||||||
|
private margin: Margin
|
||||||
|
private historyManager: HistoryManager
|
||||||
|
|
||||||
|
private rowCount: number
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, options: Required<IEditorOption>, elementList: IElement[]) {
|
||||||
|
this.canvas = canvas
|
||||||
|
this.ctx = ctx
|
||||||
|
this.options = options
|
||||||
|
this.elementList = elementList
|
||||||
|
|
||||||
|
this.historyManager = new HistoryManager()
|
||||||
|
this.position = new Position(this)
|
||||||
|
this.range = new RangeManager(ctx, options)
|
||||||
|
this.margin = new Margin(ctx, options)
|
||||||
|
|
||||||
|
const canvasEvent = new CanvasEvent(canvas, this)
|
||||||
|
this.cursor = new Cursor(canvas, this, canvasEvent)
|
||||||
|
canvasEvent.register()
|
||||||
|
const globalEvent = new GlobalEvent(canvas, this, canvasEvent)
|
||||||
|
globalEvent.register()
|
||||||
|
|
||||||
|
this.rowCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHistoryManager(): HistoryManager {
|
||||||
|
return this.historyManager
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPosition(): Position {
|
||||||
|
return this.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRange(): RangeManager {
|
||||||
|
return this.range
|
||||||
|
}
|
||||||
|
|
||||||
|
public getElementList(): IElement[] {
|
||||||
|
return this.elementList
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCursor(): Cursor {
|
||||||
|
return this.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRowCount(): number {
|
||||||
|
return this.rowCount
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(payload?: IDrawOption) {
|
||||||
|
let { curIndex, isSubmitHistory = true, isSetCursor = true } = payload || {}
|
||||||
|
// 清除光标
|
||||||
|
this.cursor.recoveryCursor()
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
this.position.setPositionList([])
|
||||||
|
const positionList = this.position.getPositionList()
|
||||||
|
// 基础信息
|
||||||
|
const { defaultSize, defaultFont } = this.options
|
||||||
|
const canvasRect = this.canvas.getBoundingClientRect()
|
||||||
|
// 绘制页边距
|
||||||
|
const { width } = canvasRect
|
||||||
|
const { margins } = this.options
|
||||||
|
const leftTopPoint: [number, number] = [margins[3], margins[0]]
|
||||||
|
const rightTopPoint: [number, number] = [width - margins[1], margins[0]]
|
||||||
|
this.margin.render(canvasRect)
|
||||||
|
// 计算行信息
|
||||||
|
const rowList: IRow[] = []
|
||||||
|
if (this.elementList.length) {
|
||||||
|
rowList.push({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
ascent: 0,
|
||||||
|
elementList: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.elementList.length; i++) {
|
||||||
|
this.ctx.save()
|
||||||
|
const curRow: IRow = rowList[rowList.length - 1]
|
||||||
|
const element = this.elementList[i]
|
||||||
|
this.ctx.font = `${element.bold ? 'bold ' : ''}${element.size || defaultSize}px ${element.font || defaultFont}`
|
||||||
|
const metrics = this.ctx.measureText(element.value)
|
||||||
|
const width = metrics.width
|
||||||
|
const fontBoundingBoxAscent = metrics.fontBoundingBoxAscent
|
||||||
|
const fontBoundingBoxDescent = metrics.fontBoundingBoxDescent
|
||||||
|
const height = fontBoundingBoxAscent + fontBoundingBoxDescent
|
||||||
|
const lineText = { ...element, metrics }
|
||||||
|
if (curRow.width + width > rightTopPoint[0] - leftTopPoint[0] || (i !== 0 && element.value === ZERO)) {
|
||||||
|
rowList.push({
|
||||||
|
width,
|
||||||
|
height: 0,
|
||||||
|
elementList: [lineText],
|
||||||
|
ascent: fontBoundingBoxAscent
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
curRow.width += width
|
||||||
|
if (curRow.height < height) {
|
||||||
|
curRow.height = height
|
||||||
|
curRow.ascent = fontBoundingBoxAscent
|
||||||
|
}
|
||||||
|
curRow.elementList.push(lineText)
|
||||||
|
}
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
// 渲染元素
|
||||||
|
let x = leftTopPoint[0]
|
||||||
|
let y = leftTopPoint[1]
|
||||||
|
let index = 0
|
||||||
|
for (let i = 0; i < rowList.length; i++) {
|
||||||
|
const curRow = rowList[i];
|
||||||
|
for (let j = 0; j < curRow.elementList.length; j++) {
|
||||||
|
this.ctx.save()
|
||||||
|
const element = curRow.elementList[j]
|
||||||
|
const metrics = element.metrics
|
||||||
|
this.ctx.font = `${element.bold ? 'bold ' : ''}${element.size || defaultSize}px ${element.font || defaultFont}`
|
||||||
|
if (element.color) {
|
||||||
|
this.ctx.fillStyle = element.color
|
||||||
|
}
|
||||||
|
const positionItem: IElementPosition = {
|
||||||
|
index,
|
||||||
|
value: element.value,
|
||||||
|
rowNo: i,
|
||||||
|
metrics,
|
||||||
|
lineHeight: curRow.height,
|
||||||
|
isLastLetter: j === curRow.elementList.length - 1,
|
||||||
|
coordinate: {
|
||||||
|
leftTop: [x, y],
|
||||||
|
leftBottom: [x, y + curRow.height],
|
||||||
|
rightTop: [x + metrics.width, y],
|
||||||
|
rightBottom: [x + metrics.width, y + curRow.height]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positionList.push(positionItem)
|
||||||
|
this.ctx.fillText(element.value, x, y + curRow.ascent)
|
||||||
|
// 选区绘制
|
||||||
|
const { startIndex, endIndex } = this.range.getRange()
|
||||||
|
if (startIndex < index && index <= endIndex) {
|
||||||
|
this.range.drawRange(x, y, metrics.width, curRow.height)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
x += metrics.width
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
x = leftTopPoint[0]
|
||||||
|
y += curRow.height
|
||||||
|
}
|
||||||
|
// 光标重绘
|
||||||
|
if (curIndex === undefined) {
|
||||||
|
curIndex = positionList.length - 1
|
||||||
|
}
|
||||||
|
if (isSetCursor) {
|
||||||
|
this.position.setCursorPosition(positionList[curIndex!] || null)
|
||||||
|
this.cursor.drawCursor()
|
||||||
|
}
|
||||||
|
// canvas高度自适应计算
|
||||||
|
const lastPosition = positionList[positionList.length - 1]
|
||||||
|
const { coordinate: { leftBottom, leftTop } } = lastPosition
|
||||||
|
if (leftBottom[1] > this.canvas.height) {
|
||||||
|
const height = Math.ceil(leftBottom[1] + (leftBottom[1] - leftTop[1]))
|
||||||
|
this.canvas.height = height
|
||||||
|
this.canvas.style.height = `${height}px`
|
||||||
|
this.render({ curIndex, isSubmitHistory: false })
|
||||||
|
}
|
||||||
|
this.rowCount = rowList.length
|
||||||
|
// 历史记录用于undo、redo
|
||||||
|
if (isSubmitHistory) {
|
||||||
|
const self = this
|
||||||
|
const oldElementList = deepClone(this.elementList)
|
||||||
|
this.historyManager.execute(function () {
|
||||||
|
self.elementList = deepClone(oldElementList)
|
||||||
|
self.render({ curIndex, isSubmitHistory: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { IEditorOption } from "../../interface/Editor"
|
||||||
|
|
||||||
|
export class Margin {
|
||||||
|
|
||||||
|
private ctx: CanvasRenderingContext2D
|
||||||
|
private options: Required<IEditorOption>
|
||||||
|
|
||||||
|
constructor(ctx: CanvasRenderingContext2D, options: Required<IEditorOption>) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
render(canvasRect: DOMRect) {
|
||||||
|
const { width, height } = canvasRect
|
||||||
|
const { marginIndicatorColor, marginIndicatorSize, margins } = this.options
|
||||||
|
this.ctx.save()
|
||||||
|
this.ctx.strokeStyle = marginIndicatorColor
|
||||||
|
this.ctx.beginPath()
|
||||||
|
const leftTopPoint: [number, number] = [margins[3], margins[0]]
|
||||||
|
const rightTopPoint: [number, number] = [width - margins[1], margins[0]]
|
||||||
|
const leftBottomPoint: [number, number] = [margins[3], height - margins[2]]
|
||||||
|
const rightBottomPoint: [number, number] = [width - margins[1], height - margins[2]]
|
||||||
|
// 上左
|
||||||
|
this.ctx.moveTo(leftTopPoint[0] - marginIndicatorSize, leftTopPoint[1])
|
||||||
|
this.ctx.lineTo(...leftTopPoint)
|
||||||
|
this.ctx.lineTo(leftTopPoint[0], leftTopPoint[1] - marginIndicatorSize)
|
||||||
|
// 上右
|
||||||
|
this.ctx.moveTo(rightTopPoint[0] + marginIndicatorSize, rightTopPoint[1])
|
||||||
|
this.ctx.lineTo(...rightTopPoint)
|
||||||
|
this.ctx.lineTo(rightTopPoint[0], rightTopPoint[1] - marginIndicatorSize)
|
||||||
|
// 下左
|
||||||
|
this.ctx.moveTo(leftBottomPoint[0] - marginIndicatorSize, leftBottomPoint[1])
|
||||||
|
this.ctx.lineTo(...leftBottomPoint)
|
||||||
|
this.ctx.lineTo(leftBottomPoint[0], leftBottomPoint[1] + marginIndicatorSize)
|
||||||
|
// 下右
|
||||||
|
this.ctx.moveTo(rightBottomPoint[0] + marginIndicatorSize, rightBottomPoint[1])
|
||||||
|
this.ctx.lineTo(...rightBottomPoint)
|
||||||
|
this.ctx.lineTo(rightBottomPoint[0], rightBottomPoint[1] + marginIndicatorSize)
|
||||||
|
this.ctx.stroke()
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,218 @@
|
|||||||
|
import { ZERO } from "../../dataset/constant/Common"
|
||||||
|
import { KeyMap } from "../../dataset/enum/Keymap"
|
||||||
|
import { IElement } from "../../interface/Element"
|
||||||
|
import { writeText } from "../../utils"
|
||||||
|
import { Cursor } from "../cursor/Cursor"
|
||||||
|
import { Draw } from "../draw/Draw"
|
||||||
|
import { HistoryManager } from "../history/HistoryManager"
|
||||||
|
import { Position } from "../position/Position"
|
||||||
|
import { RangeManager } from "../range/RangeManager"
|
||||||
|
|
||||||
|
export class CanvasEvent {
|
||||||
|
|
||||||
|
private isAllowDrag: boolean
|
||||||
|
private isCompositing: boolean
|
||||||
|
private mouseDownStartIndex: number
|
||||||
|
|
||||||
|
private draw: Draw
|
||||||
|
private canvas: HTMLCanvasElement
|
||||||
|
private position: Position
|
||||||
|
private range: RangeManager
|
||||||
|
private cursor: Cursor | null
|
||||||
|
private historyManager: HistoryManager
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, draw: Draw) {
|
||||||
|
this.isAllowDrag = false
|
||||||
|
this.isCompositing = false
|
||||||
|
this.mouseDownStartIndex = 0
|
||||||
|
|
||||||
|
this.canvas = canvas
|
||||||
|
this.draw = draw
|
||||||
|
this.cursor = null
|
||||||
|
this.position = this.draw.getPosition()
|
||||||
|
this.range = this.draw.getRange()
|
||||||
|
this.historyManager = this.draw.getHistoryManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
public register() {
|
||||||
|
// 延迟加载
|
||||||
|
this.cursor = this.draw.getCursor()
|
||||||
|
this.canvas.addEventListener('mousedown', this.cursor.setCursorPosition.bind(this))
|
||||||
|
this.canvas.addEventListener('mousedown', this.mousedown.bind(this))
|
||||||
|
this.canvas.addEventListener('mouseleave', this.mouseleave.bind(this))
|
||||||
|
this.canvas.addEventListener('mousemove', this.mousemove.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
public setIsAllowDrag(payload: boolean) {
|
||||||
|
this.isAllowDrag = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
public mousemove(evt: MouseEvent) {
|
||||||
|
if (!this.isAllowDrag) return
|
||||||
|
// 结束位置
|
||||||
|
const endIndex = this.draw.getPosition().getPositionByXY(evt.offsetX, evt.offsetY)
|
||||||
|
let end = ~endIndex ? endIndex : 0
|
||||||
|
// 开始位置
|
||||||
|
let start = this.mouseDownStartIndex
|
||||||
|
if (start > end) {
|
||||||
|
[start, end] = [end, start]
|
||||||
|
}
|
||||||
|
this.draw.getRange().setRange(start, end)
|
||||||
|
if (start === end) return
|
||||||
|
// 绘制
|
||||||
|
this.draw.render({
|
||||||
|
isSubmitHistory: false,
|
||||||
|
isSetCursor: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public mousedown(evt: MouseEvent) {
|
||||||
|
this.isAllowDrag = true
|
||||||
|
this.mouseDownStartIndex = this.draw.getPosition().getPositionByXY(evt.offsetX, evt.offsetY) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public mouseleave(evt: MouseEvent) {
|
||||||
|
// 是否还在canvas内部
|
||||||
|
const { x, y, width, height } = this.canvas.getBoundingClientRect()
|
||||||
|
if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) return
|
||||||
|
this.isAllowDrag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public keydown(evt: KeyboardEvent) {
|
||||||
|
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 isCollspace = startIndex === endIndex
|
||||||
|
if (evt.key === KeyMap.Backspace) {
|
||||||
|
// 判断是否允许删除
|
||||||
|
if (elementList[index].value === ZERO && index === 0) {
|
||||||
|
evt.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isCollspace) {
|
||||||
|
elementList.splice(startIndex + 1, endIndex - startIndex)
|
||||||
|
} else {
|
||||||
|
elementList.splice(index, 1)
|
||||||
|
}
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: isCollspace ? index - 1 : startIndex })
|
||||||
|
} else if (evt.key === KeyMap.Enter) {
|
||||||
|
const enterText: IElement = {
|
||||||
|
value: ZERO
|
||||||
|
}
|
||||||
|
if (isCollspace) {
|
||||||
|
elementList.splice(index + 1, 0, enterText)
|
||||||
|
} else {
|
||||||
|
elementList.splice(startIndex + 1, endIndex - startIndex, enterText)
|
||||||
|
}
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: index + 1 })
|
||||||
|
} else if (evt.key === KeyMap.Left) {
|
||||||
|
if (index > 0) {
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: index - 1, isSubmitHistory: false })
|
||||||
|
}
|
||||||
|
} else if (evt.key === KeyMap.Right) {
|
||||||
|
if (index < position.length - 1) {
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: index + 1, isSubmitHistory: false })
|
||||||
|
}
|
||||||
|
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: maxIndex, isSubmitHistory: false })
|
||||||
|
}
|
||||||
|
} else if (evt.ctrlKey && evt.key === KeyMap.Z) {
|
||||||
|
this.historyManager.undo()
|
||||||
|
evt.preventDefault()
|
||||||
|
} else if (evt.ctrlKey && evt.key === KeyMap.Y) {
|
||||||
|
this.historyManager.redo()
|
||||||
|
evt.preventDefault()
|
||||||
|
} else if (evt.ctrlKey && evt.key === KeyMap.C) {
|
||||||
|
if (!isCollspace) {
|
||||||
|
writeText(elementList.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
|
||||||
|
}
|
||||||
|
} else if (evt.ctrlKey && evt.key === KeyMap.X) {
|
||||||
|
if (!isCollspace) {
|
||||||
|
writeText(position.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
|
||||||
|
elementList.splice(startIndex + 1, endIndex - startIndex)
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: startIndex })
|
||||||
|
}
|
||||||
|
} else if (evt.ctrlKey && evt.key === KeyMap.A) {
|
||||||
|
this.range.setRange(0, position.length - 1)
|
||||||
|
this.draw.render({ isSubmitHistory: false, isSetCursor: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public input(data: string) {
|
||||||
|
if (!this.cursor) return
|
||||||
|
const cursorPosition = this.position.getCursorPosition()
|
||||||
|
if (!data || !cursorPosition || this.isCompositing) return
|
||||||
|
const elementList = this.draw.getElementList()
|
||||||
|
const agentDom = this.cursor.getAgentDom()
|
||||||
|
agentDom.value = ''
|
||||||
|
const { index } = cursorPosition
|
||||||
|
const { startIndex, endIndex } = this.range.getRange()
|
||||||
|
const isCollspace = startIndex === endIndex
|
||||||
|
const inputData: IElement[] = data.split('').map(value => ({
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
if (isCollspace) {
|
||||||
|
elementList.splice(index + 1, 0, ...inputData)
|
||||||
|
} else {
|
||||||
|
elementList.splice(startIndex + 1, endIndex - startIndex, ...inputData)
|
||||||
|
}
|
||||||
|
this.range.setRange(0, 0)
|
||||||
|
this.draw.render({ curIndex: (isCollspace ? index : startIndex) + inputData.length })
|
||||||
|
}
|
||||||
|
|
||||||
|
public paste(evt: ClipboardEvent) {
|
||||||
|
const text = evt.clipboardData?.getData('text')
|
||||||
|
this.input(text || '')
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
public compositionstart() {
|
||||||
|
this.isCompositing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public compositionend() {
|
||||||
|
this.isCompositing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { Cursor } from "../cursor/Cursor"
|
||||||
|
import { Draw } from "../draw/Draw"
|
||||||
|
import { CanvasEvent } from "./CanvasEvent"
|
||||||
|
|
||||||
|
export class GlobalEvent {
|
||||||
|
|
||||||
|
private canvas: HTMLCanvasElement
|
||||||
|
private draw: Draw
|
||||||
|
private cursor: Cursor | null
|
||||||
|
private canvasEvent: CanvasEvent
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement, draw: Draw, canvasEvent: CanvasEvent) {
|
||||||
|
this.canvas = canvas
|
||||||
|
this.draw = draw
|
||||||
|
this.canvasEvent = canvasEvent
|
||||||
|
this.cursor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
register() {
|
||||||
|
this.cursor = this.draw.getCursor()
|
||||||
|
|
||||||
|
document.addEventListener('click', (evt) => {
|
||||||
|
this.recoverCursor(evt)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
this.updateDragState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
recoverCursor(evt: MouseEvent) {
|
||||||
|
if (!this.cursor) return
|
||||||
|
const cursorDom = this.cursor.getCursorDom()
|
||||||
|
const agentDom = this.cursor.getAgentDom()
|
||||||
|
const innerDoms = [this.canvas, cursorDom, agentDom, this.canvas.parentNode, document.body]
|
||||||
|
if (innerDoms.includes(evt.target as any)) return
|
||||||
|
this.cursor.recoveryCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDragState() {
|
||||||
|
this.canvasEvent.setIsAllowDrag(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { ZERO } from "../../dataset/constant/Common"
|
||||||
|
import { IElement, IElementPosition } from "../../interface/Element"
|
||||||
|
import { Draw } from "../draw/Draw"
|
||||||
|
|
||||||
|
export class Position {
|
||||||
|
|
||||||
|
private cursorPosition: IElementPosition | null
|
||||||
|
private positionList: IElementPosition[]
|
||||||
|
private elementList: IElement[]
|
||||||
|
|
||||||
|
private draw: Draw
|
||||||
|
|
||||||
|
constructor(draw: Draw) {
|
||||||
|
this.positionList = []
|
||||||
|
this.elementList = []
|
||||||
|
this.cursorPosition = null
|
||||||
|
|
||||||
|
this.draw = draw
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPositionList(): IElementPosition[] {
|
||||||
|
return this.positionList
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPositionList(payload: IElementPosition[]) {
|
||||||
|
this.positionList = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCursorPosition(position: IElementPosition) {
|
||||||
|
this.cursorPosition = position
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCursorPosition(): IElementPosition | null {
|
||||||
|
return this.cursorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPositionByXY(x: number, y: number): number {
|
||||||
|
this.elementList = this.draw.getElementList()
|
||||||
|
let isTextArea = false
|
||||||
|
for (let j = 0; j < this.positionList.length; j++) {
|
||||||
|
const { index, coordinate: { leftTop, rightTop, leftBottom } } = this.positionList[j];
|
||||||
|
// 命中元素
|
||||||
|
if (leftTop[0] <= x && rightTop[0] >= x && leftTop[1] <= y && leftBottom[1] >= y) {
|
||||||
|
let curPostionIndex = j
|
||||||
|
// 判断是否元素中间前后
|
||||||
|
if (this.elementList[index].value !== ZERO) {
|
||||||
|
const valueWidth = rightTop[0] - leftTop[0]
|
||||||
|
if (x < leftTop[0] + valueWidth / 2) {
|
||||||
|
curPostionIndex = j - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isTextArea = true
|
||||||
|
return curPostionIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 非命中区域
|
||||||
|
if (!isTextArea) {
|
||||||
|
let isLastArea = false
|
||||||
|
let curPostionIndex = -1
|
||||||
|
// 判断所属行是否存在元素
|
||||||
|
const firstLetterList = this.positionList.filter(p => p.isLastLetter)
|
||||||
|
for (let j = 0; j < firstLetterList.length; j++) {
|
||||||
|
const { index, coordinate: { leftTop, leftBottom } } = firstLetterList[j]
|
||||||
|
if (y > leftTop[1] && y <= leftBottom[1]) {
|
||||||
|
curPostionIndex = index
|
||||||
|
isLastArea = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isLastArea) {
|
||||||
|
return this.positionList.length - 1
|
||||||
|
}
|
||||||
|
return curPostionIndex
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { IEditorOption } from "../../interface/Editor"
|
||||||
|
import { IRange } from "../../interface/Range"
|
||||||
|
|
||||||
|
export class RangeManager {
|
||||||
|
|
||||||
|
private ctx: CanvasRenderingContext2D
|
||||||
|
private options: Required<IEditorOption>
|
||||||
|
private range: IRange
|
||||||
|
|
||||||
|
constructor(ctx: CanvasRenderingContext2D, options: Required<IEditorOption>,) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.options = options
|
||||||
|
this.range = {
|
||||||
|
startIndex: 0,
|
||||||
|
endIndex: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRange(): IRange {
|
||||||
|
return this.range
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRange(startIndex: number, endIndex: number) {
|
||||||
|
this.range.startIndex = startIndex
|
||||||
|
this.range.endIndex = endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
public drawRange(x: number, y: number, width: number, height: number) {
|
||||||
|
const { startIndex, endIndex } = this.range
|
||||||
|
if (startIndex !== endIndex) {
|
||||||
|
this.ctx.save()
|
||||||
|
this.ctx.globalAlpha = this.options.rangeAlpha
|
||||||
|
this.ctx.fillStyle = this.options.rangeColor
|
||||||
|
this.ctx.fillRect(x, y, width, height)
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,525 +1,47 @@
|
|||||||
import './assets/css/index.css'
|
import './assets/css/index.css'
|
||||||
import { ZERO } from './dataset/constant/Common'
|
import { ZERO } from './dataset/constant/Common'
|
||||||
import { KeyMap } from './dataset/enum/Keymap'
|
|
||||||
import { deepClone, writeText } from './utils'
|
|
||||||
import { HistoryManager } from './core/history/HistoryManager'
|
|
||||||
import { IRange } from './interface/Range'
|
|
||||||
import { IRow } from './interface/Row'
|
|
||||||
import { IDrawOption } from './interface/Draw'
|
|
||||||
import { IEditorOption } from './interface/Editor'
|
import { IEditorOption } from './interface/Editor'
|
||||||
import { IElement, IElementPosition } from './interface/Element'
|
import { IElement } from './interface/Element'
|
||||||
|
import { Draw } from './core/draw/Draw'
|
||||||
|
import { Command } from './core/command/Command'
|
||||||
|
|
||||||
export default class Editor {
|
export default class Editor {
|
||||||
|
|
||||||
private readonly defaultOptions: Required<IEditorOption> = {
|
public command: Command
|
||||||
defaultType: 'TEXT',
|
|
||||||
defaultFont: 'Yahei',
|
constructor(canvas: HTMLCanvasElement, elementList: IElement[], options: IEditorOption = {}) {
|
||||||
defaultSize: 16,
|
const editorOptions: Required<IEditorOption> = {
|
||||||
rangeAlpha: 0.6,
|
defaultType: 'TEXT',
|
||||||
rangeColor: '#AECBFA',
|
defaultFont: 'Yahei',
|
||||||
marginIndicatorSize: 35,
|
defaultSize: 16,
|
||||||
marginIndicatorColor: '#BABABA',
|
rangeAlpha: 0.6,
|
||||||
margins: [100, 120, 100, 120]
|
rangeColor: '#AECBFA',
|
||||||
}
|
marginIndicatorSize: 35,
|
||||||
|
marginIndicatorColor: '#BABABA',
|
||||||
private canvas: HTMLCanvasElement
|
margins: [100, 120, 100, 120],
|
||||||
private ctx: CanvasRenderingContext2D
|
|
||||||
|
|
||||||
private options: Required<IEditorOption>
|
|
||||||
private elementList: IElement[]
|
|
||||||
private position: IElementPosition[]
|
|
||||||
private range: IRange
|
|
||||||
|
|
||||||
private cursorPosition: IElementPosition | null
|
|
||||||
private cursorDom: HTMLDivElement
|
|
||||||
private textareaDom: HTMLTextAreaElement
|
|
||||||
private isCompositing: boolean
|
|
||||||
private isAllowDrag: boolean
|
|
||||||
private rowCount: number
|
|
||||||
private mouseDownStartIndex: number
|
|
||||||
|
|
||||||
private historyManager: HistoryManager
|
|
||||||
|
|
||||||
constructor(canvas: HTMLCanvasElement, data: IElement[], options: IEditorOption = {}) {
|
|
||||||
this.options = {
|
|
||||||
...this.defaultOptions,
|
|
||||||
...options
|
...options
|
||||||
};
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
const dpr = window.devicePixelRatio;
|
|
||||||
canvas.width = parseInt(canvas.style.width) * dpr;
|
|
||||||
canvas.height = parseInt(canvas.style.height) * dpr;
|
|
||||||
canvas.style.cursor = 'text'
|
|
||||||
this.canvas = canvas
|
|
||||||
this.ctx = ctx as CanvasRenderingContext2D
|
|
||||||
this.ctx.scale(dpr, dpr)
|
|
||||||
this.elementList = []
|
|
||||||
this.position = []
|
|
||||||
this.cursorPosition = null
|
|
||||||
this.isCompositing = false
|
|
||||||
this.isAllowDrag = false
|
|
||||||
this.range = {
|
|
||||||
startIndex: 0,
|
|
||||||
endIndex: 0
|
|
||||||
}
|
}
|
||||||
this.rowCount = 0
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||||
this.mouseDownStartIndex = 0
|
const dpr = window.devicePixelRatio
|
||||||
|
canvas.width = parseInt(canvas.style.width) * dpr
|
||||||
// 历史管理
|
canvas.height = parseInt(canvas.style.height) * dpr
|
||||||
this.historyManager = new HistoryManager()
|
canvas.style.cursor = 'text'
|
||||||
|
ctx.scale(dpr, dpr)
|
||||||
// 全局事件
|
if (elementList[0].value !== ZERO) {
|
||||||
document.addEventListener('click', (evt) => {
|
elementList.unshift({
|
||||||
const innerDoms = [this.canvas, this.cursorDom, this.textareaDom, document.body]
|
|
||||||
if (innerDoms.includes(evt.target as any)) return
|
|
||||||
this.recoveryCursor()
|
|
||||||
})
|
|
||||||
document.addEventListener('mouseup', () => {
|
|
||||||
this.isAllowDrag = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 事件监听转发
|
|
||||||
const textarea = document.createElement('textarea')
|
|
||||||
textarea.autocomplete = 'off'
|
|
||||||
textarea.classList.add('inputarea')
|
|
||||||
textarea.innerText = ''
|
|
||||||
textarea.onkeydown = (evt: KeyboardEvent) => this.handleKeydown(evt)
|
|
||||||
textarea.oninput = (evt: Event) => {
|
|
||||||
const data = (evt as InputEvent).data
|
|
||||||
setTimeout(() => this.handleInput(data || ''))
|
|
||||||
}
|
|
||||||
textarea.onpaste = (evt: ClipboardEvent) => this.handlePaste(evt)
|
|
||||||
textarea.addEventListener('compositionstart', this.handleCompositionstart.bind(this))
|
|
||||||
textarea.addEventListener('compositionend', this.handleCompositionend.bind(this))
|
|
||||||
this.canvas.parentNode?.append(textarea)
|
|
||||||
this.textareaDom = textarea
|
|
||||||
|
|
||||||
// 光标
|
|
||||||
this.cursorDom = document.createElement('div')
|
|
||||||
this.cursorDom.classList.add('cursor')
|
|
||||||
this.canvas.parentNode?.append(this.cursorDom)
|
|
||||||
|
|
||||||
// canvas原生事件
|
|
||||||
canvas.addEventListener('mousedown', this.setCursor.bind(this))
|
|
||||||
canvas.addEventListener('mousedown', this.handleMousedown.bind(this))
|
|
||||||
canvas.addEventListener('mouseleave', this.handleMouseleave.bind(this))
|
|
||||||
canvas.addEventListener('mousemove', this.handleMousemove.bind(this))
|
|
||||||
|
|
||||||
// 启动
|
|
||||||
const isZeroStart = data[0].value === ZERO
|
|
||||||
if (!isZeroStart) {
|
|
||||||
data.unshift({
|
|
||||||
value: ZERO
|
value: ZERO
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data.forEach(text => {
|
elementList.forEach(text => {
|
||||||
if (text.value === '\n') {
|
if (text.value === '\n') {
|
||||||
text.value = ZERO
|
text.value = ZERO
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.elementList = data
|
// 启动
|
||||||
this.draw()
|
const draw = new Draw(canvas, ctx, editorOptions, elementList)
|
||||||
}
|
draw.render()
|
||||||
|
// 对外命令
|
||||||
private draw(options?: IDrawOption) {
|
this.command = new Command(draw)
|
||||||
let { curIndex, isSubmitHistory = true, isSetCursor = true } = options || {}
|
|
||||||
// 清除光标
|
|
||||||
this.recoveryCursor()
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
||||||
this.position = []
|
|
||||||
// 基础信息
|
|
||||||
const { defaultSize, defaultFont, margins, marginIndicatorColor, marginIndicatorSize } = this.options
|
|
||||||
const canvasRect = this.canvas.getBoundingClientRect()
|
|
||||||
const canvasWidth = canvasRect.width
|
|
||||||
const canvasHeight = canvasRect.height
|
|
||||||
// 绘制页边距
|
|
||||||
this.ctx.save()
|
|
||||||
this.ctx.strokeStyle = marginIndicatorColor
|
|
||||||
this.ctx.beginPath()
|
|
||||||
const leftTopPoint: [number, number] = [margins[3], margins[0]]
|
|
||||||
const rightTopPoint: [number, number] = [canvasWidth - margins[1], margins[0]]
|
|
||||||
const leftBottomPoint: [number, number] = [margins[3], canvasHeight - margins[2]]
|
|
||||||
const rightBottomPoint: [number, number] = [canvasWidth - margins[1], canvasHeight - margins[2]]
|
|
||||||
// 上左
|
|
||||||
this.ctx.moveTo(leftTopPoint[0] - marginIndicatorSize, leftTopPoint[1])
|
|
||||||
this.ctx.lineTo(...leftTopPoint)
|
|
||||||
this.ctx.lineTo(leftTopPoint[0], leftTopPoint[1] - marginIndicatorSize)
|
|
||||||
// 上右
|
|
||||||
this.ctx.moveTo(rightTopPoint[0] + marginIndicatorSize, rightTopPoint[1])
|
|
||||||
this.ctx.lineTo(...rightTopPoint)
|
|
||||||
this.ctx.lineTo(rightTopPoint[0], rightTopPoint[1] - marginIndicatorSize)
|
|
||||||
// 下左
|
|
||||||
this.ctx.moveTo(leftBottomPoint[0] - marginIndicatorSize, leftBottomPoint[1])
|
|
||||||
this.ctx.lineTo(...leftBottomPoint)
|
|
||||||
this.ctx.lineTo(leftBottomPoint[0], leftBottomPoint[1] + marginIndicatorSize)
|
|
||||||
// 下右
|
|
||||||
this.ctx.moveTo(rightBottomPoint[0] + marginIndicatorSize, rightBottomPoint[1])
|
|
||||||
this.ctx.lineTo(...rightBottomPoint)
|
|
||||||
this.ctx.lineTo(rightBottomPoint[0], rightBottomPoint[1] + marginIndicatorSize)
|
|
||||||
this.ctx.stroke()
|
|
||||||
this.ctx.restore()
|
|
||||||
// 计算行信息
|
|
||||||
const rowList: IRow[] = []
|
|
||||||
if (this.elementList.length) {
|
|
||||||
rowList.push({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
ascent: 0,
|
|
||||||
elementList: []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.elementList.length; i++) {
|
|
||||||
this.ctx.save()
|
|
||||||
const curRow: IRow = rowList[rowList.length - 1]
|
|
||||||
const element = this.elementList[i]
|
|
||||||
this.ctx.font = `${element.bold ? 'bold ' : ''}${element.size || defaultSize}px ${element.font || defaultFont}`
|
|
||||||
const metrics = this.ctx.measureText(element.value)
|
|
||||||
const width = metrics.width
|
|
||||||
const fontBoundingBoxAscent = metrics.fontBoundingBoxAscent
|
|
||||||
const fontBoundingBoxDescent = metrics.fontBoundingBoxDescent
|
|
||||||
const height = fontBoundingBoxAscent + fontBoundingBoxDescent
|
|
||||||
const lineText = { ...element, metrics }
|
|
||||||
if (curRow.width + width > rightTopPoint[0] - leftTopPoint[0] || (i !== 0 && element.value === ZERO)) {
|
|
||||||
rowList.push({
|
|
||||||
width,
|
|
||||||
height: 0,
|
|
||||||
elementList: [lineText],
|
|
||||||
ascent: fontBoundingBoxAscent
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
curRow.width += width
|
|
||||||
if (curRow.height < height) {
|
|
||||||
curRow.height = height
|
|
||||||
curRow.ascent = fontBoundingBoxAscent
|
|
||||||
}
|
|
||||||
curRow.elementList.push(lineText)
|
|
||||||
}
|
|
||||||
this.ctx.restore()
|
|
||||||
}
|
|
||||||
// 渲染元素
|
|
||||||
let x = leftTopPoint[0]
|
|
||||||
let y = leftTopPoint[1]
|
|
||||||
let index = 0
|
|
||||||
for (let i = 0; i < rowList.length; i++) {
|
|
||||||
const curRow = rowList[i];
|
|
||||||
for (let j = 0; j < curRow.elementList.length; j++) {
|
|
||||||
this.ctx.save()
|
|
||||||
const element = curRow.elementList[j];
|
|
||||||
const metrics = element.metrics
|
|
||||||
this.ctx.font = `${element.bold ? 'bold ' : ''}${element.size || defaultSize}px ${element.font || defaultFont}`
|
|
||||||
if (element.color) {
|
|
||||||
this.ctx.fillStyle = element.color
|
|
||||||
}
|
|
||||||
const positionItem: IElementPosition = {
|
|
||||||
index,
|
|
||||||
value: element.value,
|
|
||||||
rowNo: i,
|
|
||||||
metrics,
|
|
||||||
lineHeight: curRow.height,
|
|
||||||
isLastLetter: j === curRow.elementList.length - 1,
|
|
||||||
coordinate: {
|
|
||||||
leftTop: [x, y],
|
|
||||||
leftBottom: [x, y + curRow.height],
|
|
||||||
rightTop: [x + metrics.width, y],
|
|
||||||
rightBottom: [x + metrics.width, y + curRow.height]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.position.push(positionItem)
|
|
||||||
this.ctx.fillText(element.value, x, y + curRow.ascent)
|
|
||||||
// 选区绘制
|
|
||||||
const { startIndex, endIndex } = this.range
|
|
||||||
if (startIndex !== endIndex && startIndex < index && index <= endIndex) {
|
|
||||||
this.ctx.save()
|
|
||||||
this.ctx.globalAlpha = this.options.rangeAlpha
|
|
||||||
this.ctx.fillStyle = this.options.rangeColor
|
|
||||||
this.ctx.fillRect(x, y, metrics.width, curRow.height)
|
|
||||||
this.ctx.restore()
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
x += metrics.width
|
|
||||||
this.ctx.restore()
|
|
||||||
}
|
|
||||||
x = leftTopPoint[0]
|
|
||||||
y += curRow.height
|
|
||||||
}
|
|
||||||
// 光标重绘
|
|
||||||
if (curIndex === undefined) {
|
|
||||||
curIndex = this.position.length - 1
|
|
||||||
}
|
|
||||||
if (isSetCursor) {
|
|
||||||
this.cursorPosition = this.position[curIndex!] || null
|
|
||||||
this.drawCursor()
|
|
||||||
}
|
|
||||||
// canvas高度自适应计算
|
|
||||||
const lastPosition = this.position[this.position.length - 1]
|
|
||||||
const { coordinate: { leftBottom, leftTop } } = lastPosition
|
|
||||||
if (leftBottom[1] > this.canvas.height) {
|
|
||||||
const height = Math.ceil(leftBottom[1] + (leftBottom[1] - leftTop[1]))
|
|
||||||
this.canvas.height = height
|
|
||||||
this.canvas.style.height = `${height}px`
|
|
||||||
this.draw({ curIndex, isSubmitHistory: false })
|
|
||||||
}
|
|
||||||
this.rowCount = rowList.length
|
|
||||||
// 历史记录用于undo、redo
|
|
||||||
if (isSubmitHistory) {
|
|
||||||
const self = this
|
|
||||||
const oldelementList = deepClone(this.elementList)
|
|
||||||
this.historyManager.execute(function () {
|
|
||||||
self.elementList = deepClone(oldelementList)
|
|
||||||
self.draw({ curIndex, isSubmitHistory: false })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCursorPosition(evt: MouseEvent): number {
|
|
||||||
const x = evt.offsetX
|
|
||||||
const y = evt.offsetY
|
|
||||||
let isTextArea = false
|
|
||||||
for (let j = 0; j < this.position.length; j++) {
|
|
||||||
const { index, coordinate: { leftTop, rightTop, leftBottom } } = this.position[j];
|
|
||||||
// 命中元素
|
|
||||||
if (leftTop[0] <= x && rightTop[0] >= x && leftTop[1] <= y && leftBottom[1] >= y) {
|
|
||||||
let curPostionIndex = j
|
|
||||||
// 判断是否元素中间前后
|
|
||||||
if (this.elementList[index].value !== ZERO) {
|
|
||||||
const valueWidth = rightTop[0] - leftTop[0]
|
|
||||||
if (x < leftTop[0] + valueWidth / 2) {
|
|
||||||
curPostionIndex = j - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isTextArea = true
|
|
||||||
return curPostionIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 非命中区域
|
|
||||||
if (!isTextArea) {
|
|
||||||
let isLastArea = false
|
|
||||||
let curPostionIndex = -1
|
|
||||||
// 判断所属行是否存在元素
|
|
||||||
const firstLetterList = this.position.filter(p => p.isLastLetter)
|
|
||||||
for (let j = 0; j < firstLetterList.length; j++) {
|
|
||||||
const { index, coordinate: { leftTop, leftBottom } } = firstLetterList[j]
|
|
||||||
if (y > leftTop[1] && y <= leftBottom[1]) {
|
|
||||||
curPostionIndex = index
|
|
||||||
isLastArea = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isLastArea) {
|
|
||||||
return this.position.length - 1
|
|
||||||
}
|
|
||||||
return curPostionIndex
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private setCursor(evt: MouseEvent) {
|
|
||||||
const positionIndex = this.getCursorPosition(evt)
|
|
||||||
if (~positionIndex) {
|
|
||||||
this.range.startIndex = 0
|
|
||||||
this.range.endIndex = 0
|
|
||||||
setTimeout(() => {
|
|
||||||
this.draw({ curIndex: positionIndex, isSubmitHistory: false })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawCursor() {
|
|
||||||
if (!this.cursorPosition) return
|
|
||||||
// 设置光标代理
|
|
||||||
const { lineHeight, metrics, coordinate: { rightTop } } = this.cursorPosition
|
|
||||||
const height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
|
|
||||||
this.textareaDom.focus()
|
|
||||||
this.textareaDom.setSelectionRange(0, 0)
|
|
||||||
const lineBottom = rightTop[1] + lineHeight
|
|
||||||
const curosrleft = `${rightTop[0]}px`
|
|
||||||
this.textareaDom.style.left = curosrleft
|
|
||||||
this.textareaDom.style.top = `${lineBottom - 12}px`
|
|
||||||
// 模拟光标显示
|
|
||||||
this.cursorDom.style.left = curosrleft
|
|
||||||
this.cursorDom.style.top = `${lineBottom - height}px`
|
|
||||||
this.cursorDom.style.display = 'block'
|
|
||||||
this.cursorDom.style.height = `${height}px`
|
|
||||||
setTimeout(() => {
|
|
||||||
this.cursorDom.classList.add('cursor--animation')
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
private recoveryCursor() {
|
|
||||||
this.cursorDom.style.display = 'none'
|
|
||||||
this.cursorDom.classList.remove('cursor--animation')
|
|
||||||
}
|
|
||||||
|
|
||||||
private strokeRange() {
|
|
||||||
this.draw({
|
|
||||||
isSubmitHistory: false,
|
|
||||||
isSetCursor: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRange() {
|
|
||||||
this.range.startIndex = 0
|
|
||||||
this.range.endIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMousemove(evt: MouseEvent) {
|
|
||||||
if (!this.isAllowDrag) return
|
|
||||||
// 结束位置
|
|
||||||
const endIndex = this.getCursorPosition(evt)
|
|
||||||
let end = ~endIndex ? endIndex : 0
|
|
||||||
// 开始位置
|
|
||||||
let start = this.mouseDownStartIndex
|
|
||||||
if (start > end) {
|
|
||||||
[start, end] = [end, start]
|
|
||||||
}
|
|
||||||
this.range.startIndex = start
|
|
||||||
this.range.endIndex = end
|
|
||||||
if (start === end) return
|
|
||||||
// 绘制选区
|
|
||||||
this.strokeRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMousedown(evt: MouseEvent) {
|
|
||||||
this.isAllowDrag = true
|
|
||||||
this.mouseDownStartIndex = this.getCursorPosition(evt) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMouseleave(evt: MouseEvent) {
|
|
||||||
// 是否还在canvas内部
|
|
||||||
const { x, y, width, height } = this.canvas.getBoundingClientRect()
|
|
||||||
if (evt.x >= x && evt.x <= x + width && evt.y >= y && evt.y <= y + height) return
|
|
||||||
this.isAllowDrag = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleKeydown(evt: KeyboardEvent) {
|
|
||||||
if (!this.cursorPosition) return
|
|
||||||
const { index } = this.cursorPosition
|
|
||||||
const { startIndex, endIndex } = this.range
|
|
||||||
const isCollspace = startIndex === endIndex
|
|
||||||
if (evt.key === KeyMap.Backspace) {
|
|
||||||
// 判断是否允许删除
|
|
||||||
if (this.elementList[index].value === ZERO && index === 0) {
|
|
||||||
evt.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!isCollspace) {
|
|
||||||
this.elementList.splice(startIndex + 1, endIndex - startIndex)
|
|
||||||
} else {
|
|
||||||
this.elementList.splice(index, 1)
|
|
||||||
}
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: isCollspace ? index - 1 : startIndex })
|
|
||||||
} else if (evt.key === KeyMap.Enter) {
|
|
||||||
const enterText: IElement = {
|
|
||||||
value: ZERO
|
|
||||||
}
|
|
||||||
if (isCollspace) {
|
|
||||||
this.elementList.splice(index + 1, 0, enterText)
|
|
||||||
} else {
|
|
||||||
this.elementList.splice(startIndex + 1, endIndex - startIndex, enterText)
|
|
||||||
}
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: index + 1 })
|
|
||||||
} else if (evt.key === KeyMap.Left) {
|
|
||||||
if (index > 0) {
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: index - 1, isSubmitHistory: false })
|
|
||||||
}
|
|
||||||
} else if (evt.key === KeyMap.Right) {
|
|
||||||
if (index < this.position.length - 1) {
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: index + 1, isSubmitHistory: false })
|
|
||||||
}
|
|
||||||
} else if (evt.key === KeyMap.Up || evt.key === KeyMap.Down) {
|
|
||||||
const { rowNo, index, coordinate: { leftTop, rightTop } } = this.cursorPosition
|
|
||||||
if ((evt.key === KeyMap.Up && rowNo !== 0) || (evt.key === KeyMap.Down && rowNo !== this.rowCount)) {
|
|
||||||
// 下一个光标点所在行位置集合
|
|
||||||
const probablePosition = evt.key === KeyMap.Up
|
|
||||||
? this.position.slice(0, index).filter(p => p.rowNo === rowNo - 1)
|
|
||||||
: this.position.slice(index, this.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: maxIndex, isSubmitHistory: false })
|
|
||||||
}
|
|
||||||
} else if (evt.ctrlKey && evt.key === KeyMap.Z) {
|
|
||||||
this.historyManager.undo()
|
|
||||||
evt.preventDefault()
|
|
||||||
} else if (evt.ctrlKey && evt.key === KeyMap.Y) {
|
|
||||||
this.historyManager.redo()
|
|
||||||
evt.preventDefault()
|
|
||||||
} else if (evt.ctrlKey && evt.key === KeyMap.C) {
|
|
||||||
if (!isCollspace) {
|
|
||||||
writeText(this.elementList.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
|
|
||||||
}
|
|
||||||
} else if (evt.ctrlKey && evt.key === KeyMap.X) {
|
|
||||||
if (!isCollspace) {
|
|
||||||
writeText(this.position.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
|
|
||||||
this.elementList.splice(startIndex + 1, endIndex - startIndex)
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: startIndex })
|
|
||||||
}
|
|
||||||
} else if (evt.ctrlKey && evt.key === KeyMap.A) {
|
|
||||||
this.range.startIndex = 0
|
|
||||||
this.range.endIndex = this.position.length - 1
|
|
||||||
this.draw({ isSubmitHistory: false, isSetCursor: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInput(data: string) {
|
|
||||||
if (!data || !this.cursorPosition || this.isCompositing) return
|
|
||||||
this.textareaDom.value = ''
|
|
||||||
const { index } = this.cursorPosition
|
|
||||||
const { startIndex, endIndex } = this.range
|
|
||||||
const isCollspace = startIndex === endIndex
|
|
||||||
const inputData: IElement[] = data.split('').map(value => ({
|
|
||||||
value
|
|
||||||
}))
|
|
||||||
if (isCollspace) {
|
|
||||||
this.elementList.splice(index + 1, 0, ...inputData)
|
|
||||||
} else {
|
|
||||||
this.elementList.splice(startIndex + 1, endIndex - startIndex, ...inputData)
|
|
||||||
}
|
|
||||||
this.clearRange()
|
|
||||||
this.draw({ curIndex: (isCollspace ? index : startIndex) + inputData.length })
|
|
||||||
}
|
|
||||||
|
|
||||||
private handlePaste(evt: ClipboardEvent) {
|
|
||||||
const text = evt.clipboardData?.getData('text')
|
|
||||||
this.handleInput(text || '')
|
|
||||||
evt.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCompositionstart() {
|
|
||||||
this.isCompositing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCompositionend() {
|
|
||||||
this.isCompositing = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export interface IRange {
|
export interface IRange {
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
endIndex: number
|
endIndex: number;
|
||||||
}
|
}
|
||||||
Loading…
Reference in new issue