feat:rich text editor by canvas

pr675
黄云飞 4 years ago
commit 2739d4069f

5
.gitignore vendored

@ -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
![image](https://github.com/Hufe921/canvas-editor/blob/main/src/assets/snapshots/main.png)
## install
`yarn`
## dev
`yarn run dev`
## build
`yarn run build`

Binary file not shown.

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"
}
}

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8.131 6.9c2.035 0 2.569-.9 2.569-1.869 0-.968-.64-1.831-2.623-1.831H5.2v3.7h2.931zm.524 5.9c2.045 0 2.545-1.305 2.545-2.3 0-.985-.506-2.4-2.81-2.4H5.2v4.7h3.455zM4 2h4.71c2.367 0 3.19 1.583 3.19 3s-.325 1.852-1.1 2.5c1.2.5 1.569 1.379 1.6 3 .03 1.606-.586 3.5-3.769 3.5H4V2z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 411 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M7.997 3.429L6.398 8h3.2L7.997 3.429zM8.497 2L12 12h-1L9.949 9h-3.9L5 12H4L7.496 2h1z" fill="#3D4757" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 221 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M10.42 7.903H6.692a9.182 9.182 0 01-.41-.172 5.54 5.54 0 01-.814-.447 2.955 2.955 0 01-.655-.595 2.728 2.728 0 01-.44-.777 2.877 2.877 0 01-.162-1.006c0-.472.094-.888.282-1.25.188-.36.453-.663.793-.907s.747-.43 1.22-.558A5.97 5.97 0 018.063 2c.504 0 .95.049 1.337.147.387.097.725.23 1.013.398.287.169.53.365.73.59a3.337 3.337 0 01.772 1.486c.03.13.054.255.073.379h-1.276a2.393 2.393 0 00-.22-.615 2.315 2.315 0 00-.59-.724 2.467 2.467 0 00-.834-.44 3.376 3.376 0 00-1.005-.146 4.69 4.69 0 00-.958.097 2.77 2.77 0 00-.839.314 1.765 1.765 0 00-.597.566c-.152.233-.229.518-.229.854 0 .348.086.642.258.884.171.241.401.449.689.622.287.174.615.323.983.448s.749.247 1.142.367c.31.097.62.196.934.297a8.439 8.439 0 01.973.38zm1.376 1c.175.217.315.466.418.746.105.285.158.612.158.98 0 .554-.104 1.041-.312 1.462-.207.42-.496.772-.867 1.054-.37.282-.81.495-1.32.64A6.12 6.12 0 018.205 14c-.543 0-1.071-.09-1.586-.273a4.44 4.44 0 01-1.374-.773 3.873 3.873 0 01-.97-1.217 3.695 3.695 0 01-.395-1.612h1.27c.028.407.122.78.282 1.12a2.835 2.835 0 001.581 1.465c.363.138.76.207 1.192.207.387 0 .758-.042 1.112-.126a2.85 2.85 0 00.938-.399 2.01 2.01 0 00.647-.708c.16-.29.241-.642.241-1.054 0-.337-.087-.623-.261-.86a2.333 2.333 0 00-.69-.61 4.651 4.651 0 00-.495-.257h2.099z"/><path d="M3 7h10v1H3z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M8.213 13H6.8l6.636-6.636-4.243-4.243-7.07 7.071L5.928 13H4.515L1.06 9.546a.5.5 0 010-.707L8.839 1.06a.5.5 0 01.707 0l4.95 4.95a.5.5 0 010 .707L8.213 13z" fill-rule="nonzero"/><path d="M4.536 6.364l4.95 4.95-.707.707-4.95-4.95zM4.521 13h10.03v1H5.496z"/></g></svg>

After

Width:  |  Height:  |  Size: 394 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M13.31 5h-1.92a2.203 2.203 0 00-.39-.034c-.135 0-.27.012-.402.034H10v.18c-.578.256-1 .714-1 1.214v1.928c0 .196.117.43.3.658v1.23a3.543 3.543 0 01-.3-.4V11H8V1h1v4.265C9 4.763 10 4 10.942 4c1.19 0 1.92.422 2.367 1zM2 6c-.03-.498-.175-2 2.5-2C7.11 4 7 5 7 6.902V11H5.984v-.993c.38.662-.115.993-1.484.993C2.708 11 2 9.931 2 9c0-1.428.447-2 2.5-2h1.484c0-1 .031-2-1.484-2-1.533 0-1.577.485-1.577 1H2zm2.5 2C3.601 8 3 7.768 3 9c0 1.31.438 1 1.5 1 .617 0 1.484-.665 1.484-1.847V8H4.5z"/><path d="M13.085 6.316l-2.814 3a1 1 0 101.458 1.368l2.815-3a1 1 0 00-1.459-1.368z" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 725 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M10.017 3L8.08 13H9v1H6v-1h1.182L9 3H8V2h3v1h-.983z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 167 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M7.5 2v2.5H4a.5.5 0 00-.5.5v2a.5.5 0 00.5.5h9a.5.5 0 00.5-.5V5a.5.5 0 00-.5-.5H9.5V2a.5.5 0 00-.5-.5H8a.5.5 0 00-.5.5z" stroke="#3D4757"/><path fill="#3D4757" d="M13 7h1v4h-1z"/><path d="M11 13a2 2 0 002-2V8.764A3 3 0 118.764 13H11z" fill="#3D4757"/><path fill="#3D4757" d="M1 13h10v1H1z"/><path d="M1 13a2 2 0 002-2V8.764A3 3 0 011 14v-1z" fill="#3D4757"/><path fill="#3D4757" d="M3 7h1v4H3z"/></g></svg>

