UNPKG

17.5 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5import { default as TableWalker } from '../tablewalker';
6import { createEmptyTableCell, updateNumericAttribute } from './common';
7/**
8 * Returns a cropped table according to given dimensions.
9
10 * To return a cropped table that starts at first row and first column and end in third row and column:
11 *
12 * ```ts
13 * const croppedTable = cropTableToDimensions( table, {
14 * startRow: 1,
15 * endRow: 3,
16 * startColumn: 1,
17 * endColumn: 3
18 * }, writer );
19 * ```
20 *
21 * Calling the code above for the table below:
22 *
23 * 0 1 2 3 4 0 1 2
24 * ┌───┬───┬───┬───┬───┐
25 * 0 │ a │ b │ c │ d │ e │
26 * ├───┴───┤ ├───┴───┤ ┌───┬───┬───┐
27 * 1 │ f │ │ g │ │ │ │ g │ 0
28 * ├───┬───┴───┼───┬───┤ will return: ├───┴───┼───┤
29 * 2 │ h │ i │ j │ k │ │ i │ j │ 1
30 * ├───┤ ├───┤ │ │ ├───┤
31 * 3 │ l │ │ m │ │ │ │ m │ 2
32 * ├───┼───┬───┤ ├───┤ └───────┴───┘
33 * 4 │ n │ o │ p │ │ q │
34 * └───┴───┴───┴───┴───┘
35 */
36export function cropTableToDimensions(sourceTable, cropDimensions, writer) {
37 const { startRow, startColumn, endRow, endColumn } = cropDimensions;
38 // Create empty table with empty rows equal to crop height.
39 const croppedTable = writer.createElement('table');
40 const cropHeight = endRow - startRow + 1;
41 for (let i = 0; i < cropHeight; i++) {
42 writer.insertElement('tableRow', croppedTable, 'end');
43 }
44 const tableMap = [...new TableWalker(sourceTable, { startRow, endRow, startColumn, endColumn, includeAllSlots: true })];
45 // Iterate over source table slots (including empty - spanned - ones).
46 for (const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor, cellAnchorRow, cellAnchorColumn } of tableMap) {
47 // Row index in cropped table.
48 const rowInCroppedTable = sourceRow - startRow;
49 const row = croppedTable.getChild(rowInCroppedTable);
50 // For empty slots: fill the gap with empty table cell.
51 if (!isAnchor) {
52 // But fill the gap only if the spanning cell is anchored outside cropped area.
53 // In the table from method jsdoc those cells are: "c" & "f".
54 if (cellAnchorRow < startRow || cellAnchorColumn < startColumn) {
55 createEmptyTableCell(writer, writer.createPositionAt(row, 'end'));
56 }
57 }
58 // Otherwise clone the cell with all children and trim if it exceeds cropped area.
59 else {
60 const tableCellCopy = writer.cloneElement(tableCell);
61 writer.append(tableCellCopy, row);
62 // Trim table if it exceeds cropped area.
63 // In the table from method jsdoc those cells are: "g" & "m".
64 trimTableCellIfNeeded(tableCellCopy, sourceRow, sourceColumn, endRow, endColumn, writer);
65 }
66 }
67 // Adjust heading rows & columns in cropped table if crop selection includes headings parts.
68 addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer);
69 return croppedTable;
70}
71/**
72 * Returns slot info of cells that starts above and overlaps a given row.
73 *
74 * In a table below, passing `overlapRow = 3`
75 *
76 * ┌───┬───┬───┬───┬───┐
77 * 0 │ a │ b │ c │ d │ e │
78 * │ ├───┼───┼───┼───┤
79 * 1 │ │ f │ g │ h │ i │
80 * ├───┤ ├───┼───┤ │
81 * 2 │ j │ │ k │ l │ │
82 * │ │ │ ├───┼───┤
83 * 3 │ │ │ │ m │ n │ <- overlap row to check
84 * ├───┼───┤ │ ├───│
85 * 4 │ o │ p │ │ │ q │
86 * └───┴───┴───┴───┴───┘
87 *
88 * will return slot info for cells: "j", "f", "k".
89 *
90 * @param table The table to check.
91 * @param overlapRow The index of the row to check.
92 * @param startRow row to start analysis. Use it when it is known that the cells above that row will not overlap. Default value is 0.
93 */
94export function getVerticallyOverlappingCells(table, overlapRow, startRow = 0) {
95 const cells = [];
96 const tableWalker = new TableWalker(table, { startRow, endRow: overlapRow - 1 });
97 for (const slotInfo of tableWalker) {
98 const { row, cellHeight } = slotInfo;
99 const cellEndRow = row + cellHeight - 1;
100 if (row < overlapRow && overlapRow <= cellEndRow) {
101 cells.push(slotInfo);
102 }
103 }
104 return cells;
105}
106/**
107 * Splits the table cell horizontally.
108 *
109 * @returns Created table cell, if any were created.
110 */
111export function splitHorizontally(tableCell, splitRow, writer) {
112 const tableRow = tableCell.parent;
113 const table = tableRow.parent;
114 const rowIndex = tableRow.index;
115 const rowspan = parseInt(tableCell.getAttribute('rowspan'));
116 const newRowspan = splitRow - rowIndex;
117 const newCellAttributes = {};
118 const newCellRowSpan = rowspan - newRowspan;
119 if (newCellRowSpan > 1) {
120 newCellAttributes.rowspan = newCellRowSpan;
121 }
122 const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
123 if (colspan > 1) {
124 newCellAttributes.colspan = colspan;
125 }
126 const startRow = rowIndex;
127 const endRow = startRow + newRowspan;
128 const tableMap = [...new TableWalker(table, { startRow, endRow, includeAllSlots: true })];
129 let newCell = null;
130 let columnIndex;
131 for (const tableSlot of tableMap) {
132 const { row, column, cell } = tableSlot;
133 if (cell === tableCell && columnIndex === undefined) {
134 columnIndex = column;
135 }
136 if (columnIndex !== undefined && columnIndex === column && row === endRow) {
137 newCell = createEmptyTableCell(writer, tableSlot.getPositionBefore(), newCellAttributes);
138 }
139 }
140 // Update the rowspan attribute after updating table.
141 updateNumericAttribute('rowspan', newRowspan, tableCell, writer);
142 return newCell;
143}
144/**
145 * Returns slot info of cells that starts before and overlaps a given column.
146 *
147 * In a table below, passing `overlapColumn = 3`
148 *
149 * 0 1 2 3 4
150 * ┌───────┬───────┬───┐
151 * │ a │ b │ c │
152 * │───┬───┴───────┼───┤
153 * │ d │ e │ f │
154 * ├───┼───┬───────┴───┤
155 * │ g │ h │ i │
156 * ├───┼───┼───┬───────┤
157 * │ j │ k │ l │ m │
158 * ├───┼───┴───┼───┬───┤
159 * │ n │ o │ p │ q │
160 * └───┴───────┴───┴───┘
161 * ^
162 * Overlap column to check
163 *
164 * will return slot info for cells: "b", "e", "i".
165 *
166 * @param table The table to check.
167 * @param overlapColumn The index of the column to check.
168 */
169export function getHorizontallyOverlappingCells(table, overlapColumn) {
170 const cellsToSplit = [];
171 const tableWalker = new TableWalker(table);
172 for (const slotInfo of tableWalker) {
173 const { column, cellWidth } = slotInfo;
174 const cellEndColumn = column + cellWidth - 1;
175 if (column < overlapColumn && overlapColumn <= cellEndColumn) {
176 cellsToSplit.push(slotInfo);
177 }
178 }
179 return cellsToSplit;
180}
181/**
182 * Splits the table cell vertically.
183 *
184 * @param columnIndex The table cell column index.
185 * @param splitColumn The index of column to split cell on.
186 * @returns Created table cell.
187 */
188export function splitVertically(tableCell, columnIndex, splitColumn, writer) {
189 const colspan = parseInt(tableCell.getAttribute('colspan'));
190 const newColspan = splitColumn - columnIndex;
191 const newCellAttributes = {};
192 const newCellColSpan = colspan - newColspan;
193 if (newCellColSpan > 1) {
194 newCellAttributes.colspan = newCellColSpan;
195 }
196 const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
197 if (rowspan > 1) {
198 newCellAttributes.rowspan = rowspan;
199 }
200 const newCell = createEmptyTableCell(writer, writer.createPositionAfter(tableCell), newCellAttributes);
201 // Update the colspan attribute after updating table.
202 updateNumericAttribute('colspan', newColspan, tableCell, writer);
203 return newCell;
204}
205/**
206 * Adjusts table cell dimensions to not exceed limit row and column.
207 *
208 * If table cell width (or height) covers a column (or row) that is after a limit column (or row)
209 * this method will trim "colspan" (or "rowspan") attribute so the table cell will fit in a defined limits.
210 */
211export function trimTableCellIfNeeded(tableCell, cellRow, cellColumn, limitRow, limitColumn, writer) {
212 const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
213 const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
214 const endColumn = cellColumn + colspan - 1;
215 if (endColumn > limitColumn) {
216 const trimmedSpan = limitColumn - cellColumn + 1;
217 updateNumericAttribute('colspan', trimmedSpan, tableCell, writer, 1);
218 }
219 const endRow = cellRow + rowspan - 1;
220 if (endRow > limitRow) {
221 const trimmedSpan = limitRow - cellRow + 1;
222 updateNumericAttribute('rowspan', trimmedSpan, tableCell, writer, 1);
223 }
224}
225/**
226 * Sets proper heading attributes to a cropped table.
227 */
228function addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer) {
229 const headingRows = parseInt(sourceTable.getAttribute('headingRows') || '0');
230 if (headingRows > 0) {
231 const headingRowsInCrop = headingRows - startRow;
232 updateNumericAttribute('headingRows', headingRowsInCrop, croppedTable, writer, 0);
233 }
234 const headingColumns = parseInt(sourceTable.getAttribute('headingColumns') || '0');
235 if (headingColumns > 0) {
236 const headingColumnsInCrop = headingColumns - startColumn;
237 updateNumericAttribute('headingColumns', headingColumnsInCrop, croppedTable, writer, 0);
238 }
239}
240/**
241 * Removes columns that have no cells anchored.
242 *
243 * In table below:
244 *
245 * +----+----+----+----+----+----+----+
246 * | 00 | 01 | 03 | 04 | 06 |
247 * +----+----+----+----+ +----+
248 * | 10 | 11 | 13 | | 16 |
249 * +----+----+----+----+----+----+----+
250 * | 20 | 21 | 23 | 24 | 26 |
251 * +----+----+----+----+----+----+----+
252 * ^--- empty ---^
253 *
254 * Will remove columns 2 and 5.
255 *
256 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
257 * To remove a column from a table use {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`}.
258 *
259 * @internal
260 * @returns True if removed some columns.
261 */
262export function removeEmptyColumns(table, tableUtils) {
263 const width = tableUtils.getColumns(table);
264 const columnsMap = new Array(width).fill(0);
265 for (const { column } of new TableWalker(table)) {
266 columnsMap[column]++;
267 }
268 const emptyColumns = columnsMap.reduce((result, cellsCount, column) => {
269 return cellsCount ? result : [...result, column];
270 }, []);
271 if (emptyColumns.length > 0) {
272 // Remove only last empty column because it will recurrently trigger removing empty rows.
273 const emptyColumn = emptyColumns[emptyColumns.length - 1];
274 // @if CK_DEBUG_TABLE // console.log( `Removing empty column: ${ emptyColumn }.` );
275 tableUtils.removeColumns(table, { at: emptyColumn });
276 return true;
277 }
278 return false;
279}
280/**
281 * Removes rows that have no cells anchored.
282 *
283 * In table below:
284 *
285 * +----+----+----+
286 * | 00 | 01 | 02 |
287 * +----+----+----+
288 * | 10 | 11 | 12 |
289 * + + + +
290 * | | | | <-- empty
291 * +----+----+----+
292 * | 30 | 31 | 32 |
293 * +----+----+----+
294 * | 40 | 42 |
295 * + + +
296 * | | | <-- empty
297 * +----+----+----+
298 * | 60 | 61 | 62 |
299 * +----+----+----+
300 *
301 * Will remove rows 2 and 5.
302 *
303 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
304 * To remove a row from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`}.
305 *
306 * @internal
307 * @returns True if removed some rows.
308 */
309export function removeEmptyRows(table, tableUtils) {
310 const emptyRows = [];
311 const tableRowCount = tableUtils.getRows(table);
312 for (let rowIndex = 0; rowIndex < tableRowCount; rowIndex++) {
313 const tableRow = table.getChild(rowIndex);
314 if (tableRow.isEmpty) {
315 emptyRows.push(rowIndex);
316 }
317 }
318 if (emptyRows.length > 0) {
319 // Remove only last empty row because it will recurrently trigger removing empty columns.
320 const emptyRow = emptyRows[emptyRows.length - 1];
321 // @if CK_DEBUG_TABLE // console.log( `Removing empty row: ${ emptyRow }.` );
322 tableUtils.removeRows(table, { at: emptyRow });
323 return true;
324 }
325 return false;
326}
327/**
328 * Removes rows and columns that have no cells anchored.
329 *
330 * In table below:
331 *
332 * +----+----+----+----+
333 * | 00 | 02 |
334 * +----+----+ +
335 * | 10 | |
336 * +----+----+----+----+
337 * | 20 | 22 | 23 |
338 * + + + +
339 * | | | | <-- empty row
340 * +----+----+----+----+
341 * ^--- empty column
342 *
343 * Will remove row 3 and column 1.
344 *
345 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
346 * To remove a rows from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`} and
347 * {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`} to remove a column.
348 *
349 * @internal
350 */
351export function removeEmptyRowsColumns(table, tableUtils) {
352 const removedColumns = removeEmptyColumns(table, tableUtils);
353 // If there was some columns removed then cleaning empty rows was already triggered.
354 if (!removedColumns) {
355 removeEmptyRows(table, tableUtils);
356 }
357}
358/**
359 * Returns adjusted last row index if selection covers part of a row with empty slots (spanned by other cells).
360 * The `dimensions.lastRow` is equal to last row index but selection might be bigger.
361 *
362 * This happens *only* on rectangular selection so we analyze a case like this:
363 *
364 * +---+---+---+---+
365 * 0 | a | b | c | d |
366 * + + +---+---+
367 * 1 | | e | f | g |
368 * + +---+ +---+
369 * 2 | | h | | i | <- last row, each cell has rowspan = 2,
370 * + + + + + so we need to return 3, not 2
371 * 3 | | | | |
372 * +---+---+---+---+
373 *
374 * @returns Adjusted last row index.
375 */
376export function adjustLastRowIndex(table, dimensions) {
377 const lastRowMap = Array.from(new TableWalker(table, {
378 startColumn: dimensions.firstColumn,
379 endColumn: dimensions.lastColumn,
380 row: dimensions.lastRow
381 }));
382 const everyCellHasSingleRowspan = lastRowMap.every(({ cellHeight }) => cellHeight === 1);
383 // It is a "flat" row, so the last row index is OK.
384 if (everyCellHasSingleRowspan) {
385 return dimensions.lastRow;
386 }
387 // Otherwise get any cell's rowspan and adjust the last row index.
388 const rowspanAdjustment = lastRowMap[0].cellHeight - 1;
389 return dimensions.lastRow + rowspanAdjustment;
390}
391/**
392 * Returns adjusted last column index if selection covers part of a column with empty slots (spanned by other cells).
393 * The `dimensions.lastColumn` is equal to last column index but selection might be bigger.
394 *
395 * This happens *only* on rectangular selection so we analyze a case like this:
396 *
397 * 0 1 2 3
398 * +---+---+---+---+
399 * | a |
400 * +---+---+---+---+
401 * | b | c | d |
402 * +---+---+---+---+
403 * | e | f |
404 * +---+---+---+---+
405 * | g | h |
406 * +---+---+---+---+
407 * ^
408 * last column, each cell has colspan = 2, so we need to return 3, not 2
409 *
410 * @returns Adjusted last column index.
411 */
412export function adjustLastColumnIndex(table, dimensions) {
413 const lastColumnMap = Array.from(new TableWalker(table, {
414 startRow: dimensions.firstRow,
415 endRow: dimensions.lastRow,
416 column: dimensions.lastColumn
417 }));
418 const everyCellHasSingleColspan = lastColumnMap.every(({ cellWidth }) => cellWidth === 1);
419 // It is a "flat" column, so the last column index is OK.
420 if (everyCellHasSingleColspan) {
421 return dimensions.lastColumn;
422 }
423 // Otherwise get any cell's colspan and adjust the last column index.
424 const colspanAdjustment = lastColumnMap[0].cellWidth - 1;
425 return dimensions.lastColumn + colspanAdjustment;
426}