diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 44b314f6a..b31144d0a 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -104,8 +104,8 @@ import { PUNCTUATION_REG } from '../../dataset/constant/Regular' import { LineBreakParticle } from './particle/LineBreakParticle' import { MouseObserver } from '../observer/MouseObserver' import { LineNumber } from './frame/LineNumber' -import { PageBorder } from './frame/PageBorder' import { ITd } from '../../interface/table/Td' +import { PageBorder } from './frame/PageBorder' import { Actuator } from '../actuator/Actuator' import { TableOperate } from './particle/table/TableOperate' @@ -1250,6 +1250,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 @@ -1330,39 +1331,37 @@ 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) { - let tableIndex = i + 1 - let combineCount = 0 - while (tableIndex < elementList.length) { - const nextElement = elementList[tableIndex] - if (nextElement.pagingId === element.pagingId) { - const nexTrList = nextElement.trList!.filter( - tr => !tr.pagingRepeat - ) - element.trList!.push(...nexTrList) - element.height! += nextElement.height! - tableIndex++ - combineCount++ - } else { - break - } - } - if (combineCount) { - elementList.splice(i + 1, combineCount) - } - } - element.pagingIndex = element.pagingIndex ?? 0 - // 计算表格行列 + const { curIndex: newIndex, positionContext } = + this.tableParticle.mergeSplittedTable({ + element, + elementList, + index: i, + curIndex, + mergeForward: true + }) + curIndex = newIndex + // 计算更新单元格行列索引等信息 this.tableParticle.computeRowColInfo(element) // 计算表格内元素信息 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, isFromTable: true, @@ -1438,8 +1437,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] @@ -1453,62 +1450,282 @@ 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) + (element.pagingIndex !== 0 && element.trList![0].pagingRepeat) // 或者当前表格是被拆分出来的拼接子表格(非第一个),且子表格第一行是标题行 ) { - // 无可拆分行则切换至新页 + // 切换至新页 curPagePreHeight = marginHeight } // 表格高度超过页面高度开始截断行 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 - if ( - curPagePreHeight + rowMarginHeight + preTrHeight + trHeight > - height - ) { - // 当前行存在跨行中断-暂时忽略分页 - const rowColCount = tr.tdList.reduce( - (pre, cur) => pre + cur.colspan, - 0 - ) - if (element.colgroup?.length !== rowColCount) { - deleteCount = 0 + /** 在哪一行截断表格 */ + let splitTrIndex = -1 + /** 被截断的行在截断线之前的高度 */ + let splitTrPreHeight = 0 + // 根据表格行列数构造出一个虚拟表格 + const virtualTable = Array.from({ length: trList.length }, () => + new Array(element.colgroup?.length).fill(undefined) + ) as Array> + trList.forEach(tr => { + tr.tdList.forEach(td => { + virtualTable[td.rowIndex!][td.colIndex!] = td + }) + }) + // 加上表格上方的行间距 + curPagePreHeight += rowMargin + for (const [trIndex, tr] of trList.entries()) { + // 找到需要截断的行,以及该行在截断线之前的高度 + if ( + !tr.pagingRepeat && // 标题行不可截断 + curPagePreHeight + rowMargin + tr.height * scale > height + ) { + 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] || td.rowList![0].height < splitTrPreHeight + ) + 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 + } } - break + } + if (splitTd) { + cloneTr[tdIndex] = deepClone(splitTd) + // 如果tr可拆分,根据截断位置,将td中的内容拆分到新行中 + // 如果tr不可拆分,但要拆分的td不在当前行且是跨行单元格,同样需要拆分 + if ( + allowSplitTr || + (splitTd.rowspan > 1 && !hasTdAtCurIndex) + ) { + 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 { + 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 + ) + } + } else { + // 执行到这里表示当前td虽然被截断,但是内容高度未达到截断线位置 + cloneTr[tdIndex]!.value = [] + 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 { - deleteStart = r + 1 - deleteCount = trList.length - deleteStart - preTrHeight += trHeight + 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 } } - } - if (deleteCount) { - const cloneTrList = trList.splice(deleteStart, deleteCount) - const cloneTrHeight = cloneTrList.reduce( - (pre, cur) => pre + cur.height, + 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) { @@ -1517,38 +1734,83 @@ 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 + ) { + let positionContextFixed = false + for ( + let trIndex = 0; + trIndex < element.trList!.length; + trIndex++ + ) { + const tr = element.trList![trIndex] + const tdIndex = tr.tdList.findIndex( + d => + d.pagingOriginId === positionContext.tdId || + d.id === positionContext.tdId ) - if (~trIndex) { - newPositionContextIndex = tableIndex - newPositionContextTrIndex = trIndex - break + 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) + positionContextFixed = true + break + } else { + curIndex -= td.value.length + } + } else if (td.id === positionContext.tdId) { + positionContextFixed = true + } } - tableIndex++ } - if (~newPositionContextIndex) { - positionContext.index = newPositionContextIndex - positionContext.trIndex = newPositionContextTrIndex + if (!positionContextFixed) { + positionContext.tableId = elementList[i + 1].id + } else { this.position.setPositionContext(positionContext) } } } + } else { + // 连页模式下,修正位置上下文和选区(因为先前合并表格时只修正了部分上下文) + if ( + element.pagingId && + positionContext.isTable && + positionContext.tableId === element.id + ) { + outer: for ( + let trIndex = 0; + trIndex < element.trList!.length; + trIndex++ + ) { + const tr = element.trList![trIndex] + for (let tdIndex = 0; tdIndex < tr.tdList.length; tdIndex++) { + const td = tr.tdList[tdIndex] + if (td.id === positionContext.tdId) { + positionContext.index = i + positionContext.tdIndex = tdIndex + positionContext.trId = tr.id + positionContext.trIndex = trIndex + this.position.setPositionContext(positionContext) + if (curIndex !== undefined && curIndex > -1) { + this.range.setRange(curIndex, curIndex) + } + break outer + } + } + } + } } } else if (element.type === ElementType.SEPARATOR) { const { @@ -1845,7 +2107,7 @@ export class Draw { x += metrics.width } } - return rowList + return { rowList, curIndex } } private _computePageList(): IRow[][] { @@ -2501,7 +2763,7 @@ export class Draw { const startX = margins[3] const startY = margins[0] + extraHeight const surroundElementList = pickSurroundElementList(this.elementList) - this.rowList = this.computeRowList({ + const { rowList, curIndex: newIndex } = this.computeRowList({ startX, startY, pageHeight, @@ -2509,8 +2771,11 @@ export class Draw { isPagingMode, innerWidth, surroundElementList, - elementList: this.elementList + elementList: this.elementList, + curIndex }) + this.rowList = rowList + curIndex = newIndex // 页面信息 this.pageRowList = this._computePageList() // 位置信息 diff --git a/src/editor/core/draw/frame/Footer.ts b/src/editor/core/draw/frame/Footer.ts index 763a0c950..726cd9b26 100644 --- a/src/editor/core/draw/frame/Footer.ts +++ b/src/editor/core/draw/frame/Footer.ts @@ -58,7 +58,7 @@ export class Footer { this.rowList = this.draw.computeRowList({ innerWidth, elementList: this.elementList - }) + }).rowList } private _computePositionList() { diff --git a/src/editor/core/draw/frame/Header.ts b/src/editor/core/draw/frame/Header.ts index 98a025e85..9c6fffbca 100644 --- a/src/editor/core/draw/frame/Header.ts +++ b/src/editor/core/draw/frame/Header.ts @@ -64,7 +64,7 @@ export class Header { innerWidth, elementList: this.elementList, surroundElementList - }) + }).rowList } private _computePositionList() { diff --git a/src/editor/core/draw/frame/Placeholder.ts b/src/editor/core/draw/frame/Placeholder.ts index db21327cb..1aa5106c6 100644 --- a/src/editor/core/draw/frame/Placeholder.ts +++ b/src/editor/core/draw/frame/Placeholder.ts @@ -42,7 +42,7 @@ export class Placeholder { this.rowList = this.draw.computeRowList({ innerWidth, elementList: this.elementList - }) + }).rowList } private _computePositionList() { diff --git a/src/editor/core/draw/particle/ListParticle.ts b/src/editor/core/draw/particle/ListParticle.ts index f4e1c498a..a3091093a 100644 --- a/src/editor/core/draw/particle/ListParticle.ts +++ b/src/editor/core/draw/particle/ListParticle.ts @@ -102,6 +102,9 @@ export class ListParticle { ): Map { const listStyleMap = new Map() let start = 0 + if (!elementList[start]) { + return listStyleMap + } let curListId = elementList[start].listId let curElementList: IElement[] = [] const elementLength = elementList.length diff --git a/src/editor/core/draw/particle/table/TableOperate.ts b/src/editor/core/draw/particle/table/TableOperate.ts index 20c0176a2..9b26c001a 100644 --- a/src/editor/core/draw/particle/table/TableOperate.ts +++ b/src/editor/core/draw/particle/table/TableOperate.ts @@ -16,6 +16,7 @@ import { RangeManager } from '../../../range/RangeManager' import { Draw } from '../../Draw' import { TableParticle } from './TableParticle' import { TableTool } from './TableTool' +import { IPositionContext } from '../../../../interface/Position' export class TableOperate { private draw: Draw @@ -98,11 +99,19 @@ export class TableOperate { } public insertTableTopRow() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, trIndex, tableId } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 插入新行前先合并跨页表格 + const { element, index, positionContext } = + this.tableParticle.mergeSplittedTable({ + element: originalElement, + index: originalIndex!, + elementList: originalElementList + }) + const { trIndex, tableId } = positionContext! const curTrList = element.trList! const curTr = curTrList[trIndex!] // 之前跨行的增加跨行数 @@ -157,15 +166,22 @@ export class TableOperate { this.range.setRange(0, 0) // 重新渲染 this.draw.render({ curIndex: 0 }) - this.tableTool.render() } public insertTableBottomRow() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, trIndex, tableId } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 插入新行前先合并跨页表格 + const { element, index, positionContext } = + this.tableParticle.mergeSplittedTable({ + element: originalElement, + index: originalIndex!, + elementList: originalElementList + }) + const { trIndex, tableId } = positionContext! const curTrList = element.trList! const curTr = curTrList[trIndex!] const anchorTr = @@ -225,35 +241,64 @@ export class TableOperate { } public insertTableLeftCol() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, tdIndex, tableId } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 插入新列前先合并跨页表格 + const { element, positionContext } = this.tableParticle.mergeSplittedTable({ + element: originalElement, + index: originalIndex!, + elementList: originalElementList + }) + const { trIndex, tdIndex, tableId } = positionContext const curTrList = element.trList! - const curTdIndex = tdIndex! - // 增加列 - for (let t = 0; t < curTrList.length; t++) { - const tr = curTrList[t] - const tdId = getUUID() - tr.tdList.splice(curTdIndex, 0, { - id: tdId, - rowspan: 1, - colspan: 1, - value: [ - { - value: ZERO, - size: 16, - tableId, - trId: tr.id, - tdId + const curTdColIndex = curTrList[trIndex!].tdList[tdIndex!].colIndex! + let newPositionContext: IPositionContext | null = null + // 逐行添加单元格 + curTrList.forEach((tr, trIndex) => { + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if ( + // 之前跨列的增加跨列数 + td.colIndex! < curTdColIndex && + td.colIndex! + td.colspan > curTdColIndex + ) { + td.colspan += 1 + break + } else if (td.colIndex! >= curTdColIndex) { + const tdId = getUUID() + tr.tdList.splice(d, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: tr.id, + tdId + } + ] + }) + if (!newPositionContext) { + newPositionContext = { + ...positionContext, + tdIndex: d, + tdId, + trIndex, + trId: tr.id + } } - ] - }) - } + break + } + } + }) // 重新计算宽度 const colgroup = element.colgroup! - colgroup.splice(curTdIndex, 0, { + colgroup.splice(curTdColIndex, 0, { width: this.options.table.defaultColMinWidth }) const colgroupWidth = colgroup.reduce((pre, cur) => pre + cur.width, 0) @@ -266,51 +311,74 @@ export class TableOperate { } } // 重新设置上下文 - this.position.setPositionContext({ - isTable: true, - index, - trIndex: 0, - tdIndex: curTdIndex, - tdId: curTrList[0].tdList[curTdIndex].id, - trId: curTrList[0].id, - tableId - }) + newPositionContext && this.position.setPositionContext(newPositionContext) this.range.setRange(0, 0) // 重新渲染 this.draw.render({ curIndex: 0 }) - this.tableTool.render() } public insertTableRightCol() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, tdIndex, tableId } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 插入新列前先合并跨页表格 + const { element, positionContext } = this.tableParticle.mergeSplittedTable({ + element: originalElement, + index: originalIndex!, + elementList: originalElementList + }) + const { trIndex, tdIndex, tableId } = positionContext const curTrList = element.trList! - const curTdIndex = tdIndex! + 1 - // 增加列 - for (let t = 0; t < curTrList.length; t++) { - const tr = curTrList[t] - const tdId = getUUID() - tr.tdList.splice(curTdIndex, 0, { - id: tdId, - rowspan: 1, - colspan: 1, - value: [ - { - value: ZERO, - size: 16, - tableId, - trId: tr.id, - tdId + const curTd = curTrList[trIndex!].tdList[tdIndex!] + const newColIndex = curTd.colIndex! + curTd.colspan + let newPositionContext: IPositionContext | null = null + // 逐行添加单元格 + curTrList.forEach((tr, trIndex) => { + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if ( + // 之前跨列的增加跨列数 + td.colIndex! < newColIndex && + td.colIndex! + td.colspan > newColIndex + ) { + td.colspan += 1 + break + } else if (td.colIndex! + td.colspan >= newColIndex) { + const tdId = getUUID() + const newTdIndex = + td.colIndex! + td.colspan === newColIndex ? d + 1 : d + tr.tdList.splice(newTdIndex, 0, { + id: tdId, + rowspan: 1, + colspan: 1, + value: [ + { + value: ZERO, + size: 16, + tableId, + trId: tr.id, + tdId + } + ] + }) + if (!newPositionContext) { + newPositionContext = { + ...positionContext, + tdIndex: newTdIndex, + tdId, + trIndex, + trId: tr.id + } } - ] - }) - } + break + } + } + }) // 重新计算宽度 const colgroup = element.colgroup! - colgroup.splice(curTdIndex, 0, { + colgroup.splice(newColIndex, 0, { width: this.options.table.defaultColMinWidth }) const colgroupWidth = colgroup.reduce((pre, cur) => pre + cur.width, 0) @@ -323,26 +391,25 @@ export class TableOperate { } } // 重新设置上下文 - this.position.setPositionContext({ - isTable: true, - index, - trIndex: 0, - tdIndex: curTdIndex, - tdId: curTrList[0].tdList[curTdIndex].id, - trId: curTrList[0].id, - tableId: element.id - }) + newPositionContext && this.position.setPositionContext(newPositionContext) this.range.setRange(0, 0) // 重新渲染 this.draw.render({ curIndex: 0 }) } public deleteTableRow() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, trIndex, tdIndex } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 删除表格行前先合并跨页表格 + const { element, positionContext } = this.tableParticle.mergeSplittedTable({ + index: originalIndex!, + element: originalElement, + elementList: originalElementList + }) + const { trIndex, tdIndex } = positionContext const trList = element.trList! const curTr = trList[trIndex!] const curTdRowIndex = curTr.tdList[tdIndex!].rowIndex! @@ -366,9 +433,14 @@ export class TableOperate { for (let d = 0; d < curTr.tdList.length; d++) { const td = curTr.tdList[d] if (td.rowspan > 1) { + // 根据列索引,找到被删除的行中的跨行单元格在下一行的对应位置,补上单元格 const tdId = getUUID() const nextTr = trList[trIndex! + 1] - nextTr.tdList.splice(d, 0, { + const nextTdArray: ITd[] = new Array(element.colgroup!.length) + nextTr.tdList.forEach(nextTd => { + nextTdArray[nextTd.colIndex!] = nextTd + }) + nextTdArray[td.colIndex!] = { id: tdId, rowspan: td.rowspan - 1, colspan: td.colspan, @@ -381,7 +453,8 @@ export class TableOperate { tdId } ] - }) + } + nextTr.tdList = nextTdArray.filter(item => item !== undefined) } } // 删除当前行 @@ -399,11 +472,18 @@ export class TableOperate { } public deleteTableCol() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return - const { index, tdIndex, trIndex } = positionContext + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return + const { index: originalIndex } = originalPositionContext const originalElementList = this.draw.getOriginalElementList() - const element = originalElementList[index!] + const originalElement = originalElementList[originalIndex!] + // 删除表格列前先合并跨页表格 + const { element, positionContext } = this.tableParticle.mergeSplittedTable({ + element: originalElement, + index: originalIndex!, + elementList: originalElementList + }) + const { trIndex, tdIndex } = positionContext const curTrList = element.trList! const curTd = curTrList[trIndex!].tdList[tdIndex!] const curColIndex = curTd.colIndex! @@ -444,9 +524,15 @@ export class TableOperate { } public deleteTable() { - const positionContext = this.position.getPositionContext() - if (!positionContext.isTable) return + const originalPositionContext = this.position.getPositionContext() + if (!originalPositionContext.isTable) return const originalElementList = this.draw.getOriginalElementList() + // 删除表格前先合并跨页表格 + const { positionContext } = this.tableParticle.mergeSplittedTable({ + element: originalElementList[originalPositionContext.index!], + index: originalPositionContext.index!, + elementList: originalElementList + }) originalElementList.splice(positionContext.index!, 1) const curIndex = positionContext.index! - 1 this.position.setPositionContext({ diff --git a/src/editor/core/draw/particle/table/TableParticle.ts b/src/editor/core/draw/particle/table/TableParticle.ts index 39d914afb..85a69b8a5 100644 --- a/src/editor/core/draw/particle/table/TableParticle.ts +++ b/src/editor/core/draw/particle/table/TableParticle.ts @@ -17,6 +17,14 @@ interface IDrawTableBorderOption { isDrawFullBorder?: boolean } +interface IMergeSplittedTablePayload { + elementList: IElement[] + element: IElement + index: number + curIndex?: number + mergeForward?: boolean +} + export class TableParticle { private draw: Draw private range: RangeManager @@ -457,4 +465,187 @@ export class TableParticle { this._drawBackgroundColor(ctx, element, startX, startY) this._drawBorder(ctx, element, startX, startY) } + + public mergeSplittedTable(payload: IMergeSplittedTablePayload) { + let { element, index: i, curIndex } = payload + const { elementList, mergeForward = false } = payload + const position = this.draw.getPosition() + const positionContext = position.getPositionContext() + if (element.pagingId) { + // 如果当前表格是拆分出来的,找到起始表格 + if (!mergeForward && element.pagingIndex! > 0) { + i = i - element.pagingIndex! + element = elementList[i] + } + let positionContextChanged = false + // 位置上下文中的信息是表格拆分后记录的,这里需要将其先修正为表格合并后的上下文。渲染前排版拆分表格时将基于此再次修正位置上下文。 + if (positionContext.isTable) { + // 如果位置上下文在当前表格或其拆分出的子表格中,则修正位置上下文(这里未调用setPositionContext是为了避免触发位置上下文改变的事件) + const preTables: IElement[] = [] + outer: for (let index = i; index < elementList.length; index++) { + const table = elementList[index] + if (table.pagingId !== element.pagingId) { + break + } else if (positionContext.tableId === table.id) { + for (let r = 0; r < table.trList!.length; r++) { + for (let d = 0; d < table.trList![r].tdList.length; d++) { + const td = table.trList![r].tdList[d] + // 此时位置上下文中的tdId是拆分后的tdId,将其修正为原始td的id + // 由于后续合并表格可能导致单元格索引、行索引等变化,因此这里只修正了部分位置上下文 + if (td.id === positionContext.tdId) { + positionContext.tdId = td.pagingOriginId || td.id + positionContext.tableId = element.id + positionContext.index = i + positionContextChanged = true + if ( + td.pagingOriginId && // 位置上下文指向的td是跨页拆分出来的时才需要修正curIndex + curIndex !== undefined && + curIndex > -1 + ) { + // 找到同源表格中与位置上下文td同源的单元格,根据其内容长度修正curIndex + while (preTables.length > 0) { + const preTable = preTables.pop() + preTable!.trList!.forEach(preTr => { + for ( + let preTdIndex = 0; + preTdIndex < preTr.tdList.length; + preTdIndex++ + ) { + const preTd = preTr.tdList[preTdIndex] + if ( + preTd.pagingOriginId === td.pagingOriginId || + preTd.id === td.pagingOriginId + ) { + curIndex! += preTd.value.length + break + } + } + }) + } + } + break outer + } + } + } + } else { + preTables.push(table) + } + } + } + // 为当前表格构建一个虚拟表格 + const virtualTable = Array.from( + { length: element.trList!.length }, + () => new Array(element.colgroup!.length) + ) as Array> + 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) { + const nextElement = elementList[tableIndex] + if (nextElement.pagingId === element.pagingId) { + 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 + // 更新合并内容的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 { + break + } + } + if (combineCount) { + elementList.splice(i + 1, combineCount) + } + // 合并表格后继续修正位置上下文 + if (positionContextChanged) { + outer: for (let r = 0; r < element.trList!.length; r++) { + const tr = element.trList![r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + if (td.id === positionContext.tdId) { + positionContext.tdIndex = d + positionContext.trIndex = r + positionContext.trId = tr.id + break outer + } + } + } + } + element.pagingIndex = element.pagingIndex ?? 0 + // 计算表格行列(单元格行列索引、高宽、单元格相对于表格的坐标等信息) + this.computeRowColInfo(element) + } + return { curIndex, element, index: i, positionContext } + } } diff --git a/src/editor/core/event/handlers/input.ts b/src/editor/core/event/handlers/input.ts index 61967f6a4..d4d7ced69 100644 --- a/src/editor/core/event/handlers/input.ts +++ b/src/editor/core/event/handlers/input.ts @@ -22,6 +22,10 @@ export function input(data: string, host: CanvasEvent) { if (!isComposing) { const cursor = draw.getCursor() cursor.clearAgentDomValue() + } else if (position.getPositionContext().isTable) { + // TODO: 后续支持表格中合成输入时显示合成字符 + // 在表格中合成输入时,跳过后续对合成字符的处理,避免在表格中以composing状态跨页导致的问题 + return } const { TEXT, HYPERLINK, SUBSCRIPT, SUPERSCRIPT, DATE } = ElementType const text = data.replaceAll(`\n`, ZERO) diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts index 594ce9425..bd2040c43 100644 --- a/src/editor/interface/Draw.ts +++ b/src/editor/interface/Draw.ts @@ -76,4 +76,5 @@ export interface IComputeRowListPayload { pageHeight?: number mainOuterHeight?: number surroundElementList?: IElement[] + curIndex?: number } diff --git a/src/editor/interface/table/Td.ts b/src/editor/interface/table/Td.ts index 7431ddc9b..36b26489d 100644 --- a/src/editor/interface/table/Td.ts +++ b/src/editor/interface/table/Td.ts @@ -31,6 +31,7 @@ export interface ITd { mainHeight?: number // 内容 + 内边距高度 realHeight?: number // 真实高度(包含跨列) realMinHeight?: number // 真实最小高度(包含跨列) + pagingOriginId?: string // 被拆分到下一页的单元格的原始id disabled?: boolean // 内容不可编辑 deletable?: boolean // 内容不可删除 } diff --git a/src/editor/interface/table/Tr.ts b/src/editor/interface/table/Tr.ts index 8e66a831d..0550b82f9 100644 --- a/src/editor/interface/table/Tr.ts +++ b/src/editor/interface/table/Tr.ts @@ -8,4 +8,6 @@ export interface ITr { tdList: ITd[] minHeight?: number pagingRepeat?: boolean // 在各页顶端以标题行的形式重复出现 + pagingOriginHeight?: number // 被拆分到下一页的行的原始高度 + pagingOriginId?: string // 被拆分到下一页的行的原始id } diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index e31b494c9..8599d9ff3 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -608,13 +608,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> + 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 {