Merge pull request #58 from Hufe921/feature/latex

Feature/latex
pr675
Hufe 4 years ago committed by GitHub
commit 51b275c8a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,6 +24,7 @@
"dist", "dist",
"node_modules", "node_modules",
"yarn.lock", "yarn.lock",
"src/editor/core/draw/particle/latex/utils"
], ],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

@ -0,0 +1,37 @@
import Editor from '../../../src/editor'
describe('菜单-LaTeX', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/canvas-editor/')
cy.get('canvas').first().as('canvas').should('have.length', 1)
})
const text = 'canvas-editor'
it('LaTeX', () => {
cy.getEditor().then((editor: Editor) => {
editor.listener.saved = function (payload) {
const data = payload.data
expect(data[0].type).to.eq('latex')
expect(data[0].value).to.eq(text)
}
editor.command.executeSelectAll()
editor.command.executeBackspace()
cy.get('.menu-item__latex').click()
cy.get('.dialog-option__item [name="value"]').type(text)
cy.get('.dialog-menu button').eq(1).click()
cy.get('@canvas').type('{ctrl}s')
})
})
})

@ -173,6 +173,9 @@
<div class="menu-item__checkbox"> <div class="menu-item__checkbox">
<i></i> <i></i>
</div> </div>
<div class="menu-item__latex">
<i></i>
</div>
</div> </div>
<div class="menu-divider"></div> <div class="menu-divider"></div>
<div class="menu-item"> <div class="menu-item">

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M9 13.5H2.982a.5.5 0 01-.404-.794l3.208-4.412a.5.5 0 000-.588L2.578 3.294a.5.5 0 01.404-.794H9" stroke="#3D4757"/><path d="M14.447 7l.809.588-2.139 2.942 2.139 2.942-.81.588-1.946-2.68-1.947 2.68-.809-.588 2.138-2.942-2.138-2.942.81-.588L12.5 9.679 14.447 7z" fill="#3D4757"/></g></svg>

After

Width:  |  Height:  |  Size: 393 B

