From c26f9b04300c4ab443514ddf1811b7e937db136b Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Fri, 29 Nov 2024 18:19:04 +0800 Subject: [PATCH] feat(render-engine): support repeat table header (#4139) --- .../src/docs/default-document.data-simple.ts | 18 +- .../src/docs/data-model/empty-snapshot.ts | 3 +- .../src/services/instance/instance.service.ts | 8 +- packages/docs-ui/src/basics/table.ts | 4 +- .../commands/table/doc-table-tab.command.ts | 18 +- .../controllers/doc-auto-format.controller.ts | 18 +- .../src/services/doc-auto-format.service.ts | 3 +- .../src/basics/i-document-skeleton-cached.ts | 1 + .../layout/block/paragraph/linebreaking.ts | 16 +- .../src/components/docs/layout/block/table.ts | 424 +++++++++++------- .../components/docs/layout/doc-skeleton.ts | 7 +- 11 files changed, 316 insertions(+), 204 deletions(-) diff --git a/mockdata/src/docs/default-document.data-simple.ts b/mockdata/src/docs/default-document.data-simple.ts index ada2fccca23..ff2164c8885 100644 --- a/mockdata/src/docs/default-document.data-simple.ts +++ b/mockdata/src/docs/default-document.data-simple.ts @@ -42,15 +42,11 @@ function createTableDataStream(tables: string[][]) { } const exampleTables = [ - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], - ['Description', 'Date', 'Location'], ['Description', 'Date', 'Location'], ['Academic Senate Meeting 1 Academic Senate Meeting 2 Academic Senate Meeting 3 Academic Senate Meeting 4 Academic Senate Meeting 5', 'May 25, 2205', 'Building 99 Room 1'], + ['Faculty Council', 'June 1, 2205', 'Building 35 Room 5'], + ['Faculty Council', 'June 15, 2205', 'Building 35 Room 5'], + ['Faculty Council', 'June 30, 2205', 'Building 35 Room 5'], ['Commencement Meeting ', 'December 15, 2205', 'Building 42 Room 10'], ['Dean\'s Council', 'February 1, 2206', 'Building 35 Room 5'], ['Faculty Council', 'March 1, 2206', 'Building 35 Room 5'], @@ -201,7 +197,13 @@ const tableColumn: ITableColumn = { }, }; -const tableRows = [...new Array(exampleTables.length).fill(null).map(() => Tools.deepClone(tableRow))]; +const tableRows: ITableRow[] = [...new Array(exampleTables.length).fill(null).map((_, i) => { + return { + ...Tools.deepClone(tableRow), + isFirstRow: i === 0 ? BooleanNumber.TRUE : BooleanNumber.FALSE, + repeatHeaderRow: i === 0 ? BooleanNumber.TRUE : BooleanNumber.FALSE, + }; +})]; const tableColumns = [...new Array(exampleTables[0].length).fill(null).map(() => Tools.deepClone(tableColumn))]; tableColumns[0].size.width.v = 250; diff --git a/packages/core/src/docs/data-model/empty-snapshot.ts b/packages/core/src/docs/data-model/empty-snapshot.ts index fd12a3ad221..869ba74ced7 100644 --- a/packages/core/src/docs/data-model/empty-snapshot.ts +++ b/packages/core/src/docs/data-model/empty-snapshot.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import type { IDocumentData } from '../../types/interfaces'; import { Tools } from '../../shared/tools'; import { BooleanNumber } from '../../types/enum'; import { LocaleType } from '../../types/enum/locale-type'; -import { DocumentFlavor, type IDocumentData } from '../../types/interfaces'; +import { DocumentFlavor } from '../../types/interfaces'; export function getEmptySnapshot( unitID = Tools.generateRandomId(6), diff --git a/packages/core/src/services/instance/instance.service.ts b/packages/core/src/services/instance/instance.service.ts index fa7504284b0..06a9043d294 100644 --- a/packages/core/src/services/instance/instance.service.ts +++ b/packages/core/src/services/instance/instance.service.ts @@ -14,9 +14,12 @@ * limitations under the License. */ -import { BehaviorSubject, distinctUntilChanged, filter, map, Subject } from 'rxjs'; import type { Observable } from 'rxjs'; +import type { IDisposable } from '../../common/di'; +import type { UnitModel, UnitType } from '../../common/unit'; +import type { Nullable } from '../../shared'; +import { BehaviorSubject, distinctUntilChanged, filter, map, Subject } from 'rxjs'; import { createIdentifier, Inject, Injector } from '../../common/di'; import { UniverInstanceType } from '../../common/unit'; import { DocumentDataModel } from '../../docs/data-model/document-data-model'; @@ -25,9 +28,6 @@ import { Workbook } from '../../sheets/workbook'; import { SlideDataModel } from '../../slides/slide-model'; import { FOCUSING_DOC, FOCUSING_SHEET, FOCUSING_SLIDE, FOCUSING_UNIT } from '../context/context'; import { IContextService } from '../context/context.service'; -import type { IDisposable } from '../../common/di'; -import type { UnitModel, UnitType } from '../../common/unit'; -import type { Nullable } from '../../shared'; export type UnitCtor = new (...args: any[]) => UnitModel; diff --git a/packages/docs-ui/src/basics/table.ts b/packages/docs-ui/src/basics/table.ts index 6d09d6c2bcd..765b1fa6204 100644 --- a/packages/docs-ui/src/basics/table.ts +++ b/packages/docs-ui/src/basics/table.ts @@ -110,7 +110,7 @@ export function findBellowCell(cell: IDocumentSkeletonPage): Nullable !r.isRepeatRow)!; break; } } @@ -138,7 +138,7 @@ export function findAboveCell(cell: IDocumentSkeletonPage): Nullable = { + id: 'doc.table.tab-in-table', + type: CommandType.COMMAND, + handler: async (accessor, params: IDocTableTabCommandParams) => { const { shift } = params; const textSelectionManager = accessor.get(DocSelectionManagerService); - const activeTextRange = textSelectionManager.getActiveTextRange(); + const docRanges = textSelectionManager.getDocRanges(); const commandService = accessor.get(ICommandService); const univerInstanceService = accessor.get(IUniverInstanceService); @@ -41,25 +44,26 @@ export const DocTableTabCommand: ICommand = { return false; } + const activeRange = docRanges.find((range) => range.isActive) ?? docRanges[0]; const unitId = docDataModel.getUnitId(); const docSkeletonManagerService = getCommandSkeleton(accessor, unitId); const skeleton = docSkeletonManagerService?.getSkeleton(); - const viewModel = skeleton?.getViewModel().getSelfOrHeaderFooterViewModel(activeTextRange?.segmentId); + const viewModel = skeleton?.getViewModel().getSelfOrHeaderFooterViewModel(activeRange?.segmentId); if (viewModel == null) { return false; } - if (activeTextRange == null) { + if (activeRange == null) { return false; } let offsets: Nullable = null; if (shift) { - offsets = getCellOffsets(viewModel, activeTextRange, CellPosition.PREV); + offsets = getCellOffsets(viewModel, activeRange, CellPosition.PREV); } else { - offsets = getCellOffsets(viewModel, activeTextRange, CellPosition.NEXT); + offsets = getCellOffsets(viewModel, activeRange, CellPosition.NEXT); } if (offsets) { diff --git a/packages/docs-ui/src/controllers/doc-auto-format.controller.ts b/packages/docs-ui/src/controllers/doc-auto-format.controller.ts index 0a90a8bdd23..852936c586e 100644 --- a/packages/docs-ui/src/controllers/doc-auto-format.controller.ts +++ b/packages/docs-ui/src/controllers/doc-auto-format.controller.ts @@ -17,16 +17,19 @@ import type { Nullable } from 'vitest'; import type { ITabCommandParams } from '../commands/commands/auto-format.command'; import { Disposable, Inject, QuickListTypeMap } from '@univerjs/core'; +import { DocSkeletonManagerService } from '@univerjs/docs'; +import { IRenderManagerService } from '@univerjs/engine-render'; import { AfterSpaceCommand, EnterCommand, TabCommand } from '../commands/commands/auto-format.command'; import { BreakLineCommand } from '../commands/commands/break-line.command'; import { ChangeListNestingLevelCommand, ChangeListNestingLevelType, ListOperationCommand, QuickListCommand } from '../commands/commands/list.command'; import { DocTableTabCommand } from '../commands/commands/table/doc-table-tab.command'; import { DocAutoFormatService } from '../services/doc-auto-format.service'; -import { isInSameTableCell } from '../services/selection/convert-rect-range'; +import { isInSameTableCellData } from '../services/selection/convert-rect-range'; export class DocAutoFormatController extends Disposable { constructor( - @Inject(DocAutoFormatService) private readonly _docAutoFormatService: DocAutoFormatService + @Inject(DocAutoFormatService) private readonly _docAutoFormatService: DocAutoFormatService, + @IRenderManagerService private readonly _renderManagerService: IRenderManagerService ) { super(); @@ -76,11 +79,18 @@ export class DocAutoFormatController extends Disposable { this._docAutoFormatService.registerAutoFormat({ id: TabCommand.id, match: (context) => { - const { selection } = context; + const { selection, unit } = context; const { startNodePosition, endNodePosition } = selection; - if (startNodePosition && endNodePosition && isInSameTableCell(startNodePosition, endNodePosition)) { + const renderObject = this._renderManagerService.getRenderById(unit.getUnitId()); + const skeleton = renderObject?.with(DocSkeletonManagerService).getSkeleton(); + + if (skeleton == null) { + return false; + } + + if (startNodePosition && endNodePosition && isInSameTableCellData(skeleton, startNodePosition, endNodePosition)) { return true; } diff --git a/packages/docs-ui/src/services/doc-auto-format.service.ts b/packages/docs-ui/src/services/doc-auto-format.service.ts index 9a21fd541d7..0b6307fb3d4 100644 --- a/packages/docs-ui/src/services/doc-auto-format.service.ts +++ b/packages/docs-ui/src/services/doc-auto-format.service.ts @@ -112,7 +112,8 @@ export class DocAutoFormatService extends Disposable { onAutoFormat(id: string, params: Nullable): ICommandInfo[] { const autoFormats = this._matches.get(id) ?? []; const unit = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); - const selection = this._textSelectionManagerService.getActiveTextRange(); + const docRanges = this._textSelectionManagerService.getDocRanges(); + const selection = docRanges.find((range) => range.isActive) ?? docRanges[0]; if (unit && selection) { const doc = unit.getSelfOrHeaderFooterModel(selection.segmentId); diff --git a/packages/engine-render/src/basics/i-document-skeleton-cached.ts b/packages/engine-render/src/basics/i-document-skeleton-cached.ts index 887c85d9a90..e817170b333 100644 --- a/packages/engine-render/src/basics/i-document-skeleton-cached.ts +++ b/packages/engine-render/src/basics/i-document-skeleton-cached.ts @@ -144,6 +144,7 @@ export interface IDocumentSkeletonRow { ed: number; // endIndex 文本结束索引 rowSource: ITableRow; parent?: IDocumentSkeletonTable; + isRepeatRow: boolean; // 是否是标题重复行 } export interface IDocumentSkeletonColumn { diff --git a/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts b/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts index 3e915e0318e..3b38cd172b1 100644 --- a/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts +++ b/packages/engine-render/src/components/docs/layout/block/paragraph/linebreaking.ts @@ -191,14 +191,18 @@ export function lineBreaking( ctx.paragraphConfigCache.set(segmentId, segmentParagraphCache); } - segmentParagraphCache.set(endIndex, paragraphConfig); + if (segmentParagraphCache.has(endIndex)) { + const bulletSkeleton = segmentParagraphCache.get(endIndex)?.bulletSkeleton; - const listLevelAncestors = _getListLevelAncestors(bullet, skeListLevel); // 取得列表所有 level 的缓存 - const bulletSkeleton = dealWithBullet(bullet, lists, listLevelAncestors, localeService); // 生成 bullet + paragraphConfig.bulletSkeleton = bulletSkeleton; + } else { + const listLevelAncestors = _getListLevelAncestors(bullet, skeListLevel); // 取得列表所有 level 的缓存 + const bulletSkeleton = dealWithBullet(bullet, lists, listLevelAncestors, localeService); // 生成 bullet - _updateListLevelAncestors(paragraph, bullet, bulletSkeleton, skeListLevel); // 更新最新的 level 缓存列表 + _updateListLevelAncestors(paragraph, bullet, bulletSkeleton, skeListLevel); // 更新最新的 level 缓存列表 - paragraphConfig.bulletSkeleton = bulletSkeleton; + paragraphConfig.bulletSkeleton = bulletSkeleton; + } for (let i = 0, len = blocks.length; i < len; i++) { const charIndex = blocks[i]; @@ -218,6 +222,8 @@ export function lineBreaking( } } + segmentParagraphCache.set(endIndex, paragraphConfig); + let allPages = [curPage]; let isParagraphFirstShapedText = true; // 第一个分词 for (const [_index, { text, glyphs, breakPointType }] of shapedTextList.entries()) { diff --git a/packages/engine-render/src/components/docs/layout/block/table.ts b/packages/engine-render/src/components/docs/layout/block/table.ts index 3dedd8ca3c8..22e45e6443a 100644 --- a/packages/engine-render/src/components/docs/layout/block/table.ts +++ b/packages/engine-render/src/components/docs/layout/block/table.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { INumberUnit, ITable, ITableRow } from '@univerjs/core'; +import type { INumberUnit, ITable, ITableRow, Nullable } from '@univerjs/core'; import type { IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, IParagraphList, ISectionBreakConfig } from '../../../../basics'; import type { DataStreamTreeNode } from '../../view-model/data-stream-tree-node'; import type { DocumentViewModel } from '../../view-model/document-view-model'; @@ -44,7 +44,7 @@ export function createTableSkeleton( const row = rowNodes.indexOf(rowNode); const rowSource = table.tableRows[row]; const { trHeight } = rowSource; - const rowSkeleton = _getNullTableRowSkeleton(startIndex, endIndex, row, rowSource, tableSkeleton); + const rowSkeleton = _getNullTableRowSkeleton(startIndex, endIndex, row, rowSource, false, tableSkeleton); const { hRule, val } = trHeight; tableSkeleton.rows.push(rowSkeleton); @@ -151,6 +151,14 @@ export interface ISlicedTableSkeletonParams { fromCurrentPage: boolean; } +interface ICreateTableCache { + rowTop: number; + tableWidth: number; + remainHeight: number; + repeatRow: Nullable; + repeatRowHeight: number; +} + // Create skeletons of a table, which may be divided into different pages according to the available height of the page. export function createTableSkeletons( ctx: ILayoutContext, @@ -160,227 +168,296 @@ export function createTableSkeletons( sectionBreakConfig: ISectionBreakConfig, availableHeight: number ): ISlicedTableSkeletonParams { - let fromCurrentPage = true; const skeTables: IDocumentSkeletonTable[] = []; - const { pageWidth, marginLeft = 0, marginRight = 0, marginTop, marginBottom, pageHeight } = curPage; - const pageContentHeight = pageHeight - marginTop - marginBottom; - const { startIndex, endIndex, children: rowNodes } = tableNode; + const table = viewModel.getTableByStartIndex(startIndex)?.tableSource; if (table == null) { throw new Error('Table not found when creating table skeletons'); } - let curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table); - let rowTop = 0; - let tableWidth = 0; - let remainHeight = availableHeight; + const needRepeatHeader = table.tableRows[0].repeatHeaderRow === BooleanNumber.TRUE; + const curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table); + + const createCache: ICreateTableCache = { + rowTop: 0, + tableWidth: 0, + remainHeight: availableHeight, + repeatRow: needRepeatHeader ? rowNodes[0] : null, + repeatRowHeight: 0, + }; skeTables.push(curTableSkeleton); for (const rowNode of rowNodes) { - const { children: cellNodes, startIndex, endIndex } = rowNode; const row = rowNodes.indexOf(rowNode); - const rowSource = table.tableRows[row]; - const { trHeight, cantSplit } = rowSource; - const rowSkeletons: IDocumentSkeletonRow[] = []; - const { hRule, val } = trHeight; - const canRowSplit = cantSplit === BooleanNumber.TRUE && trHeight.hRule === TableRowHeightRule.AUTO; - // If the remain height is less than 50 pixels, you can't fit the next line, so you can start typography directly from the second page. - const MAX_FONT_SIZE = 72; - let needOpenNewTable = remainHeight <= MAX_FONT_SIZE; - const rowHeights = [0]; + dealWithTableRow( + ctx, + curPage, + skeTables, + viewModel, + sectionBreakConfig, + rowNode, + row, + table, + createCache + ); + } - for (const cellNode of cellNodes) { - const col = cellNodes.indexOf(cellNode); - const cellPageSkeletons = createSkeletonCellPages( - ctx, - viewModel, - cellNode, - sectionBreakConfig, - table, - row, - col, - canRowSplit && !needOpenNewTable ? remainHeight : pageContentHeight, - pageContentHeight - ); - - while (rowSkeletons.length < cellPageSkeletons.length) { - const rowSkeleton = _getNullTableRowSkeleton(startIndex, endIndex, row, rowSource); - const colCount = cellNodes.length; - - // Fill the row with null cell pages. - rowSkeleton.cells = [...new Array(colCount)].map((_, i) => { - const cellSkeleton = createNullCellPage( - ctx, - sectionBreakConfig, - table, - row, - i - ).page; + updateTableSkeletonsPosition(createCache, curPage, skeTables, table); - cellSkeleton.parent = rowSkeleton; + const fromCurrentPage = skeTables[0].height <= availableHeight; - return cellSkeleton; - }); + return { + skeTables, + fromCurrentPage, + }; +} - rowSkeletons.push(rowSkeleton); - } +function updateTableSkeletonsPosition( + cache: ICreateTableCache, + curPage: IDocumentSkeletonPage, + skeTables: IDocumentSkeletonTable[], + table: ITable +) { + const { pageWidth, marginLeft = 0, marginRight = 0 } = curPage; + const { tableWidth } = cache; + const tableLeft = _getTableLeft(pageWidth - marginLeft - marginRight, tableWidth, table.align, table.indent); - while (rowHeights.length < cellPageSkeletons.length) { - rowHeights.push(0); - } + let tableIndex = 0; + for (const tableSkeleton of skeTables) { + // Update table width and left. + tableSkeleton.width = tableWidth; + tableSkeleton.left = tableLeft; - for (const cellPageSkeleton of cellPageSkeletons) { - const { marginTop: cellMarginTop = 0, marginBottom: cellMarginBottom = 0 } = cellPageSkeleton; - const cellPageHeight = cellPageSkeleton.height + cellMarginTop + cellMarginBottom; - const pageIndex = cellPageSkeletons.indexOf(cellPageSkeleton); - const rowSke = rowSkeletons[pageIndex]; + // Reset table st and ed. + tableSkeleton.st = tableSkeleton.rows[0].st - 1; + tableSkeleton.ed = tableSkeleton.rows[tableSkeleton.rows.length - 1].ed + 1; - cellPageSkeleton.parent = rowSke; - rowSke.cells[col] = cellPageSkeleton; - rowHeights[pageIndex] = Math.max(rowHeights[pageIndex], cellPageHeight); - } + // Reset table id. + if (skeTables.length > 1) { + tableSkeleton.tableId = getTableSliceId(table.tableId, tableIndex); + tableIndex++; } + } +} - for (const rowSke of rowSkeletons) { - const rowIndex = rowSkeletons.indexOf(rowSke); +function getCurTableSkeleton(skeTables: IDocumentSkeletonTable[]): IDocumentSkeletonTable { + return skeTables[skeTables.length - 1]; +} - if (hRule === TableRowHeightRule.AT_LEAST) { - rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], val.v); - } else if (hRule === TableRowHeightRule.EXACT) { - rowHeights[rowIndex] = val.v; - } +function getAvailableHeight(curPage: IDocumentSkeletonPage, cache: ICreateTableCache, hasRepeatHeader: boolean) { + const { marginTop, marginBottom, pageHeight } = curPage; + let pageContentHeight = pageHeight - marginTop - marginBottom; - rowHeights[rowIndex] = Math.min(rowHeights[rowIndex], pageContentHeight); + if (hasRepeatHeader) { + pageContentHeight -= cache.repeatRowHeight; + } - let left = 0; - // Set row height to cell page height. - for (const cellPageSkeleton of rowSke.cells) { - cellPageSkeleton.left = left; - cellPageSkeleton.pageHeight = rowHeights[rowIndex]; + return pageContentHeight; +} - left += cellPageSkeleton.pageWidth; +function dealWithTableRow( + ctx: ILayoutContext, + curPage: IDocumentSkeletonPage, + skeTables: IDocumentSkeletonTable[], + viewModel: DocumentViewModel, + sectionBreakConfig: ISectionBreakConfig, + rowNode: DataStreamTreeNode, + row: number, + table: ITable, + cache: ICreateTableCache, + isRepeatRow = false +) { + const pageContentHeight = getAvailableHeight(curPage, cache, false); + const availableHeight = getAvailableHeight(curPage, cache, true); + const { children: cellNodes, startIndex, endIndex } = rowNode; + const rowSource = table.tableRows[row]; + const { trHeight, cantSplit } = rowSource; + const rowSkeletons: IDocumentSkeletonRow[] = []; + const { hRule, val } = trHeight; + const canRowSplit = cantSplit === BooleanNumber.TRUE && trHeight.hRule === TableRowHeightRule.AUTO; + // If the remain height is less than 50 pixels, you can't fit the next line, so you can start typography directly from the second page. + const MAX_FONT_SIZE = 72; + const needOpenNewTable = cache.remainHeight <= MAX_FONT_SIZE; + let curTableSkeleton = getCurTableSkeleton(skeTables); + + const rowHeights = [0]; + + for (const cellNode of cellNodes) { + const col = cellNodes.indexOf(cellNode); + const cellPageSkeletons = createSkeletonCellPages( + ctx, + viewModel, + cellNode, + sectionBreakConfig, + table, + row, + col, + canRowSplit && !needOpenNewTable ? cache.remainHeight : availableHeight, + pageContentHeight + ); + + while (rowSkeletons.length < cellPageSkeletons.length) { + const rowSkeleton = _getNullTableRowSkeleton(startIndex, endIndex, row, rowSource, isRepeatRow); + const colCount = cellNodes.length; + + // Fill the row with null cell pages. + rowSkeleton.cells = [...new Array(colCount)].map((_, i) => { + const cellSkeleton = createNullCellPage( + ctx, + sectionBreakConfig, + table, + row, + i + ).page; + + cellSkeleton.parent = rowSkeleton; + + return cellSkeleton; + }); + + rowSkeletons.push(rowSkeleton); + } - tableWidth = Math.max(tableWidth, left); - } + while (rowHeights.length < cellPageSkeletons.length) { + rowHeights.push(0); } - // Handle vertical alignment in cell. - for (let i = 0; i < rowSource.tableCells.length; i++) { - const cellConfig = rowSource.tableCells[i]; + for (const cellPageSkeleton of cellPageSkeletons) { + const { marginTop: cellMarginTop = 0, marginBottom: cellMarginBottom = 0 } = cellPageSkeleton; + const cellPageHeight = cellPageSkeleton.height + cellMarginTop + cellMarginBottom; + const pageIndex = cellPageSkeletons.indexOf(cellPageSkeleton); + const rowSke = rowSkeletons[pageIndex]; - for (const rowSkeleton of rowSkeletons) { - const cellPageSkeleton = rowSkeleton.cells[i]; + cellPageSkeleton.parent = rowSke; + rowSke.cells[col] = cellPageSkeleton; + rowHeights[pageIndex] = Math.max(rowHeights[pageIndex], cellPageHeight); + } + } - if (cellPageSkeleton == null) { - continue; - } + for (const rowSke of rowSkeletons) { + // Update row height. + const rowIndex = rowSkeletons.indexOf(rowSke); - const { vAlign = VerticalAlignmentType.CONTENT_ALIGNMENT_UNSPECIFIED } = cellConfig; - const { pageHeight, height, originMarginTop, originMarginBottom } = cellPageSkeleton; - - let marginTop = originMarginTop; - - switch (vAlign) { - case VerticalAlignmentType.TOP: { - marginTop = originMarginTop; - break; - } - case VerticalAlignmentType.CENTER: { - marginTop = (pageHeight - height) / 2; - break; - } - case VerticalAlignmentType.BOTTOM: { - marginTop = pageHeight - height - originMarginBottom; - break; - } - default: - break; - } + if (hRule === TableRowHeightRule.AT_LEAST) { + rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], val.v); + } else if (hRule === TableRowHeightRule.EXACT) { + rowHeights[rowIndex] = val.v; + } - marginTop = Math.max(originMarginTop, marginTop); + rowHeights[rowIndex] = Math.min(rowHeights[rowIndex], pageContentHeight); - cellPageSkeleton.marginTop = marginTop; - } - } + let left = 0; + // Set row height to cell page height. + for (const cellPageSkeleton of rowSke.cells) { + cellPageSkeleton.left = left; + cellPageSkeleton.pageHeight = rowHeights[rowIndex]; - if (rowHeights[0] > remainHeight) { - if (curTableSkeleton.rows.length > 0) { - needOpenNewTable = true; - } else { - fromCurrentPage = false; - remainHeight = pageContentHeight; - rowTop = 0; - } - } + left += cellPageSkeleton.pageWidth; - if (needOpenNewTable) { - curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table); - skeTables.push(curTableSkeleton); - remainHeight = pageContentHeight; - rowTop = 0; + cache.tableWidth = Math.max(cache.tableWidth, left); } - if (rowSkeletons.length > 1) { - for (let i = 0; i < rowSkeletons.length; i++) { - if (i !== 0) { - curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table); - skeTables.push(curTableSkeleton); - } + // Set row Skeleton height. + rowSke.height = rowHeights[rowIndex]; + } - const rowSkeleton = rowSkeletons[i]; - const rowHeight = rowHeights[i]; + if (row === 0 && cache.repeatRow) { + cache.repeatRowHeight = rowHeights[rowHeights.length - 1]; + } - rowSkeleton.height = rowHeight; - rowSkeleton.top = i === 0 ? rowTop : 0; + // Handle vertical alignment in cell. + for (const rowSkeleton of rowSkeletons) { + _verticalAlignInCell(rowSkeleton, rowSource); + } - curTableSkeleton.height += rowHeight; + while (rowSkeletons.length > 0) { + const rowSkeleton = rowSkeletons.shift()!; + const lastRow = curTableSkeleton.rows[curTableSkeleton.rows.length - 1]; - curTableSkeleton.rows.push(rowSkeleton); - rowSkeleton.parent = curTableSkeleton; - remainHeight = pageContentHeight - rowHeight; + if (cache.remainHeight < MAX_FONT_SIZE || cache.remainHeight < rowSkeleton.height) { + cache.remainHeight = getAvailableHeight(curPage, cache, row !== 0 && rowSkeleton.index !== lastRow.index); + cache.rowTop = 0; - rowTop = rowHeight; + if (curTableSkeleton.rows.length > 0) { + curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table); + skeTables.push(curTableSkeleton); + + // Handle repeat first row. + // 如果当前行跨页,那么不用再第二页上面重复标题行了。 + if (cache.repeatRow && isRepeatRow === false && row !== 0 && rowSkeleton.index !== lastRow.index) { + const FIRST_ROW_INDEX = 0; + cache.remainHeight = getAvailableHeight(curPage, cache, false); + dealWithTableRow( + ctx, + curPage, + skeTables, + viewModel, + sectionBreakConfig, + cache.repeatRow, + FIRST_ROW_INDEX, + table, + cache, + true + ); + } } - } else { - const rowSkeleton = rowSkeletons[0]; - const rowHeight = rowHeights[0]; - - rowSkeleton.height = rowHeight; - rowSkeleton.top = rowTop; - rowTop += rowHeight; - - curTableSkeleton.rows.push(rowSkeleton); - rowSkeleton.parent = curTableSkeleton; - remainHeight -= rowHeight; - curTableSkeleton.height = rowTop; } + + curTableSkeleton = getCurTableSkeleton(skeTables); + + rowSkeleton.top = cache.rowTop; + curTableSkeleton.height += rowSkeleton.height; + + curTableSkeleton.rows.push(rowSkeleton); + rowSkeleton.parent = curTableSkeleton; + cache.remainHeight -= rowSkeleton.height; + + cache.rowTop += rowSkeleton.height; } +} - const tableLeft = _getTableLeft(pageWidth - marginLeft - marginRight, tableWidth, table.align, table.indent); +function _verticalAlignInCell( + rowSkeleton: IDocumentSkeletonRow, + rowSource: ITableRow +) { + for (let i = 0; i < rowSource.tableCells.length; i++) { + const cellConfig = rowSource.tableCells[i]; - let tableIndex = 0; - for (const tableSkeleton of skeTables) { - tableSkeleton.width = tableWidth; - tableSkeleton.left = tableLeft; + const cellPageSkeleton = rowSkeleton.cells[i]; - // Reset table st and ed. - tableSkeleton.st = tableSkeleton.rows[0].st - 1; - tableSkeleton.ed = tableSkeleton.rows[tableSkeleton.rows.length - 1].ed + 1; + if (cellPageSkeleton == null) { + continue; + } - // Reset table id. - if (skeTables.length > 1) { - tableSkeleton.tableId = getTableSliceId(table.tableId, tableIndex); - tableIndex++; + const { vAlign = VerticalAlignmentType.CONTENT_ALIGNMENT_UNSPECIFIED } = cellConfig; + const { pageHeight, height, originMarginTop, originMarginBottom } = cellPageSkeleton; + + let marginTop = originMarginTop; + + switch (vAlign) { + case VerticalAlignmentType.TOP: { + marginTop = originMarginTop; + break; + } + case VerticalAlignmentType.CENTER: { + marginTop = (pageHeight - height) / 2; + break; + } + case VerticalAlignmentType.BOTTOM: { + marginTop = pageHeight - height - originMarginBottom; + break; + } + default: + break; } - } - return { - skeTables, - fromCurrentPage, - }; + marginTop = Math.max(originMarginTop, marginTop); + + cellPageSkeleton.marginTop = marginTop; + } } function _getTableLeft(pageWidth: number, tableWidth: number, align: TableAlignmentType, indent: INumberUnit = { v: 0 }) { @@ -394,6 +471,9 @@ function _getTableLeft(pageWidth: number, tableWidth: number, align: TableAlignm case TableAlignmentType.CENTER: { return Math.max(0, (pageWidth - tableWidth) / 2); } + default: { + throw new Error('Unknown table alignment type'); + } } } @@ -420,6 +500,7 @@ function _getNullTableRowSkeleton( ed: number, index: number, rowSource: ITableRow, + isRepeatRow = false, parent?: IDocumentSkeletonTable ): IDocumentSkeletonRow { return { @@ -431,6 +512,7 @@ function _getNullTableRowSkeleton( ed, parent, rowSource, + isRepeatRow, }; } diff --git a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts index 7f462112bb6..90603ef8b0c 100644 --- a/packages/engine-render/src/components/docs/layout/doc-skeleton.ts +++ b/packages/engine-render/src/components/docs/layout/doc-skeleton.ts @@ -886,7 +886,12 @@ export class DocumentSkeleton extends Skeleton { this._findLiquid?.translate(tableLeft, tableTop); for (const row of rows) { - const { top: rowTop, cells } = row; + const { top: rowTop, cells, isRepeatRow } = row; + + // Cursor should not in repeat row. + if (isRepeatRow) { + continue; + } this._findLiquid?.translateSave(); this._findLiquid?.translate(0, rowTop);