Compare commits

...

13 Commits

@ -14,7 +14,7 @@ new Editor(container, IEditorData | IElement[], {
```typescript
interface IEditorOption {
mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control). default: Edit
mode?: EditorMode // Editor mode: Edit, Clean (Visual aids are not displayed, For example: page break), ReadOnly, Form (Only editable within the control), Print (Visual aids are not displayed, Unwritten content control), Design (Do not handle configurations such as non deletable and read-only). default: Edit
defaultType?: string // Default element type. default: TEXT
defaultColor?: string // Default color. default: #000000
defaultFont?: string // Default font. default: Microsoft YaHei

@ -14,7 +14,7 @@ new Editor(container, IEditorData | IElement[], {
```typescript
interface IEditorOption {
mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)。默认:编辑
mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)、设计模式(不可删除、只读等配置不控制)。默认:编辑
defaultType?: string // 默认元素类型。默认TEXT
defaultColor?: string // 默认字体颜色。默认:#000000
defaultFont?: string // 默认字体。默认Microsoft YaHei

@ -365,7 +365,7 @@
<span>页面:<span class="page-no">1</span>/<span class="page-size">1</span></span>
<span>字数:<span class="word-count">0</span></span>
</div>
<div class="editor-mode" title="编辑模式(编辑、清洁、只读、表单)">编辑模式</div>
<div class="editor-mode" title="编辑模式(编辑、清洁、只读、表单、设计)">编辑模式</div>
<div>
<div class="page-scale-minus" title="缩小(Ctrl+-)">
<i></i>

@ -1,11 +1,11 @@
{
"name": "@hufe921/canvas-editor",
"name": "@liushuai05/canvas-editor",
"author": "Hufe",
"license": "MIT",
"version": "0.9.91",
"version": "0.9.89",
"description": "rich text editor by canvas/svg",
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"registry": "https://registry.yarnpkg.com/",
"access": "public"
},
"files": [

@ -827,7 +827,9 @@ export class CommandAdapt {
formatElementList([element], {
editorOptions: this.options
})
formatElementContext(elementList, [element], startIndex)
formatElementContext(elementList, [element], startIndex, {
editorOptions: this.options
})
const curIndex = startIndex + 1
this.draw.spliceElementList(
elementList,
@ -1588,7 +1590,9 @@ export class CommandAdapt {
}))
if (!newElementList) return
const start = startIndex + 1
formatElementContext(elementList, newElementList, startIndex)
formatElementContext(elementList, newElementList, startIndex, {
editorOptions: this.options
})
this.draw.spliceElementList(
elementList,
start,
@ -1733,7 +1737,9 @@ export class CommandAdapt {
dashArray: payload
}
// 从行头增加分割线
formatElementContext(elementList, [newElement], startIndex)
formatElementContext(elementList, [newElement], startIndex, {
editorOptions: this.options
})
if (startIndex !== 0 && elementList[startIndex].value === ZERO) {
this.draw.spliceElementList(elementList, startIndex, 1, newElement)
curIndex = startIndex - 1
@ -2238,7 +2244,8 @@ export class CommandAdapt {
const { startIndex } = this.range.getRange()
const elementList = this.draw.getElementList()
formatElementContext(elementList, cloneElementList, startIndex, {
isBreakWhenWrap: true
isBreakWhenWrap: true,
editorOptions: this.options
})
this.draw.insertElementList(cloneElementList)
}

@ -102,6 +102,7 @@ import { PUNCTUATION_REG } from '../../dataset/constant/Regular'
import { LineBreakParticle } from './particle/LineBreakParticle'
import { MouseObserver } from '../observer/MouseObserver'
import { LineNumber } from './frame/LineNumber'
import { ITd } from '../../interface/table/Td'
import { PageBorder } from './frame/PageBorder'
export class Draw {
@ -314,6 +315,8 @@ export class Draw {
public isReadonly() {
switch (this.mode) {
case EditorMode.DESIGN:
return false
case EditorMode.READONLY:
case EditorMode.PRINT:
return true
@ -325,6 +328,7 @@ export class Draw {
}
public isDisabled() {
if (this.mode === EditorMode.DESIGN) return false
const { startIndex, endIndex } = this.range.getRange()
const elementList = this.getElementList()
if (startIndex === endIndex) {
@ -341,6 +345,10 @@ export class Draw {
)
}
public isDesignMode() {
return this.mode === EditorMode.DESIGN
}
public getOriginalWidth(): number {
const { paperDirection, width, height } = this.options
return paperDirection === PaperDirection.VERTICAL ? width : height
@ -698,6 +706,7 @@ export class Draw {
deleteCount: number,
...items: IElement[]
) {
const isDesignMode = this.isDesignMode()
if (deleteCount > 0) {
// 当最后元素与开始元素列表信息不一致时:清除当前列表信息
const endIndex = start + deleteCount
@ -728,8 +737,9 @@ export class Draw {
while (deleteIndex >= start) {
const deleteElement = elementList[deleteIndex]
if (
deleteElement?.control?.deletable !== false &&
deleteElement?.title?.deletable !== false
isDesignMode ||
(deleteElement?.control?.deletable !== false &&
deleteElement?.title?.deletable !== false)
) {
elementList.splice(deleteIndex, 1)
}
@ -1205,6 +1215,7 @@ export class Draw {
table: { tdPadding },
defaultTabWidth
} = this.options
let curIndex = payload.curIndex
const defaultBasicRowMarginHeight = this.getDefaultBasicRowMarginHeight()
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
@ -1278,9 +1289,30 @@ export class Draw {
} else if (element.type === ElementType.TABLE) {
const tdPaddingWidth = tdPadding[1] + tdPadding[3]
const tdPaddingHeight = tdPadding[0] + tdPadding[2]
const height = this.getHeight()
const marginHeight = this.getMainOuterHeight()
const emptyMainHeight = (height - marginHeight - rowMargin * 2) / scale
// 表格分页处理进度https://github.com/Hufe921/canvas-editor/issues/41
// 查看后续表格是否属于同一个源表格-存在即合并
if (element.pagingId) {
// 为当前表格构建一个虚拟表格
const virtualTable = Array.from(
{ length: element.trList!.length },
() => new Array(element.colgroup!.length)
) as Array<Array<ITd | undefined | null>>
element.trList!.forEach((tr, trIndex) => {
let tdIndex = 0
tr.tdList.forEach(td => {
while (virtualTable[trIndex][tdIndex] === null) {
tdIndex++
}
virtualTable[trIndex][tdIndex] = td
for (let i = 1; i < td.rowspan; i++) {
virtualTable[trIndex + i][tdIndex] = null
}
tdIndex += td.colspan
})
})
let tableIndex = i + 1
let combineCount = 0
while (tableIndex < elementList.length) {
@ -1289,8 +1321,65 @@ export class Draw {
const nexTrList = nextElement.trList!.filter(
tr => !tr.pagingRepeat
)
element.trList!.push(...nexTrList)
element.height! += nextElement.height!
// 判断后续表格第一行是拆分出来的还是从原表格挪到下一页的
const isNextTrSplit =
element.trList![element.trList!.length - 1].id ===
nexTrList[0].pagingOriginId
let tdIndex = 0
const mergedTds: ITd[] = []
nexTrList[0].tdList.forEach(td => {
let targetTd
// 如果虚拟表格最后一行对应位置有单元格,则其就为目标单元格,否则向上查找
if (virtualTable[virtualTable.length - 1][tdIndex]) {
targetTd = virtualTable[virtualTable.length - 1][tdIndex]
} else {
for (let i = virtualTable.length - 2; i >= 0; i--) {
if (virtualTable[i][tdIndex]) {
targetTd = virtualTable[i][tdIndex]
break
}
}
}
if (targetTd) {
if (targetTd.id === td.pagingOriginId) {
targetTd.value.push(...td.value)
if (isNextTrSplit) {
targetTd.rowspan = targetTd.rowspan + td.rowspan - 1
} else {
targetTd.rowspan = targetTd.rowspan + td.rowspan
mergedTds.push(td)
}
}
tdIndex += targetTd.colspan
}
})
nexTrList[0].tdList = nexTrList[0].tdList.filter(td => {
const isNotMerged = mergedTds.every(
mergedTd => mergedTd.id !== td.id
)
delete td.pagingOriginId
return isNotMerged
})
while (nexTrList.length > 0) {
const lastTr = element.trList![element.trList!.length - 1]
const nextTr = nexTrList.shift()!
if (lastTr.id === nextTr.pagingOriginId) {
lastTr.height += nextTr.pagingOriginHeight || 0
// 更新id关联
lastTr.tdList.forEach(td => {
td.value.forEach(v => {
v.tdId = td.id
v.trId = lastTr.id
v.tableId = element.id
})
})
} else {
nextTr.height = nextTr.pagingOriginHeight || nextTr.height
element.trList!.push(nextTr)
}
delete nextTr.pagingOriginHeight
delete nextTr.pagingOriginId
}
tableIndex++
combineCount++
} else {
@ -1308,9 +1397,17 @@ export class Draw {
const trList = element.trList!
for (let t = 0; t < trList.length; t++) {
const tr = trList[t]
// 行的minHeight最大只能为页面主内容区域高度如果表格前第一行是空行由于此时表格不能换页因此需减掉空行的高度
if (tr.minHeight && tr.minHeight > emptyMainHeight) {
if (i === 1 && elementList[0].value === ZERO) {
tr.minHeight = emptyMainHeight - rowList[0].height
} else {
tr.minHeight = emptyMainHeight
}
}
for (let d = 0; d < tr.tdList.length; d++) {
const td = tr.tdList[d]
const rowList = this.computeRowList({
const { rowList } = this.computeRowList({
innerWidth: (td.width! - tdPaddingWidth) * scale,
elementList: td.value,
isPagingMode
@ -1385,8 +1482,6 @@ export class Draw {
metrics.boundingBoxAscent = -rowMargin
// 表格分页处理(拆分表格)
if (isPagingMode) {
const height = this.getHeight()
const marginHeight = this.getMainOuterHeight()
let curPagePreHeight = marginHeight
for (let r = 0; r < rowList.length; r++) {
const row = rowList[r]
@ -1400,9 +1495,11 @@ export class Draw {
}
}
// 当前剩余高度是否能容下当前表格第一行(可拆分)的高度,排除掉表头类型
const rowMarginHeight = rowMargin * 2 * scale
const rowMarginHeight = rowMargin * 2
if (
curPagePreHeight + element.trList![0].height! + rowMarginHeight >
curPagePreHeight +
element.trList![0].height! * scale +
rowMarginHeight >
height ||
(element.pagingIndex !== 0 && element.trList![0].pagingRepeat)
) {
@ -1412,50 +1509,262 @@ export class Draw {
// 表格高度超过页面高度开始截断行
if (curPagePreHeight + rowMarginHeight + elementHeight > height) {
const trList = element.trList!
// 计算需要移除的行数
let deleteStart = 0
let deleteCount = 0
let preTrHeight = 0
// 大于一行时再拆分避免循环
if (trList.length > 1) {
for (let r = 0; r < trList.length; r++) {
const tr = trList[r]
const trHeight = tr.height * scale
/** 在哪一行截断表格 */
let splitTrIndex = -1
/** 被截断的行在截断线之前的高度 */
let splitTrPreHeight = 0
// 根据表格行列数构造出一个虚拟表格
const virtualTable = Array.from({ length: trList.length }, () =>
new Array(element.colgroup?.length).fill(undefined)
) as Array<Array<ITd | undefined>>
trList.forEach(tr => {
tr.tdList.forEach(td => {
virtualTable[td.rowIndex!][td.colIndex!] = td
})
})
// 加上表格上方的行间距
curPagePreHeight += rowMargin
for (const [trIndex, tr] of trList.entries()) {
// 找到需要截断的行,以及该行在截断线之前的高度
if (
curPagePreHeight + rowMarginHeight + preTrHeight + trHeight >
height
!tr.pagingRepeat && // 标题行不可截断
curPagePreHeight + rowMargin + tr.height * scale > height
) {
// 当前行存在跨行中断-暂时忽略分页
const rowColCount = tr.tdList.reduce(
(pre, cur) => pre + cur.colspan,
0
splitTrIndex = trIndex
splitTrPreHeight = height - curPagePreHeight - rowMargin // 额外减去被拆分的表格到下方的行间距
break
} else {
curPagePreHeight += tr.height * scale
}
}
if (splitTrIndex > -1) {
// 构建出一个行作为被截断出来的新行
const cloneTr = new Array(element.colgroup!.length) as Array<
ITd | undefined
>
// 判断目标行是否可截断
const allowSplitTr =
splitTrPreHeight >= trList[splitTrIndex].minHeight! * scale && // 最小行高区间内不可截断
trList[splitTrIndex].tdList.every(
// 如果截断线穿过该行所有单元格中第一个排版行,此时该行也不可截断
td => td.rowList![0].height < splitTrPreHeight
)
if (element.colgroup?.length !== rowColCount) {
deleteCount = 0
if (!allowSplitTr) {
splitTrPreHeight = 0
}
// 遍历虚拟表格中的截断行如果某个列的位置有td表明截断的是这个td如果该位置没有td且该位置不是被列合并单元格所占此时向上查找实际被截断的td
let preMergedEndIndex = -1
let splitTrReduceHeight =
trList[splitTrIndex].height - splitTrPreHeight / scale // 被拆分的行减少的高度
virtualTable[splitTrIndex].forEach((td, tdIndex) => {
if (tdIndex <= preMergedEndIndex) {
return
}
// 虚拟表格中截断行的当前索引位置是否有td
let hasTdAtCurIndex = false
// 找到被截断的td
let splitTd
if (td) {
splitTd = td
hasTdAtCurIndex = true
preMergedEndIndex = td.colIndex! + td.colspan - 1
} else {
for (let i = splitTrIndex; i >= 0; i--) {
if (virtualTable[i][tdIndex]) {
splitTd = virtualTable[i][tdIndex]
preMergedEndIndex =
splitTd!.colIndex! + splitTd!.colspan - 1
break
}
}
}
if (splitTd) {
cloneTr[tdIndex] = deepClone(splitTd)
// 如果tr可拆分根据截断位置将td中的内容拆分到新行中
// 如果tr不可拆分但当前位置td是跨行单元格同样需要拆分
if (allowSplitTr || splitTd.rowspan > 1) {
cloneTr[tdIndex]!.pagingOriginId =
splitTd.pagingOriginId || splitTd.id
cloneTr[tdIndex]!.id = getUUID()
// 计算当前td在截断线之前的高度默认为被截断行在截断线之前的高度若td跨行则加上其他行高度
let splitTdPreHeight = splitTrPreHeight
for (let i = splitTd.rowIndex!; i < splitTrIndex; i++) {
splitTdPreHeight += trList[i].height * scale
}
// 根据td中已排版的内容的高度计算出哪些内容需要拆分到新行中
let splitTdPreRowHeight = 0
let splitTdRowIndex = -1
for (const [rowIndex, row] of splitTd.rowList!.entries()) {
if (
row.height +
splitTdPreRowHeight +
tdPaddingHeight * scale >
splitTdPreHeight
) {
splitTdRowIndex = rowIndex
break
} else {
deleteStart = r + 1
deleteCount = trList.length - deleteStart
preTrHeight += trHeight
splitTdPreRowHeight += row.height
}
}
if (splitTdRowIndex > -1) {
// 拆分td中的内容
cloneTr[tdIndex]!.rowList =
splitTd.rowList!.splice(splitTdRowIndex)
cloneTr[tdIndex]!.value = cloneTr[tdIndex]!.rowList!.map(
row => row.elementList
).flat()
splitTd.value = splitTd
.rowList!.map(row => row.elementList)
.flat()
// 计算被拆分的单元格高度减少了多少
const splitTdReduceMainHeight = cloneTr[
tdIndex
]!.rowList!.reduce((ret, row) => {
return ret + row.height / scale
}, 0)
splitTd.mainHeight! -= splitTdReduceMainHeight
// 如果目标行可拆分,计算出行减小的高度
if (
allowSplitTr &&
splitTd.rowspan === 1 &&
splitTdRowIndex > 0
) {
splitTrReduceHeight = Math.min(
splitTd.height! - splitTdPreRowHeight / scale,
splitTrReduceHeight
)
}
if (deleteCount) {
const cloneTrList = trList.splice(deleteStart, deleteCount)
const cloneTrHeight = cloneTrList.reduce(
(pre, cur) => pre + cur.height,
} else {
// 执行到这里表示当前td虽然被截断但是内容高度未达到截断线位置
// FIXME: 此时拆分出来的单元格为空value中不应有元素否则会影响单元格可编辑性以及单元格尺寸
cloneTr[tdIndex]!.value = [{ value: ZERO }]
if (allowSplitTr && splitTd.rowspan === 1) {
splitTrReduceHeight = Math.min(
splitTd.height! - splitTdPreHeight / scale,
splitTrReduceHeight
)
}
}
// 更新td的rowspan
if (hasTdAtCurIndex) {
cloneTr[tdIndex]!.rowspan = splitTd.rowspan
splitTd.rowspan = 1
} else {
cloneTr[tdIndex]!.rowspan =
splitTd.rowspan - splitTrIndex + splitTd.rowIndex!
splitTd.rowspan = splitTrIndex - splitTd.rowIndex! + 1
// 如果tr不可截断且当前td是跨行单元格由于tr会被挪到后续表格因此需要将跨行单元格的rowspan减1
if (!allowSplitTr && splitTd.rowspan > 1) {
splitTd.rowspan--
}
}
}
}
})
// 构造新行
const newTr = deepClone(trList[splitTrIndex])
newTr.tdList = cloneTr.filter(td => td !== undefined) as ITd[]
if (allowSplitTr) {
newTr.pagingOriginId = newTr.pagingOriginId || newTr.id
newTr.id = getUUID()
trList[splitTrIndex].height -= splitTrReduceHeight
trList[splitTrIndex].tdList.forEach(td => {
td.realHeight! -= splitTrReduceHeight
td.height! -= splitTrReduceHeight
})
// 记录拆分出来的新行的原始高度
newTr.pagingOriginHeight = splitTrReduceHeight
newTr.height = Math.max(
splitTrReduceHeight + tdPaddingHeight,
newTr.minHeight!
)
} else {
// 记录被挪到下一页的行的原始高度
newTr.pagingOriginHeight = newTr.height
}
// 构造出后续表格的行
const cloneTrList = trList.splice(splitTrIndex + 1)
cloneTrList.unshift(newTr)
// 如果当前表格行不可从中间拆分,此行将被挪至后续表格,因此将其从当前表格中移除
if (!allowSplitTr) {
trList.splice(splitTrIndex, 1)
}
let totalHeight = 0
// 更新后续表格首行行高
const crossRowTds: ITd[] = []
newTr.tdList.forEach(td => {
if (td.rowspan === 1) {
const mainHeight =
td.rowList!.reduce(
(ret, row) => ret + row.height / scale,
0
) + tdPaddingHeight
newTr.height = Math.max(mainHeight, newTr.height!)
} else {
crossRowTds.push(td)
}
})
totalHeight += newTr.height
const groupTds = crossRowTds.reduce((ret, td) => {
const key = td.rowspan
if (ret[key]) {
ret[key].push(td)
} else {
ret[key] = [td]
}
return ret
}, {} as { [key: number]: ITd[] })
const maxRowspan = Math.max(
...Object.keys(groupTds).map(parseInt)
)
// 计算出跨行单元格最大内容高度如果高于其rowspan区间的行高之和增加rowspan区间中最后一行的行高
for (let i = 2; i <= maxRowspan; i++) {
const tds = groupTds[i]
if (tds) {
let maxMainHeight = 0
tds.forEach(td => {
const mainHeight =
td.rowList!.reduce(
(ret, row) => ret + row.height / scale,
0
) + tdPaddingHeight
maxMainHeight = Math.max(mainHeight, maxMainHeight)
})
if (maxMainHeight > totalHeight) {
// 记录此行原始高度,便于合并时还原行高
cloneTrList[i - 1].pagingOriginHeight =
cloneTrList[i - 1].height
cloneTrList[i - 1].height += maxMainHeight - totalHeight
}
totalHeight += maxMainHeight
}
}
const totalOriginHeight = cloneTrList.reduce(
(ret, tr) => ret + (tr.pagingOriginHeight || tr.height),
0
)
// 追加拆分表格
const pagingId = element.pagingId || getUUID()
element.pagingId = pagingId
element.height -= cloneTrHeight
metrics.height -= cloneTrHeight
metrics.boundingBoxDescent -= cloneTrHeight
// 追加拆分表格
element.height -= totalOriginHeight
metrics.height -= totalOriginHeight * scale
metrics.boundingBoxDescent -= totalOriginHeight * scale
const cloneElement = deepClone(element)
cloneElement.id = getUUID()
cloneElement.pagingId = pagingId
cloneElement.pagingIndex = element.pagingIndex! + 1
this.tableParticle.computeRowColInfo(element)
// 更新拆分出来的新表格中的id关联信息
cloneTrList.forEach(tr => {
tr.tdList.forEach(td => {
td.value.forEach(v => {
v.tdId = td.id
v.trId = tr.id
v.tableId = cloneElement.id
})
})
})
// 处理分页重复表头
const repeatTrList = trList.filter(tr => tr.pagingRepeat)
if (repeatTrList.length) {
@ -1464,35 +1773,46 @@ export class Draw {
cloneTrList.unshift(...cloneRepeatTrList)
}
cloneElement.trList = cloneTrList
cloneElement.id = getUUID()
this.spliceElementList(elementList, i + 1, 0, cloneElement)
}
}
// 表格经过分页处理-需要处理上下文
// 表格经过分页处理-需要处理上下文和选区
if (element.pagingId) {
const positionContext = this.position.getPositionContext()
if (positionContext.isTable) {
// 查找光标所在表格索引根据trId搜索
let newPositionContextIndex = -1
let newPositionContextTrIndex = -1
let tableIndex = i
while (tableIndex < elementList.length) {
const curElement = elementList[tableIndex]
if (curElement.pagingId !== element.pagingId) break
const trIndex = curElement.trList!.findIndex(
r => r.id === positionContext.trId
if (
positionContext.isTable &&
positionContext.tableId === element.id
) {
const trIndex = element.trList!.findIndex(
r =>
r.pagingOriginId === positionContext.trId ||
r.id === positionContext.trId
)
if (~trIndex) {
newPositionContextIndex = tableIndex
newPositionContextTrIndex = trIndex
break
const tr = element.trList![trIndex]
const tdIndex = tr.tdList!.findIndex(
d =>
d.pagingOriginId === positionContext.tdId ||
d.id === positionContext.tdId
)
if (~tdIndex) {
const td = tr.tdList![tdIndex]
if (curIndex !== undefined && curIndex > -1) {
if (td.value[curIndex]) {
positionContext.index = i
positionContext.trIndex = trIndex
positionContext.tdIndex = tdIndex
positionContext.trId = tr.id
positionContext.tdId = td.id
this.range.setRange(curIndex, curIndex)
} else {
positionContext.tableId = elementList[i + 1].id
curIndex -= td.value.length
}
tableIndex++
}
if (~newPositionContextIndex) {
positionContext.index = newPositionContextIndex
positionContext.trIndex = newPositionContextTrIndex
this.position.setPositionContext(positionContext)
}
} else {
positionContext.tableId = elementList[i + 1].id
}
}
}
@ -1739,7 +2059,7 @@ export class Draw {
}
}
}
return rowList
return { rowList, curIndex }
}
private _computePageList(): IRow[][] {
@ -2372,11 +2692,14 @@ export class Draw {
}
}
// 行信息
this.rowList = this.computeRowList({
const { rowList, curIndex: newIndex } = this.computeRowList({
isPagingMode,
innerWidth,
elementList: this.elementList
elementList: this.elementList,
curIndex
})
this.rowList = rowList
curIndex = newIndex
// 页面信息
this.pageRowList = this._computePageList()
// 位置信息

@ -208,7 +208,7 @@ export class Control {
}
public getIsDisabledControl(context: IControlContext = {}): boolean {
if (!this.activeControl) return false
if (this.draw.isDesignMode() || !this.activeControl) return false
const { startIndex, endIndex } = context.range || this.range.getRange()
if (startIndex === endIndex && ~startIndex && ~endIndex) {
const elementList = context.elementList || this.getElementList()
@ -452,8 +452,11 @@ export class Control {
): number | null {
const elementList = context.elementList || this.getElementList()
const startElement = elementList[startIndex]
// 设计模式不验证删除权限
if (!this.draw.isDesignMode()) {
const { deletable = true } = startElement.control!
if (!deletable) return null
}
let leftIndex = -1
let rightIndex = -1
// 向左查找
@ -538,7 +541,9 @@ export class Control {
controlComponent: ControlComponent.PLACEHOLDER,
color: this.controlOptions.placeholderColor
}
formatElementContext(elementList, [newElement], startIndex)
formatElementContext(elementList, [newElement], startIndex, {
editorOptions: this.options
})
this.draw.spliceElementList(
elementList,
startIndex + p + 1,

@ -6,11 +6,13 @@ import {
import { ControlComponent } from '../../../../dataset/enum/Control'
import { ElementType } from '../../../../dataset/enum/Element'
import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { DeepRequired } from '../../../../interface/Common'
import {
IControlContext,
IControlInstance,
IControlRuleOption
} from '../../../../interface/Control'
import { IEditorOption } from '../../../../interface/Editor'
import { IElement } from '../../../../interface/Element'
import { omitObject, pickObject } from '../../../../utils'
import { formatElementContext } from '../../../../utils/element'
@ -24,10 +26,12 @@ export class DateControl implements IControlInstance {
private control: Control
private isPopup: boolean
private datePicker: DatePicker | null
private options: DeepRequired<IEditorOption>
constructor(element: IElement, control: Control) {
const draw = control.getDraw()
this.draw = draw
this.options = draw.getOptions()
this.element = element
this.control = control
this.isPopup = false
@ -138,7 +142,9 @@ export class DateControl implements IControlInstance {
...data[i],
controlComponent: ControlComponent.VALUE
}
formatElementContext(elementList, [newElement], startIndex)
formatElementContext(elementList, [newElement], startIndex, {
editorOptions: this.options
})
draw.spliceElementList(elementList, start + i, 0, newElement)
}
return start + data.length - 1
@ -207,7 +213,9 @@ export class DateControl implements IControlInstance {
value: date[i],
controlComponent: ControlComponent.VALUE
}
formatElementContext(elementList, [newElement], prefixIndex)
formatElementContext(elementList, [newElement], prefixIndex, {
editorOptions: this.options
})
draw.spliceElementList(elementList, start + i, 0, newElement)
}
// 重新渲染控件

@ -10,11 +10,13 @@ import { ControlComponent } from '../../../../dataset/enum/Control'
import { EditorComponent } from '../../../../dataset/enum/Editor'
import { ElementType } from '../../../../dataset/enum/Element'
import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { DeepRequired } from '../../../../interface/Common'
import {
IControlContext,
IControlInstance,
IControlRuleOption
} from '../../../../interface/Control'
import { IEditorOption } from '../../../../interface/Editor'
import { IElement } from '../../../../interface/Element'
import { omitObject, pickObject, splitText } from '../../../../utils'
import { formatElementContext } from '../../../../utils/element'
@ -25,8 +27,10 @@ export class SelectControl implements IControlInstance {
private control: Control
private isPopup: boolean
private selectDom: HTMLDivElement | null
private options: DeepRequired<IEditorOption>
constructor(element: IElement, control: Control) {
this.options = control.getDraw().getOptions()
this.element = element
this.control = control
this.isPopup = false
@ -270,7 +274,9 @@ export class SelectControl implements IControlInstance {
value: data[i],
controlComponent: ControlComponent.VALUE
}
formatElementContext(elementList, [newElement], prefixIndex)
formatElementContext(elementList, [newElement], prefixIndex, {
editorOptions: this.options
})
draw.spliceElementList(elementList, start + i, 0, newElement)
}
// 设置状态

@ -4,11 +4,13 @@ import {
} from '../../../../dataset/constant/Element'
import { ControlComponent } from '../../../../dataset/enum/Control'
import { KeyMap } from '../../../../dataset/enum/KeyMap'
import { DeepRequired } from '../../../../interface/Common'
import {
IControlContext,
IControlInstance,
IControlRuleOption
} from '../../../../interface/Control'
import { IEditorOption } from '../../../../interface/Editor'
import { IElement } from '../../../../interface/Element'
import { omitObject, pickObject } from '../../../../utils'
import { formatElementContext } from '../../../../utils/element'
@ -17,8 +19,10 @@ import { Control } from '../Control'
export class TextControl implements IControlInstance {
private element: IElement
private control: Control
private options: DeepRequired<IEditorOption>
constructor(element: IElement, control: Control) {
this.options = control.getDraw().getOptions()
this.element = element
this.control = control
}
@ -114,7 +118,9 @@ export class TextControl implements IControlInstance {
...data[i],
controlComponent: ControlComponent.VALUE
}
formatElementContext(elementList, [newElement], startIndex)
formatElementContext(elementList, [newElement], startIndex, {
editorOptions: this.options
})
draw.spliceElementList(elementList, start + i, 0, newElement)
}
return start + data.length - 1

@ -58,7 +58,7 @@ export class Footer {
this.rowList = this.draw.computeRowList({
innerWidth,
elementList: this.elementList
})
}).rowList
}
private _computePositionList() {

@ -58,7 +58,7 @@ export class Header {
this.rowList = this.draw.computeRowList({
innerWidth,
elementList: this.elementList
})
}).rowList
}
private _computePositionList() {

@ -42,7 +42,7 @@ export class Placeholder {
this.rowList = this.draw.computeRowList({
innerWidth,
elementList: this.elementList
})
}).rowList
}
private _computePositionList() {

@ -1,4 +1,6 @@
import { ElementType } from '../../../../dataset/enum/Element'
import { DeepRequired } from '../../../../interface/Common'
import { IEditorOption } from '../../../../interface/Editor'
import { IElement, IElementPosition } from '../../../../interface/Element'
import { formatElementContext } from '../../../../utils/element'
import { RangeManager } from '../../../range/RangeManager'
@ -9,9 +11,11 @@ export class DateParticle {
private draw: Draw
private range: RangeManager
private datePicker: DatePicker
private options: DeepRequired<IEditorOption>
constructor(draw: Draw) {
this.draw = draw
this.options = draw.getOptions()
this.range = draw.getRange()
this.datePicker = new DatePicker(draw, {
onSubmit: this._setValue.bind(this)
@ -43,7 +47,9 @@ export class DateParticle {
}
]
}
formatElementContext(elementList, [dateElement], leftIndex)
formatElementContext(elementList, [dateElement], leftIndex, {
editorOptions: this.options
})
this.draw.insertElementList([dateElement])
}

@ -30,11 +30,15 @@ export function input(data: string, host: CanvasEvent) {
const elementList = draw.getElementList()
const copyElement = getAnchorElement(elementList, endIndex)
if (!copyElement) return
const isDesignMode = draw.isDesignMode()
const inputData: IElement[] = splitText(text).map(value => {
const newElement: IElement = {
value
}
if (!copyElement.title?.disabled && !copyElement.control?.disabled) {
if (
isDesignMode ||
(!copyElement.title?.disabled && !copyElement.control?.disabled)
) {
const nextElement = elementList[endIndex + 1]
if (
!copyElement.type ||
@ -69,7 +73,9 @@ export function input(data: string, host: CanvasEvent) {
if (startIndex !== endIndex) {
draw.spliceElementList(elementList, start, endIndex - startIndex)
}
formatElementContext(elementList, inputData, startIndex)
formatElementContext(elementList, inputData, startIndex, {
editorOptions: draw.getOptions()
})
draw.spliceElementList(elementList, start, 0, ...inputData)
curIndex = startIndex + inputData.length
}

@ -40,7 +40,8 @@ export function enter(evt: KeyboardEvent, host: CanvasEvent) {
}
// 格式化上下文
formatElementContext(elementList, [enterText], startIndex, {
isBreakWhenWrap: true
isBreakWhenWrap: true,
editorOptions: draw.getOptions()
})
// 标题结尾处回车无需格式化及样式复制
if (

@ -25,7 +25,9 @@ export function tab(evt: KeyboardEvent, host: CanvasEvent) {
const rangeManager = draw.getRange()
const { startIndex } = rangeManager.getRange()
const elementList = draw.getElementList()
formatElementContext(elementList, [tabElement], startIndex)
formatElementContext(elementList, [tabElement], startIndex, {
editorOptions: draw.getOptions()
})
draw.insertElementList([tabElement])
}
}

@ -176,7 +176,9 @@ export function mouseup(evt: MouseEvent, host: CanvasEvent) {
return newElement
}
})
formatElementContext(elementList, replaceElementList, range.startIndex)
formatElementContext(elementList, replaceElementList, range.startIndex, {
editorOptions: draw.getOptions()
})
// 缓存拖拽选区开始元素、位置、开始结束id
const cacheStartElement = cacheElementList[cacheStartIndex]
const cacheStartPosition = cachePositionList[cacheStartIndex]

@ -50,7 +50,8 @@ export function pasteElement(host: CanvasEvent, elementList: IElement[]) {
}
}
formatElementContext(originalElementList, elementList, startIndex, {
isBreakWhenWrap: true
isBreakWhenWrap: true,
editorOptions: draw.getOptions()
})
}
draw.insertElementList(elementList)
@ -87,7 +88,9 @@ export function pasteImage(host: CanvasEvent, file: File | Blob) {
height: image.height
}
if (~startIndex) {
formatElementContext(elementList, [imageElement], startIndex)
formatElementContext(elementList, [imageElement], startIndex, {
editorOptions: draw.getOptions()
})
}
draw.insertElementList([imageElement])
}

@ -19,7 +19,8 @@ export enum EditorMode {
CLEAN = 'clean', // 清洁模式(隐藏辅助元素)
READONLY = 'readonly', // 只读模式(文档不可编辑)
FORM = 'form', // 表单模式(仅控件内可编辑)
PRINT = 'print' // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框)
PRINT = 'print', // 打印模式(文档不可编辑、隐藏辅助元素、选区、未书写控件及边框)
DESIGN = 'design' // 设计模式(不可删除、只读等配置不控制)
}
export enum EditorZone {

@ -70,4 +70,5 @@ export interface IComputeRowListPayload {
innerWidth: number
elementList: IElement[]
isPagingMode?: boolean
curIndex?: number
}

@ -29,4 +29,5 @@ export interface ITd {
mainHeight?: number // 内容 + 内边距高度
realHeight?: number // 真实高度(包含跨列)
realMinHeight?: number // 真实最小高度(包含跨列)
pagingOriginId?: string // 被拆分到下一页的单元格的原始id
}

@ -6,4 +6,6 @@ export interface ITr {
tdList: ITd[]
minHeight?: number
pagingRepeat?: boolean // 在各页顶端以标题行的形式重复出现
pagingOriginHeight?: number // 被拆分到下一页的行的原始高度
pagingOriginId?: string // 被拆分到下一页的行的原始id
}

@ -9,6 +9,7 @@ import {
splitText
} from '.'
import {
EditorMode,
ElementType,
IEditorOption,
IElement,
@ -606,13 +607,85 @@ export function zipElementList(
} else if (element.type === ElementType.TABLE) {
// 分页表格先进行合并
if (element.pagingId) {
// 为当前表格构建一个虚拟表格
const virtualTable = Array.from(
{ length: element.trList!.length },
() => new Array(element.colgroup!.length)
) as Array<Array<ITd | undefined | null>>
element.trList!.forEach((tr, trIndex) => {
let tdIndex = 0
tr.tdList.forEach(td => {
while (virtualTable[trIndex][tdIndex] === null) {
tdIndex++
}
virtualTable[trIndex][tdIndex] = td
for (let i = 1; i < td.rowspan; i++) {
virtualTable[trIndex + i][tdIndex] = null
}
tdIndex += td.colspan
})
})
let tableIndex = e + 1
let combineCount = 0
while (tableIndex < elementList.length) {
const nextElement = elementList[tableIndex]
if (nextElement.pagingId === element.pagingId) {
element.height! += nextElement.height!
element.trList!.push(...nextElement.trList!)
const nexTrList = nextElement.trList!.filter(
tr => !tr.pagingRepeat
)
// 判断后续表格第一行是拆分出来的还是从原表格挪到下一页的
const isNextTrSplit =
element.trList![element.trList!.length - 1].id ===
nexTrList[0].pagingOriginId
// 遍历后续表格首行中的单元格,在虚拟表格中找到其对应单元格
let tdIndex = 0
const mergedTds: ITd[] = []
nexTrList[0].tdList.forEach(td => {
let targetTd
// 如果虚拟表格最后一行对应位置有单元格,则其就为目标单元格,否则向上查找
if (virtualTable[virtualTable.length - 1][tdIndex]) {
targetTd = virtualTable[virtualTable.length - 1][tdIndex]
} else {
for (let i = virtualTable.length - 2; i >= 0; i--) {
if (virtualTable[i][tdIndex]) {
targetTd = virtualTable[i][tdIndex]
break
}
}
}
if (targetTd) {
if (targetTd.id === td.pagingOriginId) {
targetTd.value.push(...td.value)
if (isNextTrSplit) {
targetTd.rowspan = targetTd.rowspan + td.rowspan - 1
} else {
targetTd.rowspan = targetTd.rowspan + td.rowspan
mergedTds.push(td)
}
}
tdIndex += targetTd.colspan
}
})
nexTrList[0].tdList = nexTrList[0].tdList.filter(td => {
const isNotMerged = mergedTds.every(
mergedTd => mergedTd.id !== td.id
)
delete td.pagingOriginId
return isNotMerged
})
// 更新行高,逐行合并
while (nexTrList.length > 0) {
const lastTr = element.trList![element.trList!.length - 1]
const nextTr = nexTrList.shift()!
if (lastTr.id === nextTr.pagingOriginId) {
lastTr.height += nextTr.pagingOriginHeight || 0
} else {
nextTr.height = nextTr.pagingOriginHeight || nextTr.height
element.trList!.push(nextTr)
}
delete nextTr.pagingOriginHeight
delete nextTr.pagingOriginId
}
tableIndex++
combineCount++
} else {
@ -821,7 +894,8 @@ export function getAnchorElement(
}
export interface IFormatElementContextOption {
isBreakWhenWrap: boolean
isBreakWhenWrap?: boolean
editorOptions?: DeepRequired<IEditorOption>
}
export function formatElementContext(
@ -832,11 +906,12 @@ export function formatElementContext(
) {
let copyElement = getAnchorElement(sourceElementList, anchorIndex)
if (!copyElement) return
// 标题元素禁用时不复制标题属性
if (copyElement.title?.disabled) {
const { isBreakWhenWrap = false, editorOptions } = options || {}
const { mode } = editorOptions || {}
// 非设计模式时:标题元素禁用时不复制标题属性
if (mode !== EditorMode.DESIGN && copyElement.title?.disabled) {
copyElement = omitObject(copyElement, TITLE_CONTEXT_ATTR)
}
const { isBreakWhenWrap = false } = options || {}
// 是否已经换行
let isBreakWarped = false
for (let e = 0; e < formatElementList.length; e++) {
@ -865,7 +940,8 @@ export function formatElementContext(
formatElementContext(
sourceElementList,
targetElement.valueList,
anchorIndex
anchorIndex,
options
)
}
// 非块类元素,需处理行属性

@ -1412,6 +1412,10 @@ window.onload = function () {
{
mode: EditorMode.PRINT,
name: '打印模式'
},
{
mode: EditorMode.DESIGN,
name: '设计模式'
}
]
const modeElement = document.querySelector<HTMLDivElement>('.editor-mode')!

Loading…
Cancel
Save