/
normalizeTable.ts
155 lines (134 loc) · 5 KB
/
normalizeTable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { addBlock } from '../common/addBlock';
import { addSegment } from '../common/addSegment';
import { createBr } from '../creators/createBr';
import { createParagraph } from '../creators/createParagraph';
import type {
ContentModelSegment,
ContentModelSegmentFormat,
ContentModelTable,
ContentModelTableCell,
} from 'roosterjs-content-model-types';
/**
* Minimum width for a table cell
*/
export const MIN_ALLOWED_TABLE_CELL_WIDTH: number = 30;
const MIN_HEIGHT = 22;
/**
* Normalize a Content Model table, make sure:
* 1. Fist cells are not spanned
* 2. Inner cells are not header
* 3. All cells have content and width
* 4. Table and table row have correct width/height
* 5. Spanned cell has no child blocks
* 6. default format is correctly applied
* @param table The table to normalize
* @param defaultSegmentFormat @optional Default segment format to apply to cell
*/
export function normalizeTable(
table: ContentModelTable,
defaultSegmentFormat?: ContentModelSegmentFormat
) {
// Always collapse border and use border box for table in roosterjs to make layout simpler
const format = table.format;
if (!format.borderCollapse || !format.useBorderBox) {
format.borderCollapse = true;
format.useBorderBox = true;
}
// Make sure all first cells are not spanned
// Make sure all inner cells are not header
// Make sure all cells have content and width
table.rows.forEach((row, rowIndex) => {
row.cells.forEach((cell, colIndex) => {
if (cell.blocks.length == 0) {
const format = cell.format.textColor
? {
...defaultSegmentFormat,
textColor: cell.format.textColor,
}
: defaultSegmentFormat;
addBlock(
cell,
createParagraph(undefined /*isImplicit*/, undefined /*blockFormat*/, format)
);
addSegment(cell, createBr(format));
}
if (rowIndex == 0) {
cell.spanAbove = false;
} else if (rowIndex > 0 && cell.isHeader) {
cell.isHeader = false;
delete cell.cachedElement;
}
if (colIndex == 0) {
cell.spanLeft = false;
}
cell.format.useBorderBox = true;
});
// Make sure table has correct width and height array
if (row.height < MIN_HEIGHT) {
row.height = MIN_HEIGHT;
}
});
const columns = Math.max(...table.rows.map(row => row.cells.length));
for (let i = 0; i < columns; i++) {
if (table.widths[i] === undefined) {
table.widths[i] = getTableCellWidth(columns);
} else if (table.widths[i] < MIN_ALLOWED_TABLE_CELL_WIDTH) {
table.widths[i] = MIN_ALLOWED_TABLE_CELL_WIDTH;
}
}
// Move blocks from spanned cell to its main cell if any,
// and remove rows/columns if all cells in it are spanned
const colCount = table.rows[0]?.cells.length || 0;
for (let colIndex = colCount - 1; colIndex > 0; colIndex--) {
table.rows.forEach(row => {
const cell = row.cells[colIndex];
const leftCell = row.cells[colIndex - 1];
if (cell && leftCell && cell.spanLeft) {
tryMoveBlocks(leftCell, cell);
}
});
if (table.rows.every(row => row.cells[colIndex]?.spanLeft)) {
table.rows.forEach(row => row.cells.splice(colIndex, 1));
table.widths.splice(
colIndex - 1,
2,
table.widths[colIndex - 1] + table.widths[colIndex]
);
}
}
for (let rowIndex = table.rows.length - 1; rowIndex > 0; rowIndex--) {
const row = table.rows[rowIndex];
row.cells.forEach((cell, colIndex) => {
const aboveCell = table.rows[rowIndex - 1]?.cells[colIndex];
if (aboveCell && cell.spanAbove) {
tryMoveBlocks(aboveCell, cell);
}
});
if (row.cells.every(cell => cell.spanAbove)) {
table.rows[rowIndex - 1].height += row.height;
table.rows.splice(rowIndex, 1);
}
}
}
function getTableCellWidth(columns: number): number {
if (columns <= 4) {
return 120;
} else if (columns <= 6) {
return 100;
} else {
return 70;
}
}
function tryMoveBlocks(targetCell: ContentModelTableCell, sourceCell: ContentModelTableCell) {
const onlyHasEmptyOrBr = sourceCell.blocks.every(
block => block.blockType == 'Paragraph' && hasOnlyBrSegment(block.segments)
);
if (!onlyHasEmptyOrBr) {
targetCell.blocks.push(...sourceCell.blocks);
sourceCell.blocks = [];
}
}
function hasOnlyBrSegment(segments: ContentModelSegment[]): boolean {
segments = segments.filter(s => s.segmentType != 'SelectionMarker');
return segments.length == 0 || (segments.length == 1 && segments[0].segmentType == 'Br');
}