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,4 +1,4 @@
|
|||||||
export interface IRange {
|
export interface IRange {
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
endIndex: number
|
endIndex: number;
|
||||||
}
|
}
|
||||||
Loading…
Reference in new issue