feat(render-engine): split table row (#4054)

This commit is contained in:
Ran Luo
2024-11-23 10:30:16 +08:00
committed by GitHub
parent 0d07c86d7d
commit 379221e514
20 changed files with 707 additions and 465 deletions
+3 -68
View File
@@ -19,79 +19,18 @@ import {
BooleanNumber,
ColumnSeparatorType,
DocumentFlavor,
DrawingTypeEnum,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
PresetListType,
SectionType,
WrapTextType,
} from '@univerjs/core';
import { ptToPixel } from '@univerjs/engine-render';
export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = {
id: 'd-en',
drawings: {
shapeTest1: {
unitId: 'd',
subUnitId: 'd',
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
drawingId: 'shapeTest1',
title: 'test shape',
description: 'test shape',
docTransform: {
size: {
width: 1484 * 0.15,
height: 864 * 0.15,
},
positionH: {
relativeFrom: ObjectRelativeFromH.COLUMN,
posOffset: 130,
},
positionV: {
relativeFrom: ObjectRelativeFromV.PAGE,
posOffset: 560,
},
angle: 0,
},
distL: 10,
distB: 10,
distR: 10,
distT: 10,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
behindDoc: BooleanNumber.FALSE,
wrapText: WrapTextType.BOTH_SIDES,
},
shapeTest2: {
unitId: 'd',
subUnitId: 'd',
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
drawingId: 'shapeTest2',
title: 'test shape',
description: 'test shape',
docTransform: {
size: {
width: 2548 * 0.1,
height: 2343 * 0.1,
},
positionH: {
relativeFrom: ObjectRelativeFromH.COLUMN,
posOffset: 130,
},
positionV: {
relativeFrom: ObjectRelativeFromV.PAGE,
posOffset: 220,
},
angle: 0,
},
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
behindDoc: BooleanNumber.FALSE,
wrapText: WrapTextType.BOTH_SIDES,
},
},
drawings: {},
drawingsOrder: [],
body: {
dataStream:
'What’s New in the 2022 Gartner Hype Cycle for Emerging Technologies\rEmerging technologies for 2022 fit into three main themes: evolving/expanding immersive experiences, accelerated artificial intelligence automation, and OPTIMIZED technologist delivery.\rThe 2022 Gartner Hype Cycle identifies 25 must-know emerging technologies designed to help enterprise architecture and technology innovation leaders: \rExpand immersive experiences\rAccelerate artificial intelligence (AI) automation\rOptimize technologist delivery \rThese technologies are expected to greatly impact business and society over the next two to 10 years, but will especially enable CIOs and IT leaders to deliver on digital business transformation. \rThree Hype Cycle themes to think about in 2022 and beyond\rThe 2022 Gartner Hype Cycle features emerging technologies and distills insights from more than 2,000 technologies into a succinct high-potential set. Most technologies have multiple use cases but enterprise architecture and technology innovation leaders should prioritize those with the greatest potential benefit for their organization. (They will also need to launch a proof-of-concept project to demonstrate the feasibility of a technology for their target use case.)\b\r\nThe benefit of these technologies is that they provide individuals with more control over their identities and data, and expand their range of experiences into virtual venues and ecosystems that can be integrated with digital currencies. These technologies also provide new ways to reach customers to strengthen or open up new revenue streams.\rDigital twin of the customer (DToC) is a dynamic virtual representation of a customer that simulates and learns to emulate and anticipate behavior. It can be used to modify and enhance the customer experience (CX) and support new digitalization efforts, products, services and opportunities. DToC will take five to 10 years until mainstream adoption but will be transformational to organizations.\rOther critical technologies in immersive experiences include the following:\rDecentralized identity (DCI) allows an entity (typically a human user) to control their own digital identity by leveraging technologies such as blockchain or other distributed ledger technologies (DLTs), along with digital wallets.\rDigital humans are interactive, AI-driven representations that have some of the characteristics, personality, knowledge and mindset of a human.\rInternal talent marketplaces match internal employees and, in some cases, a pool of contingent workers, to time-boxed projects and various work opportunities, with no recruiter involvement.\r\n',
'What’s New in the 2022 Gartner Hype Cycle for Emerging Technologies\rEmerging technologies for 2022 fit into three main themes: evolving/expanding immersive experiences, accelerated artificial intelligence automation, and OPTIMIZED technologist delivery.\rThe 2022 Gartner Hype Cycle identifies 25 must-know emerging technologies designed to help enterprise architecture and technology innovation leaders: \rExpand immersive experiences\rAccelerate artificial intelligence (AI) automation\rOptimize technologist delivery \rThese technologies are expected to greatly impact business and society over the next two to 10 years, but will especially enable CIOs and IT leaders to deliver on digital business transformation. \rThree Hype Cycle themes to think about in 2022 and beyond\rThe 2022 Gartner Hype Cycle features emerging technologies and distills insights from more than 2,000 technologies into a succinct high-potential set. Most technologies have multiple use cases but enterprise architecture and technology innovation leaders should prioritize those with the greatest potential benefit for their organization. (They will also need to launch a proof-of-concept project to demonstrate the feasibility of a technology for their target use case. )\r\nThe benefit of these technologies is that they provide individuals with more control over their identities and data, and expand their range of experiences into virtual venues and ecosystems that can be integrated with digital currencies. These technologies also provide new ways to reach customers to strengthen or open up new revenue streams.\rDigital twin of the customer (DToC) is a dynamic virtual representation of a customer that simulates and learns to emulate and anticipate behavior. It can be used to modify and enhance the customer experience (CX) and support new digitalization efforts, products, services and opportunities. DToC will take five to 10 years until mainstream adoption but will be transformational to organizations.\rOther critical technologies in immersive experiences include the following:\rDecentralized identity (DCI) allows an entity (typically a human user) to control their own digital identity by leveraging technologies such as blockchain or other distributed ledger technologies (DLTs), along with digital wallets.\rDigital humans are interactive, AI-driven representations that have some of the characteristics, personality, knowledge and mindset of a human.\rInternal talent marketplaces match internal employees and, in some cases, a pool of contingent workers, to time-boxed projects and various work opportunities, with no recruiter involvement.\r\n',
textRuns: [
{
st: 0,
@@ -435,10 +374,6 @@ export const DEFAULT_DOCUMENT_DATA_EN: IDocumentData = {
},
],
customBlocks: [
{
startIndex: 1243,
blockId: 'shapeTest1',
},
],
},
documentStyle: {
@@ -50,7 +50,7 @@ const exampleTables = [
['Description', 'Date', 'Location'],
['Description', 'Date', 'Location'],
['Description', 'Date', 'Location'],
['Academic Senate Meeting Academic Senate Meeting Academic Senate Meeting Academic Senate Meeting Academic Senate Meeting', 'May 25, 2205', 'Building 99 Room 1'],
['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'],
['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'],
@@ -64,7 +64,7 @@ const tableStream = createTableDataStream(exampleTables);
const dataStream = `${title}${description}${tableStream}${summary}\n`;
const startIndex = dataStream.indexOf(TABLE_START);
const endIndex = dataStream.indexOf(TABLE_END);
const endIndex = tableStream.length + startIndex;
function createParagraphAndSectionBreaks(dataStream: string) {
const paragraphs: IParagraph[] = [];
@@ -139,7 +139,7 @@ function createTextRuns() {
cl: {
rgb: '#54585a',
},
bl: BooleanNumber.FALSE,
bl: BooleanNumber.TRUE,
},
});
@@ -186,7 +186,7 @@ const tableCell: ITableCell = {
const tableRow: ITableRow = {
tableCells: [...new Array(exampleTables[0].length).fill(Tools.deepClone(tableCell))],
trHeight: {
val: { v: 120 },
val: { v: 50 },
hRule: TableRowHeightRule.AUTO,
},
cantSplit: BooleanNumber.TRUE,
+25 -12
View File
@@ -16,7 +16,7 @@
import type { Nullable } from '@univerjs/core';
import type { IDocumentSkeletonCached, IDocumentSkeletonLine, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable } from '@univerjs/engine-render';
import { DocumentSkeletonPageType, getLastLine, lineIterator } from '@univerjs/engine-render';
import { DocumentSkeletonPageType, getLastColumn, getLastLine, lineIterator } from '@univerjs/engine-render';
export function firstLineInTable(table: IDocumentSkeletonTable) {
const firstRow = table.rows[0];
@@ -80,7 +80,11 @@ export function findLineBeforeAndAfterTable(table: Nullable<IDocumentSkeletonTab
};
}
export function findBellowCell(cell: IDocumentSkeletonPage) {
function isEmptyCellPage(cell: IDocumentSkeletonPage) {
return cell.sections[0].columns[0].lines.length === 0;
}
export function findBellowCell(cell: IDocumentSkeletonPage): Nullable<IDocumentSkeletonPage> {
const row = cell.parent as IDocumentSkeletonRow;
const table = row?.parent as IDocumentSkeletonTable;
@@ -92,7 +96,7 @@ export function findBellowCell(cell: IDocumentSkeletonPage) {
const col = row.cells.indexOf(cell);
const bellowRow = table.rows[table.rows.indexOf(row) + 1];
let bellowRow = table.rows[table.rows.indexOf(row) + 1];
if (bellowRow == null) {
if (tableId.indexOf('#-#')) {
@@ -106,19 +110,23 @@ export function findBellowCell(cell: IDocumentSkeletonPage) {
if (skeTables.has(nextTableId)) {
const nextTable = skeTables.get(nextTableId);
if (nextTable?.rows.length) {
return nextTable.rows[0].cells[col];
bellowRow = nextTable.rows[0];
break;
}
}
}
}
}
return;
}
return bellowRow.cells[col];
if (bellowRow != null) {
const cell = bellowRow.cells[col];
return isEmptyCellPage(cell) ? findBellowCell(cell) : cell;
}
}
export function findAboveCell(cell: IDocumentSkeletonPage) {
export function findAboveCell(cell: IDocumentSkeletonPage): Nullable<IDocumentSkeletonPage> {
const row = cell.parent as IDocumentSkeletonRow;
const table = row?.parent as IDocumentSkeletonTable;
@@ -126,7 +134,7 @@ export function findAboveCell(cell: IDocumentSkeletonPage) {
return;
}
const aboveRow = table.rows[table.rows.indexOf(row) - 1];
let aboveRow = table.rows[table.rows.indexOf(row) - 1];
const col = row.cells.indexOf(cell);
@@ -141,16 +149,20 @@ export function findAboveCell(cell: IDocumentSkeletonPage) {
if (skeTables.has(nextTableId)) {
const nextTable = skeTables.get(nextTableId);
if (nextTable?.rows.length) {
return nextTable.rows[nextTable.rows.length - 1].cells[col];
aboveRow = nextTable.rows[nextTable.rows.length - 1];
break;
}
}
}
}
}
return;
}
return aboveRow.cells[col];
if (aboveRow != null) {
const cell = aboveRow.cells[col];
return isEmptyCellPage(cell) ? findAboveCell(cell) : cell;
}
}
export function findTableBeforeLine(line: IDocumentSkeletonLine, page: IDocumentSkeletonPage) {
@@ -176,7 +188,8 @@ export function firstLineInCell(cell: IDocumentSkeletonPage) {
}
export function lastLineInCell(cell: IDocumentSkeletonPage) {
const lastLine = getLastLine(cell);
const lastColumn = getLastColumn(cell);
const lastLine = lastColumn.lines[lastColumn.lines.length - 1];
return lastLine;
}
@@ -28,19 +28,20 @@ export const DocSelectAllCommand: ICommand<ISelectAllCommandParams> = {
const univerInstanceService = accessor.get(IUniverInstanceService);
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
const docDataModel = univerInstanceService.getCurrentUnitForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
const activeTextRange = docSelectionManagerService.getActiveTextRange();
if (docDataModel == null || activeTextRange == null) {
const docRanges = docSelectionManagerService.getDocRanges();
const activeRange = docRanges.find((range) => range.isActive) ?? docRanges[0];
if (docDataModel == null || activeRange == null) {
return false;
}
const { segmentId } = activeTextRange;
const { segmentId } = activeRange;
const unitId = docDataModel.getUnitId();
const prevBody = docDataModel.getSelfOrHeaderFooterModel(segmentId).getSnapshot().body;
if (prevBody == null) {
const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getSnapshot().body;
if (body == null) {
return false;
}
const { tables = [] } = prevBody;
const { tables = [] } = body;
const textRanges: ISuccinctDocRangeParam[] = [];
let offset = 0;
@@ -64,10 +65,10 @@ export const DocSelectAllCommand: ICommand<ISelectAllCommandParams> = {
offset = endIndex;
}
if (offset !== prevBody.dataStream.length - 2) {
if (offset !== body.dataStream.length - 2) {
textRanges.push({
startOffset: offset,
endOffset: prevBody.dataStream.length - 2,
endOffset: body.dataStream.length - 2,
rangeType: DOC_RANGE_TYPE.TEXT,
});
}
@@ -607,9 +607,5 @@ function findAboveOrBellowCellLine(
}
}
if (newLine != null) {
return newLine;
}
return newLine;
}
@@ -232,7 +232,7 @@ export function convertBodyToHtml(doc: IDocumentData): string {
const result: IHtmlResult = { html: '' };
const nodeList = parseDataStreamToTree(dataStream);
const nodeList = parseDataStreamToTree(dataStream).sectionList;
for (const node of nodeList) {
processNode(node, doc, result);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { DocumentSkeleton, IDocumentOffsetConfig, IDocumentSkeletonGlyph, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, INodePosition, IPoint } from '@univerjs/engine-render';
import type { DocumentSkeleton, IDocumentOffsetConfig, IDocumentSkeletonPage, IDocumentSkeletonRow, INodePosition, IPoint } from '@univerjs/engine-render';
import { type Nullable, Tools } from '@univerjs/core';
import { DocumentSkeletonPageType, getPageFromPath, getTableIdAndSliceIndex, Liquid } from '@univerjs/engine-render';
import { compareNodePositionLogic, pushToPoints } from './convert-text-range';
@@ -57,6 +57,7 @@ export function isValidRectRange(anchorNodePosition: INodePosition, focusNodePos
return true;
}
// Determine whether the selection is in a table cell skeleton.
export function isInSameTableCell(anchorNodePosition: INodePosition, focusNodePosition: INodePosition): boolean {
const { path: anchorPath } = anchorNodePosition;
const { path: focusPath } = focusNodePosition;
@@ -65,9 +66,41 @@ export function isInSameTableCell(anchorNodePosition: INodePosition, focusNodePo
return false;
}
if (anchorPath.length !== focusPath.length) {
return false;
}
return Tools.diffValue(anchorPath, focusPath);
}
// Determine whether the selection is in the same table cell support across pages.
export function isInSameTableCellData(skeleton: DocumentSkeleton, anchorNodePosition: INodePosition, focusNodePosition: INodePosition): boolean {
const { path: anchorPath } = anchorNodePosition;
const { path: focusPath } = focusNodePosition;
if (anchorPath.indexOf('cells') === -1 || focusPath.indexOf('cells') === -1) {
return false;
}
const anchorGlyph = skeleton.findGlyphByPosition(anchorNodePosition);
const focusGlyph = skeleton.findGlyphByPosition(focusNodePosition);
const anchorCellPage = anchorGlyph?.parent?.parent?.parent?.parent?.parent;
const focusCellPage = focusGlyph?.parent?.parent?.parent?.parent?.parent;
if (anchorCellPage == null || focusCellPage == null) {
return false;
}
const anchorRow = anchorCellPage.parent as IDocumentSkeletonRow;
const focusRow = focusCellPage.parent as IDocumentSkeletonRow;
const anchorColIndex = anchorRow.cells.indexOf(anchorCellPage);
const focusColIndex = focusRow.cells.indexOf(focusCellPage);
return anchorColIndex === focusColIndex && anchorRow.index === focusRow.index;
}
// Return true if a is before b.
export function compareNodePositionInTable(a: INodePosition, b: INodePosition): boolean {
if (isInSameTableCell(a, b)) {
@@ -103,19 +136,33 @@ export function compareNodePositionInTable(a: INodePosition, b: INodePosition):
return aCellCount <= bCellCount;
}
function firstGlyphInCellPage(cellPage: IDocumentSkeletonPage): Nullable<IDocumentSkeletonGlyph> {
return cellPage.sections[0].columns[0].lines[0].divides[0].glyphGroup[0];
function isEmptyCellPage(cell: IDocumentSkeletonPage) {
return cell.sections[0].columns[0].lines.length === 0;
}
function lastGlyphInCellPage(cellPage: IDocumentSkeletonPage): Nullable<IDocumentSkeletonGlyph> {
const { sections } = cellPage;
const lastSection = sections[sections.length - 1];
const lastColumn = lastSection.columns[lastSection.columns.length - 1];
const lastLine = lastColumn.lines[lastColumn.lines.length - 1];
const lastDivide = lastLine.divides[lastLine.divides.length - 1];
const lastGlyphGroup = lastDivide.glyphGroup;
function findNonEmptyCellPages(
cells: IDocumentSkeletonPage[],
startCol: number,
endCol: number
): Nullable<IDocumentSkeletonPage[]> {
let s = startCol;
let e = endCol;
let startCell = cells[s];
let endCell = cells[e];
return lastGlyphGroup[lastGlyphGroup.length - 2];
while (s < e && (isEmptyCellPage(startCell) || isEmptyCellPage(endCell))) {
if (isEmptyCellPage(startCell)) {
s++;
startCell = cells[s];
} else if (isEmptyCellPage(endCell)) {
e--;
endCell = cells[e];
}
}
if (!isEmptyCellPage(startCell) && !isEmptyCellPage(endCell)) {
return [startCell, endCell];
}
}
interface IRectRangeNodePositions {
@@ -146,7 +193,7 @@ export class NodePositionConvertToRectRange {
const { pages } = skeletonData;
const { segmentPage: startSegmentPage, page: startPage, pageType } = startNodePosition;
const { segmentPage: endSegmentPage, page: endPage } = endNodePosition;
// const { segmentPage: endSegmentPage, page: endPage } = endNodePosition;
const rectInfo = this._getTableRectRangeInfo(startNodePosition, endNodePosition);
@@ -173,9 +220,9 @@ export class NodePositionConvertToRectRange {
this._liquid.translatePage(page, pageLayoutType, pageMarginLeft, pageMarginTop);
}
const endIndex = pageType === DocumentSkeletonPageType.BODY || pageType === DocumentSkeletonPageType.CELL ? endPage : endSegmentPage;
// const endIndex = pageType === DocumentSkeletonPageType.BODY || pageType === DocumentSkeletonPageType.CELL ? endPage : endSegmentPage;
for (let p = skipPageIndex; p <= endIndex; p++) {
for (let p = skipPageIndex; p < pages.length; p++) {
const page = pages[p];
this._liquid.translatePagePadding(page);
const { skeTables } = page;
@@ -202,8 +249,13 @@ export class NodePositionConvertToRectRange {
for (const row of table.rows) {
if (row.index >= startRow && row.index <= endRow) {
const rowStartCell = row.cells[startColumn];
const rowEndCell = row.cells[endColumn];
const cells = findNonEmptyCellPages(row.cells, startColumn, endColumn);
if (cells == null) {
continue;
}
const [rowStartCell, rowEndCell] = cells;
const position = {
startX: x + rowStartCell.left + tableLeft,
@@ -236,115 +288,73 @@ export class NodePositionConvertToRectRange {
focusNodePosition: INodePosition
): Nullable<IRectRangeNodePositions[]> {
const nodePositionGroup: IRectRangeNodePositions[] = [];
const compare = compareNodePositionInTable(anchorNodePosition, focusNodePosition);
const startNodePosition = compare ? anchorNodePosition : focusNodePosition;
const endNodePosition = compare ? focusNodePosition : anchorNodePosition;
const anchorIndex = this._docSkeleton.findCharIndexByPosition(anchorNodePosition);
const focusIndex = this._docSkeleton.findCharIndexByPosition(focusNodePosition);
if (anchorIndex == null || focusIndex == null) {
return;
}
const compare = anchorIndex < focusIndex;
// Start segmentPage will equal to end segmentPage.
const { segmentPage } = startNodePosition;
const rectInfo = this._getTableRectRangeInfo(startNodePosition, endNodePosition);
const rectInfo = this._getTableRectRangeInfo(anchorNodePosition, focusNodePosition);
if (rectInfo == null) {
return;
}
const { tableId, pages, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex } = rectInfo;
const { tableId, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex } = rectInfo;
const tables: IDocumentSkeletonTable[] = [];
const tableNode = this._docSkeleton.getViewModel().findTableNodeById(tableId);
// TODO: @JOCS handle table in header and footer.
for (const page of pages) {
const { skeTables } = page;
for (const [id, table] of skeTables.entries()) {
if (id.startsWith(tableId)) {
tables.push(table);
}
}
}
if (tables.length === 0) {
if (tableNode == null) {
return;
}
for (const table of tables) {
this._collectPositionGroup(table, nodePositionGroup, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex, segmentPage, compare);
}
const totalColumns = tables[0].rows[0].cells.length;
const totalColumns = tableNode.children[0].children.length;
// Span entires row.
if (startColumnIndex === 0 && endColumnIndex === totalColumns - 1) {
const firstPosition = nodePositionGroup[0];
const lastPosition = nodePositionGroup[nodePositionGroup.length - 1];
const spanEntireRow = startColumnIndex === 0 && endColumnIndex === totalColumns - 1;
nodePositionGroup.length = 0;
if (spanEntireRow) {
const startCellNode = tableNode.children[startRowIndex].children[startColumnIndex];
const startNodePosition = this._docSkeleton.findNodePositionByCharIndex(startCellNode.startIndex + 1);
const endCellNode = tableNode.children[endRowIndex].children[endColumnIndex];
const endNodePosition = this._docSkeleton.findNodePositionByCharIndex(endCellNode.endIndex - 2);
if (startNodePosition == null || endNodePosition == null) {
return;
}
nodePositionGroup.push({
anchor: compare ? firstPosition.anchor : lastPosition.anchor,
focus: compare ? lastPosition.focus : firstPosition.focus,
anchor: compare ? startNodePosition : endNodePosition,
focus: compare ? endNodePosition : startNodePosition,
});
} else {
for (let i = startRowIndex; i <= endRowIndex; i++) {
const rowNode = tableNode.children[i];
const startCellNode = rowNode.children[startColumnIndex];
const endCellNode = rowNode.children[endColumnIndex];
const startNodePosition = this._docSkeleton.findNodePositionByCharIndex(startCellNode.startIndex + 1);
const endNodePosition = this._docSkeleton.findNodePositionByCharIndex(endCellNode.endIndex - 2);
if (startNodePosition == null || endNodePosition == null) {
return;
}
nodePositionGroup.push({
anchor: compare ? startNodePosition : endNodePosition,
focus: compare ? endNodePosition : startNodePosition,
});
}
}
return nodePositionGroup;
}
private _collectPositionGroup(
table: IDocumentSkeletonTable,
nodePositionGroup: IRectRangeNodePositions[],
startRowIndex: number,
endRowIndex: number,
startColumnIndex: number,
endColumnIndex: number,
segmentPage: number,
compare: boolean
) {
// Not span entires row.
for (let i = 0; i < table.rows.length; i++) {
const row = table.rows[i];
if (row.index < startRowIndex) {
continue;
}
if (row.index > endRowIndex) {
break;
}
const startCellInRow = row.cells[startColumnIndex];
const endCellInRow = row.cells[endColumnIndex];
const startCellGlyph = firstGlyphInCellPage(startCellInRow);
const endCellGlyph = lastGlyphInCellPage(endCellInRow);
if (startCellGlyph == null || endCellGlyph == null) {
continue;
}
const startPosition = this._docSkeleton.findPositionByGlyph(startCellGlyph, segmentPage);
const endPosition = this._docSkeleton.findPositionByGlyph(endCellGlyph, segmentPage);
if (startPosition == null || endPosition == null) {
continue;
}
const anchor = compare ? startPosition : endPosition;
const focus = compare ? endPosition : startPosition;
nodePositionGroup.push({
anchor: {
...anchor,
isBack: true, // true or false is the same.
},
focus: {
...focus,
isBack: true,
},
});
}
}
private _getTableRectRangeInfo(startNodePosition: INodePosition, endNodePosition: INodePosition) {
private _getTableRectRangeInfo(anchorPosition: INodePosition, focusPosition: INodePosition) {
const docSkeleton = this._docSkeleton;
const skeletonData = docSkeleton.getSkeletonData();
@@ -354,31 +364,35 @@ export class NodePositionConvertToRectRange {
const { pages } = skeletonData;
const { path: startPath } = startNodePosition;
const { path: endPath } = endNodePosition;
const startCell = getPageFromPath(skeletonData, startPath);
const endCell = getPageFromPath(skeletonData, endPath);
const { path: anchorPath } = anchorPosition;
const { path: focusPath } = focusPosition;
const anchorCell = getPageFromPath(skeletonData, anchorPath);
const focusCell = getPageFromPath(skeletonData, focusPath);
if (startCell == null || endCell == null) {
if (anchorCell == null || focusCell == null) {
return;
}
const tableId = startCell.segmentId;
const startRow = (startCell.parent as IDocumentSkeletonRow).index;
const startColumn = (startCell.parent as IDocumentSkeletonRow).cells.indexOf(startCell);
const tableId = anchorCell.segmentId;
const anchorRow = (anchorCell.parent as IDocumentSkeletonRow).index;
const anchorColumn = (anchorCell.parent as IDocumentSkeletonRow).cells.indexOf(anchorCell);
const endRow = (endCell?.parent as IDocumentSkeletonRow).index;
const endColumn = (endCell?.parent as IDocumentSkeletonRow).cells.indexOf(endCell);
const focusRow = (focusCell?.parent as IDocumentSkeletonRow).index;
const focusColumn = (focusCell?.parent as IDocumentSkeletonRow).cells.indexOf(focusCell);
const startRowIndex = Math.min(anchorRow, focusRow);
const endRowIndex = Math.max(anchorRow, focusRow);
const startColumnIndex = Math.min(anchorColumn, focusColumn);
const endColumnIndex = Math.max(anchorColumn, focusColumn);
return {
pages,
tableId,
startCell,
endCell,
startRowIndex: startRow,
startColumnIndex: startColumn,
endRowIndex: endRow,
endColumnIndex: endColumn,
startRowIndex,
startColumnIndex,
endRowIndex,
endColumnIndex,
};
}
}
@@ -173,6 +173,24 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
const document = mainComponent as Documents;
const docSkeleton = this._docSkeletonManagerService.getSkeleton();
const generalAddRange = (startOffset: number, endOffset: number) => {
const rangeList = getRangeListFromCharIndex(
startOffset, endOffset, scene, document, docSkeleton, style, segmentId, segmentPage
);
if (rangeList == null) {
return;
}
const { textRanges, rectRanges } = rangeList;
for (const textRange of textRanges) {
this._addTextRange(textRange);
}
this._addRectRanges(rectRanges);
};
for (const range of ranges) {
const { startOffset, endOffset, rangeType, startNodePosition, endNodePosition } = range as ITextRangeWithStyle;
@@ -192,53 +210,45 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
this._addRectRanges([rectRange]);
}
} else if (rangeType === DOC_RANGE_TYPE.TEXT) {
let textRange: Nullable<TextRange> = null;
// TODO: Remove try catch when text range in cell support across pages.
try {
let textRange: Nullable<TextRange> = null;
if (startNodePosition && endNodePosition) {
textRange = getTextRangeFromCharIndex(
startNodePosition.isBack ? startOffset : startOffset - 1,
endNodePosition.isBack ? endOffset : endOffset - 1,
scene,
document,
docSkeleton,
style,
segmentId,
segmentPage,
startNodePosition.isBack,
endNodePosition.isBack
);
} else {
textRange = getTextRangeFromCharIndex(
startOffset,
endOffset,
scene,
document,
docSkeleton,
style,
segmentId,
segmentPage
);
}
if (startNodePosition && endNodePosition) {
textRange = getTextRangeFromCharIndex(
startNodePosition.isBack ? startOffset : startOffset - 1,
endNodePosition.isBack ? endOffset : endOffset - 1,
scene,
document,
docSkeleton,
style,
segmentId,
segmentPage,
startNodePosition.isBack,
endNodePosition.isBack
);
} else {
textRange = getTextRangeFromCharIndex(
startOffset,
endOffset,
scene,
document,
docSkeleton,
style,
segmentId,
segmentPage
);
}
if (textRange) {
this._addTextRange(textRange);
if (textRange) {
this._addTextRange(textRange);
}
// eslint-disable-next-line unused-imports/no-unused-vars
} catch (_e) {
generalAddRange(startOffset, endOffset);
}
} else {
const rangeList = getRangeListFromCharIndex(
startOffset, endOffset, scene, document, docSkeleton, style, segmentId, segmentPage
);
if (rangeList == null) {
continue;
}
const { textRanges, rectRanges } = rangeList;
for (const textRange of textRanges) {
this._addTextRange(textRange);
}
this._addRectRanges(rectRanges);
generalAddRange(startOffset, endOffset);
}
}
@@ -34,7 +34,7 @@ import type { Documents, DocumentSkeleton, Engine, IDocumentSkeletonGlyph, INode
import type { IDocRange } from './range-interface';
import { type Nullable, RANGE_DIRECTION, Tools } from '@univerjs/core';
import { getOffsetRectForDom } from '@univerjs/engine-render';
import { isInSameTableCell, isValidRectRange } from './convert-rect-range';
import { isInSameTableCell, isInSameTableCellData, isValidRectRange } from './convert-rect-range';
import { convertPositionsToRectRanges, RectRange } from './rect-range';
import { TextRange } from './text-range';
@@ -140,13 +140,27 @@ export function getRangeListFromSelection(
// TODO: @JOCS handle NEST table.
// Handle selection in same table cell.
if (isInSameTableCell(anchorPosition, focusPosition)) {
textRanges.push(new TextRange(...rangeParams));
if (isInSameTableCellData(skeleton, anchorPosition, focusPosition)) {
// Table cell in one page.
if (isInSameTableCell(anchorPosition, focusPosition)) {
textRanges.push(new TextRange(...rangeParams));
return {
textRanges,
rectRanges,
};
return {
textRanges,
rectRanges,
};
} else {
const ranges = convertPositionsToRectRanges(
...rangeParams
);
rectRanges.push(...ranges);
return {
textRanges,
rectRanges,
};
}
}
// Handle selection in different table cell but in the same table.
@@ -21,6 +21,7 @@ import type {
IDocDrawingBase,
IDocumentRenderConfig,
INestingLevel,
IParagraph,
IParagraphProperties,
ITable,
ITableRow,
@@ -38,11 +39,16 @@ export interface IDocumentSkeletonCached extends ISkeletonResourceReference {
parent?: unknown;
}
export interface IParagraphList {
bullet: IDocumentSkeletonBullet;
paragraph: IParagraph;
}
export interface ISkeletonResourceReference {
skeHeaders: Map<string, Map<number, IDocumentSkeletonHeaderFooter>>; // id:{ width: IDocumentSkeletonHeaderFooter }
skeFooters: Map<string, Map<number, IDocumentSkeletonHeaderFooter>>;
/* Global cache, does not participate in rendering, only helps skeleton generation */
skeListLevel?: Map<string, IDocumentSkeletonBullet[]>; // 有序列表缓存,id:{ level: max(width)的bullet }
skeListLevel?: Map<string, IParagraphList[][]>; // 有序列表缓存,id:{ level: max(width)的bullet }
drawingAnchor?: Map<string, Map<number, IDocumentSkeletonDrawingAnchor>>; // Anchor point to assist floating element positioning
}
@@ -30,6 +30,7 @@ import type {
LocaleService,
} from '@univerjs/core';
import type { DataStreamTreeNode } from '../components/docs/view-model/data-stream-tree-node';
import type { DocumentViewModel } from '../components/docs/view-model/document-view-model';
import type {
DocumentSkeletonPageType,
@@ -111,6 +112,8 @@ export interface IParagraphTableCache {
tableId: string;
table: IDocumentSkeletonTable;
hasPositioned: boolean;
isSlideTable: boolean;
tableNode: DataStreamTreeNode;
}
export interface IParagraphConfig {
@@ -61,7 +61,7 @@ import {
lineIterator,
mergeByV,
} from '../../tools';
import { splitTable } from '../table';
import { createTableSkeletons, rollbackListCache } from '../table';
export function layoutParagraph(
ctx: ILayoutContext,
@@ -549,7 +549,7 @@ function _lineOperator(
}
if (skeTablesInParagraph != null && skeTablesInParagraph.length > 0) {
needOpenNewPageByTableLayout = _updateAndPositionTable(ctx, lineTop, lineHeight, lastPage, column, section, skeTablesInParagraph, paragraphConfig.paragraphIndex, pDrawingAnchor?.get(paragraphIndex)?.top);
needOpenNewPageByTableLayout = _updateAndPositionTable(ctx, lineTop, lineHeight, lastPage, column, section, skeTablesInParagraph, paragraphConfig.paragraphIndex, sectionBreakConfig, pDrawingAnchor?.get(paragraphIndex)?.top);
}
const newLineTop = calculateLineTopByDrawings(
@@ -776,24 +776,24 @@ function _updateAndPositionTable(
section: IDocumentSkeletonSection,
skeTablesInParagraph: IParagraphTableCache[],
paragraphIndex: number,
sectionBreakConfig: ISectionBreakConfig,
drawingAnchorTop?: number
): boolean {
if (skeTablesInParagraph.length === 0) {
return false;
}
// Paragraph will only have one table.
const lastTable = skeTablesInParagraph[skeTablesInParagraph.length - 1];
// Paragraph will only have one table, but will have multiple table slices.
const firstUnPositionedTable = skeTablesInParagraph.find((table) => table.hasPositioned === false);
if (lastTable.hasPositioned) {
if (firstUnPositionedTable == null) {
return false;
}
const { tableId, table } = lastTable;
const { tableId, table } = firstUnPositionedTable;
const { tableSource } = table;
const isOriginTable = tableId.indexOf('#-#') === -1;
if (isOriginTable) {
if (firstUnPositionedTable.isSlideTable === false) {
switch (tableSource.textWrap) {
case TableTextWrapType.NONE: {
table.top = lineTop;
@@ -817,38 +817,71 @@ function _updateAndPositionTable(
}
}
const { top, height } = table;
const { top, left, height } = table;
if (top + height > section.height) {
if (!ctx.isDirty && top + height > section.height && firstUnPositionedTable.isSlideTable === false) {
// Need split table.
skeTablesInParagraph.pop();
const availableHeight = section.height - top;
const [newTable, remainTable] = splitTable(table, availableHeight);
// TODO: handle nested table.
const { segmentId } = page;
const viewModel = ctx.viewModel.getSelfOrHeaderFooterViewModel(segmentId);
const tableNode = firstUnPositionedTable.tableNode;
if (newTable != null) {
page.skeTables.set(newTable.tableId, newTable);
newTable.parent = page;
rollbackListCache(ctx.skeletonResourceReference.skeListLevel!, tableNode);
const {
fromCurrentPage,
skeTables,
} = createTableSkeletons(
ctx,
page,
viewModel,
tableNode,
sectionBreakConfig,
availableHeight
);
// Reset the position of the first table.
skeTables.forEach((table, i) => {
table.top = i === 0 && fromCurrentPage ? top : 0;
table.left = left;
});
if (fromCurrentPage) {
const firstTable = skeTables.shift()!;
page.skeTables.set(firstTable.tableId, firstTable);
firstTable.parent = page;
skeTablesInParagraph.push({
table: newTable,
tableId: newTable.tableId,
table: firstTable,
tableId: firstTable.tableId,
hasPositioned: true,
isSlideTable: true,
tableNode,
});
}
if (remainTable != null) {
skeTablesInParagraph.push({
table: remainTable,
tableId: remainTable.tableId,
skeTablesInParagraph.push(...skeTables.map((table) => {
return {
table,
tableId: table.tableId,
hasPositioned: false,
});
}
isSlideTable: true,
tableNode,
};
}));
return true;
} else {
page.skeTables.set(tableId, table);
table.parent = page;
lastTable.hasPositioned = true;
firstUnPositionedTable.hasPositioned = true;
return false;
const isLastTable = firstUnPositionedTable === skeTablesInParagraph[skeTablesInParagraph.length - 1];
return !isLastTable;
}
}
@@ -945,8 +978,6 @@ function _reLayoutCheck(
const collision = collisionDetection(targetObject, lineHeight, top, columnLeft, columnWidth);
if (collision) {
// console.log(page, line.top + line.lineHeight, line.divides[0].glyphGroup[0].content);
// console.log('drawing: ', targetDrawing, 'lineHeight: ', lineHeight, 'top: ', top, 'width: ', width);
// No need to loop next line.
needBreakLineIterator = true;
@@ -1119,14 +1150,6 @@ function __getParagraphSpace(
};
}
function __makeColumnsFull(columns: IDocumentSkeletonColumn[] = []) {
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
setColumnFullState(column, true);
}
}
function __getLineHeight(
glyphLineHeight: number,
paragraphLineGapDefault: number,
@@ -1293,10 +1316,6 @@ function __getDrawingPosition(
drawings.set(drawing.drawingId, drawing);
}
// if (drawings.size) {
// console.log(`lineTop: ${lineTop}, blockAnchorTop: ${blockAnchorTop}`, drawings);
// }
return drawings;
}
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import type { IBullet, IDocDrawingBase, IDrawings, Nullable } from '@univerjs/core';
import type { IDocumentSkeletonBullet, IDocumentSkeletonDrawing, IDocumentSkeletonPage, IDocumentSkeletonTable } from '../../../../../basics/i-document-skeleton-cached';
import type { IBullet, IDocDrawingBase, IDrawings, IParagraph, Nullable } from '@univerjs/core';
import type { IDocumentSkeletonBullet, IDocumentSkeletonDrawing, IDocumentSkeletonPage, IDocumentSkeletonTable, IParagraphList } from '../../../../../basics/i-document-skeleton-cached';
import type { IParagraphConfig, ISectionBreakConfig } from '../../../../../basics/interfaces';
import type { DataStreamTreeNode } from '../../../view-model/data-stream-tree-node';
import type { DocumentViewModel } from '../../../view-model/document-view-model';
@@ -31,7 +31,7 @@ import { layoutParagraph } from './layout-ruler';
function _getListLevelAncestors(
bullet?: IBullet,
listLevel?: Map<string, IDocumentSkeletonBullet[]>
listLevel?: Map<string, IParagraphList[][]>
): Array<Nullable<IDocumentSkeletonBullet>> | undefined {
if (!bullet || !listLevel) {
return;
@@ -50,17 +50,23 @@ function _getListLevelAncestors(
const listLevelAncestors: Array<Nullable<IDocumentSkeletonBullet>> = [];
for (let i = level; i >= 0; i--) {
const bs = sameList?.[i];
listLevelAncestors[i] = bs || null;
if (Array.isArray(sameList?.[i])) {
const len = sameList[i].length;
listLevelAncestors[i] = sameList[i][len - 1]?.bullet ?? null;
} else {
listLevelAncestors[i] = null;
}
}
return listLevelAncestors;
}
function _updateListLevelAncestors(
paragraph: IParagraph,
bullet?: IBullet,
bulletSkeleton?: IDocumentSkeletonBullet,
listLevel?: Map<string, IDocumentSkeletonBullet[]>
listLevel?: Map<string, IParagraphList[][]>
) {
if (!bullet || !bulletSkeleton) {
return;
@@ -68,11 +74,17 @@ function _updateListLevelAncestors(
const { listId, nestingLevel } = bullet;
const cacheItem: IDocumentSkeletonBullet[] = [...(listLevel?.get(listId) || [])];
const cacheItem: IParagraphList[][] = [...(listLevel?.get(listId) || [])];
// [[nestingLevel, bulletSkeleton]];
cacheItem[nestingLevel] = bulletSkeleton;
if (cacheItem[nestingLevel] == null) {
cacheItem[nestingLevel] = [];
}
cacheItem[nestingLevel].push({
bullet: bulletSkeleton,
paragraph,
});
cacheItem.splice(nestingLevel + 1); // 文档自上而下渲染,如果一个level被更新,则它以下的level数据的startIndex就要重置
@@ -131,10 +143,10 @@ export function lineBreaking(
localeService,
} = sectionBreakConfig;
const { endIndex, blocks = [] } = paragraphNode;
const { endIndex, blocks = [], children } = paragraphNode;
const { segmentId } = curPage;
const paragraph = viewModel.getParagraph(endIndex) || { startIndex: 0 };
const paragraph = viewModel.getParagraph(endIndex, true) || { startIndex: 0 };
const { paragraphStyle = {}, bullet } = paragraph;
@@ -162,6 +174,8 @@ export function lineBreaking(
tableId: tableSkeleton.tableId,
table: tableSkeleton,
hasPositioned: false,
isSlideTable: false,
tableNode: children[0],
},
]
: undefined,
@@ -182,7 +196,7 @@ export function lineBreaking(
const listLevelAncestors = _getListLevelAncestors(bullet, skeListLevel); // 取得列表所有 level 的缓存
const bulletSkeleton = dealWithBullet(bullet, lists, listLevelAncestors, localeService); // 生成 bullet
_updateListLevelAncestors(bullet, bulletSkeleton, skeListLevel); // 更新最新的 level 缓存列表
_updateListLevelAncestors(paragraph, bullet, bulletSkeleton, skeListLevel); // 更新最新的 level 缓存列表
paragraphConfig.bulletSkeleton = bulletSkeleton;
@@ -123,7 +123,7 @@ export function shaping(
const shapedTextList: IShapedText[] = [];
let breaker = new LineBreaker(content);
const { endIndex } = paragraphNode;
const paragraph = viewModel.getParagraph(endIndex) || { startIndex: 0 };
const paragraph = viewModel.getParagraph(endIndex, true) || { startIndex: 0 };
const { paragraphStyle = {} } = paragraph;
const { snapToGrid = BooleanNumber.TRUE } = paragraphStyle;
let last = 0;
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import type { INumberUnit, ITable, ITableRow, Nullable } from '@univerjs/core';
import type { IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, ISectionBreakConfig } from '../../../../basics';
import type { INumberUnit, ITable, ITableRow } 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';
import type { ILayoutContext } from '../tools';
import { TableAlignmentType, TableRowHeightRule, VerticalAlignmentType } from '@univerjs/core';
import { createSkeletonCellPage } from '../model/page';
import { BooleanNumber, TableAlignmentType, TableRowHeightRule, VerticalAlignmentType } from '@univerjs/core';
import { createNullCellPage, createSkeletonCellPages } from '../model/page';
export function createTableSkeleton(
ctx: ILayoutContext,
@@ -53,7 +53,7 @@ export function createTableSkeleton(
for (const cellNode of cellNodes) {
const col = cellNodes.indexOf(cellNode);
const cellPageSkeleton = createSkeletonCellPage(
const cellPageSkeleton = createSkeletonCellPages(
ctx,
viewModel,
cellNode,
@@ -61,7 +61,7 @@ export function createTableSkeleton(
table,
row,
col
);
)[0];
const { marginTop = 0, marginBottom = 0 } = cellPageSkeleton;
const pageHeight = cellPageSkeleton.height + marginTop + marginBottom;
@@ -132,65 +132,255 @@ export function createTableSkeleton(
return tableSkeleton;
}
// When a table spreads pages, you need to split the table into two tables and place them on different pages,
// and if you allow the spread to break the rows, you also need to split the rows.
export function splitTable(
tableSke: IDocumentSkeletonTable,
export function rollbackListCache(listLevel: Map<string, IParagraphList[][]>, table: DataStreamTreeNode) {
const { startIndex, endIndex } = table;
for (const paragraphLists of listLevel.values()) {
for (const paragraphList of paragraphLists) {
const paragraphListIndex = paragraphList.findIndex((p) => p.paragraph.startIndex > startIndex && p.paragraph.startIndex < endIndex);
if (paragraphListIndex > -1) {
paragraphList.splice(paragraphListIndex);
}
}
}
}
export interface ISlicedTableSkeletonParams {
skeTables: IDocumentSkeletonTable[];
fromCurrentPage: boolean;
}
// 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,
curPage: IDocumentSkeletonPage,
viewModel: DocumentViewModel,
tableNode: DataStreamTreeNode,
sectionBreakConfig: ISectionBreakConfig,
availableHeight: number
): [
Nullable<IDocumentSkeletonTable>,
Nullable<IDocumentSkeletonTable>
] {
// 处理极端情况,表格第一行高度都大于可用高度,那么表格从下一页开始排版
if (tableSke.rows[0].height > availableHeight) {
return [null, tableSke];
): 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.getTable(startIndex);
if (table == null) {
throw new Error('Table not found when creating table skeletons');
}
const { tableId: tableSliceId, tableSource } = tableSke;
const { tableId, sliceIndex } = getTableIdAndSliceIndex(tableSliceId);
const newTable = getNullTableSkeleton(0, 0, tableSource);
// Reset table id;
newTable.tableId = getTableSliceId(tableId, sliceIndex);
newTable.left = tableSke.left;
newTable.width = tableSke.width;
newTable.height = 0;
newTable.top = tableSke.top;
tableSke.top = 0;
let curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table);
let rowTop = 0;
let tableWidth = 0;
let remainHeight = availableHeight;
while (tableSke.rows.length && remainHeight >= tableSke.rows[0].height) {
const row = tableSke.rows.shift()!;
skeTables.push(curTableSkeleton);
newTable.rows.push(row);
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;
tableSke.height -= row.height;
newTable.height += row.height;
const rowHeights = [0];
// Reset row's parent index.
row.parent = newTable;
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
);
remainHeight -= row.height;
}
while (rowSkeletons.length < cellPageSkeletons.length) {
const rowSkeleton = _getNullTableRowSkeleton(startIndex, endIndex, row, rowSource);
const colCount = cellNodes.length;
tableSke.tableId = getTableSliceId(tableId, sliceIndex + 1);
// Fill the row with null cell pages.
rowSkeleton.cells = [...new Array(colCount)].map((_, i) => {
const cellSkeleton = createNullCellPage(
ctx,
sectionBreakConfig,
table,
row,
i
).page;
// Reset st and ed.
newTable.st = newTable.rows[0].st - 1;
newTable.ed = newTable.rows[newTable.rows.length - 1].ed + 1;
cellSkeleton.parent = rowSkeleton;
if (tableSke.rows.length > 0) {
tableSke.st = tableSke.rows[0].st - 1;
tableSke.ed = tableSke.rows[tableSke.rows.length - 1].ed + 1;
return cellSkeleton;
});
// Reset row top.
for (const row of tableSke.rows) {
row.top -= newTable.height;
rowSkeletons.push(rowSkeleton);
}
while (rowHeights.length < cellPageSkeletons.length) {
rowHeights.push(0);
}
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];
cellPageSkeleton.parent = rowSke;
rowSke.cells[col] = cellPageSkeleton;
rowHeights[pageIndex] = Math.max(rowHeights[pageIndex], cellPageHeight);
}
}
for (const rowSke of rowSkeletons) {
const rowIndex = rowSkeletons.indexOf(rowSke);
if (hRule === TableRowHeightRule.AT_LEAST) {
rowHeights[rowIndex] = Math.max(rowHeights[rowIndex], val.v);
} else if (hRule === TableRowHeightRule.EXACT) {
rowHeights[rowIndex] = val.v;
}
rowHeights[rowIndex] = Math.min(rowHeights[rowIndex], pageContentHeight);
let left = 0;
// Set row height to cell page height.
for (const cellPageSkeleton of rowSke.cells) {
cellPageSkeleton.left = left;
cellPageSkeleton.pageHeight = rowHeights[rowIndex];
left += cellPageSkeleton.pageWidth;
tableWidth = Math.max(tableWidth, left);
}
}
// Handle vertical alignment in cell.
for (let i = 0; i < rowSource.tableCells.length; i++) {
const cellConfig = rowSource.tableCells[i];
for (const rowSkeleton of rowSkeletons) {
const cellPageSkeleton = rowSkeleton.cells[i];
if (cellPageSkeleton == null) {
continue;
}
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;
}
marginTop = Math.max(originMarginTop, marginTop);
cellPageSkeleton.marginTop = marginTop;
}
}
if (rowHeights[0] > remainHeight) {
if (curTableSkeleton.rows.length > 0) {
needOpenNewTable = true;
} else {
fromCurrentPage = false;
remainHeight = pageContentHeight;
rowTop = 0;
}
}
if (needOpenNewTable) {
curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table);
skeTables.push(curTableSkeleton);
remainHeight = pageContentHeight;
rowTop = 0;
}
if (rowSkeletons.length > 1) {
for (let i = 0; i < rowSkeletons.length; i++) {
if (i !== 0) {
curTableSkeleton = getNullTableSkeleton(startIndex, endIndex, table);
skeTables.push(curTableSkeleton);
}
const rowSkeleton = rowSkeletons[i];
const rowHeight = rowHeights[i];
rowSkeleton.height = rowHeight;
rowSkeleton.top = i === 0 ? rowTop : 0;
curTableSkeleton.height += rowHeight;
curTableSkeleton.rows.push(rowSkeleton);
rowSkeleton.parent = curTableSkeleton;
remainHeight = pageContentHeight - rowHeight;
rowTop = rowHeight;
}
} 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;
}
}
return [newTable, tableSke.rows.length > 0 ? tableSke : null];
const tableLeft = _getTableLeft(pageWidth - marginLeft - marginRight, tableWidth, table.align, table.indent);
let tableIndex = 0;
for (const tableSkeleton of skeTables) {
tableSkeleton.width = tableWidth;
tableSkeleton.left = tableLeft;
// Reset table st and ed.
tableSkeleton.st = tableSkeleton.rows[0].st - 1;
tableSkeleton.ed = tableSkeleton.rows[tableSkeleton.rows.length - 1].ed + 1;
// Reset table id.
if (skeTables.length > 1) {
tableSkeleton.tableId = getTableSliceId(table.tableId, tableIndex);
tableIndex++;
}
}
return {
skeTables,
fromCurrentPage,
};
}
function _getTableLeft(pageWidth: number, tableWidth: number, align: TableAlignmentType, indent: INumberUnit = { v: 0 }) {
@@ -230,7 +420,7 @@ function _getNullTableRowSkeleton(
ed: number,
index: number,
rowSource: ITableRow,
parent: IDocumentSkeletonTable
parent?: IDocumentSkeletonTable
): IDocumentSkeletonRow {
return {
cells: [],
@@ -279,21 +279,20 @@ function _createSkeletonHeaderFooter(
return page;
}
export function createSkeletonCellPage(
export function createNullCellPage(
ctx: ILayoutContext,
viewModel: DocumentViewModel,
cellNode: DataStreamTreeNode,
sectionBreakConfig: ISectionBreakConfig,
tableConfig: ITable,
row: number,
col: number
col: number,
availableHeight: number = Number.POSITIVE_INFINITY,
maxCellPageHeight: number = Number.POSITIVE_INFINITY
) {
const { lists, footerTreeMap, headerTreeMap, localeService, drawings } = sectionBreakConfig;
const { skeletonResourceReference } = ctx;
const { cellMargin, tableRows, tableColumns, tableId } = tableConfig;
const cellConfig = tableRows[row].tableCells[col];
// Table cell only has one section.
const sectionNode = cellNode.children[0];
const {
start = { v: 10 },
end = { v: 10 },
@@ -301,7 +300,7 @@ export function createSkeletonCellPage(
bottom = { v: 5 },
} = cellConfig.margin ?? cellMargin ?? {};
const pageWidth = tableColumns[col].size.width.v;
const pageHeight = Number.POSITIVE_INFINITY;
const pageHeight = maxCellPageHeight;
const cellSectionBreakConfig: ISectionBreakConfig = {
lists,
@@ -319,21 +318,66 @@ export function createSkeletonCellPage(
drawings,
};
const areaPage = createSkeletonPage(ctx, cellSectionBreakConfig, skeletonResourceReference);
const areaPage = createSkeletonPage(
ctx,
// Set first page height to availableHeight.
Object.assign({}, cellSectionBreakConfig, {
pageSize: {
width: pageWidth,
height: Number.isFinite(availableHeight) ? availableHeight : pageHeight,
},
}),
skeletonResourceReference
);
areaPage.type = DocumentSkeletonPageType.CELL;
areaPage.segmentId = tableId;
const page = dealWithSection(
return {
page: areaPage,
sectionBreakConfig: cellSectionBreakConfig,
};
}
export function createSkeletonCellPages(
ctx: ILayoutContext,
viewModel: DocumentViewModel,
cellNode: DataStreamTreeNode,
sectionBreakConfig: ISectionBreakConfig,
tableConfig: ITable,
row: number,
col: number,
availableHeight: number = Number.POSITIVE_INFINITY,
maxCellPageHeight: number = Number.POSITIVE_INFINITY
) {
// Table cell only has one section.
const sectionNode = cellNode.children[0];
const { page: areaPage, sectionBreakConfig: cellSectionBreakConfig } = createNullCellPage(
ctx,
sectionBreakConfig,
tableConfig,
row,
col,
availableHeight,
maxCellPageHeight
);
const { pages } = dealWithSection(
ctx,
viewModel,
sectionNode,
areaPage,
cellSectionBreakConfig
).pages[0];
);
updateBlockIndex([page], cellNode.startIndex);
for (const p of pages) {
p.type = DocumentSkeletonPageType.CELL;
p.segmentId = tableConfig.tableId;
}
return page;
updateBlockIndex(pages, cellNode.startIndex);
return pages;
}
function _getVerticalMargin(
@@ -15,10 +15,10 @@
*/
import type { ISectionColumnProperties } from '@univerjs/core';
import { ColumnSeparatorType } from '@univerjs/core';
import type { IDocumentSkeletonColumn, IDocumentSkeletonSection } from '../../../../basics/i-document-skeleton-cached';
import { ColumnSeparatorType } from '@univerjs/core';
export function createSkeletonSection(
columnProperties: ISectionColumnProperties[] = [],
columnSeparatorType: ColumnSeparatorType = ColumnSeparatorType.NONE,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { ICustomDecorationForInterceptor, ICustomRangeForInterceptor, IDisposable, IDocumentBody, ITextRun, Nullable } from '@univerjs/core';
import type { ICustomDecorationForInterceptor, ICustomRangeForInterceptor, ICustomTable, IDisposable, IDocumentBody, ITextRun, Nullable } from '@univerjs/core';
import { DataStreamTreeNodeType, DataStreamTreeTokenType, DocumentDataModel, toDisposable } from '@univerjs/core';
import { BehaviorSubject } from 'rxjs';
import { DataStreamTreeNode } from './data-stream-tree-node';
@@ -24,6 +24,10 @@ interface ITableCache {
isFinished: boolean;
}
interface ITableNodeCache {
table: DataStreamTreeNode;
}
export interface ICustomRangeInterceptor {
getCustomRange: (index: number) => Nullable<ICustomRangeForInterceptor>;
getCustomDecoration: (index: number) => Nullable<ICustomDecorationForInterceptor>;
@@ -55,10 +59,11 @@ function batchParent(
parent.setIndexRange(allChildren[0].startIndex - startOffset, allChildren[allChildren.length - 1].endIndex + 1);
}
export function parseDataStreamToTree(dataStream: string) {
export function parseDataStreamToTree(dataStream: string, tables?: ICustomTable[]) {
let content = '';
const dataStreamLen = dataStream.length;
const sectionList: DataStreamTreeNode[] = [];
const tableNodeCache: Map<string, ITableNodeCache> = new Map();
// Only use to cache the outer paragraphs.
const paragraphList: DataStreamTreeNode[] = [];
// Use to cache paragraphs in cell.
@@ -81,6 +86,13 @@ export function parseDataStreamToTree(dataStream: string) {
// Paragraph Node will only has one table node.
batchParent(paragraphNode, [lastTableCache.table], DataStreamTreeNodeType.PARAGRAPH);
if (tables) {
const table = tables.find((table) => table.startIndex === lastTableCache.table.startIndex && table.endIndex === lastTableCache.table.endIndex + 1);
if (table) {
tableNodeCache.set(table.tableId, { table: lastTableCache.table });
}
}
tableList.pop();
}
@@ -154,19 +166,24 @@ export function parseDataStreamToTree(dataStream: string) {
}
}
return sectionList;
return { sectionList, tableNodeCache };
}
export class DocumentViewModel implements IDisposable {
private _cacheSize = 1000;
private _textRunsCache: Map<number, Map<number, ITextRun>> = new Map();
private _interceptor: Nullable<ICustomRangeInterceptor> = null;
private _tableNodeCache: Map<string, ITableNodeCache> = new Map();
children: DataStreamTreeNode[] = [];
private _sectionBreakCurrentIndex = 0;
private _paragraphCurrentIndex = 0;
private _textRunCurrentIndex = 0;
private _customBlockCurrentIndex = 0;
private _tableBlockCurrentIndex = 0;
private _customRangeCurrentIndex = 0;
private _editArea: DocumentEditArea = DocumentEditArea.BODY;
private readonly _editAreaChange$ = new BehaviorSubject<Nullable<DocumentEditArea>>(null);
@@ -183,7 +200,13 @@ export class DocumentViewModel implements IDisposable {
return;
}
this.children = parseDataStreamToTree(_documentDataModel.getBody()!.dataStream);
const body = _documentDataModel.getBody()!;
const { sectionList, tableNodeCache } = parseDataStreamToTree(body.dataStream, body.tables);
this._buildTextRunsCache();
this.children = sectionList;
this._tableNodeCache = tableNodeCache;
this._buildHeaderFooterViewModel();
}
@@ -197,6 +220,8 @@ export class DocumentViewModel implements IDisposable {
this.children.forEach((child) => {
child.dispose();
});
this._textRunsCache.clear();
}
selfPlus(_len: number, _index: number) {
@@ -253,7 +278,14 @@ export class DocumentViewModel implements IDisposable {
reset(documentDataModel: DocumentDataModel) {
this._documentDataModel = documentDataModel;
this.children = parseDataStreamToTree(documentDataModel.getBody()!.dataStream);
const body = documentDataModel.getBody()!;
const { sectionList, tableNodeCache } = parseDataStreamToTree(body.dataStream, body.tables);
this.children = sectionList;
this._tableNodeCache = tableNodeCache;
this._buildTextRunsCache();
this._buildHeaderFooterViewModel();
}
@@ -375,10 +407,8 @@ export class DocumentViewModel implements IDisposable {
resetCache() {
this._sectionBreakCurrentIndex = 0;
this._paragraphCurrentIndex = 0;
this._textRunCurrentIndex = 0;
this._customBlockCurrentIndex = 0;
this._tableBlockCurrentIndex = 0;
this._customRangeCurrentIndex = 0;
if (this.headerTreeMap.size > 0) {
for (const header of this.headerTreeMap.values()) {
@@ -412,6 +442,7 @@ export class DocumentViewModel implements IDisposable {
}
}
// TODO: @jocs, Use hash map to instead of array.
getParagraph(index: number, fromStart = false) {
const paragraphs = this.getBody()!.paragraphs;
if (paragraphs == null) {
@@ -430,86 +461,14 @@ export class DocumentViewModel implements IDisposable {
}
}
getTextRunRange(startIndex: number = 0, endIndex: number) {
const textRuns = this.getBody()!.textRuns;
if (textRuns == null) {
return [
{
st: startIndex,
ed: endIndex,
},
];
}
const trRange: ITextRun[] = [];
for (let i = this._textRunCurrentIndex, textRunsLen = textRuns.length; i < textRunsLen; i++) {
const textRun = textRuns[i];
if (textRun.st > endIndex) {
this._textRunCurrentIndex = i;
break;
} else if (textRun.ed < startIndex) {
this._textRunCurrentIndex = i;
continue;
} else {
trRange.push({
st: textRun.st < startIndex ? startIndex : textRun.st,
ed: textRun.ed > endIndex ? endIndex : textRun.ed,
sId: textRun.sId,
ts: textRun.ts,
});
this._textRunCurrentIndex = i;
}
}
const firstTr = trRange[0] || { st: endIndex + 1 };
if (firstTr.st > startIndex) {
trRange.push({
st: startIndex,
ed: firstTr.st - 1,
});
}
const lastTr = trRange[trRange.length - 1] || { ed: startIndex - 1 };
if (lastTr.ed < endIndex) {
trRange.push({
st: lastTr.ed + 1,
ed: endIndex,
});
}
return trRange;
}
/**
* textRun matches according to the selection. If the text length is 10, then the range of textRun is from 0 to 11.
*/
getTextRun(index: number) {
const textRuns = this.getBody()?.textRuns;
if (textRuns == null) {
return;
}
getTextRun(index: number): Nullable<ITextRun> {
const cacheIndex = Math.floor(index / this._cacheSize);
const textRunsCache = this._textRunsCache.get(cacheIndex);
const curTextRun = textRuns[this._textRunCurrentIndex];
if (curTextRun != null) {
if (index >= curTextRun.st && index < curTextRun.ed) {
return curTextRun;
}
if (index < curTextRun.st) {
return;
}
}
for (let i = this._textRunCurrentIndex, textRunsLen = textRuns.length; i < textRunsLen; i++) {
const textRun = textRuns[i];
if (index >= textRun.st && index < textRun.ed) {
this._textRunCurrentIndex = i;
return textRun;
}
}
return textRunsCache?.get(index % this._cacheSize);
}
getCustomBlock(index: number) {
@@ -564,6 +523,10 @@ export class DocumentViewModel implements IDisposable {
}
}
findTableNodeById(id: string) {
return this._tableNodeCache.get(id)?.table;
}
getCustomRangeRaw(index: number) {
const customRanges = this.getBody()!.customRanges;
if (customRanges == null) {
@@ -608,6 +571,25 @@ export class DocumentViewModel implements IDisposable {
return this.getCustomDecorationRaw(index);
}
private _buildTextRunsCache() {
const textRuns = this.getBody()?.textRuns ?? [];
this._textRunsCache.clear();
for (const textRun of textRuns) {
const { st, ed } = textRun;
for (let i = st; i < ed; i++) {
const cacheIndex = Math.floor(i / this._cacheSize);
if (!this._textRunsCache.has(cacheIndex)) {
this._textRunsCache.set(cacheIndex, new Map());
}
this._textRunsCache.get(cacheIndex)!.set(i % this._cacheSize, textRun);
}
}
}
private _buildHeaderFooterViewModel() {
const { headerModelMap, footerModelMap } = this._documentDataModel;
const viewModels = [];
@@ -144,7 +144,7 @@ export class Background extends SheetExtension {
renderBGContext.backgroundPaths = backgroundPaths;
ctx.beginPath();
mergeRanges.forEach((range) => {
// bgConfig is requried to be checked in each color loop.
// bgConfig is required to be checked in each color loop.
const bgConfig = bgColorMatrix.getValue(range.startRow, range.startColumn);
if (bgConfig) {
const cellInfo = spreadsheetSkeleton.getCellWithCoordByIndex(range.startRow, range.startColumn, false);
+1
View File
@@ -33,6 +33,7 @@ export { DataStreamTreeNode } from './components/docs/view-model/data-stream-tre
export { DocumentViewModel } from './components/docs/view-model/document-view-model';
export { DocumentEditArea } from './components/docs/view-model/document-view-model';
export { parseDataStreamToTree } from './components/docs/view-model/document-view-model';
export { getLastColumn } from './components/docs/layout/tools';
export { DEFAULT_PADDING_DATA } from './components/sheets/sheet-skeleton';
export * from './context';
export * from './custom';