@ckeditor/ckeditor5-table 40.0.0 → 40.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +15 -15
  2. package/LICENSE.md +3 -3
  3. package/build/table.js +1 -1
  4. package/build/translations/fi.js +1 -1
  5. package/lang/translations/fi.po +3 -3
  6. package/package.json +2 -2
  7. package/src/augmentation.d.ts +76 -76
  8. package/src/augmentation.js +5 -5
  9. package/src/commands/insertcolumncommand.d.ts +55 -55
  10. package/src/commands/insertcolumncommand.js +67 -67
  11. package/src/commands/insertrowcommand.d.ts +54 -54
  12. package/src/commands/insertrowcommand.js +66 -66
  13. package/src/commands/inserttablecommand.d.ts +44 -44
  14. package/src/commands/inserttablecommand.js +69 -69
  15. package/src/commands/mergecellcommand.d.ts +68 -68
  16. package/src/commands/mergecellcommand.js +198 -198
  17. package/src/commands/mergecellscommand.d.ts +28 -28
  18. package/src/commands/mergecellscommand.js +94 -94
  19. package/src/commands/removecolumncommand.d.ts +29 -29
  20. package/src/commands/removecolumncommand.js +109 -109
  21. package/src/commands/removerowcommand.d.ts +29 -29
  22. package/src/commands/removerowcommand.js +82 -82
  23. package/src/commands/selectcolumncommand.d.ts +33 -33
  24. package/src/commands/selectcolumncommand.js +60 -60
  25. package/src/commands/selectrowcommand.d.ts +33 -33
  26. package/src/commands/selectrowcommand.js +56 -56
  27. package/src/commands/setheadercolumncommand.d.ts +50 -50
  28. package/src/commands/setheadercolumncommand.js +71 -71
  29. package/src/commands/setheaderrowcommand.d.ts +53 -53
  30. package/src/commands/setheaderrowcommand.js +79 -79
  31. package/src/commands/splitcellcommand.d.ts +43 -43
  32. package/src/commands/splitcellcommand.js +54 -54
  33. package/src/converters/downcast.d.ts +63 -63
  34. package/src/converters/downcast.js +146 -146
  35. package/src/converters/table-caption-post-fixer.d.ts +20 -20
  36. package/src/converters/table-caption-post-fixer.js +53 -53
  37. package/src/converters/table-cell-paragraph-post-fixer.d.ts +32 -32
  38. package/src/converters/table-cell-paragraph-post-fixer.js +107 -107
  39. package/src/converters/table-cell-refresh-handler.d.ts +18 -18
  40. package/src/converters/table-cell-refresh-handler.js +45 -45
  41. package/src/converters/table-headings-refresh-handler.d.ts +17 -17
  42. package/src/converters/table-headings-refresh-handler.js +49 -49
  43. package/src/converters/table-layout-post-fixer.d.ts +226 -226
  44. package/src/converters/table-layout-post-fixer.js +367 -367
  45. package/src/converters/tableproperties.d.ts +54 -54
  46. package/src/converters/tableproperties.js +159 -159
  47. package/src/converters/upcasttable.d.ts +49 -49
  48. package/src/converters/upcasttable.js +243 -243
  49. package/src/index.d.ts +60 -60
  50. package/src/index.js +30 -30
  51. package/src/plaintableoutput.d.ts +26 -26
  52. package/src/plaintableoutput.js +123 -123
  53. package/src/table.d.ts +40 -40
  54. package/src/table.js +44 -44
  55. package/src/tablecaption/tablecaptionediting.d.ts +63 -63
  56. package/src/tablecaption/tablecaptionediting.js +122 -122
  57. package/src/tablecaption/tablecaptionui.d.ts +21 -21
  58. package/src/tablecaption/tablecaptionui.js +57 -57
  59. package/src/tablecaption/toggletablecaptioncommand.d.ts +68 -68
  60. package/src/tablecaption/toggletablecaptioncommand.js +105 -104
  61. package/src/tablecaption/utils.d.ts +38 -42
  62. package/src/tablecaption/utils.js +57 -67
  63. package/src/tablecaption.d.ts +24 -24
  64. package/src/tablecaption.js +28 -28
  65. package/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.d.ts +32 -32
  66. package/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js +30 -30
  67. package/src/tablecellproperties/commands/tablecellbordercolorcommand.d.ts +37 -37
  68. package/src/tablecellproperties/commands/tablecellbordercolorcommand.js +44 -44
  69. package/src/tablecellproperties/commands/tablecellborderstylecommand.d.ts +37 -37
  70. package/src/tablecellproperties/commands/tablecellborderstylecommand.js +44 -44
  71. package/src/tablecellproperties/commands/tablecellborderwidthcommand.d.ts +51 -51
  72. package/src/tablecellproperties/commands/tablecellborderwidthcommand.js +64 -64
  73. package/src/tablecellproperties/commands/tablecellheightcommand.d.ts +46 -46
  74. package/src/tablecellproperties/commands/tablecellheightcommand.js +51 -51
  75. package/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.d.ts +32 -32
  76. package/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js +30 -30
  77. package/src/tablecellproperties/commands/tablecellpaddingcommand.d.ts +51 -51
  78. package/src/tablecellproperties/commands/tablecellpaddingcommand.js +64 -64
  79. package/src/tablecellproperties/commands/tablecellpropertycommand.d.ts +62 -62
  80. package/src/tablecellproperties/commands/tablecellpropertycommand.js +92 -92
  81. package/src/tablecellproperties/commands/tablecellverticalalignmentcommand.d.ts +40 -40
  82. package/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js +38 -38
  83. package/src/tablecellproperties/tablecellpropertiesediting.d.ts +43 -43
  84. package/src/tablecellproperties/tablecellpropertiesediting.js +241 -241
  85. package/src/tablecellproperties/tablecellpropertiesui.d.ts +112 -112
  86. package/src/tablecellproperties/tablecellpropertiesui.js +330 -330
  87. package/src/tablecellproperties/ui/tablecellpropertiesview.d.ts +228 -228
  88. package/src/tablecellproperties/ui/tablecellpropertiesview.js +548 -548
  89. package/src/tablecellproperties.d.ts +30 -30
  90. package/src/tablecellproperties.js +34 -34
  91. package/src/tablecellwidth/commands/tablecellwidthcommand.d.ts +46 -46
  92. package/src/tablecellwidth/commands/tablecellwidthcommand.js +51 -51
  93. package/src/tablecellwidth/tablecellwidthediting.d.ts +29 -29
  94. package/src/tablecellwidth/tablecellwidthediting.js +45 -45
  95. package/src/tableclipboard.d.ts +65 -65
  96. package/src/tableclipboard.js +450 -450
  97. package/src/tablecolumnresize/constants.d.ts +20 -20
  98. package/src/tablecolumnresize/constants.js +20 -20
  99. package/src/tablecolumnresize/converters.d.ts +18 -18
  100. package/src/tablecolumnresize/converters.js +46 -46
  101. package/src/tablecolumnresize/tablecolumnresizeediting.d.ts +139 -139
  102. package/src/tablecolumnresize/tablecolumnresizeediting.js +583 -583
  103. package/src/tablecolumnresize/tablewidthscommand.d.ts +38 -38
  104. package/src/tablecolumnresize/tablewidthscommand.js +61 -61
  105. package/src/tablecolumnresize/utils.d.ts +148 -148
  106. package/src/tablecolumnresize/utils.js +358 -358
  107. package/src/tablecolumnresize.d.ts +26 -26
  108. package/src/tablecolumnresize.js +30 -30
  109. package/src/tableconfig.d.ts +343 -343
  110. package/src/tableconfig.js +5 -5
  111. package/src/tableediting.d.ts +98 -98
  112. package/src/tableediting.js +191 -191
  113. package/src/tablekeyboard.d.ts +68 -68
  114. package/src/tablekeyboard.js +279 -279
  115. package/src/tablemouse/mouseeventsobserver.d.ts +62 -62
  116. package/src/tablemouse/mouseeventsobserver.js +35 -35
  117. package/src/tablemouse.d.ts +48 -48
  118. package/src/tablemouse.js +172 -172
  119. package/src/tableproperties/commands/tablealignmentcommand.d.ts +32 -32
  120. package/src/tableproperties/commands/tablealignmentcommand.js +30 -30
  121. package/src/tableproperties/commands/tablebackgroundcolorcommand.d.ts +32 -32
  122. package/src/tableproperties/commands/tablebackgroundcolorcommand.js +30 -30
  123. package/src/tableproperties/commands/tablebordercolorcommand.d.ts +37 -37
  124. package/src/tableproperties/commands/tablebordercolorcommand.js +44 -44
  125. package/src/tableproperties/commands/tableborderstylecommand.d.ts +37 -37
  126. package/src/tableproperties/commands/tableborderstylecommand.js +44 -44
  127. package/src/tableproperties/commands/tableborderwidthcommand.d.ts +51 -51
  128. package/src/tableproperties/commands/tableborderwidthcommand.js +64 -64
  129. package/src/tableproperties/commands/tableheightcommand.d.ts +46 -46
  130. package/src/tableproperties/commands/tableheightcommand.js +54 -54
  131. package/src/tableproperties/commands/tablepropertycommand.d.ts +61 -61
  132. package/src/tableproperties/commands/tablepropertycommand.js +81 -80
  133. package/src/tableproperties/commands/tablewidthcommand.d.ts +46 -46
  134. package/src/tableproperties/commands/tablewidthcommand.js +54 -54
  135. package/src/tableproperties/tablepropertiesediting.d.ts +40 -40
  136. package/src/tableproperties/tablepropertiesediting.js +206 -206
  137. package/src/tableproperties/tablepropertiesui.d.ts +114 -114
  138. package/src/tableproperties/tablepropertiesui.js +321 -321
  139. package/src/tableproperties/ui/tablepropertiesview.d.ts +207 -207
  140. package/src/tableproperties/ui/tablepropertiesview.js +466 -466
  141. package/src/tableproperties.d.ts +30 -30
  142. package/src/tableproperties.js +34 -34
  143. package/src/tableselection.d.ts +107 -107
  144. package/src/tableselection.js +297 -297
  145. package/src/tabletoolbar.d.ts +32 -32
  146. package/src/tabletoolbar.js +57 -57
  147. package/src/tableui.d.ts +53 -53
  148. package/src/tableui.js +309 -309
  149. package/src/tableutils.d.ts +448 -448
  150. package/src/tableutils.js +1055 -1055
  151. package/src/tablewalker.d.ts +362 -362
  152. package/src/tablewalker.js +393 -393
  153. package/src/ui/colorinputview.d.ts +140 -140
  154. package/src/ui/colorinputview.js +271 -271
  155. package/src/ui/formrowview.d.ts +61 -61
  156. package/src/ui/formrowview.js +57 -57
  157. package/src/ui/inserttableview.d.ts +77 -77
  158. package/src/ui/inserttableview.js +169 -169
  159. package/src/utils/common.d.ts +46 -42
  160. package/src/utils/common.js +68 -57
  161. package/src/utils/structure.d.ts +245 -245
  162. package/src/utils/structure.js +426 -426
  163. package/src/utils/table-properties.d.ts +67 -67
  164. package/src/utils/table-properties.js +86 -86
  165. package/src/utils/ui/contextualballoon.d.ts +34 -34
  166. package/src/utils/ui/contextualballoon.js +110 -106
  167. package/src/utils/ui/table-properties.d.ts +195 -195
  168. package/src/utils/ui/table-properties.js +362 -362
  169. package/src/utils/ui/widget.d.ts +20 -16
  170. package/src/utils/ui/widget.js +48 -38
  171. package/build/table.js.map +0 -1
