commit 2739d4069f591306ebad810ed2942a5bd1658b03 Author: 黄云飞 Date: Fri Nov 12 22:22:05 2021 +0800 feat:rich text editor by canvas diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53f7466 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54955c9 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +

canvas-editor

+ +

a rich text editor by canvas

+ +## 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` diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..d05e470 Binary files /dev/null and b/favicon.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..d1acbbb --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + + + canvas-editor + + + +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c93f08a --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/assets/images/bold.svg b/src/assets/images/bold.svg new file mode 100644 index 0000000..80728d0 --- /dev/null +++ b/src/assets/images/bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/color.svg b/src/assets/images/color.svg new file mode 100644 index 0000000..2b84e88 --- /dev/null +++ b/src/assets/images/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/deleteline.svg b/src/assets/images/deleteline.svg new file mode 100644 index 0000000..c2c83ca --- /dev/null +++ b/src/assets/images/deleteline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/format.svg b/src/assets/images/format.svg new file mode 100644 index 0000000..aae6e6b --- /dev/null +++ b/src/assets/images/format.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/highlight.svg b/src/assets/images/highlight.svg new file mode 100644 index 0000000..c4b2e8b --- /dev/null +++ b/src/assets/images/highlight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/italic.svg b/src/assets/images/italic.svg new file mode 100644 index 0000000..73b2af5 --- /dev/null +++ b/src/assets/images/italic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/painter.svg b/src/assets/images/painter.svg new file mode 100644 index 0000000..a865d1d --- /dev/null +++ b/src/assets/images/painter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/print.svg b/src/assets/images/print.svg new file mode 100644 index 0000000..5ee44a0 --- /dev/null +++ b/src/assets/images/print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/redo.svg b/src/assets/images/redo.svg new file mode 100644 index 0000000..fc88331 --- /dev/null +++ b/src/assets/images/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/search.svg b/src/assets/images/search.svg new file mode 100644 index 0000000..9d515dc --- /dev/null +++ b/src/assets/images/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/size-add.svg b/src/assets/images/size-add.svg new file mode 100644 index 0000000..aa1073c --- /dev/null +++ b/src/assets/images/size-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/size-minus.svg b/src/assets/images/size-minus.svg new file mode 100644 index 0000000..7bfa958 --- /dev/null +++ b/src/assets/images/size-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/underline.svg b/src/assets/images/underline.svg new file mode 100644 index 0000000..dcd81b0 --- /dev/null +++ b/src/assets/images/underline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/undo.svg b/src/assets/images/undo.svg new file mode 100644 index 0000000..820f852 --- /dev/null +++ b/src/assets/images/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/snapshots/main.png b/src/assets/snapshots/main.png new file mode 100644 index 0000000..49741f3 Binary files /dev/null and b/src/assets/snapshots/main.png differ diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css new file mode 100644 index 0000000..02c31fe --- /dev/null +++ b/src/editor/assets/css/index.css @@ -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 + } +} \ No newline at end of file diff --git a/src/editor/core/history/HistoryManager.ts b/src/editor/core/history/HistoryManager.ts new file mode 100644 index 0000000..e87bcdd --- /dev/null +++ b/src/editor/core/history/HistoryManager.ts @@ -0,0 +1,35 @@ +export class HistoryManager { + + private readonly MAX_RECORD_COUNT = 1000 + private undoStack: Array = [] + private redoStack: Array = [] + + 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() + } + } + +} \ No newline at end of file diff --git a/src/editor/dataset/constant/Common.ts b/src/editor/dataset/constant/Common.ts new file mode 100644 index 0000000..6d915ad --- /dev/null +++ b/src/editor/dataset/constant/Common.ts @@ -0,0 +1,2 @@ +export const ZERO = '\u200B' +export const WRAP = '\n' \ No newline at end of file diff --git a/src/editor/dataset/enum/Keymap.ts b/src/editor/dataset/enum/Keymap.ts new file mode 100644 index 0000000..a293df2 --- /dev/null +++ b/src/editor/dataset/enum/Keymap.ts @@ -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" +} \ No newline at end of file diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 0000000..e4908c9 --- /dev/null +++ b/src/editor/index.ts @@ -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 = { + 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 + 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 + } + +} \ No newline at end of file diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts new file mode 100644 index 0000000..ac0253b --- /dev/null +++ b/src/editor/interface/Draw.ts @@ -0,0 +1,5 @@ +export interface IDrawOption { + curIndex?: number; + isSetCursor?: boolean + isSubmitHistory?: boolean; +} \ No newline at end of file diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts new file mode 100644 index 0000000..aac9221 --- /dev/null +++ b/src/editor/interface/Editor.ts @@ -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] +} \ No newline at end of file diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts new file mode 100644 index 0000000..9111d7a --- /dev/null +++ b/src/editor/interface/Element.ts @@ -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[]; + } +} \ No newline at end of file diff --git a/src/editor/interface/Range.ts b/src/editor/interface/Range.ts new file mode 100644 index 0000000..6d9c85b --- /dev/null +++ b/src/editor/interface/Range.ts @@ -0,0 +1,4 @@ +export interface IRange { + startIndex: number; + endIndex: number +} \ No newline at end of file diff --git a/src/editor/interface/Row.ts b/src/editor/interface/Row.ts new file mode 100644 index 0000000..af8d44b --- /dev/null +++ b/src/editor/interface/Row.ts @@ -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[]; +} diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts new file mode 100644 index 0000000..ccbc23f --- /dev/null +++ b/src/editor/utils/index.ts @@ -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; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..493ace7 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,46 @@ +import './style.css' +import Editor from './editor' + +window.onload = function () { + const canvas = document.querySelector('canvas') + if (!canvas) return + const text = `\n主诉:\n发热三天,咳嗽五天。\n现病史:\n发病前14天内有病历报告社区的旅行时或居住史;发病前14天内与新型冠状病毒感染的患者或无症状感染者有接触史;发病前14天内解除过来自病历报告社区的发热或有呼吸道症状的患者;聚集性发病,2周内在小范围如家庭、办公室、学校班级等场所,出现2例及以上发热或呼吸道症状的病例。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。\n体格检查:\nT:36.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10^9/L(偏高)参考值:0.1~0.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); +} \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..b36f06a --- /dev/null +++ b/src/style.css @@ -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; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4824d01 --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..7db1408 --- /dev/null +++ b/yarn.lock @@ -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"