After

Width:  |  Height:  |  Size: 532 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M12 4h-1V2H5v2H4V2a1 1 0 011-1h6a1 1 0 011 1v2zm0 5v4a1 1 0 01-1 1H5a1 1 0 01-1-1V9h1v4h6V9h1z"/><path d="M12 12v-1h2V5H2v6h2v1H2a1 1 0 01-1-1V5a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1h-2z"/><path d="M3 8h10v1H3zm8-2h2v1h-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 369 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M3 14v-3a4 4 0 014-4h3V6H7a5 5 0 00-5 5v3h1zm7.016-11.282v7.543l4.29-3.73z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 190 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle stroke="#3D4757" cx="6" cy="6" r="4.5"/><path d="M10.061 10.968L8.707 9.414l.707-.707 1.514 1.457.435-.404 2.632 2.462a1.154 1.154 0 01.05 1.635 1.184 1.184 0 01-1.655.064l-2.788-2.527.46-.426z" fill="#3D4757" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 367 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M6.215 3.29H7.64L11.855 14H10.52l-1.14-3H4.46l-1.14 3H2L6.215 3.29zM4.85 9.965h4.14L6.965 4.61h-.06L4.85 9.965z"/><path d="M12 4V2h1v2h2v1h-2v2h-1V5h-2V4h2z" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 319 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path fill-rule="nonzero" d="M11 4h4v1h-4z"/><path d="M6.215 3.29H7.64L11.855 14H10.52l-1.14-3H4.46l-1.14 3H2L6.215 3.29zM4.85 9.965h4.14L6.965 4.61h-.06L4.85 9.965z"/></g></svg>

After

Width:  |  Height:  |  Size: 299 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M5 2v6a3 3 0 106 0V2h1v6a4 4 0 11-8 0V2h1zM4 13h8v1H4z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 170 B

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M6 2.763v7.544l-4.29-3.73zM13 14v-3a4 4 0 00-4-4H6V6h3a5 5 0 015 5v3h-1z" fill="#3D4757"/></svg>

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

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,46 @@
import './style.css'
import Editor from './editor'
window.onload = function () {
const canvas = document.querySelector<HTMLCanvasElement>('canvas')
if (!canvas) return
const text = `\n主诉\n发热三天咳嗽五天。\n现病史\n发病前14天内有病历报告社区的旅行时或居住史发病前14天内与新型冠状病毒感染的患者或无症状感染者有接触史发病前14天内解除过来自病历报告社区的发热或有呼吸道症状的患者聚集性发病2周内在小范围如家庭、办公室、学校班级等场所出现2例及以上发热或呼吸道症状的病例。\n既往史\n有糖尿病10年有高血压2年有传染性疾病1年。\n体格检查\nT36.5℃P80bpmR20次/分BP120/80mmHg\n辅助检查\n2020年6月10日普放血细胞比容36.50%偏低4050单核细胞绝对值0.75*10^9/L偏高参考值0.10.6\n门诊诊断\n1.高血压\n处置治疗\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n4.氯化钠注射液 250ml/袋、1袋\n5.七叶皂苷钠片欧开、30mg/片*24/盒、1片、口服、BID每日两次、1天`
// 模拟加粗字
const boldText = ['主诉:', '现病史:', '既往史:', '体格检查:', '辅助检查:', '门诊诊断:', '处置治疗:']
const boldIndex: number[] = boldText.map(b => {
const i = text.indexOf(b)
return ~i ? Array(b.length).fill(i).map((_, j) => i + j) : []
}).flat()
// 模拟颜色字
const colorText = ['传染性疾病']
const colorIndex: number[] = colorText.map(b => {
const i = text.indexOf(b)
return ~i ? Array(b.length).fill(i).map((_, j) => i + j) : []
}).flat()
// 组合数据
const data = text.split('').map((value, index) => {
if (boldIndex.includes(index)) {
return {
value,
size: 18,
bold: true
}
}
if (colorIndex.includes(index)) {
return {
value,
color: 'red',
size: 16
}
}
return {
value,
size: 16
}
})
// 初始化编辑器
const instance = new Editor(canvas, data, {
margins: [120, 120, 200, 120]
})
console.log('编辑器实例: ', instance);
}

@ -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;
}

1
src/vite-env.d.ts vendored

@ -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"
Loading…
Cancel
Save