From b9f2b92cde3de35f2d0e2c68695bfee3f309e614 Mon Sep 17 00:00:00 2001 From: yulei Date: Sun, 18 Aug 2024 11:41:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E8=B7=A8=E9=A1=B5=E6=8B=86=E5=88=86=20#41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/editor/core/draw/Draw.ts | 342 +++++++++++++++++++++++++++---- src/editor/interface/table/Td.ts | 1 + src/editor/interface/table/Tr.ts | 2 + src/editor/utils/element.ts | 76 ++++++- 4 files changed, 383 insertions(+), 38 deletions(-) diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index f0ca513..d73a0f3 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -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' export class Draw { private container: HTMLDivElement @@ -1265,6 +1266,24 @@ export class Draw { // 表格分页处理进度: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> + element.trList!.forEach((tr, trIndex) => { + let tdIndex = 0 + while (virtualTable[trIndex][tdIndex] === null) { + tdIndex++ + } + tr.tdList.forEach(td => { + 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) { @@ -1273,8 +1292,57 @@ 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 + } else { + nextTr.height = nextTr.pagingOriginHeight || nextTr.height + element.trList!.push(nextTr) + } + delete nextTr.pagingOriginHeight + delete nextTr.pagingOriginId + } tableIndex++ combineCount++ } else { @@ -1384,7 +1452,7 @@ export class Draw { } } // 当前剩余高度是否能容下当前表格第一行(可拆分)的高度,排除掉表头类型 - const rowMarginHeight = rowMargin * 2 * scale + const rowMarginHeight = rowMargin * 2 if ( curPagePreHeight + element.trList![0].height! + rowMarginHeight > height || @@ -1396,50 +1464,251 @@ 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 - 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].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) { + cloneTr[tdIndex]!.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虽然被截断,但是内容高度未达到截断线位置 + // 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.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.id = getUUID() + // 记录被挪到下一页的行的原始高度 + 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 { - deleteStart = r + 1 - deleteCount = trList.length - deleteStart - preTrHeight += trHeight + 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 } } - } - 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.pagingId = pagingId cloneElement.pagingIndex = element.pagingIndex! + 1 + this.tableParticle.computeRowColInfo(element) // 处理分页重复表头 const repeatTrList = trList.filter(tr => tr.pagingRepeat) if (repeatTrList.length) { @@ -1453,6 +1722,7 @@ export class Draw { } } // 表格经过分页处理-需要处理上下文 + // FIXME: 分页后上下文有误 if (element.pagingId) { const positionContext = this.position.getPositionContext() if (positionContext.isTable) { diff --git a/src/editor/interface/table/Td.ts b/src/editor/interface/table/Td.ts index 0535990..cc29225 100644 --- a/src/editor/interface/table/Td.ts +++ b/src/editor/interface/table/Td.ts @@ -29,4 +29,5 @@ export interface ITd { mainHeight?: number // 内容 + 内边距高度 realHeight?: number // 真实高度(包含跨列) realMinHeight?: number // 真实最小高度(包含跨列) + pagingOriginId?: string // 被拆分到下一页的单元格的原始id } diff --git a/src/editor/interface/table/Tr.ts b/src/editor/interface/table/Tr.ts index 4b74afd..da70795 100644 --- a/src/editor/interface/table/Tr.ts +++ b/src/editor/interface/table/Tr.ts @@ -6,4 +6,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 a6e2789..26fcaaf 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -599,13 +599,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 + while (virtualTable[trIndex][tdIndex] === null) { + tdIndex++ + } + tr.tdList.forEach(td => { + 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 {