@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@ -0,0 +1,19 @@
|
||||
<h1 align="center">canvas-editor</h1>
|
||||
|
||||
<p align="center"> a rich text editor by canvas</p>
|
||||
|
||||
## snapshot
|
||||
|
||||

|
||||
|
||||
## install
|
||||
|
||||
`yarn`
|
||||
|
||||
## dev
|
||||
|
||||
`yarn run dev`
|
||||
|
||||
## build
|
||||
|
||||
`yarn run build`
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>canvas-editor</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="menu">
|
||||
<div class="menu-item">
|
||||
<div class="menu-item__undo">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__redo">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__painter">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__format">
|
||||
<i></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item">
|
||||
<div class="menu-item__size-add">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__size-minus">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__bold">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__italic">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__underline">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__deleteline">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__color">
|
||||
<i></i>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="menu-item__highlight">
|
||||
<i></i>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item">
|
||||
<div class="menu-item__search">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="menu-item__print">
|
||||
<i></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<canvas style="width: 794px;height: 1123px;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.4.2"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 411 B |
|
After Width: | Height: | Size: 221 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 725 B |
|
After Width: | Height: | Size: 167 B |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 190 B |
|
After Width: | Height: | Size: 367 B |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 299 B |
|
After Width: | Height: | Size: 170 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 87 KiB |
@ -0,0 +1,58 @@
|
||||
.inputarea {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
letter-spacing: 0;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
outline: none;
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
color: transparent;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
outline: none;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.cursor--animation {
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: cursorAnimation;
|
||||
}
|
||||
|
||||
@keyframes cursorAnimation {
|
||||
from {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
13% {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
63% {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
export class HistoryManager {
|
||||
|
||||
private readonly MAX_RECORD_COUNT = 1000
|
||||
private undoStack: Array<Function> = []
|
||||
private redoStack: Array<Function> = []
|
||||
|
||||
undo() {
|
||||
if (this.undoStack.length > 1) {
|
||||
const pop = this.undoStack.pop()!
|
||||
this.redoStack.push(pop)
|
||||
if (this.undoStack.length) {
|
||||
this.undoStack[this.undoStack.length - 1]()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redoStack.length) {
|
||||
const pop = this.redoStack.pop()!
|
||||
this.undoStack.push(pop)
|
||||
pop()
|
||||
}
|
||||
}
|
||||
|
||||
execute(fn: Function) {
|
||||
this.undoStack.push(fn)
|
||||
if (this.redoStack.length) {
|
||||
this.redoStack = []
|
||||
}
|
||||
while (this.undoStack.length > this.MAX_RECORD_COUNT) {
|
||||
this.undoStack.shift()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export const ZERO = '\u200B'
|
||||
export const WRAP = '\n'
|
||||
@ -0,0 +1,13 @@
|
||||
export enum KeyMap {
|
||||
Backspace = 'Backspace',
|
||||
Enter = "Enter",
|
||||
Left = "ArrowLeft",
|
||||
Right = "ArrowRight",
|
||||
Up = "ArrowUp",
|
||||
Down = "ArrowDown",
|
||||
A = "a",
|
||||
C = "c",
|
||||
X = "x",
|
||||
Y = "y",
|
||||
Z = "z"
|
||||
}
|
||||
@ -0,0 +1,525 @@
|
||||
import './assets/css/index.css'
|
||||
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 { IElement, IElementPosition } from './interface/Element'
|
||||
|
||||
export default class Editor {
|
||||
|
||||
private readonly defaultOptions: Required<IEditorOption> = {
|
||||
defaultType: 'TEXT',
|
||||
defaultFont: 'Yahei',
|
||||
defaultSize: 16,
|
||||
rangeAlpha: 0.6,
|
||||
rangeColor: '#AECBFA',
|
||||
marginIndicatorSize: 35,
|
||||
marginIndicatorColor: '#BABABA',
|
||||
margins: [100, 120, 100, 120]
|
||||
}
|
||||
|
||||
private canvas: HTMLCanvasElement
|
||||
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
|
||||
};
|
||||
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
|
||||
this.mouseDownStartIndex = 0
|
||||
|
||||
// 历史管理
|
||||
this.historyManager = new HistoryManager()
|
||||
|
||||
// 全局事件
|
||||
document.addEventListener('click', (evt) => {
|
||||
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
|
||||
})
|
||||
}
|
||||
data.forEach(text => {
|
||||
if (text.value === '\n') {
|
||||
text.value = ZERO
|
||||
}
|
||||
})
|
||||
this.elementList = data
|
||||
this.draw()
|
||||
}
|
||||
|
||||
private draw(options?: IDrawOption) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export interface IDrawOption {
|
||||
curIndex?: number;
|
||||
isSetCursor?: boolean
|
||||
isSubmitHistory?: boolean;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
export interface IEditorOption {
|
||||
defaultType?: string;
|
||||
defaultFont?: string;
|
||||
defaultSize?: number;
|
||||
rangeColor?: string;
|
||||
rangeAlpha?: number;
|
||||
marginIndicatorSize?: number;
|
||||
marginIndicatorColor?: string,
|
||||
margins?: [top: number, right: number, bootom: number, left: number]
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
export interface IElement {
|
||||
type?: 'TEXT' | 'IMAGE';
|
||||
value: string;
|
||||
font?: string;
|
||||
size?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
bold?: boolean;
|
||||
color?: string;
|
||||
italic?: boolean;
|
||||
underline?: boolean;
|
||||
strikeout?: boolean;
|
||||
}
|
||||
|
||||
export interface IElementPosition {
|
||||
index: number;
|
||||
value: string,
|
||||
rowNo: number;
|
||||
lineHeight: number;
|
||||
metrics: TextMetrics;
|
||||
isLastLetter: boolean,
|
||||
coordinate: {
|
||||
leftTop: number[];
|
||||
leftBottom: number[];
|
||||
rightTop: number[];
|
||||
rightBottom: number[];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface IRange {
|
||||
startIndex: number;
|
||||
endIndex: number
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { IElement } from "./Element";
|
||||
|
||||
export type IRowElement = IElement & {
|
||||
metrics: TextMetrics
|
||||
}
|
||||
|
||||
export interface IRow {
|
||||
width: number;
|
||||
height: number;
|
||||
ascent: number;
|
||||
elementList: IRowElement[];
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import { ZERO } from "../dataset/constant/Common"
|
||||
|
||||
export function debounce(func: Function, delay: number) {
|
||||
let timer: number
|
||||
return function (...args: any) {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
// @ts-ignore
|
||||
func.apply(this, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
export function writeText(text: string) {
|
||||
if (!text) return
|
||||
window.navigator.clipboard.writeText(text.replaceAll(ZERO, `\n`))
|
||||
}
|
||||
|
||||
export function deepClone(obj: any) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
let newObj: any = {};
|
||||
if (Array.isArray(obj)) {
|
||||
newObj = obj.map(item => deepClone(item));
|
||||
} else {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
return newObj[key] = deepClone(obj[key]);
|
||||
})
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F7;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F2F4F7;
|
||||
box-shadow: 0 2px 4px 0 transparent;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 6px;
|
||||
display: inline-block;
|
||||
background-color: #cfd2d8;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item>div {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.menu-item>div:hover {
|
||||
background: rgba(25, 55, 88, .04);
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.menu-item>div span {
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
display: inline-block;
|
||||
border: 1px solid #e2e6ed;
|
||||
}
|
||||
|
||||
.menu-item__undo i {
|
||||
background-image: url('./assets/images/undo.svg');
|
||||
}
|
||||
|
||||
.menu-item__redo i {
|
||||
background-image: url('./assets/images/redo.svg');
|
||||
}
|
||||
|
||||
.menu-item__painter i {
|
||||
background-image: url('./assets/images/painter.svg');
|
||||
}
|
||||
|
||||
.menu-item__format i {
|
||||
background-image: url('./assets/images/format.svg');
|
||||
}
|
||||
|
||||
.menu-item__size-add i {
|
||||
background-image: url('./assets/images/size-add.svg');
|
||||
}
|
||||
|
||||
.menu-item__size-minus i {
|
||||
background-image: url('./assets/images/size-minus.svg');
|
||||
}
|
||||
|
||||
.menu-item__bold i {
|
||||
background-image: url('./assets/images/bold.svg');
|
||||
}
|
||||
|
||||
.menu-item__italic i {
|
||||
background-image: url('./assets/images/italic.svg');
|
||||
}
|
||||
|
||||
.menu-item__underline i {
|
||||
background-image: url('./assets/images/underline.svg');
|
||||
}
|
||||
|
||||
.menu-item__deleteline i {
|
||||
background-image: url('./assets/images/deleteline.svg');
|
||||
}
|
||||
|
||||
.menu-item__color,
|
||||
.menu-item__highlight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-item__color i {
|
||||
background-image: url('./assets/images/color.svg');
|
||||
}
|
||||
|
||||
.menu-item__color span {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.menu-item__highlight i {
|
||||
background-image: url('./assets/images/highlight.svg');
|
||||
}
|
||||
|
||||
.menu-item__highlight span {
|
||||
background-color: #ffff00;
|
||||
}
|
||||
|
||||
.menu-item__search i {
|
||||
background-image: url('./assets/images/search.svg');
|
||||
}
|
||||
|
||||
.menu-item__print i {
|
||||
background-image: url('./assets/images/print.svg');
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 794px;
|
||||
height: 1123px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
box-shadow: rgb(158 161 165 / 40%) 0px 2px 12px 0px;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
esbuild-android-arm64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz#da07b5fb2daf7d83dcd725f7cf58a6758e6e702a"
|
||||
integrity sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ==
|
||||
|
||||
esbuild-darwin-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz#e94e9fd3b4b5455a2e675cd084a19a71b6904bbf"
|
||||
integrity sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA==
|
||||
|
||||
esbuild-darwin-arm64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz#8c320eafbb3ba2c70d8062128c5b71503e342471"
|
||||
integrity sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g==
|
||||
|
||||
esbuild-freebsd-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz#ce0ca5b8c4c274cfebc9326f9b316834bd9dd151"
|
||||
integrity sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A==
|
||||
|
||||
esbuild-freebsd-arm64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz#463da17562fdcfdf03b3b94b28497d8d8dcc8f62"
|
||||
integrity sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw==
|
||||
|
||||
esbuild-linux-32@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz#2035793160da2c4be48a929e5bafb14a31789acc"
|
||||
integrity sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w==
|
||||
|
||||
esbuild-linux-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz#fbe4802a8168c6d339d0749f977b099449b56f22"
|
||||
integrity sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw==
|
||||
|
||||
esbuild-linux-arm64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz#f08d98df28d436ed4aad1529615822bb74d4d978"
|
||||
integrity sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng==
|
||||
|
||||
esbuild-linux-arm@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz#6f968c3a98b64e30c80b212384192d0cfcb32e7f"
|
||||
integrity sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ==
|
||||
|
||||
esbuild-linux-mips64le@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz#690c78dc4725efe7d06a1431287966fbf7774c7f"
|
||||
integrity sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w==
|
||||
|
||||
esbuild-linux-ppc64le@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz#7ec9048502de46754567e734aae7aebd2df6df02"
|
||||
integrity sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ==
|
||||
|
||||
esbuild-netbsd-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz#439bdaefffa03a8fa84324f5d83d636f548a2de3"
|
||||
integrity sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ==
|
||||
|
||||
esbuild-openbsd-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz#c9958e5291a00a3090c1ec482d6bcdf2d5b5d107"
|
||||
integrity sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w==
|
||||
|
||||
esbuild-sunos-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz#ac9ead8287379cd2f6d00bd38c5997fda9c1179e"
|
||||
integrity sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ==
|
||||
|
||||
esbuild-windows-32@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz#a3820fc86631ca594cb7b348514b5cc3f058cfd6"
|
||||
integrity sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw==
|
||||
|
||||
esbuild-windows-64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz#1da748441f228d75dff474ddb7d584b81887323c"
|
||||
integrity sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA==
|
||||
|
||||
esbuild-windows-arm64@0.13.13:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz#06dfa52a6b178a5932a9a6e2fdb240c09e6da30c"
|
||||
integrity sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A==
|
||||
|
||||
esbuild@^0.13.2:
|
||||
version "0.13.13"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.13.tgz#0b5399c20f219f663c8c1048436fb0f59ab17a41"
|
||||
integrity sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q==
|
||||
optionalDependencies:
|
||||
esbuild-android-arm64 "0.13.13"
|
||||
esbuild-darwin-64 "0.13.13"
|
||||
esbuild-darwin-arm64 "0.13.13"
|
||||
esbuild-freebsd-64 "0.13.13"
|
||||
esbuild-freebsd-arm64 "0.13.13"
|
||||
esbuild-linux-32 "0.13.13"
|
||||
esbuild-linux-64 "0.13.13"
|
||||
esbuild-linux-arm "0.13.13"
|
||||
esbuild-linux-arm64 "0.13.13"
|
||||
esbuild-linux-mips64le "0.13.13"
|
||||
esbuild-linux-ppc64le "0.13.13"
|
||||
esbuild-netbsd-64 "0.13.13"
|
||||
esbuild-openbsd-64 "0.13.13"
|
||||
esbuild-sunos-64 "0.13.13"
|
||||
esbuild-windows-32 "0.13.13"
|
||||
esbuild-windows-64 "0.13.13"
|
||||
esbuild-windows-arm64 "0.13.13"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
is-core-module@^2.2.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
|
||||
integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
nanoid@^3.1.30:
|
||||
version "3.1.30"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
|
||||
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@^8.3.8:
|
||||
version "8.3.11"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
|
||||
integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==
|
||||
dependencies:
|
||||
nanoid "^3.1.30"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^0.6.2"
|
||||
|
||||
resolve@^1.20.0:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
|
||||
dependencies:
|
||||
is-core-module "^2.2.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
rollup@^2.57.0:
|
||||
version "2.60.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.0.tgz#4ee60ab7bdd0356763f87d7099f413e5460fc193"
|
||||
integrity sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
source-map-js@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
|
||||
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
|
||||
|
||||
typescript@^4.3.2:
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
|
||||
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
|
||||
|
||||
vite@^2.4.2:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-2.6.14.tgz#35c09a15e4df823410819a2a239ab11efb186271"
|
||||
integrity sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==
|
||||
dependencies:
|
||||
esbuild "^0.13.2"
|
||||
postcss "^8.3.8"
|
||||
resolve "^1.20.0"
|
||||
rollup "^2.57.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||