feat:add table component

pr675
黄云飞 4 years ago
parent 5869d1b3e9
commit 1a9bcfe3d0

@ -96,10 +96,24 @@
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__table">
<i></i>
</div>
<div class="menu-item__table__collapse">
<div class="table-close">×</div>
<div class="table-title">
<span class="table-select">插入</span>
<span>表格</span>
</div>
<div class="table-panel"></div>
</div>
<div class="menu-item__image">
<i></i>
<input type="file" id="image" accept=".png, .jpg, .jpeg">
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__search">
<i></i>
</div>

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect x=".5" y=".5" width="12" height="12" rx="1" transform="translate(1 1)" stroke="#3D4757"/><path fill="#3D4757" fill-rule="nonzero" d="M2 9h12v1H2zm0-4h12v1H2z"/><path fill="#3D4757" fill-rule="nonzero" d="M5 1h1v13H5zm4 0h1v13H9z"/></g></svg>

After

Width:  |  Height:  |  Size: 345 B

@ -21,6 +21,7 @@ export class Command {
private static center: Function
private static right: Function
private static rowMargin: Function
private static insertTable: Function
private static image: Function
private static search: Function
private static print: Function
@ -46,6 +47,7 @@ export class Command {
Command.center = adapt.rowFlex.bind(adapt)
Command.right = adapt.rowFlex.bind(adapt)
Command.rowMargin = adapt.rowMargin.bind(adapt)
Command.insertTable = adapt.insertTable.bind(adapt)
Command.image = adapt.image.bind(adapt)
Command.search = adapt.search.bind(adapt)
Command.print = adapt.print.bind(adapt)
@ -124,7 +126,11 @@ export class Command {
return Command.rowMargin(payload)
}
// 图片上传、搜索、打印
// 表格、图片上传、搜索、打印
public executeInsertTable(row: number, col: number) {
return Command.insertTable(row, col)
}
public executeImage(payload: IDrawImagePayload) {
return Command.image(payload)
}

@ -6,7 +6,11 @@ import { RowFlex } from "../../dataset/enum/Row"
import { IDrawImagePayload } from "../../interface/Draw"
import { IEditorOption } from "../../interface/Editor"
import { IElement, IElementStyle } from "../../interface/Element"
import { IColgroup } from "../../interface/table/Colgroup"
import { ITd } from "../../interface/table/Td"
import { ITr } from "../../interface/table/Tr"
import { getUUID } from "../../utils"
import { formatElementList } from "../../utils/element"
import { printImageBase64 } from "../../utils/print"
import { Draw } from "../draw/Draw"
import { HistoryManager } from "../history/HistoryManager"
@ -214,6 +218,55 @@ export class CommandAdapt {
this.draw.render({ curIndex, isSetCursor })
}
public insertTable(row: number, col: number) {
const { startIndex, endIndex } = this.range.getRange()
if (startIndex === 0 && endIndex === 0) return
const elementList = this.draw.getElementList()
const { width, margins } = this.options
const innerWidth = width - margins[1] - margins[3]
// colgroup
const colgroup: IColgroup[] = []
const colWidth = innerWidth / col
for (let c = 0; c < col; c++) {
colgroup.push({
width: colWidth
})
}
// trlist
const trList: ITr[] = []
for (let r = 0; r < row; r++) {
const tdList: ITd[] = []
const tr: ITr = {
height: 40,
tdList
}
for (let c = 0; c < col; c++) {
tdList.push({
colspan: 1,
rowspan: 1,
value: [{ value: ZERO, size: 16 }]
})
}
trList.push(tr)
}
const element: IElement = {
type: ElementType.TABLE,
value: ZERO,
colgroup,
trList
}
// 格式化element
formatElementList([element])
const curIndex = startIndex + 1
if (startIndex === endIndex) {
elementList.splice(curIndex, 0, element)
} else {
elementList.splice(curIndex, endIndex - startIndex, element)
}
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex, isSetCursor: false })
}
public image(payload: IDrawImagePayload) {
const { startIndex, endIndex } = this.range.getRange()
if (startIndex === 0 && endIndex === 0) return

@ -1,6 +1,6 @@
import { ZERO } from "../../dataset/constant/Common"
import { RowFlex } from "../../dataset/enum/Row"
import { IDrawOption } from "../../interface/Draw"
import { IDrawOption, IDrawRowPayload, IDrawRowResult } from "../../interface/Draw"
import { IEditorOption } from "../../interface/Editor"
import { IElement, IElementMetrics, IElementPosition, IElementStyle } from "../../interface/Element"
import { IRow, IRowElement } from "../../interface/Row"
@ -23,6 +23,7 @@ import { ImageParticle } from "./particle/ImageParticle"
import { TextParticle } from "./particle/TextParticle"
import { PageNumber } from "./frame/PageNumber"
import { GlobalObserver } from "../observer/GlobalObserver"
import { TableParticle } from "./particle/table/TableParticle"
export class Draw {
@ -47,6 +48,7 @@ export class Draw {
private historyManager: HistoryManager
private imageParticle: ImageParticle
private textParticle: TextParticle
private tableParticle: TableParticle
private pageNumber: PageNumber
private rowList: IRow[]
@ -83,6 +85,7 @@ export class Draw {
this.highlight = new Highlight(this)
this.imageParticle = new ImageParticle(this)
this.textParticle = new TextParticle(this)
this.tableParticle = new TableParticle(this)
this.pageNumber = new PageNumber(this)
new GlobalObserver(this)
@ -135,6 +138,10 @@ export class Draw {
return this.options.defaultBasicRowMarginHeight * this.options.scale
}
public getTdPadding(): number {
return this.options.tdPadding * this.options.scale
}
public getContainer(): HTMLDivElement {
return this.container
}
@ -202,6 +209,15 @@ export class Draw {
}
public getElementList(): IElement[] {
const positionContext = this.position.getPositionContext()
if (positionContext.isTable) {
const { index, trIndex, tdIndex } = positionContext
return this.elementList[index!].trList![trIndex!].tdList[tdIndex!].value
}
return this.elementList
}
public getOriginalElementList() {
return this.elementList
}
@ -306,25 +322,25 @@ export class Draw {
return `${el.italic ? 'italic ' : ''}${el.bold ? 'bold ' : ''}${(el.size || defaultSize) * scale}px ${el.font || defaultFont}`
}
private _computeRowList() {
const { defaultSize, defaultRowMargin, scale } = this.options
const innerWidth = this.getInnerWidth()
private _computeRowList(innerWidth: number, elementList: IElement[]) {
const { defaultSize, defaultRowMargin, scale, tdPadding } = this.options
const defaultBasicRowMarginHeight = this.getDefaultBasicRowMarginHeight()
const tdGap = tdPadding * 2
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
const rowList: IRow[] = []
if (this.elementList.length) {
if (elementList.length) {
rowList.push({
width: 0,
height: 0,
ascent: 0,
elementList: [],
rowFlex: this.elementList?.[1]?.rowFlex
rowFlex: elementList?.[1]?.rowFlex
})
}
for (let i = 0; i < this.elementList.length; i++) {
for (let i = 0; i < elementList.length; i++) {
const curRow: IRow = rowList[rowList.length - 1]
const element = this.elementList[i]
const element = elementList[i]
const rowMargin = defaultBasicRowMarginHeight * (element.rowMargin || defaultRowMargin)
let metrics: IElementMetrics = {
width: 0,
@ -350,6 +366,40 @@ export class Draw {
metrics.boundingBoxDescent = elementHeight
}
metrics.boundingBoxAscent = 0
} else if (element.type === ElementType.TABLE) {
// 计算表格行列
this.tableParticle.computeRowColInfo(element)
// 计算表格内元素信息
const trList = element.trList!
for (let t = 0; t < trList.length; t++) {
const tr = trList[t]
let maxTrHeight = 0
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const rowList = this._computeRowList((td.width! - tdGap) * scale, td.value)
const rowHeight = rowList.reduce((pre, cur) => pre + cur.height, 0)
td.rowList = rowList
// 移除缩放导致的行高变化-渲染时会进行缩放调整
const curTrHeight = (rowHeight + tdGap) / scale
if (maxTrHeight < curTrHeight) {
maxTrHeight = curTrHeight
}
}
tr.height = maxTrHeight
}
// 需要重新计算表格内值
this.tableParticle.computeRowColInfo(element)
// 计算出表格高度
const tableHeight = trList.reduce((pre, cur) => pre + cur.height, 0)
const tableWidth = element.colgroup!.reduce((pre, cur) => pre + cur.width, 0)
element.width = tableWidth
element.height = tableHeight
const elementWidth = tableWidth * scale
const elementHeight = tableHeight * scale
metrics.width = elementWidth
metrics.height = elementHeight
metrics.boundingBoxDescent = elementHeight
metrics.boundingBoxAscent = 0
} else {
metrics.height = (element.size || this.options.defaultSize) * scale
ctx.font = this._getFont(element)
@ -367,7 +417,12 @@ export class Draw {
style: this._getFont(element, scale)
}
// 超过限定宽度
if (curRow.width + metrics.width > innerWidth || (i !== 0 && element.value === ZERO)) {
const preElement = elementList[i - 1]
if (
(preElement && preElement.type === ElementType.TABLE)
|| curRow.width + metrics.width > innerWidth
|| (i !== 0 && element.value === ZERO)
) {
rowList.push({
width: metrics.width,
height,
@ -388,35 +443,29 @@ export class Draw {
curRow.elementList.push(rowElement)
}
}
this.rowList = rowList
return rowList
}
private _drawElement(positionList: IElementPosition[], rowList: IRow[], pageNo: number) {
const width = this.getWidth()
const height = this.getHeight()
const margins = this.getMargins()
const ctx = this.ctxList[pageNo]
ctx.clearRect(0, 0, width, height)
// 绘制背景
this.background.render(ctx)
// 绘制页边距
const leftTopPoint: [number, number] = [margins[3], margins[0]]
this.margin.render(ctx)
// 渲染元素
let x = leftTopPoint[0]
let y = leftTopPoint[1]
let index = positionList.length
private _drawRow(ctx: CanvasRenderingContext2D, payload: IDrawRowPayload): IDrawRowResult {
const { positionList, rowList, pageNo, startX, startY, startIndex, innerWidth } = payload
const { scale, tdPadding } = this.options
const tdGap = tdPadding * 2
let x = startX
let y = startY
let index = startIndex
for (let i = 0; i < rowList.length; i++) {
const curRow = rowList[i]
// 计算行偏移量(行居左、居中、居右)
if (curRow.rowFlex && curRow.rowFlex !== RowFlex.LEFT) {
const canvasInnerWidth = width - margins[1] - margins[3]
if (curRow.rowFlex === RowFlex.CENTER) {
x += (canvasInnerWidth - curRow.width) / 2
x += (innerWidth - curRow.width) / 2
} else {
x += canvasInnerWidth - curRow.width
x += innerWidth - curRow.width
}
}
// 当前td所在位置
let tablePreX = x
let tablePreY = y
for (let j = 0; j < curRow.elementList.length; j++) {
const element = curRow.elementList[j]
const metrics = element.metrics
@ -456,25 +505,89 @@ export class Draw {
if (element.type === ElementType.IMAGE) {
this.textParticle.complete()
this.imageParticle.render(ctx, element, x, y + offsetY)
} else if (element.type === ElementType.TABLE) {
this.tableParticle.render(ctx, element, x, y)
} else {
this.textParticle.record(ctx, element, x, y + offsetY)
}
// 选区绘制
const { startIndex, endIndex } = this.range.getRange()
if (startIndex !== endIndex && startIndex < index && index <= endIndex) {
let rangeWidth = metrics.width
if (rangeWidth === 0 && curRow.elementList.length === 1) {
rangeWidth = this.options.rangeMinWidth
const positionContext = this.position.getPositionContext()
// 表格需限定上下文
if (
(!positionContext.isTable && !element.tdId)
|| positionContext.tdId === element.tdId
) {
let rangeWidth = metrics.width
if (rangeWidth === 0 && curRow.elementList.length === 1) {
rangeWidth = this.options.rangeMinWidth
}
this.range.render(ctx, x, y, rangeWidth, curRow.height)
}
this.range.render(ctx, x, y, rangeWidth, curRow.height)
}
index++
x += metrics.width
// 绘制表格内元素
if (element.type === ElementType.TABLE) {
for (let t = 0; t < element.trList!.length; t++) {
const tr = element.trList![t]
for (let d = 0; d < tr.tdList!.length; d++) {
const td = tr.tdList[d]
td.positionList = []
const drawRowResult = this._drawRow(ctx, {
positionList: td.positionList,
rowList: td.rowList!,
pageNo,
startIndex: 0,
startX: (td.x! + tdPadding) * scale + tablePreX,
startY: td.y! * scale + tablePreY,
innerWidth: (td.width! - tdGap) * scale
})
x = drawRowResult.x
y = drawRowResult.y
}
}
// 恢复初始x、y
x = tablePreX
y = tablePreY
}
}
this.textParticle.complete()
x = leftTopPoint[0]
x = startX
y += curRow.height
}
return { x, y, index }
}
private _drawPage(positionList: IElementPosition[], rowList: IRow[], pageNo: number) {
const width = this.getWidth()
const height = this.getHeight()
const margins = this.getMargins()
const innerWidth = this.getInnerWidth()
const ctx = this.ctxList[pageNo]
ctx.clearRect(0, 0, width, height)
// 绘制背景
this.background.render(ctx)
// 绘制页边距
const leftTopPoint: [number, number] = [margins[3], margins[0]]
this.margin.render(ctx)
// 渲染元素
let x = leftTopPoint[0]
let y = leftTopPoint[1]
let index = positionList.length
const drawRowResult = this._drawRow(ctx, {
positionList,
rowList,
pageNo,
startIndex: index,
startX: x,
startY: y,
innerWidth
})
x = drawRowResult.x
y = drawRowResult.y
index = drawRowResult.index
// 绘制页码
this.pageNumber.render(ctx, pageNo)
// 搜索匹配绘制
@ -491,14 +604,15 @@ export class Draw {
isComputeRowList = true
} = payload || {}
const height = this.getHeight()
const innerWidth = this.getInnerWidth()
// 计算行信息
if (isComputeRowList) {
this._computeRowList()
this.rowList = this._computeRowList(innerWidth, this.elementList)
}
// 清除光标等副作用
this.cursor.recoveryCursor()
this.position.setPositionList([])
const positionList = this.position.getPositionList()
const positionList = this.position.getOriginalPositionList()
// 按页渲染
const margins = this.getMargins()
const marginHeight = margins[0] + margins[2]
@ -522,7 +636,7 @@ export class Draw {
this._createPage(i)
}
const rowList = pageRowList[i]
this._drawElement(positionList, rowList, i)
this._drawPage(positionList, rowList, i)
}
// 移除多余页
setTimeout(() => {
@ -540,7 +654,14 @@ export class Draw {
if (curIndex === undefined) {
curIndex = positionList.length - 1
}
this.position.setCursorPosition(positionList[curIndex!] || null)
const positionContext = this.position.getPositionContext()
if (positionContext.isTable) {
const { index, trIndex, tdIndex } = positionContext
const tablePosition = this.elementList[index!].trList?.[trIndex!].tdList[tdIndex!].positionList?.[curIndex!]
this.position.setCursorPosition(tablePosition || null)
} else {
this.position.setCursorPosition(positionList[curIndex!] || null)
}
this.cursor.drawCursor()
}
// 历史记录用于undo、redo
@ -549,8 +670,10 @@ export class Draw {
const oldElementList = deepClone(this.elementList)
const { startIndex, endIndex } = this.range.getRange()
const pageNo = this.pageNo
const oldPositionContext = deepClone(this.position.getPositionContext())
this.historyManager.execute(function () {
self.setPageNo(pageNo)
self.position.setPositionContext(oldPositionContext)
self.range.setRange(startIndex, endIndex)
self.elementList = deepClone(oldElementList)
self.render({ curIndex, isSubmitHistory: false })

@ -18,7 +18,7 @@ export class Search {
const searchMatch = this.draw.getSearchMatch()
if (!searchMatch || !searchMatch.length) return
const searchMatchList = searchMatch.flat()
const positionList = this.position.getPositionList()
const positionList = this.position.getOriginalPositionList()
ctx.save()
ctx.globalAlpha = this.options.searchMatchAlpha
ctx.fillStyle = this.options.searchMatchColor

@ -0,0 +1,172 @@
import { IElement } from "../../../.."
import { IEditorOption } from "../../../../interface/Editor"
import { Draw } from "../../Draw"
export class TableParticle {
private options: Required<IEditorOption>
constructor(draw: Draw) {
this.options = draw.getOptions()
}
private _drawBorder(ctx: CanvasRenderingContext2D, startX: number, startY: number, width: number, height: number) {
ctx.beginPath()
ctx.moveTo(startX, startY)
ctx.lineTo(startX + width, startY)
ctx.lineTo(startX + width, startY + height)
ctx.lineTo(startX, startY + height)
ctx.closePath()
ctx.stroke()
}
// @ts-ignore
private _drawRange(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) {
const { rangeAlpha, rangeColor } = this.options
ctx.save()
ctx.globalAlpha = rangeAlpha
ctx.fillStyle = rangeColor
ctx.fillRect(x, y, width, height)
ctx.restore()
}
public computeRowColInfo(element: IElement) {
const { colgroup, trList } = element
if (!colgroup || !trList) return
let x = 0
let y = 0
for (let t = 0; t < trList.length; t++) {
const tr = trList[t]
// 表格最后一行
const isLastTr = trList.length - 1 === t
// 当前行最小高度
let rowMinHeight = 0
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
// 计算之前行x轴偏移量
let offsetXIndex = 0
if (trList.length > 1 && t !== 0) {
for (let pT = 0; pT < t; pT++) {
const pTr = trList[pT]
// 相同x轴是否存在跨行
for (let pD = 0; pD < pTr.tdList.length; pD++) {
const pTd = pTr.tdList[pD]
const pTdX = pTd.x!
const pTdY = pTd.y!
const pTdWidth = pTd.width!
const pTdHeight = pTd.height!
// 小于
if (pTdX < x) continue
if (pTdX > x) break
if (pTd.x === x && pTdY + pTdHeight > y) {
x += pTdWidth
offsetXIndex += 1
}
}
}
}
// 计算格列数
let colIndex = 0
const preTd = tr.tdList[d - 1]
if (preTd) {
colIndex = preTd.colIndex! + (offsetXIndex || 1)
if (preTd.colspan > 1) {
colIndex += preTd.colspan - 1
}
} else {
colIndex += offsetXIndex
}
// 计算格宽高
let width = 0
for (let col = 0; col < td.colspan; col++) {
width += colgroup[col + colIndex].width
}
let height = 0
for (let row = 0; row < td.rowspan; row++) {
height += trList[row + t].height
}
// y偏移量
if (rowMinHeight === 0 || rowMinHeight > height) {
rowMinHeight = height
}
// 当前行最后一个td
const isLastRowTd = tr.tdList.length - 1 === d
// 当前列最后一个td
let isLastColTd = isLastTr
if (!isLastColTd) {
if (td.rowspan > 1) {
const nextTrLength = trList.length - 1 - t
isLastColTd = td.rowspan - 1 === nextTrLength
}
}
// 当前表格最后一个td
const isLastTd = isLastTr && isLastRowTd
td.isLastRowTd = isLastRowTd
td.isLastColTd = isLastColTd
td.isLastTd = isLastTd
// 修改当前格clientBox
td.x = x
td.y = y
td.width = width
td.height = height
td.rowIndex = t
td.colIndex = colIndex
// 当前列x轴累加
x += width
// 一行中的最后td
if (isLastRowTd && !isLastTd) {
x = 0
y += rowMinHeight
}
}
}
}
public render(ctx: CanvasRenderingContext2D, element: IElement, startX: number, startY: number) {
const { colgroup, trList } = element
if (!colgroup || !trList) return
const { scale } = this.options
const tableWidth = element.width! * scale
const tableHeight = element.height! * scale
ctx.save()
// 渲染边框
this._drawBorder(ctx, startX, startY, tableWidth, tableHeight)
// 渲染表格
for (let t = 0; t < trList.length; t++) {
const tr = trList[t]
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const { isLastRowTd, isLastColTd, isLastTd } = td
const width = td.width! * scale
const height = td.height! * scale
const x = td.x! * scale + startX + width
const y = td.y! * scale + startY
// 绘制线条
ctx.beginPath()
if (isLastRowTd && !isLastTd) {
// 是否跨行到底
if (y + height < startY + tableHeight) {
ctx.moveTo(x, y + height)
ctx.lineTo(x - width, y + height)
}
} else if (isLastColTd && !isLastTd) {
ctx.moveTo(x, y)
ctx.lineTo(x, y + height)
} else if (!isLastRowTd && !isLastColTd && !isLastTd) {
ctx.moveTo(x, y)
ctx.lineTo(x, y + height)
ctx.lineTo(x - width, y + height)
} else if (isLastTd) {
// 是否跨列到最右
if (x + width === startX + tableWidth) {
ctx.moveTo(x, y)
ctx.lineTo(x, y + height)
}
}
ctx.stroke()
}
}
ctx.restore()
}
}

@ -80,7 +80,12 @@ export class CanvasEvent {
this.draw.setPageNo(Number(pageIndex))
}
// 结束位置
const { index: endIndex } = this.position.getPositionByXY(evt.offsetX, evt.offsetY)
const positionResult = this.position.getPositionByXY({
x: evt.offsetX,
y: evt.offsetY
})
const { index, isTable, tdValueIndex } = positionResult
let endIndex = isTable ? tdValueIndex! : index
let end = ~endIndex ? endIndex : 0
// 开始位置
let start = this.mouseDownStartIndex
@ -105,15 +110,46 @@ export class CanvasEvent {
this.draw.setPageNo(Number(pageIndex))
}
this.isAllowDrag = true
const { index, isDirectHit, isImage } = this.position.getPositionByXY(evt.offsetX, evt.offsetY)
const positionResult = this.position.getPositionByXY({
x: evt.offsetX,
y: evt.offsetY
})
const {
index,
isDirectHit,
isImage,
isTable,
trIndex,
tdIndex,
tdValueIndex,
tdId,
trId,
tableId
} = positionResult
// 设置位置上下文
this.position.setPositionContext({
isTable: isTable || false,
index,
trIndex,
tdIndex,
tdId,
trId,
tableId
})
// 记录选区开始位置
this.mouseDownStartIndex = index
this.mouseDownStartIndex = isTable ? tdValueIndex! : index
// 绘制
const isDirectHitImage = isDirectHit && isImage
if (~index) {
this.range.setRange(index, index)
let curIndex = index
if (isTable) {
this.range.setRange(tdValueIndex!, tdValueIndex!)
curIndex = tdValueIndex!
} else {
this.range.setRange(index, index)
}
this.draw.render({
curIndex: index,
curIndex,
isSubmitHistory: false,
isSetCursor: !isDirectHitImage,
isComputeRowList: false
@ -124,7 +160,8 @@ export class CanvasEvent {
if (isDirectHitImage) {
const elementList = this.draw.getElementList()
const positionList = this.position.getPositionList()
this.imageParticle.drawResizer(elementList[index], positionList[index])
const curIndex = isTable ? tdValueIndex! : index
this.imageParticle.drawResizer(elementList[curIndex], positionList[curIndex])
}
}
@ -167,8 +204,16 @@ export class CanvasEvent {
this.range.setRange(curIndex, curIndex)
this.draw.render({ curIndex })
} else if (evt.key === KeyMap.Enter) {
// 表格需要上下文信息
const positionContext = this.position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const enterText: IElement = {
value: ZERO
value: ZERO,
...restArg
}
if (isCollspace) {
elementList.splice(index + 1, 0, enterText)
@ -277,8 +322,16 @@ export class CanvasEvent {
const { index } = cursorPosition
const { startIndex, endIndex } = this.range.getRange()
const isCollspace = startIndex === endIndex
// 表格需要上下文信息
const positionContext = this.position.getPositionContext()
let restArg = {}
if (positionContext.isTable) {
const { tdId, trId, tableId } = positionContext
restArg = { tdId, trId, tableId }
}
const inputData: IElement[] = data.split('').map(value => ({
value
value,
...restArg
}))
let start = 0
if (isCollspace) {

@ -1,29 +1,41 @@
import { ElementType } from "../.."
import { ZERO } from "../../dataset/constant/Common"
import { IEditorOption } from "../../interface/Editor"
import { IElement, IElementPosition } from "../../interface/Element"
import { ICurrentPosition } from "../../interface/Position"
import { IElementPosition } from "../../interface/Element"
import { ICurrentPosition, IGetPositionByXYPayload, IPositionContext } from "../../interface/Position"
import { Draw } from "../draw/Draw"
export class Position {
private cursorPosition: IElementPosition | null
private positionContext: IPositionContext
private positionList: IElementPosition[]
private elementList: IElement[]
private draw: Draw
private options: Required<IEditorOption>
constructor(draw: Draw) {
this.positionList = []
this.elementList = []
this.cursorPosition = null
this.positionContext = {
isTable: false
}
this.draw = draw
this.options = draw.getOptions()
}
public getOriginalPositionList(): IElementPosition[] {
return this.positionList
}
public getPositionList(): IElementPosition[] {
const { isTable } = this.positionContext
if (isTable) {
const { index, trIndex, tdIndex } = this.positionContext
const elementList = this.draw.getOriginalElementList()
return elementList[index!].trList![trIndex!].tdList[tdIndex!].positionList || []
}
return this.positionList
}
@ -31,7 +43,7 @@ export class Position {
this.positionList = payload
}
public setCursorPosition(position: IElementPosition) {
public setCursorPosition(position: IElementPosition | null) {
this.cursorPosition = position
}
@ -39,22 +51,69 @@ export class Position {
return this.cursorPosition
}
public getPositionByXY(x: number, y: number): ICurrentPosition {
this.elementList = this.draw.getElementList()
public getPositionContext(): IPositionContext {
return this.positionContext
}
public setPositionContext(payload: IPositionContext) {
this.positionContext = payload
}
public getPositionByXY(payload: IGetPositionByXYPayload): ICurrentPosition {
const { x, y, isTable } = payload
let { elementList, positionList } = payload
if (!elementList) {
elementList = this.draw.getOriginalElementList()
}
if (!positionList) {
positionList = this.positionList
}
const curPageNo = this.draw.getPageNo()
for (let j = 0; j < this.positionList.length; j++) {
const { index, pageNo, coordinate: { leftTop, rightTop, leftBottom } } = this.positionList[j]
for (let j = 0; j < positionList.length; j++) {
const { index, pageNo, coordinate: { leftTop, rightTop, leftBottom } } = positionList[j]
if (curPageNo !== pageNo) continue
// 命中元素
if (leftTop[0] <= x && rightTop[0] >= x && leftTop[1] <= y && leftBottom[1] >= y) {
let curPostionIndex = j
const element = this.elementList[j]
const element = elementList[j]
// 表格被命中
if (element.type === ElementType.TABLE) {
for (let t = 0; t < element.trList!.length; t++) {
const tr = element.trList![t]
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const tablePosition = this.getPositionByXY({
x,
y,
td,
tablePosition: positionList[j],
isTable: true,
elementList: td.value,
positionList: td.positionList
})
if (~tablePosition.index) {
return {
index,
isImage: tablePosition.isImage,
isDirectHit: tablePosition.isDirectHit,
isTable: true,
tdIndex: d,
trIndex: t,
tdValueIndex: tablePosition.index,
tdId: td.id,
trId: tr.id,
tableId: element.id
}
}
}
}
}
// 图片区域均为命中
if (element.type === ElementType.IMAGE) {
return { index: curPostionIndex, isDirectHit: true, isImage: true }
}
// 判断是否在文字中间前后
if (this.elementList[index].value !== ZERO) {
if (elementList[index].value !== ZERO) {
const valueWidth = rightTop[0] - leftTop[0]
if (x < leftTop[0] + valueWidth / 2) {
curPostionIndex = j - 1
@ -66,8 +125,22 @@ export class Position {
// 非命中区域
let isLastArea = false
let curPostionIndex = -1
// 判断是否在表格内
if (isTable) {
const { td, tablePosition } = payload
if (td && tablePosition) {
const { leftTop } = tablePosition.coordinate
const tdX = td.x! + leftTop[0]
const tdY = td.y! + leftTop[1]
const tdWidth = td.width!
const tdHeight = td.height!
if (!(tdX < x && x < tdX + tdWidth && tdY < y && y < tdY + tdHeight)) {
return { index: curPostionIndex }
}
}
}
// 判断所属行是否存在元素
const firstLetterList = this.positionList.filter(p => p.isLastLetter && p.pageNo === curPageNo)
const firstLetterList = positionList.filter(p => p.isLastLetter && p.pageNo === curPageNo)
for (let j = 0; j < firstLetterList.length; j++) {
const { index, pageNo, coordinate: { leftTop, leftBottom } } = firstLetterList[j]
if (curPageNo !== pageNo) continue
@ -75,7 +148,7 @@ export class Position {
const isHead = x < this.options.margins[3]
// 是否在头部
if (isHead) {
const headIndex = this.positionList.findIndex(p => p.rowNo === firstLetterList[j].rowNo)
const headIndex = positionList.findIndex(p => p.rowNo === firstLetterList[j].rowNo)
curPostionIndex = ~headIndex ? headIndex : index
} else {
curPostionIndex = index
@ -86,7 +159,7 @@ export class Position {
}
if (!isLastArea) {
// 当前页最后一行
return { index: firstLetterList[firstLetterList.length - 1]?.index || this.positionList.length - 1 }
return { index: firstLetterList[firstLetterList.length - 1]?.index || positionList.length - 1 }
}
return { index: curPostionIndex }
}

@ -1,4 +1,5 @@
export enum ElementType {
TEXT = 'text',
IMAGE = 'image'
IMAGE = 'image',
TABLE = 'table'
}

@ -1,5 +1,4 @@
import './assets/css/index.css'
import { ZERO } from './dataset/constant/Common'
import { IEditorOption } from './interface/Editor'
import { IElement } from './interface/Element'
import { Draw } from './core/draw/Draw'
@ -7,8 +6,8 @@ import { Command } from './core/command/Command'
import { CommandAdapt } from './core/command/CommandAdapt'
import { Listener } from './core/listener/Listener'
import { RowFlex } from './dataset/enum/Row'
import { getUUID } from './utils'
import { ElementType } from './dataset/enum/Element'
import { formatElementList } from './utils/element'
export default class Editor {
@ -42,22 +41,11 @@ export default class Editor {
marginIndicatorSize: 35,
marginIndicatorColor: '#BABABA',
margins: [100, 120, 100, 120],
tdPadding: 5,
defaultTdHeight: 40,
...options
}
if (elementList[0]?.value !== ZERO) {
elementList.unshift({
value: ZERO
})
}
for (let i = 0; i < elementList.length; i++) {
const el = elementList[i]
if (el.value === '\n') {
el.value = ZERO
}
if (el.type === ElementType.IMAGE) {
el.id = getUUID()
}
}
formatElementList(elementList)
// 监听
this.listener = new Listener()
// 启动

@ -1,3 +1,6 @@
import { IElementPosition } from "./Element"
import { IRow } from "./Row"
export interface IDrawOption {
curIndex?: number;
isSetCursor?: boolean;
@ -16,4 +19,20 @@ export interface IImageParticleCreateResult {
resizerHandleList: HTMLDivElement[];
resizerImageContainer: HTMLDivElement;
resizerImage: HTMLImageElement;
}
export interface IDrawRowPayload {
positionList: IElementPosition[];
rowList: IRow[];
pageNo: number;
startIndex: number;
startX: number;
startY: number;
innerWidth: number;
}
export interface IDrawRowResult {
x: number;
y: number;
index: number;
}

@ -23,5 +23,7 @@ export interface IEditorOption {
resizerSize?: number;
marginIndicatorSize?: number;
marginIndicatorColor?: string,
margins?: [top: number, right: number, bootom: number, left: number]
margins?: [top: number, right: number, bootom: number, left: number],
tdPadding?: number;
defaultTdHeight?: number;
}

@ -1,11 +1,12 @@
import { ElementType } from "../dataset/enum/Element"
import { RowFlex } from "../dataset/enum/Row"
import { IColgroup } from "./table/Colgroup"
import { ITr } from "./table/Tr"
export interface IElementMetrics {
width: number;
height: number;
boundingBoxAscent: number;
boundingBoxDescent: number;
export interface IElementBasic {
id?: string;
type?: ElementType;
value: string;
}
export interface IElementStyle {
@ -23,13 +24,27 @@ export interface IElementStyle {
rowMargin?: number;
}
export interface IElementBasic {
id?: string;
type?: ElementType;
value: string;
export interface ITableAttr {
colgroup?: IColgroup[];
trList?: ITr[];
}
export interface ITableElement {
tdId?: string;
trId?: string;
tableId?: string;
}
export type IElement = IElementBasic & IElementStyle
export type ITable = ITableAttr & ITableElement
export type IElement = IElementBasic & IElementStyle & ITable
export interface IElementMetrics {
width: number;
height: number;
boundingBoxAscent: number;
boundingBoxDescent: number;
}
export interface IElementPosition {
pageNo: number;

@ -1,5 +1,36 @@
import { IElement } from ".."
import { IElementPosition } from "./Element"
import { ITd } from "./table/Td"
export interface ICurrentPosition {
index: number;
isImage?: boolean;
isTable?: boolean;
isDirectHit?: boolean;
trIndex?: number;
tdIndex?: number;
tdValueIndex?: number;
tdId?: string;
trId?: string;
tableId?: string;
}
export interface IGetPositionByXYPayload {
x: number;
y: number;
isTable?: boolean;
td?: ITd;
tablePosition?: IElementPosition;
elementList?: IElement[];
positionList?: IElementPosition[];
}
export interface IPositionContext {
isTable: boolean;
index?: number;
trIndex?: number;
tdIndex?: number;
tdId?: string;
trId?: string;
tableId?: string;
}

@ -0,0 +1,4 @@
export interface IColgroup {
id?: string;
width: number;
}

@ -0,0 +1,20 @@
import { IElement, IElementPosition } from "../Element"
import { IRow } from "../Row"
export interface ITd {
id?: string;
x?: number;
y?: number;
width?: number;
height?: number;
colspan: number;
rowspan: number;
value: IElement[];
isLastRowTd?: boolean;
isLastColTd?: boolean;
isLastTd?: boolean;
rowIndex?: number;
colIndex?: number;
rowList?: IRow[];
positionList?: IElementPosition[];
}

@ -0,0 +1,7 @@
import { ITd } from "./Td"
export interface ITr {
id?: string;
height: number;
tdList: ITd[];
}

@ -0,0 +1,43 @@
import { getUUID } from "."
import { ElementType, IElement } from ".."
import { ZERO } from "../dataset/constant/Common"
export function formatElementList(elementList: IElement[]) {
if (elementList[0]?.value !== ZERO) {
elementList.unshift({
value: ZERO
})
}
for (let i = 0; i < elementList.length; i++) {
const el = elementList[i]
if (el.type === ElementType.TABLE) {
const tableId = getUUID()
el.id = tableId
if (el.trList) {
for (let t = 0; t < el.trList.length; t++) {
const tr = el.trList[t]
const trId = getUUID()
tr.id = trId
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const tdId = getUUID()
td.id = tdId
formatElementList(td.value)
for (let v = 0; v < td.value.length; v++) {
const value = td.value[v]
value.tdId = tdId
value.trId = trId
value.tableId = tableId
}
}
}
}
}
if (el.value === '\n') {
el.value = ZERO
}
if (el.type === ElementType.IMAGE) {
el.id = getUUID()
}
}
}

@ -3,7 +3,7 @@ import Editor, { ElementType, IElement, RowFlex } from './editor'
window.onload = function () {
const text = `人民医院门诊病历\n主诉\n发热三天咳嗽五天。\n现病史\n患者于三天前无明显诱因感冒后发现面部水肿无皮疹尿量减少出现乏力在外治疗无好转现来我院就诊。\n既往史\n有糖尿病10年有高血压2年有传染性疾病1年。没有报告其他既往疾病。\n流行病史\n否认14天内接触过新冠肺炎确诊患者、疑似患者、无症状感染者及其密切接触者否认14天内去过以下场所水产、肉类批发市场农贸市场集市大型超市夜市否认14天内与以下场所工作人员密切接触水产、肉类批发市场农贸市场集市大型超市否认14天内周围如家庭、办公室有2例以上聚集性发病否认14天内接触过有发热或呼吸道症状的人员否认14天内自身有发热或呼吸道症状否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形陪同家属{有无选择代码}有以上情况。\n体格检查\nT36.5℃P80bpmR20次/分BP120/80mmHg\n辅助检查\n2020年6月10日普放血细胞比容36.50%偏低4050单核细胞绝对值0.75*10^9/L偏高参考值0.10.6\n门诊诊断\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名【】`
const text = `人民医院门诊病历\n主诉\n发热三天咳嗽五天。\n现病史\n患者于三天前无明显诱因感冒后发现面部水肿无皮疹尿量减少出现乏力在外治疗无好转现来我院就诊。\n既往史\n有糖尿病10年有高血压2年有传染性疾病1年。没有报告其他既往疾病。\n流行病史\n否认14天内接触过新冠肺炎确诊患者、疑似患者、无症状感染者及其密切接触者否认14天内去过以下场所水产、肉类批发市场农贸市场集市大型超市夜市否认14天内与以下场所工作人员密切接触水产、肉类批发市场农贸市场集市大型超市否认14天内周围如家庭、办公室有2例以上聚集性发病否认14天内接触过有发热或呼吸道症状的人员否认14天内自身有发热或呼吸道症状否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形陪同家属{有无选择代码}有以上情况。\n体格检查\nT36.5℃P80bpmR20次/分BP120/80mmHg\n辅助检查\n2020年6月10日普放血细胞比容36.50%偏低4050单核细胞绝对值0.75*10^9/L偏高参考值0.10.6\n门诊诊断\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名【】\n其他记录`
// 模拟行居中
const centerText = ['人民医院门诊病历']
const centerIndex: number[] = centerText.map(c => {
@ -11,7 +11,7 @@ window.onload = function () {
return ~i ? Array(c.length).fill(i).map((_, j) => i + j) : []
}).flat()
// 模拟加粗字
const boldText = ['主诉:', '现病史:', '既往史:', '流行病史:', '体格检查:', '辅助检查:', '门诊诊断:', '处置治疗:', '电子签名:']
const boldText = ['主诉:', '现病史:', '既往史:', '流行病史:', '体格检查:', '辅助检查:', '门诊诊断:', '处置治疗:', '电子签名:', '其他记录:']
const boldIndex: number[] = boldText.map(b => {
const i = text.indexOf(b)
return ~i ? Array(b.length).fill(i).map((_, j) => i + j) : []
@ -69,12 +69,116 @@ window.onload = function () {
id: 'signature',
type: ElementType.IMAGE
})
data.push({
type: ElementType.TABLE,
value: `\n`,
colgroup: [{
width: 180
}, {
width: 80
}, {
width: 130
}, {
width: 130
}],
trList: [{
height: 40,
tdList: [{
colspan: 1,
rowspan: 2,
value: [
{ value: `1`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `2`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 2,
rowspan: 1,
value: [
{ value: `3`, size: 16 },
{ value: '.', size: 16 }
]
}]
}, {
height: 40,
tdList: [{
colspan: 1,
rowspan: 1,
value: [
{ value: `4`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `5`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `6`, size: 16 },
{ value: '.', size: 16 }
]
}]
}, {
height: 40,
tdList: [{
colspan: 1,
rowspan: 1,
value: [
{ value: `7`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `8`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `9`, size: 16 },
{ value: '.', size: 16 }
]
}, {
colspan: 1,
rowspan: 1,
value: [
{ value: `1`, size: 16 },
{ value: `0`, size: 16 },
{ value: '.', size: 16 }
]
}]
}]
})
data.push(...[{
value: 'E',
size: 16
}, {
value: 'O',
size: 16
}, {
value: 'F',
size: 16
}])
// 初始化编辑器
const container = document.querySelector<HTMLDivElement>('.editor')!
const instance = new Editor(container, data, {
margins: [100, 120, 100, 120]
})
console.log('编辑器实例: ', instance)
console.log('实例: ', instance)
// 撤销、重做、格式刷、清除格式
const undoDom = document.querySelector<HTMLDivElement>('.menu-item__undo')!
@ -183,7 +287,81 @@ window.onload = function () {
const li = evt.target as HTMLLIElement
instance.command.executeRowMargin(Number(li.dataset.rowmargin!))
}
// 图片上传、搜索、打印
// 表格插入、图片上传、搜索、打印
const tableDom = document.querySelector<HTMLDivElement>('.menu-item__table')!
const tablePanelContainer = document.querySelector<HTMLDivElement>('.menu-item__table__collapse')!
const tableClose = document.querySelector<HTMLDivElement>('.table-close')!
const tableTitle = document.querySelector<HTMLDivElement>('.table-select')!
const tablePanel = document.querySelector<HTMLDivElement>('.table-panel')!
// 绘制行列
const tableCellList: HTMLDivElement[][] = []
for (let i = 0; i < 10; i++) {
const tr = document.createElement('tr')
tr.classList.add('table-row')
const trCellList: HTMLDivElement[] = []
for (let j = 0; j < 10; j++) {
const td = document.createElement('td')
td.classList.add('table-cel')
tr.append(td)
trCellList.push(td)
}
tablePanel.append(tr)
tableCellList.push(trCellList)
}
let colIndex = 0
let rowIndex = 0
// 移除所有格选择
function removeAllTableCellSelect() {
tableCellList.forEach(tr => {
tr.forEach(td => td.classList.remove('active'))
})
}
// 设置标题内容
function setTableTitle(payload: string) {
tableTitle.innerText = payload
}
// 恢复初始状态
function recoveryTable() {
// 还原选择样式、标题、选择行列
removeAllTableCellSelect()
setTableTitle('插入')
colIndex = 0
rowIndex = 0
// 隐藏panel
tablePanelContainer.style.display = 'none'
}
tableDom.onclick = function () {
console.log('table')
tablePanelContainer!.style.display = 'block'
}
tablePanel.onmousemove = function (evt) {
const celSize = 16
const rowMarginTop = 10
const celMarginRight = 6
const { offsetX, offsetY } = evt
// 移除所有选择
removeAllTableCellSelect()
colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1
rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1
// 改变选择样式
tableCellList.forEach((tr, trIndex) => {
tr.forEach((td, tdIndex) => {
if (tdIndex < colIndex && trIndex < rowIndex) {
td.classList.add('active')
}
})
})
// 改变表格标题
setTableTitle(`${rowIndex}×${colIndex}`)
}
tableClose.onclick = function () {
recoveryTable()
}
tablePanel.onclick = function () {
// 应用选择
instance.command.executeInsertTable(rowIndex, colIndex)
recoveryTable()
}
const imageDom = document.querySelector<HTMLDivElement>('.menu-item__image')!
const imageFileDom = document.querySelector<HTMLInputElement>('#image')!
imageDom.onclick = function () {
@ -308,7 +486,6 @@ window.onload = function () {
}
instance.listener.pageScaleChange = function (payload) {
console.log('payload: ', payload);
document.querySelector<HTMLSpanElement>('.page-scale-percentage')!.innerText = `${Math.floor(payload * 10 * 10)}%`
}

@ -78,7 +78,7 @@ ul {
display: flex;
align-items: center;
justify-content: center;
margin: 0 6px;
margin: 0 5px;
}
.menu-item>div:hover {
@ -97,7 +97,7 @@ ul {
background-size: 100% 100%;
}
.menu-item>div span {
.menu-item>div>span {
width: 16px;
height: 3px;
display: inline-block;
@ -272,6 +272,91 @@ ul {
display: none;
}
.menu-item__table {
position: relative;
}
.menu-item__table i {
background-image: url('./assets/images/table.svg');
}
.menu-item .menu-item__table__collapse {
width: 270px;
height: 310px;
background: #fff;
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
border: 1px solid #e2e6ed;
box-sizing: border-box;
border-radius: 2px;
position: absolute;
display: none;
z-index: 99;
top: 25px;
left: 0;
padding: 14px 27px;
cursor: auto;
}
.menu-item .menu-item__table__collapse .table-close {
position: absolute;
right: 10px;
top: 5px;
cursor: pointer;
}
.menu-item .menu-item__table__collapse .table-close:hover {
color: #7d7e80;
}
.menu-item .menu-item__table__collapse:hover {
background: #fff;
}
.menu-item .menu-item__table__collapse .table-title {
display: flex;
justify-content: flex-start;
padding-bottom: 5px;
border-bottom: 1px solid #e2e6ed;
}
.table-title span {
font-size: 12px;
color: #3d4757;
display: inline;
margin: 0;
}
.table-panel {
cursor: pointer;
}
.table-panel .table-row {
display: flex;
flex-wrap: nowrap;
margin-top: 10px;
pointer-events: none;
}
.table-panel .table-cel {
width: 16px;
height: 16px;
box-sizing: border-box;
border: 1px solid #e2e6ed;
background: #fff;
position: relative;
margin-right: 6px;
pointer-events: none;
}
.table-panel .table-cel.active {
border: 1px solid rgba(73, 145, 242, .2);
background: rgba(73, 145, 242, .15);
}
.table-panel .table-row .table-cel:last-child {
margin-right: 0;
}
.menu-item__search {
position: relative;
}

Loading…
Cancel
Save