@ -21,6 +21,7 @@ import { Strikeout } from './richtext/Strikeout'
import { Underline } from './richtext/Underline' import { Underline } from './richtext/Underline'
import { ElementType } from '../../dataset/enum/Element' import { ElementType } from '../../dataset/enum/Element'
import { ImageParticle } from './particle/ImageParticle' import { ImageParticle } from './particle/ImageParticle'
import { LaTexParticle } from './particle/latex/LaTexParticle'
import { TextParticle } from './particle/TextParticle' import { TextParticle } from './particle/TextParticle'
import { PageNumber } from './frame/PageNumber' import { PageNumber } from './frame/PageNumber'
import { ScrollObserver } from '../observer/ScrollObserver' import { ScrollObserver } from '../observer/ScrollObserver'
@ -42,6 +43,7 @@ import { DeepRequired } from '../../interface/Common'
import { ControlComponent } from '../../dataset/enum/Control' import { ControlComponent } from '../../dataset/enum/Control'
import { formatElementList } from '../../utils/element' import { formatElementList } from '../../utils/element'
import { WorkerManager } from '../worker/WorkerManager' import { WorkerManager } from '../worker/WorkerManager'
import { Previewer } from './particle/previewer/Previewer'
export class Draw { export class Draw {
@ -66,7 +68,9 @@ export class Draw {
private strikeout: Strikeout private strikeout: Strikeout
private highlight: Highlight private highlight: Highlight
private historyManager: HistoryManager private historyManager: HistoryManager
private previewer: Previewer
private imageParticle: ImageParticle private imageParticle: ImageParticle
private laTexParticle: LaTexParticle
private textParticle: TextParticle private textParticle: TextParticle
private tableParticle: TableParticle private tableParticle: TableParticle
private tableTool: TableTool private tableTool: TableTool
@ -116,7 +120,9 @@ export class Draw {
this.underline = new Underline(this) this.underline = new Underline(this)
this.strikeout = new Strikeout(this) this.strikeout = new Strikeout(this)
this.highlight = new Highlight(this) this.highlight = new Highlight(this)
this.previewer = new Previewer(this)
this.imageParticle = new ImageParticle(this) this.imageParticle = new ImageParticle(this)
this.laTexParticle = new LaTexParticle(this)
this.textParticle = new TextParticle(this) this.textParticle = new TextParticle(this)
this.tableParticle = new TableParticle(this) this.tableParticle = new TableParticle(this)
this.tableTool = new TableTool(this) this.tableTool = new TableTool(this)
@ -351,6 +357,10 @@ export class Draw {
return this.cursor return this.cursor
} }
public getPreviewer(): Previewer {
return this.previewer
}
public getImageParticle(): ImageParticle { public getImageParticle(): ImageParticle {
return this.imageParticle return this.imageParticle
} }
@ -528,7 +538,7 @@ export class Draw {
boundingBoxAscent: 0, boundingBoxAscent: 0,
boundingBoxDescent: 0 boundingBoxDescent: 0
} }
if (element.type === ElementType.IMAGE) { if (element.type === ElementType.IMAGE || element.type === ElementType.LATEX) {
const elementWidth = element.width! * scale const elementWidth = element.width! * scale
const elementHeight = element.height! * scale const elementHeight = element.height! * scale
// 图片超出尺寸后自适应 // 图片超出尺寸后自适应
@ -705,7 +715,7 @@ export class Draw {
curRow.width += metrics.width curRow.width += metrics.width
if (curRow.height < height) { if (curRow.height < height) {
curRow.height = height curRow.height = height
if (element.type === ElementType.IMAGE) { if (element.type === ElementType.IMAGE || element.type === ElementType.LATEX) {
curRow.ascent = metrics.height curRow.ascent = metrics.height
} else { } else {
curRow.ascent = ascent curRow.ascent = ascent
@ -749,7 +759,7 @@ export class Draw {
for (let j = 0; j < curRow.elementList.length; j++) { for (let j = 0; j < curRow.elementList.length; j++) {
const element = curRow.elementList[j] const element = curRow.elementList[j]
const metrics = element.metrics const metrics = element.metrics
const offsetY = element.type === ElementType.IMAGE const offsetY = element.type === ElementType.IMAGE || element.type === ElementType.LATEX
? curRow.ascent - metrics.height ? curRow.ascent - metrics.height
: curRow.ascent : curRow.ascent
const positionItem: IElementPosition = { const positionItem: IElementPosition = {
@ -773,6 +783,9 @@ export class Draw {
if (element.type === ElementType.IMAGE) { if (element.type === ElementType.IMAGE) {
this.textParticle.complete() this.textParticle.complete()
this.imageParticle.render(ctx, element, x, y + offsetY) this.imageParticle.render(ctx, element, x, y + offsetY)
} else if (element.type === ElementType.LATEX) {
this.textParticle.complete()
this.laTexParticle.render(ctx, element, x, y + offsetY)
} else if (element.type === ElementType.TABLE) { } else if (element.type === ElementType.TABLE) {
if (isCrossRowCol) { if (isCrossRowCol) {
rangeRecord.x = x rangeRecord.x = x

@ -1,5 +1,5 @@
import { ControlComponent } from '../../../../dataset/enum/Control' import { ControlComponent } from '../../../../dataset/enum/Control'
import { KeyMap } from '../../../../dataset/enum/Keymap' import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { IControlInstance } from '../../../../interface/Control' import { IControlInstance } from '../../../../interface/Control'
import { IElement } from '../../../../interface/Element' import { IElement } from '../../../../interface/Element'
import { Control } from '../Control' import { Control } from '../Control'

@ -1,7 +1,7 @@
import { EDITOR_COMPONENT } from '../../../../dataset/constant/Editor' import { EDITOR_COMPONENT } from '../../../../dataset/constant/Editor'
import { ControlComponent } from '../../../../dataset/enum/Control' import { ControlComponent } from '../../../../dataset/enum/Control'
import { EditorComponent } from '../../../../dataset/enum/Editor' import { EditorComponent } from '../../../../dataset/enum/Editor'
import { KeyMap } from '../../../../dataset/enum/Keymap' import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { IControlInstance } from '../../../../interface/Control' import { IControlInstance } from '../../../../interface/Control'
import { IElement } from '../../../../interface/Element' import { IElement } from '../../../../interface/Element'
import { splitText } from '../../../../utils' import { splitText } from '../../../../utils'

@ -1,5 +1,5 @@
import { ControlComponent } from '../../../../dataset/enum/Control' import { ControlComponent } from '../../../../dataset/enum/Control'
import { KeyMap } from '../../../../dataset/enum/Keymap' import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { IControlInstance } from '../../../../interface/Control' import { IControlInstance } from '../../../../interface/Control'
import { IElement } from '../../../../interface/Element' import { IElement } from '../../../../interface/Element'
import { Control } from '../Control' import { Control } from '../Control'

@ -1,334 +1,15 @@
import { IImageParticleCreateResult } from '../../../interface/Draw'
import { IEditorOption } from '../../../interface/Editor' import { IEditorOption } from '../../../interface/Editor'
import { IElement, IElementPosition } from '../../../interface/Element' import { IElement } from '../../../interface/Element'
import { downloadFile } from '../../../utils'
import { Draw } from '../Draw' import { Draw } from '../Draw'
export class ImageParticle { export class ImageParticle {
private container: HTMLDivElement protected options: Required<IEditorOption>
private canvas: HTMLCanvasElement protected imageCache: Map<string, HTMLImageElement>
private draw: Draw
private options: Required<IEditorOption>
private curElement: IElement | null
private curPosition: IElementPosition | null
private imageCache: Map<string, HTMLImageElement>
// 拖拽改变尺寸
private resizerSelection: HTMLDivElement
private resizerHandleList: HTMLDivElement[]
private resizerImageContainer: HTMLDivElement
private resizerImage: HTMLImageElement
private width: number
private height: number
private mousedownX: number
private mousedownY: number
private curHandleIndex: number
// 预览选区
private previewerContainer: HTMLDivElement | null
private previewerImage: HTMLImageElement | null
constructor(draw: Draw) { constructor(draw: Draw) {
this.container = draw.getContainer()
this.canvas = draw.getPage()
this.draw = draw
this.options = draw.getOptions() this.options = draw.getOptions()
this.curElement = null
this.curPosition = null
this.imageCache = new Map() this.imageCache = new Map()
// 图片尺寸缩放
const { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage } = this._createResizerDom()
this.resizerSelection = resizerSelection
this.resizerHandleList = resizerHandleList
this.resizerImageContainer = resizerImageContainer
this.resizerImage = resizerImage
this.width = 0
this.height = 0
this.mousedownX = 0
this.mousedownY = 0
this.curHandleIndex = 0 // 默认右下角
// 图片预览
resizerSelection.ondblclick = this._dblclick.bind(this)
this.previewerContainer = null
this.previewerImage = null
}
private _createResizerDom(): IImageParticleCreateResult {
// 拖拽边框
const resizerSelection = document.createElement('div')
resizerSelection.classList.add('resizer-selection')
resizerSelection.style.display = 'none'
resizerSelection.style.borderColor = this.options.resizerColor
const resizerHandleList: HTMLDivElement[] = []
for (let i = 0; i < 8; i++) {
const handleDom = document.createElement('div')
handleDom.style.background = this.options.resizerColor
handleDom.classList.add(`handle-${i}`)
handleDom.setAttribute('data-index', String(i))
handleDom.onmousedown = this._mousedown.bind(this)
resizerSelection.append(handleDom)
resizerHandleList.push(handleDom)
}
this.container.append(resizerSelection)
// 拖拽镜像
const resizerImageContainer = document.createElement('div')
resizerImageContainer.classList.add('resizer-image')
resizerImageContainer.style.display = 'none'
const resizerImage = document.createElement('img')
resizerImageContainer.append(resizerImage)
this.container.append(resizerImageContainer)
return { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage }
}
private _mousedown(evt: MouseEvent) {
this.canvas = this.draw.getPage()
if (!this.curPosition || !this.curElement) return
const { scale } = this.options
const height = this.draw.getHeight()
const pageGap = this.draw.getPageGap()
this.mousedownX = evt.x
this.mousedownY = evt.y
const target = evt.target as HTMLDivElement
this.curHandleIndex = Number(target.dataset.index)
// 改变光标
const cursor = window.getComputedStyle(target).cursor
document.body.style.cursor = cursor
this.canvas.style.cursor = cursor
// 拖拽图片镜像
this.resizerImage.src = this.curElement?.value || ''
this.resizerImageContainer.style.display = 'block'
const { coordinate: { leftTop: [left, top] } } = this.curPosition
const prePageHeight = this.draw.getPageNo() * (height + pageGap)
this.resizerImageContainer.style.left = `${left}px`
this.resizerImageContainer.style.top = `${top + prePageHeight}px`
this.resizerImage.style.width = `${this.curElement.width! * scale}px`
this.resizerImage.style.height = `${this.curElement.height! * scale}px`
// 追加全局事件
const mousemoveFn = this._mousemove.bind(this)
document.addEventListener('mousemove', mousemoveFn)
document.addEventListener('mouseup', () => {
// 改变尺寸
if (this.curElement && this.curPosition) {
this.curElement.width = this.width
this.curElement.height = this.height
this.draw.render({ isSetCursor: false })
this.drawResizer(this.curElement, this.curPosition)
}
// 还原副作用
this.resizerImageContainer.style.display = 'none'
document.removeEventListener('mousemove', mousemoveFn)
document.body.style.cursor = ''
this.canvas.style.cursor = 'text'
}, {
once: true
})
evt.preventDefault()
}
private _mousemove(evt: MouseEvent) {
if (!this.curElement) return
const { scale } = this.options
let dx = 0
let dy = 0
switch (this.curHandleIndex) {
case 0:
dx = this.mousedownX - evt.x
dy = this.mousedownY - evt.y
break
case 1:
dy = this.mousedownY - evt.y
break
case 2:
dx = evt.x - this.mousedownX
dy = this.mousedownY - evt.y
break
case 3:
dx = evt.x - this.mousedownX
break
case 5:
dy = evt.y - this.mousedownY
break
case 6:
dx = this.mousedownX - evt.x
dy = evt.y - this.mousedownY
break
case 7:
dx = this.mousedownX - evt.x
break
default:
dx = evt.x - this.mousedownX
dy = evt.y - this.mousedownY
break
}
this.width = this.curElement.width! + dx
this.height = this.curElement.height! + dy
this.resizerImage.style.width = `${this.width * scale}px`
this.resizerImage.style.height = `${this.height * scale}px`
evt.preventDefault()
}
private _dblclick() {
this._drawPreviewer()
document.body.style.overflow = 'hidden'
}
private _drawPreviewer() {
const previewerContainer = document.createElement('div')
previewerContainer.classList.add('image-previewer')
// 关闭按钮
const closeBtn = document.createElement('i')
closeBtn.classList.add('image-close')
closeBtn.onclick = () => {
this._clearPreviewer()
}
previewerContainer.append(closeBtn)
// 图片
const imgContainer = document.createElement('div')
imgContainer.classList.add('image-container')
const img = document.createElement('img')
img.src = this.curElement?.value || ''
img.draggable = false
imgContainer.append(img)
this.previewerImage = img
previewerContainer.append(imgContainer)
// 操作栏
let x = 0
let y = 0
let scaleSize = 1
let rotateSize = 0
const menuContainer = document.createElement('div')
menuContainer.classList.add('image-menu')
const zoomIn = document.createElement('i')
zoomIn.classList.add('zoom-in')
zoomIn.onclick = () => {
scaleSize += 0.1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(zoomIn)
const zoomOut = document.createElement('i')
zoomOut.onclick = () => {
if (scaleSize - 0.1 <= 0.1) return
scaleSize -= 0.1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
zoomOut.classList.add('zoom-out')
menuContainer.append(zoomOut)
const rotate = document.createElement('i')
rotate.classList.add('rotate')
rotate.onclick = () => {
rotateSize += 1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(rotate)
const originalSize = document.createElement('i')
originalSize.classList.add('original-size')
originalSize.onclick = () => {
x = 0
y = 0
scaleSize = 1
rotateSize = 0
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(originalSize)
const imageDownload = document.createElement('i')
imageDownload.classList.add('image-download')
imageDownload.onclick = () => {
downloadFile(img.src, `${this.curElement?.id}.png`)
}
menuContainer.append(imageDownload)
previewerContainer.append(menuContainer)
this.previewerContainer = previewerContainer
document.body.append(previewerContainer)
// 拖拽调整位置
let startX = 0
let startY = 0
let isAllowDrag = false
img.onmousedown = (evt) => {
isAllowDrag = true
startX = evt.x
startY = evt.y
previewerContainer.style.cursor = 'move'
}
previewerContainer.onmousemove = (evt: MouseEvent) => {
if (!isAllowDrag) return
x += (evt.x - startX)
y += (evt.y - startY)
startX = evt.x
startY = evt.y
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
previewerContainer.onmouseup = () => {
isAllowDrag = false
previewerContainer.style.cursor = 'auto'
}
previewerContainer.onwheel = (evt) => {
evt.preventDefault()
if (evt.deltaY < 0) {
// 放大
scaleSize += 0.1
} else {
// 缩小
if (scaleSize - 0.1 <= 0.1) return
scaleSize -= 0.1
}
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
}
public _setPreviewerTransform(scale: number, rotate: number, x: number, y: number) {
if (!this.previewerImage) return
this.previewerImage.style.left = `${x}px`
this.previewerImage.style.top = `${y}px`
this.previewerImage.style.transform = `scale(${scale}) rotate(${rotate * 90}deg)`
}
private _clearPreviewer() {
this.previewerContainer?.remove()
this.previewerContainer = null
document.body.style.overflow = 'auto'
}
public getImageCache(): Map<string, HTMLImageElement> {
return this.imageCache
}
public drawResizer(element: IElement, position: IElementPosition) {
const { scale } = this.options
const { coordinate: { leftTop: [left, top] } } = position
const elementWidth = element.width! * scale
const elementHeight = element.height! * scale
const height = this.draw.getHeight()
const pageGap = this.draw.getPageGap()
const handleSize = this.options.resizerSize
const preY = this.draw.getPageNo() * (height + pageGap)
// 边框
this.resizerSelection.style.left = `${left}px`
this.resizerSelection.style.top = `${top + preY}px`
this.resizerSelection.style.width = `${elementWidth}px`
this.resizerSelection.style.height = `${elementHeight}px`
// handle
for (let i = 0; i < 8; i++) {
const left = i === 0 || i === 6 || i === 7
? -handleSize
: i === 1 || i === 5
? elementWidth / 2
: elementWidth - handleSize
const top = i === 0 || i === 1 || i === 2
? -handleSize
: i === 3 || i === 7
? elementHeight / 2 - handleSize
: elementHeight - handleSize
this.resizerHandleList[i].style.left = `${left}px`
this.resizerHandleList[i].style.top = `${top}px`
}
this.resizerSelection.style.display = 'block'
this.curElement = element
this.curPosition = position
this.width = this.curElement.width! * scale
this.height = this.curElement.height! * scale
}
public clearResizer() {
this.resizerSelection.style.display = 'none'
} }
public render(ctx: CanvasRenderingContext2D, element: IElement, x: number, y: number) { public render(ctx: CanvasRenderingContext2D, element: IElement, x: number, y: number) {

@ -0,0 +1,34 @@
import { IElement } from '../../../../interface/Element'
import { ImageParticle } from '../ImageParticle'
import { LaTexSVG, LaTexUtils } from './utils/LaTeXUtils'
export class LaTexParticle extends ImageParticle {
public static convertLaTextToSVG(laTex: string): LaTexSVG {
return new LaTexUtils(laTex).svg({
SCALE_X: 10,
SCALE_Y: 10,
MARGIN_X: 0,
MARGIN_Y: 0
})
}
public render(ctx: CanvasRenderingContext2D, element: IElement, x: number, y: number) {
const { scale } = this.options
const width = element.width! * scale
const height = element.height! * scale
if (this.imageCache.has(element.value)) {
const img = this.imageCache.get(element.value)!
ctx.drawImage(img, x, y, width, height)
} else {
const img = new Image()
img.src = element.laTexSVG!
img.onload = () => {
ctx.drawImage(img, x, y, width, height)
this.imageCache.set(element.value, img)
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,298 @@
/*
https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols
https://en.wikibooks.org/wiki/LaTeX/Mathematics
*/
export interface Symb {
glyph: number;
arity?: number;
flags: Record<string, boolean>;
}
const SYMB: Record<string, Symb> = {
'\\frac': { glyph: 0, arity: 2, flags: {} },
'\\binom': { glyph: 0, arity: 2, flags: {} },
'\\sqrt': { glyph: 2267, arity: 1, flags: { opt: true, xfl: true, yfl: true } },
'^': { glyph: 0, arity: 1, flags: {} },
'_': { glyph: 0, arity: 1, flags: {} },
'(': { glyph: 2221, arity: 0, flags: { yfl: true } },
')': { glyph: 2222, arity: 0, flags: { yfl: true } },
'[': { glyph: 2223, arity: 0, flags: { yfl: true } },
']': { glyph: 2224, arity: 0, flags: { yfl: true } },
'\\langle': { glyph: 2227, arity: 0, flags: { yfl: true } },
'\\rangle': { glyph: 2228, arity: 0, flags: { yfl: true } },
'|': { glyph: 2229, arity: 0, flags: { yfl: true } },
'\\|': { glyph: 2230, arity: 0, flags: { yfl: true } },
'\\{': { glyph: 2225, arity: 0, flags: { yfl: true } },
'\\}': { glyph: 2226, arity: 0, flags: { yfl: true } },
'\\#': { glyph: 2275, arity: 0, flags: {} },
'\\$': { glyph: 2274, arity: 0, flags: {} },
'\\&': { glyph: 2273, arity: 0, flags: {} },
'\\%': { glyph: 2271, arity: 0, flags: {} },
/*semantics*/
'\\begin': { glyph: 0, arity: 1, flags: {} },
'\\end': { glyph: 0, arity: 1, flags: {} },
'\\left': { glyph: 0, arity: 1, flags: {} },
'\\right': { glyph: 0, arity: 1, flags: {} },
'\\middle': { glyph: 0, arity: 1, flags: {} },
/*operators*/
'\\cdot': { glyph: 2236, arity: 0, flags: {} },
'\\pm': { glyph: 2233, arity: 0, flags: {} },
'\\mp': { glyph: 2234, arity: 0, flags: {} },
'\\times': { glyph: 2235, arity: 0, flags: {} },
'\\div': { glyph: 2237, arity: 0, flags: {} },
'\\leqq': { glyph: 2243, arity: 0, flags: {} },
'\\geqq': { glyph: 2244, arity: 0, flags: {} },
'\\leq': { glyph: 2243, arity: 0, flags: {} },
'\\geq': { glyph: 2244, arity: 0, flags: {} },
'\\propto': { glyph: 2245, arity: 0, flags: {} },
'\\sim': { glyph: 2246, arity: 0, flags: {} },
'\\equiv': { glyph: 2240, arity: 0, flags: {} },
'\\dagger': { glyph: 2277, arity: 0, flags: {} },
'\\ddagger': { glyph: 2278, arity: 0, flags: {} },
'\\ell': { glyph: 662, arity: 0, flags: {} },
/*accents*/
'\\vec': { glyph: 2261, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
'\\overrightarrow': { glyph: 2261, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
'\\overleftarrow': { glyph: 2263, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
'\\bar': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } },
'\\overline': { glyph: 2231, arity: 1, flags: { hat: true, xfl: true } },
'\\widehat': { glyph: 2247, arity: 1, flags: { hat: true, xfl: true, yfl: true } },
'\\hat': { glyph: 2247, arity: 1, flags: { hat: true } },
'\\acute': { glyph: 2248, arity: 1, flags: { hat: true } },
'\\grave': { glyph: 2249, arity: 1, flags: { hat: true } },
'\\breve': { glyph: 2250, arity: 1, flags: { hat: true } },
'\\tilde': { glyph: 2246, arity: 1, flags: { hat: true } },
'\\underline': { glyph: 2231, arity: 1, flags: { mat: true, xfl: true } },
'\\not': { glyph: 2220, arity: 1, flags: {} },
'\\neq': { glyph: 2239, arity: 1, flags: {} },
'\\ne': { glyph: 2239, arity: 1, flags: {} },
'\\exists': { glyph: 2279, arity: 0, flags: {} },
'\\in': { glyph: 2260, arity: 0, flags: {} },
'\\subset': { glyph: 2256, arity: 0, flags: {} },
'\\supset': { glyph: 2258, arity: 0, flags: {} },
'\\cup': { glyph: 2257, arity: 0, flags: {} },
'\\cap': { glyph: 2259, arity: 0, flags: {} },
'\\infty': { glyph: 2270, arity: 0, flags: {} },
'\\partial': { glyph: 2265, arity: 0, flags: {} },
'\\nabla': { glyph: 2266, arity: 0, flags: {} },
'\\aleph': { glyph: 2077, arity: 0, flags: {} },
'\\wp': { glyph: 2190, arity: 0, flags: {} },
'\\therefore': { glyph: 740, arity: 0, flags: {} },
'\\mid': { glyph: 2229, arity: 0, flags: {} },
'\\sum': { glyph: 2402, arity: 0, flags: { big: true } },
'\\prod': { glyph: 2401, arity: 0, flags: { big: true } },
'\\bigoplus': { glyph: 2284, arity: 0, flags: { big: true } },
'\\bigodot': { glyph: 2281, arity: 0, flags: { big: true } },
'\\int': { glyph: 2412, arity: 0, flags: { yfl: true } },
'\\oint': { glyph: 2269, arity: 0, flags: { yfl: true } },
'\\oplus': { glyph: 1284, arity: 0, flags: {} },
'\\odot': { glyph: 1281, arity: 0, flags: {} },
'\\perp': { glyph: 738, arity: 0, flags: {} },
'\\angle': { glyph: 739, arity: 0, flags: {} },
'\\triangle': { glyph: 842, arity: 0, flags: {} },
'\\Box': { glyph: 841, arity: 0, flags: {} },
'\\rightarrow': { glyph: 2261, arity: 0, flags: {} },
'\\to': { glyph: 2261, arity: 0, flags: {} },
'\\leftarrow': { glyph: 2263, arity: 0, flags: {} },
'\\gets': { glyph: 2263, arity: 0, flags: {} },
'\\circ': { glyph: 902, arity: 0, flags: {} },
'\\bigcirc': { glyph: 904, arity: 0, flags: {} },
'\\bullet': { glyph: 828, arity: 0, flags: {} },
'\\star': { glyph: 856, arity: 0, flags: {} },
'\\diamond': { glyph: 743, arity: 0, flags: {} },
'\\ast': { glyph: 728, arity: 0, flags: {} },
/*verbatim symbols*/
'\\log': { glyph: 0, arity: 0, flags: { txt: true } },
'\\ln': { glyph: 0, arity: 0, flags: { txt: true } },
'\\exp': { glyph: 0, arity: 0, flags: { txt: true } },
'\\mod': { glyph: 0, arity: 0, flags: { txt: true } },
'\\lim': { glyph: 0, arity: 0, flags: { txt: true, big: true } },
'\\sin': { glyph: 0, arity: 0, flags: { txt: true } },
'\\cos': { glyph: 0, arity: 0, flags: { txt: true } },
'\\tan': { glyph: 0, arity: 0, flags: { txt: true } },
'\\csc': { glyph: 0, arity: 0, flags: { txt: true } },
'\\sec': { glyph: 0, arity: 0, flags: { txt: true } },
'\\cot': { glyph: 0, arity: 0, flags: { txt: true } },
'\\sinh': { glyph: 0, arity: 0, flags: { txt: true } },
'\\cosh': { glyph: 0, arity: 0, flags: { txt: true } },
'\\tanh': { glyph: 0, arity: 0, flags: { txt: true } },
'\\csch': { glyph: 0, arity: 0, flags: { txt: true } },
'\\sech': { glyph: 0, arity: 0, flags: { txt: true } },
'\\coth': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arcsin': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arccos': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arctan': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arccsc': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arcsec': { glyph: 0, arity: 0, flags: { txt: true } },
'\\arccot': { glyph: 0, arity: 0, flags: { txt: true } },
/*font modes*/
'\\text': { glyph: 0, arity: 1, flags: {} },
'\\mathnormal': { glyph: 0, arity: 1, flags: {} },
'\\mathrm': { glyph: 0, arity: 1, flags: {} },
'\\mathit': { glyph: 0, arity: 1, flags: {} },
'\\mathbf': { glyph: 0, arity: 1, flags: {} },
'\\mathsf': { glyph: 0, arity: 1, flags: {} },
'\\mathtt': { glyph: 0, arity: 1, flags: {} },
'\\mathfrak': { glyph: 0, arity: 1, flags: {} },
'\\mathcal': { glyph: 0, arity: 1, flags: {} },
'\\mathbb': { glyph: 0, arity: 1, flags: {} },
'\\mathscr': { glyph: 0, arity: 1, flags: {} },
'\\rm': { glyph: 0, arity: 1, flags: {} },
'\\it': { glyph: 0, arity: 1, flags: {} },
'\\bf': { glyph: 0, arity: 1, flags: {} },
'\\sf': { glyph: 0, arity: 1, flags: {} },
'\\tt': { glyph: 0, arity: 1, flags: {} },
'\\frak': { glyph: 0, arity: 1, flags: {} },
'\\cal': { glyph: 0, arity: 1, flags: {} },
'\\bb': { glyph: 0, arity: 1, flags: {} },
'\\scr': { glyph: 0, arity: 1, flags: {} },
'\\quad': { glyph: 0, arity: 0, flags: {} },
'\\,': { glyph: 0, arity: 0, flags: {} },
'\\.': { glyph: 0, arity: 0, flags: {} },
'\\;': { glyph: 0, arity: 0, flags: {} },
'\\!': { glyph: 0, arity: 0, flags: {} },
/*greek letters*/
'\\alpha': { glyph: 2127, flags: {} },
'\\beta': { glyph: 2128, flags: {} },
'\\gamma': { glyph: 2129, flags: {} },
'\\delta': { glyph: 2130, flags: {} },
'\\varepsilon': { glyph: 2131, flags: {} },
'\\zeta': { glyph: 2132, flags: {} },
'\\eta': { glyph: 2133, flags: {} },
'\\vartheta': { glyph: 2134, flags: {} },
'\\iota': { glyph: 2135, flags: {} },
'\\kappa': { glyph: 2136, flags: {} },
'\\lambda': { glyph: 2137, flags: {} },
'\\mu': { glyph: 2138, flags: {} },
'\\nu': { glyph: 2139, flags: {} },
'\\xi': { glyph: 2140, flags: {} },
'\\omicron': { glyph: 2141, flags: {} },
'\\pi': { glyph: 2142, flags: {} },
'\\rho': { glyph: 2143, flags: {} },
'\\sigma': { glyph: 2144, flags: {} },
'\\tau': { glyph: 2145, flags: {} },
'\\upsilon': { glyph: 2146, flags: {} },
'\\varphi': { glyph: 2147, flags: {} },
'\\chi': { glyph: 2148, flags: {} },
'\\psi': { glyph: 2149, flags: {} },
'\\omega': { glyph: 2150, flags: {} },
'\\epsilon': { glyph: 2184, flags: {} },
'\\theta': { glyph: 2185, flags: {} },
'\\phi': { glyph: 2186, flags: {} },
'\\varsigma': { glyph: 2187, flags: {} },
'\\Alpha': { glyph: 2027, flags: {} },
'\\Beta': { glyph: 2028, flags: {} },
'\\Gamma': { glyph: 2029, flags: {} },
'\\Delta': { glyph: 2030, flags: {} },
'\\Epsilon': { glyph: 2031, flags: {} },
'\\Zeta': { glyph: 2032, flags: {} },
'\\Eta': { glyph: 2033, flags: {} },
'\\Theta': { glyph: 2034, flags: {} },
'\\Iota': { glyph: 2035, flags: {} },
'\\Kappa': { glyph: 2036, flags: {} },
'\\Lambda': { glyph: 2037, flags: {} },
'\\Mu': { glyph: 2038, flags: {} },
'\\Nu': { glyph: 2039, flags: {} },
'\\Xi': { glyph: 2040, flags: {} },
'\\Omicron': { glyph: 2041, flags: {} },
'\\Pi': { glyph: 2042, flags: {} },
'\\Rho': { glyph: 2043, flags: {} },
'\\Sigma': { glyph: 2044, flags: {} },
'\\Tau': { glyph: 2045, flags: {} },
'\\Upsilon': { glyph: 2046, flags: {} },
'\\Phi': { glyph: 2047, flags: {} },
'\\Chi': { glyph: 2048, flags: {} },
'\\Psi': { glyph: 2049, flags: {} },
'\\Omega': { glyph: 2050, flags: {} },
}
export { SYMB }
export function asciiMap(x: string, mode = 'math'): number {
const c = x.charCodeAt(0)
if (65 <= c && c <= 90) {
const d = c - 65
if (mode == 'text' || mode == 'rm') {
return d + 2001
} else if (mode == 'tt') {
return d + 501
} else if (mode == 'bf' || mode == 'bb') {
return d + 3001
} else if (mode == 'sf') {
return d + 2501
} else if (mode == 'frak') {
return d + 3301
} else if (mode == 'scr' || mode == 'cal') {
return d + 2551
} else {
return d + 2051
}
}
if (97 <= c && c <= 122) {
const d = c - 97
if (mode == 'text' || mode == 'rm') {
return d + 2101
} else if (mode == 'tt') {
return d + 601
} else if (mode == 'bf' || mode == 'bb') {
return d + 3101
} else if (mode == 'sf') {
return d + 2601
} else if (mode == 'frak') {
return d + 3401
} else if (mode == 'scr' || mode == 'cal') {
return d + 2651
} else {
return d + 2151
}
}
if (48 <= c && c <= 57) {
const d = c - 48
if (mode == 'it') {
return d + 2750
} else if (mode == 'bf') {
return d + 3200
} else if (mode == 'tt') {
return d + 700
} else {
return d + 2200
}
}
return <number>{
'.': 2210,
',': 2211,
':': 2212,
';': 2213,
'!': 2214,
'?': 2215,
'\'': 2216,
'"': 2217,
'*': 2219,
'/': 2220,
'-': 2231,
'+': 2232,
'=': 2238,
'<': 2241,
'>': 2242,
'~': 2246,
'@': 2273,
'\\': 804,
}[x]
}

@ -0,0 +1,335 @@
import { IEditorOption } from '../../../../interface/Editor'
import { IElement, IElementPosition } from '../../../../interface/Element'
import { IPreviewerCreateResult, IPreviewerDrawOption } from '../../../../interface/Previewer'
import { downloadFile } from '../../../../utils'
import { Draw } from '../../Draw'
export class Previewer {
private container: HTMLDivElement
private canvas: HTMLCanvasElement
private draw: Draw
private options: Required<IEditorOption>
private curElement: IElement | null
private curElementSrc: string
private previewerDrawOption: IPreviewerDrawOption
private curPosition: IElementPosition | null
// 拖拽改变尺寸
private resizerSelection: HTMLDivElement
private resizerHandleList: HTMLDivElement[]
private resizerImageContainer: HTMLDivElement
private resizerImage: HTMLImageElement
private width: number
private height: number
private mousedownX: number
private mousedownY: number
private curHandleIndex: number
// 预览选区
private previewerContainer: HTMLDivElement | null
private previewerImage: HTMLImageElement | null
constructor(draw: Draw) {
this.container = draw.getContainer()
this.canvas = draw.getPage()
this.draw = draw
this.options = draw.getOptions()
this.curElement = null
this.curElementSrc = ''
this.previewerDrawOption = {}
this.curPosition = null
// 图片尺寸缩放
const { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage } = this._createResizerDom()
this.resizerSelection = resizerSelection
this.resizerHandleList = resizerHandleList
this.resizerImageContainer = resizerImageContainer
this.resizerImage = resizerImage
this.width = 0
this.height = 0
this.mousedownX = 0
this.mousedownY = 0
this.curHandleIndex = 0 // 默认右下角
// 图片预览
resizerSelection.ondblclick = this._dblclick.bind(this)
this.previewerContainer = null
this.previewerImage = null
}
private _createResizerDom(): IPreviewerCreateResult {
// 拖拽边框
const resizerSelection = document.createElement('div')
resizerSelection.classList.add('resizer-selection')
resizerSelection.style.display = 'none'
resizerSelection.style.borderColor = this.options.resizerColor
const resizerHandleList: HTMLDivElement[] = []
for (let i = 0; i < 8; i++) {
const handleDom = document.createElement('div')
handleDom.style.background = this.options.resizerColor
handleDom.classList.add(`handle-${i}`)
handleDom.setAttribute('data-index', String(i))
handleDom.onmousedown = this._mousedown.bind(this)
resizerSelection.append(handleDom)
resizerHandleList.push(handleDom)
}
this.container.append(resizerSelection)
// 拖拽镜像
const resizerImageContainer = document.createElement('div')
resizerImageContainer.classList.add('resizer-image')
resizerImageContainer.style.display = 'none'
const resizerImage = document.createElement('img')
resizerImageContainer.append(resizerImage)
this.container.append(resizerImageContainer)
return { resizerSelection, resizerHandleList, resizerImageContainer, resizerImage }
}
private _mousedown(evt: MouseEvent) {
this.canvas = this.draw.getPage()
if (!this.curPosition || !this.curElement) return
const { scale } = this.options
const height = this.draw.getHeight()
const pageGap = this.draw.getPageGap()
this.mousedownX = evt.x
this.mousedownY = evt.y
const target = evt.target as HTMLDivElement
this.curHandleIndex = Number(target.dataset.index)
// 改变光标
const cursor = window.getComputedStyle(target).cursor
document.body.style.cursor = cursor
this.canvas.style.cursor = cursor
// 拖拽图片镜像
this.resizerImage.src = this.curElementSrc
this.resizerImageContainer.style.display = 'block'
const { coordinate: { leftTop: [left, top] } } = this.curPosition
const prePageHeight = this.draw.getPageNo() * (height + pageGap)
this.resizerImageContainer.style.left = `${left}px`
this.resizerImageContainer.style.top = `${top + prePageHeight}px`
this.resizerImage.style.width = `${this.curElement.width! * scale}px`
this.resizerImage.style.height = `${this.curElement.height! * scale}px`
// 追加全局事件
const mousemoveFn = this._mousemove.bind(this)
document.addEventListener('mousemove', mousemoveFn)
document.addEventListener('mouseup', () => {
// 改变尺寸
if (this.curElement && this.curPosition) {
this.curElement.width = this.width
this.curElement.height = this.height
this.draw.render({ isSetCursor: false })
this.drawResizer(this.curElement, this.curPosition, this.previewerDrawOption)
}
// 还原副作用
this.resizerImageContainer.style.display = 'none'
document.removeEventListener('mousemove', mousemoveFn)
document.body.style.cursor = ''
this.canvas.style.cursor = 'text'
}, {
once: true
})
evt.preventDefault()
}
private _mousemove(evt: MouseEvent) {
if (!this.curElement) return
const { scale } = this.options
let dx = 0
let dy = 0
switch (this.curHandleIndex) {
case 0:
dx = this.mousedownX - evt.x
dy = this.mousedownY - evt.y
break
case 1:
dy = this.mousedownY - evt.y
break
case 2:
dx = evt.x - this.mousedownX
dy = this.mousedownY - evt.y
break
case 3:
dx = evt.x - this.mousedownX
break
case 5:
dy = evt.y - this.mousedownY
break
case 6:
dx = this.mousedownX - evt.x
dy = evt.y - this.mousedownY
break
case 7:
dx = this.mousedownX - evt.x
break
default:
dx = evt.x - this.mousedownX
dy = evt.y - this.mousedownY
break
}
this.width = this.curElement.width! + dx
this.height = this.curElement.height! + dy
this.resizerImage.style.width = `${this.width * scale}px`
this.resizerImage.style.height = `${this.height * scale}px`
evt.preventDefault()
}
private _dblclick() {
this._drawPreviewer()
document.body.style.overflow = 'hidden'
}
private _drawPreviewer() {
const previewerContainer = document.createElement('div')
previewerContainer.classList.add('image-previewer')
// 关闭按钮
const closeBtn = document.createElement('i')
closeBtn.classList.add('image-close')
closeBtn.onclick = () => {
this._clearPreviewer()
}
previewerContainer.append(closeBtn)
// 图片
const imgContainer = document.createElement('div')
imgContainer.classList.add('image-container')
const img = document.createElement('img')
img.src = this.curElementSrc
img.draggable = false
imgContainer.append(img)
this.previewerImage = img
previewerContainer.append(imgContainer)
// 操作栏
let x = 0
let y = 0
let scaleSize = 1
let rotateSize = 0
const menuContainer = document.createElement('div')
menuContainer.classList.add('image-menu')
const zoomIn = document.createElement('i')
zoomIn.classList.add('zoom-in')
zoomIn.onclick = () => {
scaleSize += 0.1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(zoomIn)
const zoomOut = document.createElement('i')
zoomOut.onclick = () => {
if (scaleSize - 0.1 <= 0.1) return
scaleSize -= 0.1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
zoomOut.classList.add('zoom-out')
menuContainer.append(zoomOut)
const rotate = document.createElement('i')
rotate.classList.add('rotate')
rotate.onclick = () => {
rotateSize += 1
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(rotate)
const originalSize = document.createElement('i')
originalSize.classList.add('original-size')
originalSize.onclick = () => {
x = 0
y = 0
scaleSize = 1
rotateSize = 0
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
menuContainer.append(originalSize)
const imageDownload = document.createElement('i')
imageDownload.classList.add('image-download')
imageDownload.onclick = () => {
const { mime } = this.previewerDrawOption
downloadFile(img.src, `${this.curElement?.id}.${mime || 'png'}`)
}
menuContainer.append(imageDownload)
previewerContainer.append(menuContainer)
this.previewerContainer = previewerContainer
document.body.append(previewerContainer)
// 拖拽调整位置
let startX = 0
let startY = 0
let isAllowDrag = false
img.onmousedown = (evt) => {
isAllowDrag = true
startX = evt.x
startY = evt.y
previewerContainer.style.cursor = 'move'
}
previewerContainer.onmousemove = (evt: MouseEvent) => {
if (!isAllowDrag) return
x += (evt.x - startX)
y += (evt.y - startY)
startX = evt.x
startY = evt.y
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
previewerContainer.onmouseup = () => {
isAllowDrag = false
previewerContainer.style.cursor = 'auto'
}
previewerContainer.onwheel = (evt) => {
evt.preventDefault()
if (evt.deltaY < 0) {
// 放大
scaleSize += 0.1
} else {
// 缩小
if (scaleSize - 0.1 <= 0.1) return
scaleSize -= 0.1
}
this._setPreviewerTransform(scaleSize, rotateSize, x, y)
}
}
public _setPreviewerTransform(scale: number, rotate: number, x: number, y: number) {
if (!this.previewerImage) return
this.previewerImage.style.left = `${x}px`
this.previewerImage.style.top = `${y}px`
this.previewerImage.style.transform = `scale(${scale}) rotate(${rotate * 90}deg)`
}
private _clearPreviewer() {
this.previewerContainer?.remove()
this.previewerContainer = null
document.body.style.overflow = 'auto'
}
public drawResizer(element: IElement, position: IElementPosition, options: IPreviewerDrawOption = {}) {
this.previewerDrawOption = options
const { scale } = this.options
const { coordinate: { leftTop: [left, top] } } = position
const elementWidth = element.width! * scale
const elementHeight = element.height! * scale
const height = this.draw.getHeight()
const pageGap = this.draw.getPageGap()
const handleSize = this.options.resizerSize
const preY = this.draw.getPageNo() * (height + pageGap)
// 边框
this.resizerSelection.style.left = `${left}px`
this.resizerSelection.style.top = `${top + preY}px`
this.resizerSelection.style.width = `${elementWidth}px`
this.resizerSelection.style.height = `${elementHeight}px`
// handle
for (let i = 0; i < 8; i++) {
const left = i === 0 || i === 6 || i === 7
? -handleSize
: i === 1 || i === 5
? elementWidth / 2
: elementWidth - handleSize
const top = i === 0 || i === 1 || i === 2
? -handleSize
: i === 3 || i === 7
? elementHeight / 2 - handleSize
: elementHeight - handleSize
this.resizerHandleList[i].style.left = `${left}px`
this.resizerHandleList[i].style.top = `${top}px`
}
this.resizerSelection.style.display = 'block'
this.curElement = element
this.curElementSrc = element[options.srcKey || 'value'] || ''
this.curPosition = position
this.width = this.curElement.width! * scale
this.height = this.curElement.height! * scale
}
public clearResizer() {
this.resizerSelection.style.display = 'none'
}
}

@ -3,14 +3,13 @@ import { ZERO } from '../../dataset/constant/Common'
import { EDITOR_ELEMENT_COPY_ATTR } from '../../dataset/constant/Element' import { EDITOR_ELEMENT_COPY_ATTR } from '../../dataset/constant/Element'
import { ElementStyleKey } from '../../dataset/enum/ElementStyle' import { ElementStyleKey } from '../../dataset/enum/ElementStyle'
import { MouseEventButton } from '../../dataset/enum/Event' import { MouseEventButton } from '../../dataset/enum/Event'
import { KeyMap } from '../../dataset/enum/Keymap' import { KeyMap } from '../../dataset/enum/KeyMap'
import { IElement } from '../../interface/Element' import { IElement } from '../../interface/Element'
import { ICurrentPosition } from '../../interface/Position' import { ICurrentPosition } from '../../interface/Position'
import { writeElementList } from '../../utils/clipboard' import { writeElementList } from '../../utils/clipboard'
import { Cursor } from '../cursor/Cursor' import { Cursor } from '../cursor/Cursor'
import { Draw } from '../draw/Draw' import { Draw } from '../draw/Draw'
import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle' import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle'
import { ImageParticle } from '../draw/particle/ImageParticle'
import { TableTool } from '../draw/particle/table/TableTool' import { TableTool } from '../draw/particle/table/TableTool'
import { HistoryManager } from '../history/HistoryManager' import { HistoryManager } from '../history/HistoryManager'
import { Listener } from '../listener/Listener' import { Listener } from '../listener/Listener'
@ -20,6 +19,7 @@ import { LETTER_REG, NUMBER_LIKE_REG } from '../../dataset/constant/Regular'
import { Control } from '../draw/control/Control' import { Control } from '../draw/control/Control'
import { CheckboxControl } from '../draw/control/checkbox/CheckboxControl' import { CheckboxControl } from '../draw/control/checkbox/CheckboxControl'
import { splitText } from '../../utils' import { splitText } from '../../utils'
import { Previewer } from '../draw/particle/previewer/Previewer'
export class CanvasEvent { export class CanvasEvent {
@ -34,7 +34,7 @@ export class CanvasEvent {
private range: RangeManager private range: RangeManager
private cursor: Cursor | null private cursor: Cursor | null
private historyManager: HistoryManager private historyManager: HistoryManager
private imageParticle: ImageParticle private previewer: Previewer
private tableTool: TableTool private tableTool: TableTool
private hyperlinkParticle: HyperlinkParticle private hyperlinkParticle: HyperlinkParticle
private listener: Listener private listener: Listener
@ -52,7 +52,7 @@ export class CanvasEvent {
this.position = this.draw.getPosition() this.position = this.draw.getPosition()
this.range = this.draw.getRange() this.range = this.draw.getRange()
this.historyManager = this.draw.getHistoryManager() this.historyManager = this.draw.getHistoryManager()
this.imageParticle = this.draw.getImageParticle() this.previewer = this.draw.getPreviewer()
this.tableTool = this.draw.getTableTool() this.tableTool = this.draw.getTableTool()
this.hyperlinkParticle = this.draw.getHyperlinkParticle() this.hyperlinkParticle = this.draw.getHyperlinkParticle()
this.listener = this.draw.getListener() this.listener = this.draw.getListener()
@ -258,10 +258,16 @@ export class CanvasEvent {
isComputeRowList: false isComputeRowList: false
}) })
} }
// 图片尺寸拖拽组件 // 预览工具组件
this.imageParticle.clearResizer() this.previewer.clearResizer()
if (isDirectHitImage && !isReadonly) { if (isDirectHitImage && !isReadonly) {
this.imageParticle.drawResizer(curElement, positionList[curIndex]) this.previewer.drawResizer(curElement, positionList[curIndex],
curElement.type === ElementType.LATEX
? {
mime: 'svg',
srcKey: 'laTexSVG'
}
: {})
} }
// 表格工具组件 // 表格工具组件
this.tableTool.dispose() this.tableTool.dispose()

@ -5,7 +5,7 @@ import { Cursor } from '../cursor/Cursor'
import { Control } from '../draw/control/Control' import { Control } from '../draw/control/Control'
import { Draw } from '../draw/Draw' import { Draw } from '../draw/Draw'
import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle' import { HyperlinkParticle } from '../draw/particle/HyperlinkParticle'
import { ImageParticle } from '../draw/particle/ImageParticle' import { Previewer } from '../draw/particle/previewer/Previewer'
import { TableTool } from '../draw/particle/table/TableTool' import { TableTool } from '../draw/particle/table/TableTool'
import { RangeManager } from '../range/RangeManager' import { RangeManager } from '../range/RangeManager'
import { CanvasEvent } from './CanvasEvent' import { CanvasEvent } from './CanvasEvent'
@ -18,7 +18,7 @@ export class GlobalEvent {
private cursor: Cursor | null private cursor: Cursor | null
private canvasEvent: CanvasEvent private canvasEvent: CanvasEvent
private range: RangeManager private range: RangeManager
private imageParticle: ImageParticle private previewer: Previewer
private tableTool: TableTool private tableTool: TableTool
private hyperlinkParticle: HyperlinkParticle private hyperlinkParticle: HyperlinkParticle
private control: Control private control: Control
@ -30,7 +30,7 @@ export class GlobalEvent {
this.canvasEvent = canvasEvent this.canvasEvent = canvasEvent
this.cursor = null this.cursor = null
this.range = draw.getRange() this.range = draw.getRange()
this.imageParticle = draw.getImageParticle() this.previewer = draw.getPreviewer()
this.tableTool = draw.getTableTool() this.tableTool = draw.getTableTool()
this.hyperlinkParticle = draw.getHyperlinkParticle() this.hyperlinkParticle = draw.getHyperlinkParticle()
this.control = draw.getControl() this.control = draw.getControl()
@ -74,7 +74,7 @@ export class GlobalEvent {
this.cursor.recoveryCursor() this.cursor.recoveryCursor()
this.range.recoveryRangeStyle() this.range.recoveryRangeStyle()
this.range.setRange(-1, -1) this.range.setRange(-1, -1)
this.imageParticle.clearResizer() this.previewer.clearResizer()
this.tableTool.dispose() this.tableTool.dispose()
this.hyperlinkParticle.clearHyperlinkPopup() this.hyperlinkParticle.clearHyperlinkPopup()
this.control.destroyControl() this.control.destroyControl()

@ -116,7 +116,7 @@ export class Position {
} }
} }
// 图片区域均为命中 // 图片区域均为命中
if (element.type === ElementType.IMAGE) { if (element.type === ElementType.IMAGE || element.type === ElementType.LATEX) {
return { return {
index: curPositionIndex, index: curPositionIndex,
isDirectHit: true, isDirectHit: true,

@ -8,5 +8,6 @@ export enum ElementType {
SEPARATOR = 'separator', SEPARATOR = 'separator',
PAGE_BREAK = 'pageBreak', PAGE_BREAK = 'pageBreak',
CONTROL = 'control', CONTROL = 'control',
CHECKBOX = 'checkbox' CHECKBOX = 'checkbox',
LATEX = 'latex'
} }

@ -14,13 +14,6 @@ export interface IDrawImagePayload {
value: string; value: string;
} }
export interface IImageParticleCreateResult {
resizerSelection: HTMLDivElement;
resizerHandleList: HTMLDivElement[];
resizerImageContainer: HTMLDivElement;
resizerImage: HTMLImageElement;
}
export interface IDrawRowPayload { export interface IDrawRowPayload {
positionList: IElementPosition[]; positionList: IElementPosition[];
rowList: IRow[]; rowList: IRow[];

@ -65,6 +65,10 @@ export interface ICheckboxElement {
checkbox?: ICheckbox; checkbox?: ICheckbox;
} }
export interface ILaTexElement {
laTexSVG?: string;
}
export type IElement = IElementBasic export type IElement = IElementBasic
& IElementStyle & IElementStyle
& ITable & ITable
@ -73,6 +77,7 @@ export type IElement = IElementBasic
& ISeparator & ISeparator
& IControlElement & IControlElement
& ICheckboxElement & ICheckboxElement
& ILaTexElement
export interface IElementMetrics { export interface IElementMetrics {
width: number; width: number;

@ -0,0 +1,13 @@
import { IElement } from './Element'
export interface IPreviewerCreateResult {
resizerSelection: HTMLDivElement;
resizerHandleList: HTMLDivElement[];
resizerImageContainer: HTMLDivElement;
resizerImage: HTMLImageElement;
}
export interface IPreviewerDrawOption {
mime?: 'png' | 'jpg' | 'jpeg' | 'svg';
srcKey?: keyof Pick<IElement, 'value' | 'laTexSVG'>;
}

@ -1,5 +1,6 @@
import { deepClone, getUUID, splitText } from '.' import { deepClone, getUUID, splitText } from '.'
import { ElementType, IEditorOption, IElement } from '..' import { ElementType, IEditorOption, IElement } from '..'
import { LaTexParticle } from '../core/draw/particle/latex/LaTexParticle'
import { defaultCheckboxOption } from '../dataset/constant/Checkbox' import { defaultCheckboxOption } from '../dataset/constant/Checkbox'
import { ZERO } from '../dataset/constant/Common' import { ZERO } from '../dataset/constant/Common'
import { defaultControlOption } from '../dataset/constant/Control' import { defaultControlOption } from '../dataset/constant/Control'
@ -214,6 +215,13 @@ export function formatElementList(elementList: IElement[], options: IFormatEleme
if (el.type === ElementType.IMAGE) { if (el.type === ElementType.IMAGE) {
el.id = getUUID() el.id = getUUID()
} }
if (el.type === ElementType.LATEX) {
const { svg, width, height } = LaTexParticle.convertLaTextToSVG(el.value)
el.width = el.width || width
el.height = el.height || height
el.laTexSVG = svg
el.id = getUUID()
}
i++ i++
} }
} }

@ -156,7 +156,7 @@ window.onload = function () {
instance.command.executeRowMargin(Number(li.dataset.rowmargin!)) instance.command.executeRowMargin(Number(li.dataset.rowmargin!))
} }
// 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | // 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX
const tableDom = document.querySelector<HTMLDivElement>('.menu-item__table')! const tableDom = document.querySelector<HTMLDivElement>('.menu-item__table')!
const tablePanelContainer = document.querySelector<HTMLDivElement>('.menu-item__table__collapse')! const tablePanelContainer = document.querySelector<HTMLDivElement>('.menu-item__table__collapse')!
const tableClose = document.querySelector<HTMLDivElement>('.table-close')! const tableClose = document.querySelector<HTMLDivElement>('.table-close')!
@ -541,6 +541,28 @@ window.onload = function () {
}]) }])
} }
const latexDom = document.querySelector<HTMLDivElement>('.menu-item__latex')!
latexDom.onclick = function () {
console.log('LaTeX')
new Dialog({
title: 'LaTeX',
data: [{
type: 'textarea',
height: 100,
name: 'value',
placeholder: '请输入LaTeX文本'
}],
onConfirm: (payload) => {
const value = payload.find(p => p.name === 'value')?.value
if (!value) return
instance.command.executeInsertElementList([{
type: ElementType.LATEX,
value
}])
}
})
}
// 5. | 搜索&替换 | 打印 | // 5. | 搜索&替换 | 打印 |
const searchCollapseDom = document.querySelector<HTMLDivElement>('.menu-item__search__collapse')! const searchCollapseDom = document.querySelector<HTMLDivElement>('.menu-item__search__collapse')!
const searchInputDom = document.querySelector<HTMLInputElement>('.menu-item__search__collapse__search input')! const searchInputDom = document.querySelector<HTMLInputElement>('.menu-item__search__collapse__search input')!

@ -268,6 +268,17 @@ elementList.push(...<IElement[]>[{
value: '\n' value: '\n'
}]) }])
// LaTex公式
elementList.push(...<IElement[]>[{
value: '医学公式:'
},
{
value: `{E_k} = hv - {W_0}`,
type: ElementType.LATEX
}, {
value: '\n'
}])
// 模拟结尾文本 // 模拟结尾文本
elementList.push(...[{ elementList.push(...[{
value: 'E', value: 'E',

@ -443,6 +443,10 @@ ul {
background-image: url('./assets/images/checkbox.svg'); background-image: url('./assets/images/checkbox.svg');
} }
.menu-item__latex i {
background-image: url('./assets/images/latex.svg');
}
.menu-item .menu-item__control .options { .menu-item .menu-item__control .options {
width: 55px; width: 55px;
} }

Loading…
Cancel
Save