package/src/tableutils.js CHANGED
@@ -1,1055 +1,1055 @@
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
- */
5
- /**
6
- * @module table/tableutils
7
- */
8
- import { CKEditorError } from 'ckeditor5/src/utils';
9
- import { Plugin } from 'ckeditor5/src/core';
10
- import TableWalker from './tablewalker';
11
- import { createEmptyTableCell, updateNumericAttribute } from './utils/common';
12
- import { removeEmptyColumns, removeEmptyRows } from './utils/structure';
13
- import { getTableColumnElements } from './tablecolumnresize/utils';
14
- /**
15
- * The table utilities plugin.
16
- */
17
- export default class TableUtils extends Plugin {
18
- /**
19
- * @inheritDoc
20
- */
21
- static get pluginName() {
22
- return 'TableUtils';
23
- }
24
- /**
25
- * @inheritDoc
26
- */
27
- init() {
28
- this.decorate('insertColumns');
29
- this.decorate('insertRows');
30
- }
31
- /**
32
- * Returns the table cell location as an object with table row and table column indexes.
33
- *
34
- * For instance, in the table below:
35
- *
36
- * 0 1 2 3
37
- * +---+---+---+---+
38
- * 0 | a | b | c |
39
- * + + +---+
40
- * 1 | | | d |
41
- * +---+---+ +---+
42
- * 2 | e | | f |
43
- * +---+---+---+---+
44
- *
45
- * the method will return:
46
- *
47
- * ```ts
48
- * const cellA = table.getNodeByPath( [ 0, 0 ] );
49
- * editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
50
- * // will return { row: 0, column: 0 }
51
- *
52
- * const cellD = table.getNodeByPath( [ 1, 0 ] );
53
- * editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
54
- * // will return { row: 1, column: 3 }
55
- * ```
56
- *
57
- * @returns Returns a `{row, column}` object.
58
- */
59
- getCellLocation(tableCell) {
60
- const tableRow = tableCell.parent;
61
- const table = tableRow.parent;
62
- const rowIndex = table.getChildIndex(tableRow);
63
- const tableWalker = new TableWalker(table, { row: rowIndex });
64
- for (const { cell, row, column } of tableWalker) {
65
- if (cell === tableCell) {
66
- return { row, column };
67
- }
68
- }
69
- // Should be unreachable code.
70
- /* istanbul ignore next -- @preserve */
71
- return undefined;
72
- }
73
- /**
74
- * Creates an empty table with a proper structure. The table needs to be inserted into the model,
75
- * for example, by using the {@link module:engine/model/model~Model#insertContent} function.
76
- *
77
- * ```ts
78
- * model.change( ( writer ) => {
79
- * // Create a table of 2 rows and 7 columns:
80
- * const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
81
- *
82
- * // Insert a table to the model at the best position taking the current selection:
83
- * model.insertContent( table );
84
- * }
85
- * ```
86
- *
87
- * @param writer The model writer.
88
- * @param options.rows The number of rows to create. Default value is 2.
89
- * @param options.columns The number of columns to create. Default value is 2.
90
- * @param options.headingRows The number of heading rows. Default value is 0.
91
- * @param options.headingColumns The number of heading columns. Default value is 0.
92
- * @returns The created table element.
93
- */
94
- createTable(writer, options) {
95
- const table = writer.createElement('table');
96
- const rows = options.rows || 2;
97
- const columns = options.columns || 2;
98
- createEmptyRows(writer, table, 0, rows, columns);
99
- if (options.headingRows) {
100
- updateNumericAttribute('headingRows', Math.min(options.headingRows, rows), table, writer, 0);
101
- }
102
- if (options.headingColumns) {
103
- updateNumericAttribute('headingColumns', Math.min(options.headingColumns, columns), table, writer, 0);
104
- }
105
- return table;
106
- }
107
- /**
108
- * Inserts rows into a table.
109
- *
110
- * ```ts
111
- * editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
112
- * ```
113
- *
114
- * Assuming the table on the left, the above code will transform it to the table on the right:
115
- *
116
- * row index
117
- * 0 +---+---+---+ `at` = 1, +---+---+---+ 0
118
- * | a | b | c | `rows` = 2, | a | b | c |
119
- * 1 + +---+---+ <-- insert here + +---+---+ 1
120
- * | | d | e | | | | |
121
- * 2 + +---+---+ will give: + +---+---+ 2
122
- * | | f | g | | | | |
123
- * 3 +---+---+---+ + +---+---+ 3
124
- * | | d | e |
125
- * + +---+---+ 4
126
- * + + f | g |
127
- * +---+---+---+ 5
128
- *
129
- * @param table The table model element where the rows will be inserted.
130
- * @param options.at The row index at which the rows will be inserted. Default value is 0.
131
- * @param options.rows The number of rows to insert. Default value is 1.
132
- * @param options.copyStructureFromAbove The flag for copying row structure. Note that
133
- * the row structure will not be copied if this option is not provided.
134
- */
135
- insertRows(table, options = {}) {
136
- const model = this.editor.model;
137
- const insertAt = options.at || 0;
138
- const rowsToInsert = options.rows || 1;
139
- const isCopyStructure = options.copyStructureFromAbove !== undefined;
140
- const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt;
141
- const rows = this.getRows(table);
142
- const columns = this.getColumns(table);
143
- if (insertAt > rows) {
144
- /**
145
- * The `options.at` points at a row position that does not exist.
146
- *
147
- * @error tableutils-insertrows-insert-out-of-range
148
- */
149
- throw new CKEditorError('tableutils-insertrows-insert-out-of-range', this, { options });
150
- }
151
- model.change(writer => {
152
- const headingRows = table.getAttribute('headingRows') || 0;
153
- // Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
154
- if (headingRows > insertAt) {
155
- updateNumericAttribute('headingRows', headingRows + rowsToInsert, table, writer, 0);
156
- }
157
- // Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
158
- if (!isCopyStructure && (insertAt === 0 || insertAt === rows)) {
159
- createEmptyRows(writer, table, insertAt, rowsToInsert, columns);
160
- return;
161
- }
162
- // Iterate over all the rows above the inserted rows in order to check for the row-spanned cells.
163
- const walkerEndRow = isCopyStructure ? Math.max(insertAt, copyStructureFrom) : insertAt;
164
- const tableIterator = new TableWalker(table, { endRow: walkerEndRow });
165
- // Store spans of the reference row to reproduce it's structure. This array is column number indexed.
166
- const rowColSpansMap = new Array(columns).fill(1);
167
- for (const { row, column, cellHeight, cellWidth, cell } of tableIterator) {
168
- const lastCellRow = row + cellHeight - 1;
169
- const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
170
- const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;
171
- // If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map.
172
- if (isOverlappingInsertedRow) {
173
- // This cell overlaps the inserted rows so we need to expand it further.
174
- writer.setAttribute('rowspan', cellHeight + rowsToInsert, cell);
175
- // Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells.
176
- rowColSpansMap[column] = -cellWidth;
177
- }
178
- // Store the colspan from reference row.
179
- else if (isCopyStructure && isReferenceRow) {
180
- rowColSpansMap[column] = cellWidth;
181
- }
182
- }
183
- for (let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++) {
184
- const tableRow = writer.createElement('tableRow');
185
- writer.insert(tableRow, table, insertAt);
186
- for (let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++) {
187
- const colspan = rowColSpansMap[cellIndex];
188
- const insertPosition = writer.createPositionAt(tableRow, 'end');
189
- // Insert the empty cell only if this slot is not row-spanned from any other cell.
190
- if (colspan > 0) {
191
- createEmptyTableCell(writer, insertPosition, colspan > 1 ? { colspan } : undefined);
192
- }
193
- // Skip the col-spanned slots, there won't be any cells.
194
- cellIndex += Math.abs(colspan) - 1;
195
- }
196
- }
197
- });
198
- }
199
- /**
200
- * Inserts columns into a table.
201
- *
202
- * ```ts
203
- * editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
204
- * ```
205
- *
206
- * Assuming the table on the left, the above code will transform it to the table on the right:
207
- *
208
- * 0 1 2 3 0 1 2 3 4 5
209
- * +---+---+---+ +---+---+---+---+---+
210
- * | a | b | | a | b |
211
- * + +---+ + +---+
212
- * | | c | | | c |
213
- * +---+---+---+ will give: +---+---+---+---+---+
214
- * | d | e | f | | d | | | e | f |
215
- * +---+ +---+ +---+---+---+ +---+
216
- * | g | | h | | g | | | | h |
217
- * +---+---+---+ +---+---+---+---+---+
218
- * | i | | i |
219
- * +---+---+---+ +---+---+---+---+---+
220
- * ^---- insert here, `at` = 1, `columns` = 2
221
- *
222
- * @param table The table model element where the columns will be inserted.
223
- * @param options.at The column index at which the columns will be inserted. Default value is 0.
224
- * @param options.columns The number of columns to insert. Default value is 1.
225
- */
226
- insertColumns(table, options = {}) {
227
- const model = this.editor.model;
228
- const insertAt = options.at || 0;
229
- const columnsToInsert = options.columns || 1;
230
- model.change(writer => {
231
- const headingColumns = table.getAttribute('headingColumns');
232
- // Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
233
- if (insertAt < headingColumns) {
234
- writer.setAttribute('headingColumns', headingColumns + columnsToInsert, table);
235
- }
236
- const tableColumns = this.getColumns(table);
237
- // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
238
- if (insertAt === 0 || tableColumns === insertAt) {
239
- for (const tableRow of table.getChildren()) {
240
- // Ignore non-row elements inside the table (e.g. caption).
241
- if (!tableRow.is('element', 'tableRow')) {
242
- continue;
243
- }
244
- createCells(columnsToInsert, writer, writer.createPositionAt(tableRow, insertAt ? 'end' : 0));
245
- }
246
- return;
247
- }
248
- const tableWalker = new TableWalker(table, { column: insertAt, includeAllSlots: true });
249
- for (const tableSlot of tableWalker) {
250
- const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;
251
- // When iterating over column the table walker outputs either:
252
- // - cells at given column index (cell "e" from method docs),
253
- // - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeAllSlots: true`),
254
- // - or a cell from the same row which spans over this column (cell "a").
255
- if (cellAnchorColumn < insertAt) {
256
- // If cell is anchored in previous column, it is a cell that spans over an inserted column (cell "a" & "i").
257
- // For such cells expand them by a number of columns inserted.
258
- writer.setAttribute('colspan', cellWidth + columnsToInsert, cell);
259
- // This cell will overlap cells in rows below so skip them (because of `includeAllSlots` option) - (cell "a")
260
- const lastCellRow = cellAnchorRow + cellHeight - 1;
261
- for (let i = row; i <= lastCellRow; i++) {
262
- tableWalker.skipRow(i);
263
- }
264
- }
265
- else {
266
- // It's either cell at this column index or spanned cell by a row-spanned cell from row above.
267
- // In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
268
- createCells(columnsToInsert, writer, tableSlot.getPositionBefore());
269
- }
270
- }
271
- });
272
- }
273
- /**
274
- * Removes rows from the given `table`.
275
- *
276
- * This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
277
- * and table headings values.
278
- *
279
- * ```ts
280
- * editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
281
- * ```
282
- *
283
- * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
284
- *
285
- * row index
286
- * ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
287
- * 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
288
- * │ ├───┼───┤ │ ├───┼───┤
289
- * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
290
- * │ │ ├───┤ will give: ├───┼───┼───┤
291
- * 2 │ │ │ f │ │ h │ i │ j │ 2
292
- * │ │ ├───┤ └───┴───┴───┘
293
- * 3 │ │ │ g │
294
- * ├───┼───┼───┤
295
- * 4 │ h │ i │ j │
296
- * └───┴───┴───┘
297
- *
298
- * @param options.at The row index at which the removing rows will start.
299
- * @param options.rows The number of rows to remove. Default value is 1.
300
- */
301
- removeRows(table, options) {
302
- const model = this.editor.model;
303
- const rowsToRemove = options.rows || 1;
304
- const rowCount = this.getRows(table);
305
- const first = options.at;
306
- const last = first + rowsToRemove - 1;
307
- if (last > rowCount - 1) {
308
- /**
309
- * The `options.at` param must point at existing row and `options.rows` must not exceed the rows in the table.
310
- *
311
- * @error tableutils-removerows-row-index-out-of-range
312
- */
313
- throw new CKEditorError('tableutils-removerows-row-index-out-of-range', this, { table, options });
314
- }
315
- model.change(writer => {
316
- const indexesObject = { first, last };
317
- // Removing rows from the table require that most calculations to be done prior to changing table structure.
318
- // Preparations must be done in the same enqueueChange callback to use the current table structure.
319
- // 1. Preparation - get row-spanned cells that have to be modified after removing rows.
320
- const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow(table, indexesObject);
321
- // 2. Execution
322
- // 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
323
- // This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
324
- if (cellsToMove.size) {
325
- const rowAfterRemovedSection = last + 1;
326
- moveCellsToRow(table, rowAfterRemovedSection, cellsToMove, writer);
327
- }
328
- // 2b. Remove all required rows.
329
- for (let i = last; i >= first; i--) {
330
- writer.remove(table.getChild(i));
331
- }
332
- // 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
333
- for (const { rowspan, cell } of cellsToTrim) {
334
- updateNumericAttribute('rowspan', rowspan, cell, writer);
335
- }
336
- // 2d. Adjust heading rows if removed rows were in a heading section.
337
- updateHeadingRows(table, indexesObject, writer);
338
- // 2e. Remove empty columns (without anchored cells) if there are any.
339
- if (!removeEmptyColumns(table, this)) {
340
- // If there wasn't any empty columns then we still need to check if this wasn't called
341
- // because of cleaning empty rows and we only removed one of them.
342
- removeEmptyRows(table, this);
343
- }
344
- });
345
- }
346
- /**
347
- * Removes columns from the given `table`.
348
- *
349
- * This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
350
- * and table headings values.
351
- *
352
- * ```ts
353
- * editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
354
- * ```
355
- *
356
- * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
357
- *
358
- * 0 1 2 3 4 0 1 2
359
- * ┌───────────────┬───┐ ┌───────┬───┐
360
- * │ a │ b │ │ a │ b │
361
- * │ ├───┤ │ ├───┤
362
- * │ │ c │ │ │ c │
363
- * ├───┬───┬───┬───┼───┤ will give: ├───┬───┼───┤
364
- * │ d │ e │ f │ g │ h │ │ d │ g │ h │
365
- * ├───┼───┼───┤ ├───┤ ├───┤ ├───┤
366
- * │ i │ j │ k │ │ l │ │ i │ │ l │
367
- * ├───┴───┴───┴───┴───┤ ├───┴───┴───┤
368
- * │ m │ │ m │
369
- * └───────────────────┘ └───────────┘
370
- * ^---- remove from here, `at` = 1, `columns` = 2
371
- *
372
- * @param options.at The row index at which the removing columns will start.
373
- * @param options.columns The number of columns to remove.
374
- */
375
- removeColumns(table, options) {
376
- const model = this.editor.model;
377
- const first = options.at;
378
- const columnsToRemove = options.columns || 1;
379
- const last = options.at + columnsToRemove - 1;
380
- model.change(writer => {
381
- adjustHeadingColumns(table, { first, last }, writer);
382
- const tableColumns = getTableColumnElements(table);
383
- for (let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex--) {
384
- for (const { cell, column, cellWidth } of [...new TableWalker(table)]) {
385
- // If colspaned cell overlaps removed column decrease its span.
386
- if (column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex) {
387
- updateNumericAttribute('colspan', cellWidth - 1, cell, writer);
388
- }
389
- else if (column === removedColumnIndex) {
390
- // The cell in removed column has colspan of 1.
391
- writer.remove(cell);
392
- }
393
- }
394
- // If table has `tableColumn` elements, we need to update it manually.
395
- // See https://github.com/ckeditor/ckeditor5/issues/14521#issuecomment-1662102889 for details.
396
- if (tableColumns[removedColumnIndex]) {
397
- // If the removed column is the first one then we need to add its width to the next column.
398
- // Otherwise we add it to the previous column.
399
- const adjacentColumn = removedColumnIndex === 0 ? tableColumns[1] : tableColumns[removedColumnIndex - 1];
400
- const removedColumnWidth = parseFloat(tableColumns[removedColumnIndex].getAttribute('columnWidth'));
401
- const adjacentColumnWidth = parseFloat(adjacentColumn.getAttribute('columnWidth'));
402
- writer.remove(tableColumns[removedColumnIndex]);
403
- // Add the removed column width (in %) to the adjacent column.
404
- writer.setAttribute('columnWidth', removedColumnWidth + adjacentColumnWidth + '%', adjacentColumn);
405
- }
406
- }
407
- // Remove empty rows that could appear after removing columns.
408
- if (!removeEmptyRows(table, this)) {
409
- // If there wasn't any empty rows then we still need to check if this wasn't called
410
- // because of cleaning empty columns and we only removed one of them.
411
- removeEmptyColumns(table, this);
412
- }
413
- });
414
- }
415
- /**
416
- * Divides a table cell vertically into several ones.
417
- *
418
- * The cell will be visually split into more cells by updating colspans of other cells in a column
419
- * and inserting cells (columns) after that cell.
420
- *
421
- * In the table below, if cell "a" is split into 3 cells:
422
- *
423
- * +---+---+---+
424
- * | a | b | c |
425
- * +---+---+---+
426
- * | d | e | f |
427
- * +---+---+---+
428
- *
429
- * it will result in the table below:
430
- *
431
- * +---+---+---+---+---+
432
- * | a | | | b | c |
433
- * +---+---+---+---+---+
434
- * | d | e | f |
435
- * +---+---+---+---+---+
436
- *
437
- * So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
438
- *
439
- * Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
440
- * will be left to the original cell:
441
- *
442
- * +---+---+---+
443
- * | a |
444
- * +---+---+---+
445
- * | b | c | d |
446
- * +---+---+---+
447
- *
448
- * Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
449
- *
450
- * +---+---+---+
451
- * | a | |
452
- * +---+---+---+
453
- * | b | c | d |
454
- * +---+---+---+
455
- */
456
- splitCellVertically(tableCell, numberOfCells = 2) {
457
- const model = this.editor.model;
458
- const tableRow = tableCell.parent;
459
- const table = tableRow.parent;
460
- const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
461
- const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
462
- model.change(writer => {
463
- // First check - the cell spans over multiple rows so before doing anything else just split this cell.
464
- if (colspan > 1) {
465
- // Get spans of new (inserted) cells and span to update of split cell.
466
- const { newCellsSpan, updatedSpan } = breakSpanEvenly(colspan, numberOfCells);
467
- updateNumericAttribute('colspan', updatedSpan, tableCell, writer);
468
- // Each inserted cell will have the same attributes:
469
- const newCellsAttributes = {};
470
- // Do not store default value in the model.
471
- if (newCellsSpan > 1) {
472
- newCellsAttributes.colspan = newCellsSpan;
473
- }
474
- // Copy rowspan of split cell.
475
- if (rowspan > 1) {
476
- newCellsAttributes.rowspan = rowspan;
477
- }
478
- const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
479
- createCells(cellsToInsert, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
480
- }
481
- // Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
482
- if (colspan < numberOfCells) {
483
- const cellsToInsert = numberOfCells - colspan;
484
- // First step: expand cells on the same column as split cell.
485
- const tableMap = [...new TableWalker(table)];
486
- // Get the column index of split cell.
487
- const { column: splitCellColumn } = tableMap.find(({ cell }) => cell === tableCell);
488
- // Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
489
- const cellsToUpdate = tableMap.filter(({ cell, cellWidth, column }) => {
490
- const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
491
- const spansOverColumn = (column < splitCellColumn && column + cellWidth > splitCellColumn);
492
- return isOnSameColumn || spansOverColumn;
493
- });
494
- // Expand cells vertically.
495
- for (const { cell, cellWidth } of cellsToUpdate) {
496
- writer.setAttribute('colspan', cellWidth + cellsToInsert, cell);
497
- }
498
- // Second step: create columns after split cell.
499
- // Each inserted cell will have the same attributes:
500
- const newCellsAttributes = {};
501
- // Do not store default value in the model.
502
- // Copy rowspan of split cell.
503
- if (rowspan > 1) {
504
- newCellsAttributes.rowspan = rowspan;
505
- }
506
- createCells(cellsToInsert, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
507
- const headingColumns = table.getAttribute('headingColumns') || 0;
508
- // Update heading section if split cell is in heading section.
509
- if (headingColumns > splitCellColumn) {
510
- updateNumericAttribute('headingColumns', headingColumns + cellsToInsert, table, writer);
511
- }
512
- }
513
- });
514
- }
515
- /**
516
- * Divides a table cell horizontally into several ones.
517
- *
518
- * The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
519
- * below.
520
- *
521
- * If in the table below cell "b" is split into 3 cells:
522
- *
523
- * +---+---+---+
524
- * | a | b | c |
525
- * +---+---+---+
526
- * | d | e | f |
527
- * +---+---+---+
528
- *
529
- * It will result in the table below:
530
- *
531
- * +---+---+---+
532
- * | a | b | c |
533
- * + +---+ +
534
- * | | | |
535
- * + +---+ +
536
- * | | | |
537
- * +---+---+---+
538
- * | d | e | f |
539
- * +---+---+---+
540
- *
541
- * So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
542
- *
543
- * Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
544
- * will be left to the original cell:
545
- *
546
- * +---+---+---+
547
- * | a | b | c |
548
- * + +---+---+
549
- * | | d | e |
550
- * + +---+---+
551
- * | | f | g |
552
- * + +---+---+
553
- * | | h | i |
554
- * +---+---+---+
555
- *
556
- * Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
557
- *
558
- * +---+---+---+
559
- * | a | b | c |
560
- * + +---+---+
561
- * | | d | e |
562
- * +---+---+---+
563
- * | | f | g |
564
- * +---+---+---+
565
- * | | h | i |
566
- * +---+---+---+
567
- */
568
- splitCellHorizontally(tableCell, numberOfCells = 2) {
569
- const model = this.editor.model;
570
- const tableRow = tableCell.parent;
571
- const table = tableRow.parent;
572
- const splitCellRow = table.getChildIndex(tableRow);
573
- const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
574
- const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
575
- model.change(writer => {
576
- // First check - the cell spans over multiple rows so before doing anything else just split this cell.
577
- if (rowspan > 1) {
578
- // Cache table map before updating table.
579
- const tableMap = [...new TableWalker(table, {
580
- startRow: splitCellRow,
581
- endRow: splitCellRow + rowspan - 1,
582
- includeAllSlots: true
583
- })];
584
- // Get spans of new (inserted) cells and span to update of split cell.
585
- const { newCellsSpan, updatedSpan } = breakSpanEvenly(rowspan, numberOfCells);
586
- updateNumericAttribute('rowspan', updatedSpan, tableCell, writer);
587
- const { column: cellColumn } = tableMap.find(({ cell }) => cell === tableCell);
588
- // Each inserted cell will have the same attributes:
589
- const newCellsAttributes = {};
590
- // Do not store default value in the model.
591
- if (newCellsSpan > 1) {
592
- newCellsAttributes.rowspan = newCellsSpan;
593
- }
594
- // Copy colspan of split cell.
595
- if (colspan > 1) {
596
- newCellsAttributes.colspan = colspan;
597
- }
598
- for (const tableSlot of tableMap) {
599
- const { column, row } = tableSlot;
600
- // As both newly created cells and the split cell might have rowspan,
601
- // the insertion of new cells must go to appropriate rows:
602
- //
603
- // 1. It's a row after split cell + it's height.
604
- const isAfterSplitCell = row >= splitCellRow + updatedSpan;
605
- // 2. Is on the same column.
606
- const isOnSameColumn = column === cellColumn;
607
- // 3. And it's row index is after previous cell height.
608
- const isInEvenlySplitRow = (row + splitCellRow + updatedSpan) % newCellsSpan === 0;
609
- if (isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow) {
610
- createCells(1, writer, tableSlot.getPositionBefore(), newCellsAttributes);
611
- }
612
- }
613
- }
614
- // Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
615
- if (rowspan < numberOfCells) {
616
- // We already split the cell in check one so here we split to the remaining number of cells only.
617
- const cellsToInsert = numberOfCells - rowspan;
618
- // This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
619
- const tableMap = [...new TableWalker(table, { startRow: 0, endRow: splitCellRow })];
620
- // First step: expand cells.
621
- for (const { cell, cellHeight, row } of tableMap) {
622
- // Expand rowspan of cells that are either:
623
- // - on the same row as current cell,
624
- // - or are below split cell row and overlaps that row.
625
- if (cell !== tableCell && row + cellHeight > splitCellRow) {
626
- const rowspanToSet = cellHeight + cellsToInsert;
627
- writer.setAttribute('rowspan', rowspanToSet, cell);
628
- }
629
- }
630
- // Second step: create rows with single cell below split cell.
631
- const newCellsAttributes = {};
632
- // Copy colspan of split cell.
633
- if (colspan > 1) {
634
- newCellsAttributes.colspan = colspan;
635
- }
636
- createEmptyRows(writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes);
637
- // Update heading section if split cell is in heading section.
638
- const headingRows = table.getAttribute('headingRows') || 0;
639
- if (headingRows > splitCellRow) {
640
- updateNumericAttribute('headingRows', headingRows + cellsToInsert, table, writer);
641
- }
642
- }
643
- });
644
- }
645
- /**
646
- * Returns the number of columns for a given table.
647
- *
648
- * ```ts
649
- * editor.plugins.get( 'TableUtils' ).getColumns( table );
650
- * ```
651
- *
652
- * @param table The table to analyze.
653
- */
654
- getColumns(table) {
655
- // Analyze first row only as all the rows should have the same width.
656
- // Using the first row without checking if it's a tableRow because we expect
657
- // that table will have only tableRow model elements at the beginning.
658
- const row = table.getChild(0);
659
- return [...row.getChildren()].reduce((columns, row) => {
660
- const columnWidth = parseInt(row.getAttribute('colspan') || '1');
661
- return columns + columnWidth;
662
- }, 0);
663
- }
664
- /**
665
- * Returns the number of rows for a given table. Any other element present in the table model is omitted.
666
- *
667
- * ```ts
668
- * editor.plugins.get( 'TableUtils' ).getRows( table );
669
- * ```
670
- *
671
- * @param table The table to analyze.
672
- */
673
- getRows(table) {
674
- // Rowspan not included due to #6427.
675
- return Array.from(table.getChildren())
676
- .reduce((rowCount, child) => child.is('element', 'tableRow') ? rowCount + 1 : rowCount, 0);
677
- }
678
- /**
679
- * Creates an instance of the table walker.
680
- *
681
- * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
682
- * It walks row by row and column by column in order to output values defined in the options.
683
- * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
684
- * pass the `includeAllSlots` option.
685
- *
686
- * @internal
687
- * @param table A table over which the walker iterates.
688
- * @param options An object with configuration.
689
- */
690
- createTableWalker(table, options = {}) {
691
- return new TableWalker(table, options);
692
- }
693
- /**
694
- * Returns all model table cells that are fully selected (from the outside)
695
- * within the provided model selection's ranges.
696
- *
697
- * To obtain the cells selected from the inside, use
698
- * {@link #getTableCellsContainingSelection}.
699
- */
700
- getSelectedTableCells(selection) {
701
- const cells = [];
702
- for (const range of this.sortRanges(selection.getRanges())) {
703
- const element = range.getContainedElement();
704
- if (element && element.is('element', 'tableCell')) {
705
- cells.push(element);
706
- }
707
- }
708
- return cells;
709
- }
710
- /**
711
- * Returns all model table cells that the provided model selection's ranges
712
- * {@link module:engine/model/range~Range#start} inside.
713
- *
714
- * To obtain the cells selected from the outside, use
715
- * {@link #getSelectedTableCells}.
716
- */
717
- getTableCellsContainingSelection(selection) {
718
- const cells = [];
719
- for (const range of selection.getRanges()) {
720
- const cellWithSelection = range.start.findAncestor('tableCell');
721
- if (cellWithSelection) {
722
- cells.push(cellWithSelection);
723
- }
724
- }
725
- return cells;
726
- }
727
- /**
728
- * Returns all model table cells that are either completely selected
729
- * by selection ranges or host selection range
730
- * {@link module:engine/model/range~Range#start start positions} inside them.
731
- *
732
- * Combines {@link #getTableCellsContainingSelection} and
733
- * {@link #getSelectedTableCells}.
734
- */
735
- getSelectionAffectedTableCells(selection) {
736
- const selectedCells = this.getSelectedTableCells(selection);
737
- if (selectedCells.length) {
738
- return selectedCells;
739
- }
740
- return this.getTableCellsContainingSelection(selection);
741
- }
742
- /**
743
- * Returns an object with the `first` and `last` row index contained in the given `tableCells`.
744
- *
745
- * ```ts
746
- * const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
747
- *
748
- * const { first, last } = getRowIndexes( selectedTableCells );
749
- *
750
- * console.log( `Selected rows: ${ first } to ${ last }` );
751
- * ```
752
- *
753
- * @returns Returns an object with the `first` and `last` table row indexes.
754
- */
755
- getRowIndexes(tableCells) {
756
- const indexes = tableCells.map(cell => cell.parent.index);
757
- return this._getFirstLastIndexesObject(indexes);
758
- }
759
- /**
760
- * Returns an object with the `first` and `last` column index contained in the given `tableCells`.
761
- *
762
- * ```ts
763
- * const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
764
- *
765
- * const { first, last } = getColumnIndexes( selectedTableCells );
766
- *
767
- * console.log( `Selected columns: ${ first } to ${ last }` );
768
- * ```
769
- *
770
- * @returns Returns an object with the `first` and `last` table column indexes.
771
- */
772
- getColumnIndexes(tableCells) {
773
- const table = tableCells[0].findAncestor('table');
774
- const tableMap = [...new TableWalker(table)];
775
- const indexes = tableMap
776
- .filter(entry => tableCells.includes(entry.cell))
777
- .map(entry => entry.column);
778
- return this._getFirstLastIndexesObject(indexes);
779
- }
780
- /**
781
- * Checks if the selection contains cells that do not exceed rectangular selection.
782
- *
783
- * In a table below:
784
- *
785
- * ┌───┬───┬───┬───┐
786
- * │ a │ b │ c │ d │
787
- * ├───┴───┼───┤ │
788
- * │ e │ f │ │
789
- * │ ├───┼───┤
790
- * │ │ g │ h │
791
- * └───────┴───┴───┘
792
- *
793
- * Valid selections are these which create a solid rectangle (without gaps), such as:
794
- * - a, b (two horizontal cells)
795
- * - c, f (two vertical cells)
796
- * - a, b, e (cell "e" spans over four cells)
797
- * - c, d, f (cell d spans over a cell in the row below)
798
- *
799
- * While an invalid selection would be:
800
- * - a, c (the unselected cell "b" creates a gap)
801
- * - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
802
- */
803
- isSelectionRectangular(selectedTableCells) {
804
- if (selectedTableCells.length < 2 || !this._areCellInTheSameTableSection(selectedTableCells)) {
805
- return false;
806
- }
807
- // A valid selection is a fully occupied rectangle composed of table cells.
808
- // Below we will calculate the area of a selected table cells and the area of valid selection.
809
- // The area of a valid selection is defined by top-left and bottom-right cells.
810
- const rows = new Set();
811
- const columns = new Set();
812
- let areaOfSelectedCells = 0;
813
- for (const tableCell of selectedTableCells) {
814
- const { row, column } = this.getCellLocation(tableCell);
815
- const rowspan = parseInt(tableCell.getAttribute('rowspan')) || 1;
816
- const colspan = parseInt(tableCell.getAttribute('colspan')) || 1;
817
- // Record row & column indexes of current cell.
818
- rows.add(row);
819
- columns.add(column);
820
- // For cells that spans over multiple rows add also the last row that this cell spans over.
821
- if (rowspan > 1) {
822
- rows.add(row + rowspan - 1);
823
- }
824
- // For cells that spans over multiple columns add also the last column that this cell spans over.
825
- if (colspan > 1) {
826
- columns.add(column + colspan - 1);
827
- }
828
- areaOfSelectedCells += (rowspan * colspan);
829
- }
830
- // We can only merge table cells that are in adjacent rows...
831
- const areaOfValidSelection = getBiggestRectangleArea(rows, columns);
832
- return areaOfValidSelection == areaOfSelectedCells;
833
- }
834
- /**
835
- * Returns array of sorted ranges.
836
- */
837
- sortRanges(ranges) {
838
- return Array.from(ranges).sort(compareRangeOrder);
839
- }
840
- /**
841
- * Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
842
- */
843
- _getFirstLastIndexesObject(indexes) {
844
- const allIndexesSorted = indexes.sort((indexA, indexB) => indexA - indexB);
845
- const first = allIndexesSorted[0];
846
- const last = allIndexesSorted[allIndexesSorted.length - 1];
847
- return { first, last };
848
- }
849
- /**
850
- * Checks if the selection does not mix a header (column or row) with other cells.
851
- *
852
- * For instance, in the table below valid selections consist of cells with the same letter only.
853
- * So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
854
- *
855
- * header columns
856
- * ↓ ↓
857
- * ┌───┬───┬───┬───┐
858
- * │ a │ a │ b │ b │ ← header row
859
- * ├───┼───┼───┼───┤
860
- * │ c │ c │ d │ d │
861
- * ├───┼───┼───┼───┤
862
- * │ c │ c │ d │ d │
863
- * └───┴───┴───┴───┘
864
- */
865
- _areCellInTheSameTableSection(tableCells) {
866
- const table = tableCells[0].findAncestor('table');
867
- const rowIndexes = this.getRowIndexes(tableCells);
868
- const headingRows = parseInt(table.getAttribute('headingRows')) || 0;
869
- // Calculating row indexes is a bit cheaper so if this check fails we can't merge.
870
- if (!this._areIndexesInSameSection(rowIndexes, headingRows)) {
871
- return false;
872
- }
873
- const columnIndexes = this.getColumnIndexes(tableCells);
874
- const headingColumns = parseInt(table.getAttribute('headingColumns')) || 0;
875
- // Similarly cells must be in same column section.
876
- return this._areIndexesInSameSection(columnIndexes, headingColumns);
877
- }
878
- /**
879
- * Unified check if table rows/columns indexes are in the same heading/body section.
880
- */
881
- _areIndexesInSameSection({ first, last }, headingSectionSize) {
882
- const firstCellIsInHeading = first < headingSectionSize;
883
- const lastCellIsInHeading = last < headingSectionSize;
884
- return firstCellIsInHeading === lastCellIsInHeading;
885
- }
886
- }
887
- /**
888
- * Creates empty rows at the given index in an existing table.
889
- *
890
- * @param insertAt The row index of row insertion.
891
- * @param rows The number of rows to create.
892
- * @param tableCellToInsert The number of cells to insert in each row.
893
- */
894
- function createEmptyRows(writer, table, insertAt, rows, tableCellToInsert, attributes = {}) {
895
- for (let i = 0; i < rows; i++) {
896
- const tableRow = writer.createElement('tableRow');
897
- writer.insert(tableRow, table, insertAt);
898
- createCells(tableCellToInsert, writer, writer.createPositionAt(tableRow, 'end'), attributes);
899
- }
900
- }
901
- /**
902
- * Creates cells at a given position.
903
- *
904
- * @param cells The number of cells to create
905
- */
906
- function createCells(cells, writer, insertPosition, attributes = {}) {
907
- for (let i = 0; i < cells; i++) {
908
- createEmptyTableCell(writer, insertPosition, attributes);
909
- }
910
- }
911
- /**
912
- * Evenly distributes the span of a cell to a number of provided cells.
913
- * The resulting spans will always be integer values.
914
- *
915
- * For instance breaking a span of 7 into 3 cells will return:
916
- *
917
- * ```ts
918
- * { newCellsSpan: 2, updatedSpan: 3 }
919
- * ```
920
- *
921
- * as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
922
- *
923
- * @param span The span value do break.
924
- * @param numberOfCells The number of resulting spans.
925
- */
926
- function breakSpanEvenly(span, numberOfCells) {
927
- if (span < numberOfCells) {
928
- return { newCellsSpan: 1, updatedSpan: 1 };
929
- }
930
- const newCellsSpan = Math.floor(span / numberOfCells);
931
- const updatedSpan = (span - newCellsSpan * numberOfCells) + newCellsSpan;
932
- return { newCellsSpan, updatedSpan };
933
- }
934
- /**
935
- * Updates heading columns attribute if removing a row from head section.
936
- */
937
- function adjustHeadingColumns(table, removedColumnIndexes, writer) {
938
- const headingColumns = table.getAttribute('headingColumns') || 0;
939
- if (headingColumns && removedColumnIndexes.first < headingColumns) {
940
- const headingsRemoved = Math.min(headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last) -
941
- removedColumnIndexes.first + 1;
942
- writer.setAttribute('headingColumns', headingColumns - headingsRemoved, table);
943
- }
944
- }
945
- /**
946
- * Calculates a new heading rows value for removing rows from heading section.
947
- */
948
- function updateHeadingRows(table, { first, last }, writer) {
949
- const headingRows = table.getAttribute('headingRows') || 0;
950
- if (first < headingRows) {
951
- const newRows = last < headingRows ? headingRows - (last - first + 1) : first;
952
- updateNumericAttribute('headingRows', newRows, table, writer, 0);
953
- }
954
- }
955
- /**
956
- * Finds cells that will be:
957
- * - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
958
- * - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
959
- *
960
- * Sample table with overlapping & sticking out cells:
961
- *
962
- * +----+----+----+----+----+
963
- * | 00 | 01 | 02 | 03 | 04 |
964
- * +----+ + + + +
965
- * | 10 | | | | |
966
- * +----+----+ + + +
967
- * | 20 | 21 | | | | <-- removed row
968
- * + + +----+ + +
969
- * | | | 32 | | | <-- removed row
970
- * +----+ + +----+ +
971
- * | 40 | | | 43 | |
972
- * +----+----+----+----+----+
973
- *
974
- * In a table above:
975
- * - cells to trim: '02', '03' & '04'.
976
- * - cells to move: '21' & '32'.
977
- */
978
- function getCellsToMoveAndTrimOnRemoveRow(table, { first, last }) {
979
- const cellsToMove = new Map();
980
- const cellsToTrim = [];
981
- for (const { row, column, cellHeight, cell } of new TableWalker(table, { endRow: last })) {
982
- const lastRowOfCell = row + cellHeight - 1;
983
- const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
984
- if (isCellStickingOutFromRemovedRows) {
985
- const rowspanInRemovedSection = last - row + 1;
986
- const rowSpanToSet = cellHeight - rowspanInRemovedSection;
987
- cellsToMove.set(column, {
988
- cell,
989
- rowspan: rowSpanToSet
990
- });
991
- }
992
- const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
993
- if (isCellOverlappingRemovedRows) {
994
- let rowspanAdjustment;
995
- // Cell fully covers removed section - trim it by removed rows count.
996
- if (lastRowOfCell >= last) {
997
- rowspanAdjustment = last - first + 1;
998
- }
999
- // Cell partially overlaps removed section - calculate cell's span that is in removed section.
1000
- else {
1001
- rowspanAdjustment = lastRowOfCell - first + 1;
1002
- }
1003
- cellsToTrim.push({
1004
- cell,
1005
- rowspan: cellHeight - rowspanAdjustment
1006
- });
1007
- }
1008
- }
1009
- return { cellsToMove, cellsToTrim };
1010
- }
1011
- function moveCellsToRow(table, targetRowIndex, cellsToMove, writer) {
1012
- const tableWalker = new TableWalker(table, {
1013
- includeAllSlots: true,
1014
- row: targetRowIndex
1015
- });
1016
- const tableRowMap = [...tableWalker];
1017
- const row = table.getChild(targetRowIndex);
1018
- let previousCell;
1019
- for (const { column, cell, isAnchor } of tableRowMap) {
1020
- if (cellsToMove.has(column)) {
1021
- const { cell: cellToMove, rowspan } = cellsToMove.get(column);
1022
- const targetPosition = previousCell ?
1023
- writer.createPositionAfter(previousCell) :
1024
- writer.createPositionAt(row, 0);
1025
- writer.move(writer.createRangeOn(cellToMove), targetPosition);
1026
- updateNumericAttribute('rowspan', rowspan, cellToMove, writer);
1027
- previousCell = cellToMove;
1028
- }
1029
- else if (isAnchor) {
1030
- // If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
1031
- previousCell = cell;
1032
- }
1033
- }
1034
- }
1035
- function compareRangeOrder(rangeA, rangeB) {
1036
- // Since table cell ranges are disjoint, it's enough to check their start positions.
1037
- const posA = rangeA.start;
1038
- const posB = rangeB.start;
1039
- // Checking for equal position (returning 0) is not needed because this would be either:
1040
- // a. Intersecting range (not allowed by model)
1041
- // b. Collapsed range on the same position (allowed by model but should not happen).
1042
- return posA.isBefore(posB) ? -1 : 1;
1043
- }
1044
- /**
1045
- * Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
1046
- */
1047
- function getBiggestRectangleArea(rows, columns) {
1048
- const rowsIndexes = Array.from(rows.values());
1049
- const columnIndexes = Array.from(columns.values());
1050
- const lastRow = Math.max(...rowsIndexes);
1051
- const firstRow = Math.min(...rowsIndexes);
1052
- const lastColumn = Math.max(...columnIndexes);
1053
- const firstColumn = Math.min(...columnIndexes);
1054
- return (lastRow - firstRow + 1) * (lastColumn - firstColumn + 1);
1055
- }
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
+ */
5
+ /**
6
+ * @module table/tableutils
7
+ */
8
+ import { CKEditorError } from 'ckeditor5/src/utils';
9
+ import { Plugin } from 'ckeditor5/src/core';
10
+ import TableWalker from './tablewalker';
11
+ import { createEmptyTableCell, updateNumericAttribute } from './utils/common';
12
+ import { removeEmptyColumns, removeEmptyRows } from './utils/structure';
13
+ import { getTableColumnElements } from './tablecolumnresize/utils';
14
+ /**
15
+ * The table utilities plugin.
16
+ */
17
+ export default class TableUtils extends Plugin {
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ static get pluginName() {
22
+ return 'TableUtils';
23
+ }
24
+ /**
25
+ * @inheritDoc
26
+ */
27
+ init() {
28
+ this.decorate('insertColumns');
29
+ this.decorate('insertRows');
30
+ }
31
+ /**
32
+ * Returns the table cell location as an object with table row and table column indexes.
33
+ *
34
+ * For instance, in the table below:
35
+ *
36
+ * 0 1 2 3
37
+ * +---+---+---+---+
38
+ * 0 | a | b | c |
39
+ * + + +---+
40
+ * 1 | | | d |
41
+ * +---+---+ +---+
42
+ * 2 | e | | f |
43
+ * +---+---+---+---+
44
+ *
45
+ * the method will return:
46
+ *
47
+ * ```ts
48
+ * const cellA = table.getNodeByPath( [ 0, 0 ] );
49
+ * editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
50
+ * // will return { row: 0, column: 0 }
51
+ *
52
+ * const cellD = table.getNodeByPath( [ 1, 0 ] );
53
+ * editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
54
+ * // will return { row: 1, column: 3 }
55
+ * ```
56
+ *
57
+ * @returns Returns a `{row, column}` object.
58
+ */
59
+ getCellLocation(tableCell) {
60
+ const tableRow = tableCell.parent;
61
+ const table = tableRow.parent;
62
+ const rowIndex = table.getChildIndex(tableRow);
63
+ const tableWalker = new TableWalker(table, { row: rowIndex });
64
+ for (const { cell, row, column } of tableWalker) {
65
+ if (cell === tableCell) {
66
+ return { row, column };
67
+ }
68
+ }
69
+ // Should be unreachable code.
70
+ /* istanbul ignore next -- @preserve */
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Creates an empty table with a proper structure. The table needs to be inserted into the model,
75
+ * for example, by using the {@link module:engine/model/model~Model#insertContent} function.
76
+ *
77
+ * ```ts
78
+ * model.change( ( writer ) => {
79
+ * // Create a table of 2 rows and 7 columns:
80
+ * const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
81
+ *
82
+ * // Insert a table to the model at the best position taking the current selection:
83
+ * model.insertContent( table );
84
+ * }
85
+ * ```
86
+ *
87
+ * @param writer The model writer.
88
+ * @param options.rows The number of rows to create. Default value is 2.
89
+ * @param options.columns The number of columns to create. Default value is 2.
90
+ * @param options.headingRows The number of heading rows. Default value is 0.
91
+ * @param options.headingColumns The number of heading columns. Default value is 0.
92
+ * @returns The created table element.
93
+ */
94
+ createTable(writer, options) {
95
+ const table = writer.createElement('table');
96
+ const rows = options.rows || 2;
97
+ const columns = options.columns || 2;
98
+ createEmptyRows(writer, table, 0, rows, columns);
99
+ if (options.headingRows) {
100
+ updateNumericAttribute('headingRows', Math.min(options.headingRows, rows), table, writer, 0);
101
+ }
102
+ if (options.headingColumns) {
103
+ updateNumericAttribute('headingColumns', Math.min(options.headingColumns, columns), table, writer, 0);
104
+ }
105
+ return table;
106
+ }
107
+ /**
108
+ * Inserts rows into a table.
109
+ *
110
+ * ```ts
111
+ * editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
112
+ * ```
113
+ *
114
+ * Assuming the table on the left, the above code will transform it to the table on the right:
115
+ *
116
+ * row index
117
+ * 0 +---+---+---+ `at` = 1, +---+---+---+ 0
118
+ * | a | b | c | `rows` = 2, | a | b | c |
119
+ * 1 + +---+---+ <-- insert here + +---+---+ 1
120
+ * | | d | e | | | | |
121
+ * 2 + +---+---+ will give: + +---+---+ 2
122
+ * | | f | g | | | | |
123
+ * 3 +---+---+---+ + +---+---+ 3
124
+ * | | d | e |
125
+ * + +---+---+ 4
126
+ * + + f | g |
127
+ * +---+---+---+ 5
128
+ *
129
+ * @param table The table model element where the rows will be inserted.
130
+ * @param options.at The row index at which the rows will be inserted. Default value is 0.
131
+ * @param options.rows The number of rows to insert. Default value is 1.
132
+ * @param options.copyStructureFromAbove The flag for copying row structure. Note that
133
+ * the row structure will not be copied if this option is not provided.
134
+ */
135
+ insertRows(table, options = {}) {
136
+ const model = this.editor.model;
137
+ const insertAt = options.at || 0;
138
+ const rowsToInsert = options.rows || 1;
139
+ const isCopyStructure = options.copyStructureFromAbove !== undefined;
140
+ const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt;
141
+ const rows = this.getRows(table);
142
+ const columns = this.getColumns(table);
143
+ if (insertAt > rows) {
144
+ /**
145
+ * The `options.at` points at a row position that does not exist.
146
+ *
147
+ * @error tableutils-insertrows-insert-out-of-range
148
+ */
149
+ throw new CKEditorError('tableutils-insertrows-insert-out-of-range', this, { options });
150
+ }
151
+ model.change(writer => {
152
+ const headingRows = table.getAttribute('headingRows') || 0;
153
+ // Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
154
+ if (headingRows > insertAt) {
155
+ updateNumericAttribute('headingRows', headingRows + rowsToInsert, table, writer, 0);
156
+ }
157
+ // Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
158
+ if (!isCopyStructure && (insertAt === 0 || insertAt === rows)) {
159
+ createEmptyRows(writer, table, insertAt, rowsToInsert, columns);
160
+ return;
161
+ }
162
+ // Iterate over all the rows above the inserted rows in order to check for the row-spanned cells.
163
+ const walkerEndRow = isCopyStructure ? Math.max(insertAt, copyStructureFrom) : insertAt;
164
+ const tableIterator = new TableWalker(table, { endRow: walkerEndRow });
165
+ // Store spans of the reference row to reproduce it's structure. This array is column number indexed.
166
+ const rowColSpansMap = new Array(columns).fill(1);
167
+ for (const { row, column, cellHeight, cellWidth, cell } of tableIterator) {
168
+ const lastCellRow = row + cellHeight - 1;
169
+ const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
170
+ const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;
171
+ // If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map.
172
+ if (isOverlappingInsertedRow) {
173
+ // This cell overlaps the inserted rows so we need to expand it further.
174
+ writer.setAttribute('rowspan', cellHeight + rowsToInsert, cell);
175
+ // Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells.
176
+ rowColSpansMap[column] = -cellWidth;
177
+ }
178
+ // Store the colspan from reference row.
179
+ else if (isCopyStructure && isReferenceRow) {
180
+ rowColSpansMap[column] = cellWidth;
181
+ }
182
+ }
183
+ for (let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++) {
184
+ const tableRow = writer.createElement('tableRow');
185
+ writer.insert(tableRow, table, insertAt);
186
+ for (let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++) {
187
+ const colspan = rowColSpansMap[cellIndex];
188
+ const insertPosition = writer.createPositionAt(tableRow, 'end');
189
+ // Insert the empty cell only if this slot is not row-spanned from any other cell.
190
+ if (colspan > 0) {
191
+ createEmptyTableCell(writer, insertPosition, colspan > 1 ? { colspan } : undefined);
192
+ }
193
+ // Skip the col-spanned slots, there won't be any cells.
194
+ cellIndex += Math.abs(colspan) - 1;
195
+ }
196
+ }
197
+ });
198
+ }
199
+ /**
200
+ * Inserts columns into a table.
201
+ *
202
+ * ```ts
203
+ * editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
204
+ * ```
205
+ *
206
+ * Assuming the table on the left, the above code will transform it to the table on the right:
207
+ *
208
+ * 0 1 2 3 0 1 2 3 4 5
209
+ * +---+---+---+ +---+---+---+---+---+
210
+ * | a | b | | a | b |
211
+ * + +---+ + +---+
212
+ * | | c | | | c |
213
+ * +---+---+---+ will give: +---+---+---+---+---+
214
+ * | d | e | f | | d | | | e | f |
215
+ * +---+ +---+ +---+---+---+ +---+
216
+ * | g | | h | | g | | | | h |
217
+ * +---+---+---+ +---+---+---+---+---+
218
+ * | i | | i |
219
+ * +---+---+---+ +---+---+---+---+---+
220
+ * ^---- insert here, `at` = 1, `columns` = 2
221
+ *
222
+ * @param table The table model element where the columns will be inserted.
223
+ * @param options.at The column index at which the columns will be inserted. Default value is 0.
224
+ * @param options.columns The number of columns to insert. Default value is 1.
225
+ */
226
+ insertColumns(table, options = {}) {
227
+ const model = this.editor.model;
228
+ const insertAt = options.at || 0;
229
+ const columnsToInsert = options.columns || 1;
230
+ model.change(writer => {
231
+ const headingColumns = table.getAttribute('headingColumns');
232
+ // Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
233
+ if (insertAt < headingColumns) {
234
+ writer.setAttribute('headingColumns', headingColumns + columnsToInsert, table);
235
+ }
236
+ const tableColumns = this.getColumns(table);
237
+ // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
238
+ if (insertAt === 0 || tableColumns === insertAt) {
239
+ for (const tableRow of table.getChildren()) {
240
+ // Ignore non-row elements inside the table (e.g. caption).
241
+ if (!tableRow.is('element', 'tableRow')) {
242
+ continue;
243
+ }
244
+ createCells(columnsToInsert, writer, writer.createPositionAt(tableRow, insertAt ? 'end' : 0));
245
+ }
246
+ return;
247
+ }
248
+ const tableWalker = new TableWalker(table, { column: insertAt, includeAllSlots: true });
249
+ for (const tableSlot of tableWalker) {
250
+ const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;
251
+ // When iterating over column the table walker outputs either:
252
+ // - cells at given column index (cell "e" from method docs),
253
+ // - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeAllSlots: true`),
254
+ // - or a cell from the same row which spans over this column (cell "a").
255
+ if (cellAnchorColumn < insertAt) {
256
+ // If cell is anchored in previous column, it is a cell that spans over an inserted column (cell "a" & "i").
257
+ // For such cells expand them by a number of columns inserted.
258
+ writer.setAttribute('colspan', cellWidth + columnsToInsert, cell);
259
+ // This cell will overlap cells in rows below so skip them (because of `includeAllSlots` option) - (cell "a")
260
+ const lastCellRow = cellAnchorRow + cellHeight - 1;
261
+ for (let i = row; i <= lastCellRow; i++) {
262
+ tableWalker.skipRow(i);
263
+ }
264
+ }
265
+ else {
266
+ // It's either cell at this column index or spanned cell by a row-spanned cell from row above.
267
+ // In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
268
+ createCells(columnsToInsert, writer, tableSlot.getPositionBefore());
269
+ }
270
+ }
271
+ });
272
+ }
273
+ /**
274
+ * Removes rows from the given `table`.
275
+ *
276
+ * This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
277
+ * and table headings values.
278
+ *
279
+ * ```ts
280
+ * editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
281
+ * ```
282
+ *
283
+ * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
284
+ *
285
+ * row index
286
+ * ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
287
+ * 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
288
+ * │ ├───┼───┤ │ ├───┼───┤
289
+ * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
290
+ * │ │ ├───┤ will give: ├───┼───┼───┤
291
+ * 2 │ │ │ f │ │ h │ i │ j │ 2
292
+ * │ │ ├───┤ └───┴───┴───┘
293
+ * 3 │ │ │ g │
294
+ * ├───┼───┼───┤
295
+ * 4 │ h │ i │ j │
296
+ * └───┴───┴───┘
297
+ *
298
+ * @param options.at The row index at which the removing rows will start.
299
+ * @param options.rows The number of rows to remove. Default value is 1.
300
+ */
301
+ removeRows(table, options) {
302
+ const model = this.editor.model;
303
+ const rowsToRemove = options.rows || 1;
304
+ const rowCount = this.getRows(table);
305
+ const first = options.at;
306
+ const last = first + rowsToRemove - 1;
307
+ if (last > rowCount - 1) {
308
+ /**
309
+ * The `options.at` param must point at existing row and `options.rows` must not exceed the rows in the table.
310
+ *
311
+ * @error tableutils-removerows-row-index-out-of-range
312
+ */
313
+ throw new CKEditorError('tableutils-removerows-row-index-out-of-range', this, { table, options });
314
+ }
315
+ model.change(writer => {
316
+ const indexesObject = { first, last };
317
+ // Removing rows from the table require that most calculations to be done prior to changing table structure.
318
+ // Preparations must be done in the same enqueueChange callback to use the current table structure.
319
+ // 1. Preparation - get row-spanned cells that have to be modified after removing rows.
320
+ const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow(table, indexesObject);
321
+ // 2. Execution
322
+ // 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
323
+ // This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
324
+ if (cellsToMove.size) {
325
+ const rowAfterRemovedSection = last + 1;
326
+ moveCellsToRow(table, rowAfterRemovedSection, cellsToMove, writer);
327
+ }
328
+ // 2b. Remove all required rows.
329
+ for (let i = last; i >= first; i--) {
330
+ writer.remove(table.getChild(i));
331
+ }
332
+ // 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
333
+ for (const { rowspan, cell } of cellsToTrim) {
334
+ updateNumericAttribute('rowspan', rowspan, cell, writer);
335
+ }
336
+ // 2d. Adjust heading rows if removed rows were in a heading section.
337
+ updateHeadingRows(table, indexesObject, writer);
338
+ // 2e. Remove empty columns (without anchored cells) if there are any.
339
+ if (!removeEmptyColumns(table, this)) {
340
+ // If there wasn't any empty columns then we still need to check if this wasn't called
341
+ // because of cleaning empty rows and we only removed one of them.
342
+ removeEmptyRows(table, this);
343
+ }
344
+ });
345
+ }
346
+ /**
347
+ * Removes columns from the given `table`.
348
+ *
349
+ * This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
350
+ * and table headings values.
351
+ *
352
+ * ```ts
353
+ * editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
354
+ * ```
355
+ *
356
+ * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
357
+ *
358
+ * 0 1 2 3 4 0 1 2
359
+ * ┌───────────────┬───┐ ┌───────┬───┐
360
+ * │ a │ b │ │ a │ b │
361
+ * │ ├───┤ │ ├───┤
362
+ * │ │ c │ │ │ c │
363
+ * ├───┬───┬───┬───┼───┤ will give: ├───┬───┼───┤
364
+ * │ d │ e │ f │ g │ h │ │ d │ g │ h │
365
+ * ├───┼───┼───┤ ├───┤ ├───┤ ├───┤
366
+ * │ i │ j │ k │ │ l │ │ i │ │ l │
367
+ * ├───┴───┴───┴───┴───┤ ├───┴───┴───┤
368
+ * │ m │ │ m │
369
+ * └───────────────────┘ └───────────┘
370
+ * ^---- remove from here, `at` = 1, `columns` = 2
371
+ *
372
+ * @param options.at The row index at which the removing columns will start.
373
+ * @param options.columns The number of columns to remove.
374
+ */
375
+ removeColumns(table, options) {
376
+ const model = this.editor.model;
377
+ const first = options.at;
378
+ const columnsToRemove = options.columns || 1;
379
+ const last = options.at + columnsToRemove - 1;
380
+ model.change(writer => {
381
+ adjustHeadingColumns(table, { first, last }, writer);
382
+ const tableColumns = getTableColumnElements(table);
383
+ for (let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex--) {
384
+ for (const { cell, column, cellWidth } of [...new TableWalker(table)]) {
385
+ // If colspaned cell overlaps removed column decrease its span.
386
+ if (column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex) {
387
+ updateNumericAttribute('colspan', cellWidth - 1, cell, writer);
388
+ }
389
+ else if (column === removedColumnIndex) {
390
+ // The cell in removed column has colspan of 1.
391
+ writer.remove(cell);
392
+ }
393
+ }
394
+ // If table has `tableColumn` elements, we need to update it manually.
395
+ // See https://github.com/ckeditor/ckeditor5/issues/14521#issuecomment-1662102889 for details.
396
+ if (tableColumns[removedColumnIndex]) {
397
+ // If the removed column is the first one then we need to add its width to the next column.
398
+ // Otherwise we add it to the previous column.
399
+ const adjacentColumn = removedColumnIndex === 0 ? tableColumns[1] : tableColumns[removedColumnIndex - 1];
400
+ const removedColumnWidth = parseFloat(tableColumns[removedColumnIndex].getAttribute('columnWidth'));
401
+ const adjacentColumnWidth = parseFloat(adjacentColumn.getAttribute('columnWidth'));
402
+ writer.remove(tableColumns[removedColumnIndex]);
403
+ // Add the removed column width (in %) to the adjacent column.
404
+ writer.setAttribute('columnWidth', removedColumnWidth + adjacentColumnWidth + '%', adjacentColumn);
405
+ }
406
+ }
407
+ // Remove empty rows that could appear after removing columns.
408
+ if (!removeEmptyRows(table, this)) {
409
+ // If there wasn't any empty rows then we still need to check if this wasn't called
410
+ // because of cleaning empty columns and we only removed one of them.
411
+ removeEmptyColumns(table, this);
412
+ }
413
+ });
414
+ }
415
+ /**
416
+ * Divides a table cell vertically into several ones.
417
+ *
418
+ * The cell will be visually split into more cells by updating colspans of other cells in a column
419
+ * and inserting cells (columns) after that cell.
420
+ *
421
+ * In the table below, if cell "a" is split into 3 cells:
422
+ *
423
+ * +---+---+---+
424
+ * | a | b | c |
425
+ * +---+---+---+
426
+ * | d | e | f |
427
+ * +---+---+---+
428
+ *
429
+ * it will result in the table below:
430
+ *
431
+ * +---+---+---+---+---+
432
+ * | a | | | b | c |
433
+ * +---+---+---+---+---+
434
+ * | d | e | f |
435
+ * +---+---+---+---+---+
436
+ *
437
+ * So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
438
+ *
439
+ * Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
440
+ * will be left to the original cell:
441
+ *
442
+ * +---+---+---+
443
+ * | a |
444
+ * +---+---+---+
445
+ * | b | c | d |
446
+ * +---+---+---+
447
+ *
448
+ * Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
449
+ *
450
+ * +---+---+---+
451
+ * | a | |
452
+ * +---+---+---+
453
+ * | b | c | d |
454
+ * +---+---+---+
455
+ */
456
+ splitCellVertically(tableCell, numberOfCells = 2) {
457
+ const model = this.editor.model;
458
+ const tableRow = tableCell.parent;
459
+ const table = tableRow.parent;
460
+ const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
461
+ const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
462
+ model.change(writer => {
463
+ // First check - the cell spans over multiple rows so before doing anything else just split this cell.
464
+ if (colspan > 1) {
465
+ // Get spans of new (inserted) cells and span to update of split cell.
466
+ const { newCellsSpan, updatedSpan } = breakSpanEvenly(colspan, numberOfCells);
467
+ updateNumericAttribute('colspan', updatedSpan, tableCell, writer);
468
+ // Each inserted cell will have the same attributes:
469
+ const newCellsAttributes = {};
470
+ // Do not store default value in the model.
471
+ if (newCellsSpan > 1) {
472
+ newCellsAttributes.colspan = newCellsSpan;
473
+ }
474
+ // Copy rowspan of split cell.
475
+ if (rowspan > 1) {
476
+ newCellsAttributes.rowspan = rowspan;
477
+ }
478
+ const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
479
+ createCells(cellsToInsert, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
480
+ }
481
+ // Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
482
+ if (colspan < numberOfCells) {
483
+ const cellsToInsert = numberOfCells - colspan;
484
+ // First step: expand cells on the same column as split cell.
485
+ const tableMap = [...new TableWalker(table)];
486
+ // Get the column index of split cell.
487
+ const { column: splitCellColumn } = tableMap.find(({ cell }) => cell === tableCell);
488
+ // Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
489
+ const cellsToUpdate = tableMap.filter(({ cell, cellWidth, column }) => {
490
+ const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
491
+ const spansOverColumn = (column < splitCellColumn && column + cellWidth > splitCellColumn);
492
+ return isOnSameColumn || spansOverColumn;
493
+ });
494
+ // Expand cells vertically.
495
+ for (const { cell, cellWidth } of cellsToUpdate) {
496
+ writer.setAttribute('colspan', cellWidth + cellsToInsert, cell);
497
+ }
498
+ // Second step: create columns after split cell.
499
+ // Each inserted cell will have the same attributes:
500
+ const newCellsAttributes = {};
501
+ // Do not store default value in the model.
502
+ // Copy rowspan of split cell.
503
+ if (rowspan > 1) {
504
+ newCellsAttributes.rowspan = rowspan;
505
+ }
506
+ createCells(cellsToInsert, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
507
+ const headingColumns = table.getAttribute('headingColumns') || 0;
508
+ // Update heading section if split cell is in heading section.
509
+ if (headingColumns > splitCellColumn) {
510
+ updateNumericAttribute('headingColumns', headingColumns + cellsToInsert, table, writer);
511
+ }
512
+ }
513
+ });
514
+ }
515
+ /**
516
+ * Divides a table cell horizontally into several ones.
517
+ *
518
+ * The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
519
+ * below.
520
+ *
521
+ * If in the table below cell "b" is split into 3 cells:
522
+ *
523
+ * +---+---+---+
524
+ * | a | b | c |
525
+ * +---+---+---+
526
+ * | d | e | f |
527
+ * +---+---+---+
528
+ *
529
+ * It will result in the table below:
530
+ *
531
+ * +---+---+---+
532
+ * | a | b | c |
533
+ * + +---+ +
534
+ * | | | |
535
+ * + +---+ +
536
+ * | | | |
537
+ * +---+---+---+
538
+ * | d | e | f |
539
+ * +---+---+---+
540
+ *
541
+ * So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
542
+ *
543
+ * Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
544
+ * will be left to the original cell:
545
+ *
546
+ * +---+---+---+
547
+ * | a | b | c |
548
+ * + +---+---+
549
+ * | | d | e |
550
+ * + +---+---+
551
+ * | | f | g |
552
+ * + +---+---+
553
+ * | | h | i |
554
+ * +---+---+---+
555
+ *
556
+ * Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
557
+ *
558
+ * +---+---+---+
559
+ * | a | b | c |
560
+ * + +---+---+
561
+ * | | d | e |
562
+ * +---+---+---+
563
+ * | | f | g |
564
+ * +---+---+---+
565
+ * | | h | i |
566
+ * +---+---+---+
567
+ */
568
+ splitCellHorizontally(tableCell, numberOfCells = 2) {
569
+ const model = this.editor.model;
570
+ const tableRow = tableCell.parent;
571
+ const table = tableRow.parent;
572
+ const splitCellRow = table.getChildIndex(tableRow);
573
+ const rowspan = parseInt(tableCell.getAttribute('rowspan') || '1');
574
+ const colspan = parseInt(tableCell.getAttribute('colspan') || '1');
575
+ model.change(writer => {
576
+ // First check - the cell spans over multiple rows so before doing anything else just split this cell.
577
+ if (rowspan > 1) {
578
+ // Cache table map before updating table.
579
+ const tableMap = [...new TableWalker(table, {
580
+ startRow: splitCellRow,
581
+ endRow: splitCellRow + rowspan - 1,
582
+ includeAllSlots: true
583
+ })];
584
+ // Get spans of new (inserted) cells and span to update of split cell.
585
+ const { newCellsSpan, updatedSpan } = breakSpanEvenly(rowspan, numberOfCells);
586
+ updateNumericAttribute('rowspan', updatedSpan, tableCell, writer);
587
+ const { column: cellColumn } = tableMap.find(({ cell }) => cell === tableCell);
588
+ // Each inserted cell will have the same attributes:
589
+ const newCellsAttributes = {};
590
+ // Do not store default value in the model.
591
+ if (newCellsSpan > 1) {
592
+ newCellsAttributes.rowspan = newCellsSpan;
593
+ }
594
+ // Copy colspan of split cell.
595
+ if (colspan > 1) {
596
+ newCellsAttributes.colspan = colspan;
597
+ }
598
+ for (const tableSlot of tableMap) {
599
+ const { column, row } = tableSlot;
600
+ // As both newly created cells and the split cell might have rowspan,
601
+ // the insertion of new cells must go to appropriate rows:
602
+ //
603
+ // 1. It's a row after split cell + it's height.
604
+ const isAfterSplitCell = row >= splitCellRow + updatedSpan;
605
+ // 2. Is on the same column.
606
+ const isOnSameColumn = column === cellColumn;
607
+ // 3. And it's row index is after previous cell height.
608
+ const isInEvenlySplitRow = (row + splitCellRow + updatedSpan) % newCellsSpan === 0;
609
+ if (isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow) {
610
+ createCells(1, writer, tableSlot.getPositionBefore(), newCellsAttributes);
611
+ }
612
+ }
613
+ }
614
+ // Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
615
+ if (rowspan < numberOfCells) {
616
+ // We already split the cell in check one so here we split to the remaining number of cells only.
617
+ const cellsToInsert = numberOfCells - rowspan;
618
+ // This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
619
+ const tableMap = [...new TableWalker(table, { startRow: 0, endRow: splitCellRow })];
620
+ // First step: expand cells.
621
+ for (const { cell, cellHeight, row } of tableMap) {
622
+ // Expand rowspan of cells that are either:
623
+ // - on the same row as current cell,
624
+ // - or are below split cell row and overlaps that row.
625
+ if (cell !== tableCell && row + cellHeight > splitCellRow) {
626
+ const rowspanToSet = cellHeight + cellsToInsert;
627
+ writer.setAttribute('rowspan', rowspanToSet, cell);
628
+ }
629
+ }
630
+ // Second step: create rows with single cell below split cell.
631
+ const newCellsAttributes = {};
632
+ // Copy colspan of split cell.
633
+ if (colspan > 1) {
634
+ newCellsAttributes.colspan = colspan;
635
+ }
636
+ createEmptyRows(writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes);
637
+ // Update heading section if split cell is in heading section.
638
+ const headingRows = table.getAttribute('headingRows') || 0;
639
+ if (headingRows > splitCellRow) {
640
+ updateNumericAttribute('headingRows', headingRows + cellsToInsert, table, writer);
641
+ }
642
+ }
643
+ });
644
+ }
645
+ /**
646
+ * Returns the number of columns for a given table.
647
+ *
648
+ * ```ts
649
+ * editor.plugins.get( 'TableUtils' ).getColumns( table );
650
+ * ```
651
+ *
652
+ * @param table The table to analyze.
653
+ */
654
+ getColumns(table) {
655
+ // Analyze first row only as all the rows should have the same width.
656
+ // Using the first row without checking if it's a tableRow because we expect
657
+ // that table will have only tableRow model elements at the beginning.
658
+ const row = table.getChild(0);
659
+ return [...row.getChildren()].reduce((columns, row) => {
660
+ const columnWidth = parseInt(row.getAttribute('colspan') || '1');
661
+ return columns + columnWidth;
662
+ }, 0);
663
+ }
664
+ /**
665
+ * Returns the number of rows for a given table. Any other element present in the table model is omitted.
666
+ *
667
+ * ```ts
668
+ * editor.plugins.get( 'TableUtils' ).getRows( table );
669
+ * ```
670
+ *
671
+ * @param table The table to analyze.
672
+ */
673
+ getRows(table) {
674
+ // Rowspan not included due to #6427.
675
+ return Array.from(table.getChildren())
676
+ .reduce((rowCount, child) => child.is('element', 'tableRow') ? rowCount + 1 : rowCount, 0);
677
+ }
678
+ /**
679
+ * Creates an instance of the table walker.
680
+ *
681
+ * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
682
+ * It walks row by row and column by column in order to output values defined in the options.
683
+ * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
684
+ * pass the `includeAllSlots` option.
685
+ *
686
+ * @internal
687
+ * @param table A table over which the walker iterates.
688
+ * @param options An object with configuration.
689
+ */
690
+ createTableWalker(table, options = {}) {
691
+ return new TableWalker(table, options);
692
+ }
693
+ /**
694
+ * Returns all model table cells that are fully selected (from the outside)
695
+ * within the provided model selection's ranges.
696
+ *
697
+ * To obtain the cells selected from the inside, use
698
+ * {@link #getTableCellsContainingSelection}.
699
+ */
700
+ getSelectedTableCells(selection) {
701
+ const cells = [];
702
+ for (const range of this.sortRanges(selection.getRanges())) {
703
+ const element = range.getContainedElement();
704
+ if (element && element.is('element', 'tableCell')) {
705
+ cells.push(element);
706
+ }
707
+ }
708
+ return cells;
709
+ }
710
+ /**
711
+ * Returns all model table cells that the provided model selection's ranges
712
+ * {@link module:engine/model/range~Range#start} inside.
713
+ *
714
+ * To obtain the cells selected from the outside, use
715
+ * {@link #getSelectedTableCells}.
716
+ */
717
+ getTableCellsContainingSelection(selection) {
718
+ const cells = [];
719
+ for (const range of selection.getRanges()) {
720
+ const cellWithSelection = range.start.findAncestor('tableCell');
721
+ if (cellWithSelection) {
722
+ cells.push(cellWithSelection);
723
+ }
724
+ }
725
+ return cells;
726
+ }
727
+ /**
728
+ * Returns all model table cells that are either completely selected
729
+ * by selection ranges or host selection range
730
+ * {@link module:engine/model/range~Range#start start positions} inside them.
731
+ *
732
+ * Combines {@link #getTableCellsContainingSelection} and
733
+ * {@link #getSelectedTableCells}.
734
+ */
735
+ getSelectionAffectedTableCells(selection) {
736
+ const selectedCells = this.getSelectedTableCells(selection);
737
+ if (selectedCells.length) {
738
+ return selectedCells;
739
+ }
740
+ return this.getTableCellsContainingSelection(selection);
741
+ }
742
+ /**
743
+ * Returns an object with the `first` and `last` row index contained in the given `tableCells`.
744
+ *
745
+ * ```ts
746
+ * const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
747
+ *
748
+ * const { first, last } = getRowIndexes( selectedTableCells );
749
+ *
750
+ * console.log( `Selected rows: ${ first } to ${ last }` );
751
+ * ```
752
+ *
753
+ * @returns Returns an object with the `first` and `last` table row indexes.
754
+ */
755
+ getRowIndexes(tableCells) {
756
+ const indexes = tableCells.map(cell => cell.parent.index);
757
+ return this._getFirstLastIndexesObject(indexes);
758
+ }
759
+ /**
760
+ * Returns an object with the `first` and `last` column index contained in the given `tableCells`.
761
+ *
762
+ * ```ts
763
+ * const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
764
+ *
765
+ * const { first, last } = getColumnIndexes( selectedTableCells );
766
+ *
767
+ * console.log( `Selected columns: ${ first } to ${ last }` );
768
+ * ```
769
+ *
770
+ * @returns Returns an object with the `first` and `last` table column indexes.
771
+ */
772
+ getColumnIndexes(tableCells) {
773
+ const table = tableCells[0].findAncestor('table');
774
+ const tableMap = [...new TableWalker(table)];
775
+ const indexes = tableMap
776
+ .filter(entry => tableCells.includes(entry.cell))
777
+ .map(entry => entry.column);
778
+ return this._getFirstLastIndexesObject(indexes);
779
+ }
780
+ /**
781
+ * Checks if the selection contains cells that do not exceed rectangular selection.
782
+ *
783
+ * In a table below:
784
+ *
785
+ * ┌───┬───┬───┬───┐
786
+ * │ a │ b │ c │ d │
787
+ * ├───┴───┼───┤ │
788
+ * │ e │ f │ │
789
+ * │ ├───┼───┤
790
+ * │ │ g │ h │
791
+ * └───────┴───┴───┘
792
+ *
793
+ * Valid selections are these which create a solid rectangle (without gaps), such as:
794
+ * - a, b (two horizontal cells)
795
+ * - c, f (two vertical cells)
796
+ * - a, b, e (cell "e" spans over four cells)
797
+ * - c, d, f (cell d spans over a cell in the row below)
798
+ *
799
+ * While an invalid selection would be:
800
+ * - a, c (the unselected cell "b" creates a gap)
801
+ * - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
802
+ */
803
+ isSelectionRectangular(selectedTableCells) {
804
+ if (selectedTableCells.length < 2 || !this._areCellInTheSameTableSection(selectedTableCells)) {
805
+ return false;
806
+ }
807
+ // A valid selection is a fully occupied rectangle composed of table cells.
808
+ // Below we will calculate the area of a selected table cells and the area of valid selection.
809
+ // The area of a valid selection is defined by top-left and bottom-right cells.
810
+ const rows = new Set();
811
+ const columns = new Set();
812
+ let areaOfSelectedCells = 0;
813
+ for (const tableCell of selectedTableCells) {
814
+ const { row, column } = this.getCellLocation(tableCell);
815
+ const rowspan = parseInt(tableCell.getAttribute('rowspan')) || 1;
816
+ const colspan = parseInt(tableCell.getAttribute('colspan')) || 1;
817
+ // Record row & column indexes of current cell.
818
+ rows.add(row);
819
+ columns.add(column);
820
+ // For cells that spans over multiple rows add also the last row that this cell spans over.
821
+ if (rowspan > 1) {
822
+ rows.add(row + rowspan - 1);
823
+ }
824
+ // For cells that spans over multiple columns add also the last column that this cell spans over.
825
+ if (colspan > 1) {
826
+ columns.add(column + colspan - 1);
827
+ }
828
+ areaOfSelectedCells += (rowspan * colspan);
829
+ }
830
+ // We can only merge table cells that are in adjacent rows...
831
+ const areaOfValidSelection = getBiggestRectangleArea(rows, columns);
832
+ return areaOfValidSelection == areaOfSelectedCells;
833
+ }
834
+ /**
835
+ * Returns array of sorted ranges.
836
+ */
837
+ sortRanges(ranges) {
838
+ return Array.from(ranges).sort(compareRangeOrder);
839
+ }
840
+ /**
841
+ * Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
842
+ */
843
+ _getFirstLastIndexesObject(indexes) {
844
+ const allIndexesSorted = indexes.sort((indexA, indexB) => indexA - indexB);
845
+ const first = allIndexesSorted[0];
846
+ const last = allIndexesSorted[allIndexesSorted.length - 1];
847
+ return { first, last };
848
+ }
849
+ /**
850
+ * Checks if the selection does not mix a header (column or row) with other cells.
851
+ *
852
+ * For instance, in the table below valid selections consist of cells with the same letter only.
853
+ * So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
854
+ *
855
+ * header columns
856
+ * ↓ ↓
857
+ * ┌───┬───┬───┬───┐
858
+ * │ a │ a │ b │ b │ ← header row
859
+ * ├───┼───┼───┼───┤
860
+ * │ c │ c │ d │ d │
861
+ * ├───┼───┼───┼───┤
862
+ * │ c │ c │ d │ d │
863
+ * └───┴───┴───┴───┘
864
+ */
865
+ _areCellInTheSameTableSection(tableCells) {
866
+ const table = tableCells[0].findAncestor('table');
867
+ const rowIndexes = this.getRowIndexes(tableCells);
868
+ const headingRows = parseInt(table.getAttribute('headingRows')) || 0;
869
+ // Calculating row indexes is a bit cheaper so if this check fails we can't merge.
870
+ if (!this._areIndexesInSameSection(rowIndexes, headingRows)) {
871
+ return false;
872
+ }
873
+ const columnIndexes = this.getColumnIndexes(tableCells);
874
+ const headingColumns = parseInt(table.getAttribute('headingColumns')) || 0;
875
+ // Similarly cells must be in same column section.
876
+ return this._areIndexesInSameSection(columnIndexes, headingColumns);
877
+ }
878
+ /**
879
+ * Unified check if table rows/columns indexes are in the same heading/body section.
880
+ */
881
+ _areIndexesInSameSection({ first, last }, headingSectionSize) {
882
+ const firstCellIsInHeading = first < headingSectionSize;
883
+ const lastCellIsInHeading = last < headingSectionSize;
884
+ return firstCellIsInHeading === lastCellIsInHeading;
885
+ }
886
+ }
887
+ /**
888
+ * Creates empty rows at the given index in an existing table.
889
+ *
890
+ * @param insertAt The row index of row insertion.
891
+ * @param rows The number of rows to create.
892
+ * @param tableCellToInsert The number of cells to insert in each row.
893
+ */
894
+ function createEmptyRows(writer, table, insertAt, rows, tableCellToInsert, attributes = {}) {
895
+ for (let i = 0; i < rows; i++) {
896
+ const tableRow = writer.createElement('tableRow');
897
+ writer.insert(tableRow, table, insertAt);
898
+ createCells(tableCellToInsert, writer, writer.createPositionAt(tableRow, 'end'), attributes);
899
+ }
900
+ }
901
+ /**
902
+ * Creates cells at a given position.
903
+ *
904
+ * @param cells The number of cells to create
905
+ */
906
+ function createCells(cells, writer, insertPosition, attributes = {}) {
907
+ for (let i = 0; i < cells; i++) {
908
+ createEmptyTableCell(writer, insertPosition, attributes);
909
+ }
910
+ }
911
+ /**
912
+ * Evenly distributes the span of a cell to a number of provided cells.
913
+ * The resulting spans will always be integer values.
914
+ *
915
+ * For instance breaking a span of 7 into 3 cells will return:
916
+ *
917
+ * ```ts
918
+ * { newCellsSpan: 2, updatedSpan: 3 }
919
+ * ```
920
+ *
921
+ * as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
922
+ *
923
+ * @param span The span value do break.
924
+ * @param numberOfCells The number of resulting spans.
925
+ */
926
+ function breakSpanEvenly(span, numberOfCells) {
927
+ if (span < numberOfCells) {
928
+ return { newCellsSpan: 1, updatedSpan: 1 };
929
+ }
930
+ const newCellsSpan = Math.floor(span / numberOfCells);
931
+ const updatedSpan = (span - newCellsSpan * numberOfCells) + newCellsSpan;
932
+ return { newCellsSpan, updatedSpan };
933
+ }
934
+ /**
935
+ * Updates heading columns attribute if removing a row from head section.
936
+ */
937
+ function adjustHeadingColumns(table, removedColumnIndexes, writer) {
938
+ const headingColumns = table.getAttribute('headingColumns') || 0;
939
+ if (headingColumns && removedColumnIndexes.first < headingColumns) {
940
+ const headingsRemoved = Math.min(headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last) -
941
+ removedColumnIndexes.first + 1;
942
+ writer.setAttribute('headingColumns', headingColumns - headingsRemoved, table);
943
+ }
944
+ }
945
+ /**
946
+ * Calculates a new heading rows value for removing rows from heading section.
947
+ */
948
+ function updateHeadingRows(table, { first, last }, writer) {
949
+ const headingRows = table.getAttribute('headingRows') || 0;
950
+ if (first < headingRows) {
951
+ const newRows = last < headingRows ? headingRows - (last - first + 1) : first;
952
+ updateNumericAttribute('headingRows', newRows, table, writer, 0);
953
+ }
954
+ }
955
+ /**
956
+ * Finds cells that will be:
957
+ * - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
958
+ * - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
959
+ *
960
+ * Sample table with overlapping & sticking out cells:
961
+ *
962
+ * +----+----+----+----+----+
963
+ * | 00 | 01 | 02 | 03 | 04 |
964
+ * +----+ + + + +
965
+ * | 10 | | | | |
966
+ * +----+----+ + + +
967
+ * | 20 | 21 | | | | <-- removed row
968
+ * + + +----+ + +
969
+ * | | | 32 | | | <-- removed row
970
+ * +----+ + +----+ +
971
+ * | 40 | | | 43 | |
972
+ * +----+----+----+----+----+
973
+ *
974
+ * In a table above:
975
+ * - cells to trim: '02', '03' & '04'.
976
+ * - cells to move: '21' & '32'.
977
+ */
978
+ function getCellsToMoveAndTrimOnRemoveRow(table, { first, last }) {
979
+ const cellsToMove = new Map();
980
+ const cellsToTrim = [];
981
+ for (const { row, column, cellHeight, cell } of new TableWalker(table, { endRow: last })) {
982
+ const lastRowOfCell = row + cellHeight - 1;
983
+ const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
984
+ if (isCellStickingOutFromRemovedRows) {
985
+ const rowspanInRemovedSection = last - row + 1;
986
+ const rowSpanToSet = cellHeight - rowspanInRemovedSection;
987
+ cellsToMove.set(column, {
988
+ cell,
989
+ rowspan: rowSpanToSet
990
+ });
991
+ }
992
+ const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
993
+ if (isCellOverlappingRemovedRows) {
994
+ let rowspanAdjustment;
995
+ // Cell fully covers removed section - trim it by removed rows count.
996
+ if (lastRowOfCell >= last) {
997
+ rowspanAdjustment = last - first + 1;
998
+ }
999
+ // Cell partially overlaps removed section - calculate cell's span that is in removed section.
1000
+ else {
1001
+ rowspanAdjustment = lastRowOfCell - first + 1;
1002
+ }
1003
+ cellsToTrim.push({
1004
+ cell,
1005
+ rowspan: cellHeight - rowspanAdjustment
1006
+ });
1007
+ }
1008
+ }
1009
+ return { cellsToMove, cellsToTrim };
1010
+ }
1011
+ function moveCellsToRow(table, targetRowIndex, cellsToMove, writer) {
1012
+ const tableWalker = new TableWalker(table, {
1013
+ includeAllSlots: true,
1014
+ row: targetRowIndex
1015
+ });
1016
+ const tableRowMap = [...tableWalker];
1017
+ const row = table.getChild(targetRowIndex);
1018
+ let previousCell;
1019
+ for (const { column, cell, isAnchor } of tableRowMap) {
1020
+ if (cellsToMove.has(column)) {
1021
+ const { cell: cellToMove, rowspan } = cellsToMove.get(column);
1022
+ const targetPosition = previousCell ?
1023
+ writer.createPositionAfter(previousCell) :
1024
+ writer.createPositionAt(row, 0);
1025
+ writer.move(writer.createRangeOn(cellToMove), targetPosition);
1026
+ updateNumericAttribute('rowspan', rowspan, cellToMove, writer);
1027
+ previousCell = cellToMove;
1028
+ }
1029
+ else if (isAnchor) {
1030
+ // If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
1031
+ previousCell = cell;
1032
+ }
1033
+ }
1034
+ }
1035
+ function compareRangeOrder(rangeA, rangeB) {
1036
+ // Since table cell ranges are disjoint, it's enough to check their start positions.
1037
+ const posA = rangeA.start;
1038
+ const posB = rangeB.start;
1039
+ // Checking for equal position (returning 0) is not needed because this would be either:
1040
+ // a. Intersecting range (not allowed by model)
1041
+ // b. Collapsed range on the same position (allowed by model but should not happen).
1042
+ return posA.isBefore(posB) ? -1 : 1;
1043
+ }
1044
+ /**
1045
+ * Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
1046
+ */
1047
+ function getBiggestRectangleArea(rows, columns) {
1048
+ const rowsIndexes = Array.from(rows.values());
1049
+ const columnIndexes = Array.from(columns.values());
1050
+ const lastRow = Math.max(...rowsIndexes);
1051
+ const firstRow = Math.min(...rowsIndexes);
1052
+ const lastColumn = Math.max(...columnIndexes);
1053
+ const firstColumn = Math.min(...columnIndexes);
1054
+ return (lastRow - firstRow + 1) * (lastColumn - firstColumn + 1);
1055
+ }