@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
@@ -1,450 +1,450 @@
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
- import { Plugin } from 'ckeditor5/src/core';
6
- import TableSelection from './tableselection';
7
- import TableWalker from './tablewalker';
8
- import TableUtils from './tableutils';
9
- import { cropTableToDimensions, getHorizontallyOverlappingCells, getVerticallyOverlappingCells, removeEmptyRowsColumns, splitHorizontally, splitVertically, trimTableCellIfNeeded, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
10
- /**
11
- * This plugin adds support for copying/cutting/pasting fragments of tables.
12
- * It is loaded automatically by the {@link module:table/table~Table} plugin.
13
- */
14
- export default class TableClipboard extends Plugin {
15
- /**
16
- * @inheritDoc
17
- */
18
- static get pluginName() {
19
- return 'TableClipboard';
20
- }
21
- /**
22
- * @inheritDoc
23
- */
24
- static get requires() {
25
- return [TableSelection, TableUtils];
26
- }
27
- /**
28
- * @inheritDoc
29
- */
30
- init() {
31
- const editor = this.editor;
32
- const viewDocument = editor.editing.view.document;
33
- this.listenTo(viewDocument, 'copy', (evt, data) => this._onCopyCut(evt, data));
34
- this.listenTo(viewDocument, 'cut', (evt, data) => this._onCopyCut(evt, data));
35
- this.listenTo(editor.model, 'insertContent', (evt, [content, selectable]) => this._onInsertContent(evt, content, selectable), { priority: 'high' });
36
- this.decorate('_replaceTableSlotCell');
37
- }
38
- /**
39
- * Copies table content to a clipboard on "copy" & "cut" events.
40
- *
41
- * @param evt An object containing information about the handled event.
42
- * @param data Clipboard event data.
43
- */
44
- _onCopyCut(evt, data) {
45
- const tableSelection = this.editor.plugins.get(TableSelection);
46
- if (!tableSelection.getSelectedTableCells()) {
47
- return;
48
- }
49
- if (evt.name == 'cut' && !this.editor.model.canEditAt(this.editor.model.document.selection)) {
50
- return;
51
- }
52
- data.preventDefault();
53
- evt.stop();
54
- const dataController = this.editor.data;
55
- const viewDocument = this.editor.editing.view.document;
56
- const content = dataController.toView(tableSelection.getSelectionAsFragment());
57
- viewDocument.fire('clipboardOutput', {
58
- dataTransfer: data.dataTransfer,
59
- content,
60
- method: evt.name
61
- });
62
- }
63
- /**
64
- * Overrides default {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method to handle pasting table inside
65
- * selected table fragment.
66
- *
67
- * Depending on selected table fragment:
68
- * - If a selected table fragment is smaller than paste table it will crop pasted table to match dimensions.
69
- * - If dimensions are equal it will replace selected table fragment with a pasted table contents.
70
- *
71
- * @param content The content to insert.
72
- * @param selectable The selection into which the content should be inserted.
73
- * If not provided the current model document selection will be used.
74
- */
75
- _onInsertContent(evt, content, selectable) {
76
- if (selectable && !selectable.is('documentSelection')) {
77
- return;
78
- }
79
- const model = this.editor.model;
80
- const tableUtils = this.editor.plugins.get(TableUtils);
81
- // We might need to crop table before inserting so reference might change.
82
- let pastedTable = this.getTableIfOnlyTableInContent(content, model);
83
- if (!pastedTable) {
84
- return;
85
- }
86
- const selectedTableCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
87
- if (!selectedTableCells.length) {
88
- removeEmptyRowsColumns(pastedTable, tableUtils);
89
- return;
90
- }
91
- // Override default model.insertContent() handling at this point.
92
- evt.stop();
93
- model.change(writer => {
94
- const pastedDimensions = {
95
- width: tableUtils.getColumns(pastedTable),
96
- height: tableUtils.getRows(pastedTable)
97
- };
98
- // Prepare the table for pasting.
99
- const selection = prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils);
100
- // Beyond this point we operate on a fixed content table with rectangular selection and proper last row/column values.
101
- const selectionHeight = selection.lastRow - selection.firstRow + 1;
102
- const selectionWidth = selection.lastColumn - selection.firstColumn + 1;
103
- // Crop pasted table if:
104
- // - Pasted table dimensions exceeds selection area.
105
- // - Pasted table has broken layout (ie some cells sticks out by the table dimensions established by the first and last row).
106
- //
107
- // Note: The table dimensions are established by the width of the first row and the total number of rows.
108
- // It is possible to programmatically create a table that has rows which would have cells anchored beyond first row width but
109
- // such table will not be created by other editing solutions.
110
- const cropDimensions = {
111
- startRow: 0,
112
- startColumn: 0,
113
- endRow: Math.min(selectionHeight, pastedDimensions.height) - 1,
114
- endColumn: Math.min(selectionWidth, pastedDimensions.width) - 1
115
- };
116
- pastedTable = cropTableToDimensions(pastedTable, cropDimensions, writer);
117
- // Content table to which we insert a pasted table.
118
- const selectedTable = selectedTableCells[0].findAncestor('table');
119
- const cellsToSelect = this._replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer);
120
- if (this.editor.plugins.get('TableSelection').isEnabled) {
121
- // Selection ranges must be sorted because the first and last selection ranges are considered
122
- // as anchor/focus cell ranges for multi-cell selection.
123
- const selectionRanges = tableUtils.sortRanges(cellsToSelect.map(cell => writer.createRangeOn(cell)));
124
- writer.setSelection(selectionRanges);
125
- }
126
- else {
127
- // Set selection inside first cell if multi-cell selection is disabled.
128
- writer.setSelection(cellsToSelect[0], 0);
129
- }
130
- });
131
- }
132
- /**
133
- * Replaces the part of selectedTable with pastedTable.
134
- */
135
- _replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer) {
136
- const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
137
- // Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
138
- const pastedTableLocationMap = createLocationMap(pastedTable, pastedWidth, pastedHeight);
139
- const selectedTableMap = [...new TableWalker(selectedTable, {
140
- startRow: selection.firstRow,
141
- endRow: selection.lastRow,
142
- startColumn: selection.firstColumn,
143
- endColumn: selection.lastColumn,
144
- includeAllSlots: true
145
- })];
146
- // Selection must be set to pasted cells (some might be removed or new created).
147
- const cellsToSelect = [];
148
- // Store next cell insert position.
149
- let insertPosition;
150
- // Content table replace cells algorithm iterates over a selected table fragment and:
151
- //
152
- // - Removes existing table cells at current slot (location).
153
- // - Inserts cell from a pasted table for a matched slots.
154
- //
155
- // This ensures proper table geometry after the paste
156
- for (const tableSlot of selectedTableMap) {
157
- const { row, column } = tableSlot;
158
- // Save the insert position for current row start.
159
- if (column === selection.firstColumn) {
160
- insertPosition = tableSlot.getPositionBefore();
161
- }
162
- // Map current table slot location to an pasted table slot location.
163
- const pastedRow = row - selection.firstRow;
164
- const pastedColumn = column - selection.firstColumn;
165
- const pastedCell = pastedTableLocationMap[pastedRow % pastedHeight][pastedColumn % pastedWidth];
166
- // Clone cell to insert (to duplicate its attributes and children).
167
- // Cloning is required to support repeating pasted table content when inserting to a bigger selection.
168
- const cellToInsert = pastedCell ? writer.cloneElement(pastedCell) : null;
169
- // Replace the cell from the current slot with new table cell.
170
- const newTableCell = this._replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer);
171
- // The cell was only removed.
172
- if (!newTableCell) {
173
- continue;
174
- }
175
- // Trim the cell if it's row/col-spans would exceed selection area.
176
- trimTableCellIfNeeded(newTableCell, row, column, selection.lastRow, selection.lastColumn, writer);
177
- cellsToSelect.push(newTableCell);
178
- insertPosition = writer.createPositionAfter(newTableCell);
179
- }
180
- // If there are any headings, all the cells that overlap from heading must be splitted.
181
- const headingRows = parseInt(selectedTable.getAttribute('headingRows') || '0');
182
- const headingColumns = parseInt(selectedTable.getAttribute('headingColumns') || '0');
183
- const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
184
- const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
185
- if (areHeadingRowsIntersectingSelection) {
186
- const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
187
- const newCells = doHorizontalSplit(selectedTable, headingRows, columnsLimit, writer, selection.firstRow);
188
- cellsToSelect.push(...newCells);
189
- }
190
- if (areHeadingColumnsIntersectingSelection) {
191
- const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
192
- const newCells = doVerticalSplit(selectedTable, headingColumns, rowsLimit, writer);
193
- cellsToSelect.push(...newCells);
194
- }
195
- return cellsToSelect;
196
- }
197
- /**
198
- * Replaces a single table slot.
199
- *
200
- * @returns Inserted table cell or null if slot should remain empty.
201
- * @private
202
- */
203
- _replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer) {
204
- const { cell, isAnchor } = tableSlot;
205
- // If the slot is occupied by a cell in a selected table - remove it.
206
- // The slot of this cell will be either:
207
- // - Replaced by a pasted table cell.
208
- // - Spanned by a previously pasted table cell.
209
- if (isAnchor) {
210
- writer.remove(cell);
211
- }
212
- // There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
213
- if (!cellToInsert) {
214
- return null;
215
- }
216
- writer.insert(cellToInsert, insertPosition);
217
- return cellToInsert;
218
- }
219
- /**
220
- * Extracts the table for pasting into a table.
221
- *
222
- * @param content The content to insert.
223
- * @param model The editor model.
224
- */
225
- getTableIfOnlyTableInContent(content, model) {
226
- if (!content.is('documentFragment') && !content.is('element')) {
227
- return null;
228
- }
229
- // Table passed directly.
230
- if (content.is('element', 'table')) {
231
- return content;
232
- }
233
- // We do not support mixed content when pasting table into table.
234
- // See: https://github.com/ckeditor/ckeditor5/issues/6817.
235
- if (content.childCount == 1 && content.getChild(0).is('element', 'table')) {
236
- return content.getChild(0);
237
- }
238
- // If there are only whitespaces around a table then use that table for pasting.
239
- const contentRange = model.createRangeIn(content);
240
- for (const element of contentRange.getItems()) {
241
- if (element.is('element', 'table')) {
242
- // Stop checking if there is some content before table.
243
- const rangeBefore = model.createRange(contentRange.start, model.createPositionBefore(element));
244
- if (model.hasContent(rangeBefore, { ignoreWhitespaces: true })) {
245
- return null;
246
- }
247
- // Stop checking if there is some content after table.
248
- const rangeAfter = model.createRange(model.createPositionAfter(element), contentRange.end);
249
- if (model.hasContent(rangeAfter, { ignoreWhitespaces: true })) {
250
- return null;
251
- }
252
- // There wasn't any content neither before nor after.
253
- return element;
254
- }
255
- }
256
- return null;
257
- }
258
- }
259
- /**
260
- * Prepares a table for pasting and returns adjusted selection dimensions.
261
- */
262
- function prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils) {
263
- const selectedTable = selectedTableCells[0].findAncestor('table');
264
- const columnIndexes = tableUtils.getColumnIndexes(selectedTableCells);
265
- const rowIndexes = tableUtils.getRowIndexes(selectedTableCells);
266
- const selection = {
267
- firstColumn: columnIndexes.first,
268
- lastColumn: columnIndexes.last,
269
- firstRow: rowIndexes.first,
270
- lastRow: rowIndexes.last
271
- };
272
- // Single cell selected - expand selection to pasted table dimensions.
273
- const shouldExpandSelection = selectedTableCells.length === 1;
274
- if (shouldExpandSelection) {
275
- selection.lastRow += pastedDimensions.height - 1;
276
- selection.lastColumn += pastedDimensions.width - 1;
277
- expandTableSize(selectedTable, selection.lastRow + 1, selection.lastColumn + 1, tableUtils);
278
- }
279
- // In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
280
- // like in the case of a non-rectangular area. This might be fixed by re-setting selected cells array but this shortcut is safe.
281
- if (shouldExpandSelection || !tableUtils.isSelectionRectangular(selectedTableCells)) {
282
- // For a non-rectangular selection (ie in which some cells sticks out from a virtual selection rectangle) we need to create
283
- // a table layout that has a rectangular selection. This will split cells so the selection become rectangular.
284
- // Beyond this point we will operate on fixed content table.
285
- splitCellsToRectangularSelection(selectedTable, selection, writer);
286
- }
287
- // However a selected table fragment might be invalid if examined alone. Ie such table fragment:
288
- //
289
- // +---+---+---+---+
290
- // 0 | a | b | c | d |
291
- // + + +---+---+
292
- // 1 | | e | f | g |
293
- // + +---+ +---+
294
- // 2 | | h | | i | <- last row, each cell has rowspan = 2,
295
- // + + + + + so we need to return 3, not 2
296
- // 3 | | | | |
297
- // +---+---+---+---+
298
- //
299
- // is invalid as the cells "h" and "i" have rowspans.
300
- // This case needs only adjusting the selection dimension as the rest of the algorithm operates on empty slots also.
301
- else {
302
- selection.lastRow = adjustLastRowIndex(selectedTable, selection);
303
- selection.lastColumn = adjustLastColumnIndex(selectedTable, selection);
304
- }
305
- return selection;
306
- }
307
- /**
308
- * Expand table (in place) to expected size.
309
- */
310
- function expandTableSize(table, expectedHeight, expectedWidth, tableUtils) {
311
- const tableWidth = tableUtils.getColumns(table);
312
- const tableHeight = tableUtils.getRows(table);
313
- if (expectedWidth > tableWidth) {
314
- tableUtils.insertColumns(table, {
315
- at: tableWidth,
316
- columns: expectedWidth - tableWidth
317
- });
318
- }
319
- if (expectedHeight > tableHeight) {
320
- tableUtils.insertRows(table, {
321
- at: tableHeight,
322
- rows: expectedHeight - tableHeight
323
- });
324
- }
325
- }
326
- /**
327
- * Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
328
- *
329
- * At given row & column location it might be one of:
330
- *
331
- * * cell - cell from pasted table anchored at this location.
332
- * * null - if no cell is anchored at this location.
333
- *
334
- * For instance, from a table below:
335
- *
336
- * +----+----+----+----+
337
- * | 00 | 01 | 02 | 03 |
338
- * + +----+----+----+
339
- * | | 11 | 13 |
340
- * +----+ +----+
341
- * | 20 | | 23 |
342
- * +----+----+----+----+
343
- *
344
- * The method will return an array (numbers represents cell element):
345
- *
346
- * ```ts
347
- * const map = [
348
- * [ '00', '01', '02', '03' ],
349
- * [ null, '11', null, '13' ],
350
- * [ '20', null, null, '23' ]
351
- * ]
352
- * ```
353
- *
354
- * This allows for a quick access to table at give row & column. For instance to access table cell "13" from pasted table call:
355
- *
356
- * ```ts
357
- * const cell = map[ 1 ][ 3 ]
358
- * ```
359
- */
360
- function createLocationMap(table, width, height) {
361
- // Create height x width (row x column) two-dimensional table to store cells.
362
- const map = new Array(height).fill(null)
363
- .map(() => new Array(width).fill(null));
364
- for (const { column, row, cell } of new TableWalker(table)) {
365
- map[row][column] = cell;
366
- }
367
- return map;
368
- }
369
- /**
370
- * Make selected cells rectangular by splitting the cells that stand out from a rectangular selection.
371
- *
372
- * In the table below a selection is shown with "::" and slots with anchor cells are named.
373
- *
374
- * +----+----+----+----+----+ +----+----+----+----+----+
375
- * | 00 | 01 | 02 | 03 | | 00 | 01 | 02 | 03 |
376
- * + +----+ +----+----+ | ::::::::::::::::----+
377
- * | | 11 | | 13 | 14 | | ::11 | | 13:: 14 | <- first row
378
- * +----+----+ + +----+ +----::---| | ::----+
379
- * | 20 | 21 | | | 24 | select cells: | 20 ::21 | | :: 24 |
380
- * +----+----+ +----+----+ 11 -> 33 +----::---| |---::----+
381
- * | 30 | | 33 | 34 | | 30 :: | | 33:: 34 | <- last row
382
- * + + +----+ + | :::::::::::::::: +
383
- * | | | 43 | | | | | 43 | |
384
- * +----+----+----+----+----+ +----+----+----+----+----+
385
- * ^ ^
386
- * first & last columns
387
- *
388
- * Will update table to:
389
- *
390
- * +----+----+----+----+----+
391
- * | 00 | 01 | 02 | 03 |
392
- * + +----+----+----+----+
393
- * | | 11 | | 13 | 14 |
394
- * +----+----+ + +----+
395
- * | 20 | 21 | | | 24 |
396
- * +----+----+ +----+----+
397
- * | 30 | | | 33 | 34 |
398
- * + +----+----+----+ +
399
- * | | | | 43 | |
400
- * +----+----+----+----+----+
401
- *
402
- * In th example above:
403
- * - Cell "02" which have `rowspan = 4` must be trimmed at first and at after last row.
404
- * - Cell "03" which have `rowspan = 2` and `colspan = 2` must be trimmed at first column and after last row.
405
- * - Cells "00", "03" & "30" which cannot be cut by this algorithm as they are outside the trimmed area.
406
- * - Cell "13" cannot be cut as it is inside the trimmed area.
407
- */
408
- function splitCellsToRectangularSelection(table, dimensions, writer) {
409
- const { firstRow, lastRow, firstColumn, lastColumn } = dimensions;
410
- const rowIndexes = { first: firstRow, last: lastRow };
411
- const columnIndexes = { first: firstColumn, last: lastColumn };
412
- // 1. Split cells vertically in two steps as first step might create cells that needs to split again.
413
- doVerticalSplit(table, firstColumn, rowIndexes, writer);
414
- doVerticalSplit(table, lastColumn + 1, rowIndexes, writer);
415
- // 2. Split cells horizontally in two steps as first step might create cells that needs to split again.
416
- doHorizontalSplit(table, firstRow, columnIndexes, writer);
417
- doHorizontalSplit(table, lastRow + 1, columnIndexes, writer, firstRow);
418
- }
419
- function doHorizontalSplit(table, splitRow, limitColumns, writer, startRow = 0) {
420
- // If selection starts at first row then no split is needed.
421
- if (splitRow < 1) {
422
- return;
423
- }
424
- const overlappingCells = getVerticallyOverlappingCells(table, splitRow, startRow);
425
- // Filter out cells that are not touching insides of the rectangular selection.
426
- const cellsToSplit = overlappingCells.filter(({ column, cellWidth }) => isAffectedBySelection(column, cellWidth, limitColumns));
427
- return cellsToSplit.map(({ cell }) => splitHorizontally(cell, splitRow, writer));
428
- }
429
- function doVerticalSplit(table, splitColumn, limitRows, writer) {
430
- // If selection starts at first column then no split is needed.
431
- if (splitColumn < 1) {
432
- return;
433
- }
434
- const overlappingCells = getHorizontallyOverlappingCells(table, splitColumn);
435
- // Filter out cells that are not touching insides of the rectangular selection.
436
- const cellsToSplit = overlappingCells.filter(({ row, cellHeight }) => isAffectedBySelection(row, cellHeight, limitRows));
437
- return cellsToSplit.map(({ cell, column }) => splitVertically(cell, column, splitColumn, writer));
438
- }
439
- /**
440
- * Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
441
- *
442
- * The same check is used for row as for column.
443
- */
444
- function isAffectedBySelection(index, span, limit) {
445
- const endIndex = index + span - 1;
446
- const { first, last } = limit;
447
- const isInsideSelection = index >= first && index <= last;
448
- const overlapsSelectionFromOutside = index < first && endIndex >= first;
449
- return isInsideSelection || overlapsSelectionFromOutside;
450
- }
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
+ import { Plugin } from 'ckeditor5/src/core';
6
+ import TableSelection from './tableselection';
7
+ import TableWalker from './tablewalker';
8
+ import TableUtils from './tableutils';
9
+ import { cropTableToDimensions, getHorizontallyOverlappingCells, getVerticallyOverlappingCells, removeEmptyRowsColumns, splitHorizontally, splitVertically, trimTableCellIfNeeded, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
10
+ /**
11
+ * This plugin adds support for copying/cutting/pasting fragments of tables.
12
+ * It is loaded automatically by the {@link module:table/table~Table} plugin.
13
+ */
14
+ export default class TableClipboard extends Plugin {
15
+ /**
16
+ * @inheritDoc
17
+ */
18
+ static get pluginName() {
19
+ return 'TableClipboard';
20
+ }
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ static get requires() {
25
+ return [TableSelection, TableUtils];
26
+ }
27
+ /**
28
+ * @inheritDoc
29
+ */
30
+ init() {
31
+ const editor = this.editor;
32
+ const viewDocument = editor.editing.view.document;
33
+ this.listenTo(viewDocument, 'copy', (evt, data) => this._onCopyCut(evt, data));
34
+ this.listenTo(viewDocument, 'cut', (evt, data) => this._onCopyCut(evt, data));
35
+ this.listenTo(editor.model, 'insertContent', (evt, [content, selectable]) => this._onInsertContent(evt, content, selectable), { priority: 'high' });
36
+ this.decorate('_replaceTableSlotCell');
37
+ }
38
+ /**
39
+ * Copies table content to a clipboard on "copy" & "cut" events.
40
+ *
41
+ * @param evt An object containing information about the handled event.
42
+ * @param data Clipboard event data.
43
+ */
44
+ _onCopyCut(evt, data) {
45
+ const tableSelection = this.editor.plugins.get(TableSelection);
46
+ if (!tableSelection.getSelectedTableCells()) {
47
+ return;
48
+ }
49
+ if (evt.name == 'cut' && !this.editor.model.canEditAt(this.editor.model.document.selection)) {
50
+ return;
51
+ }
52
+ data.preventDefault();
53
+ evt.stop();
54
+ const dataController = this.editor.data;
55
+ const viewDocument = this.editor.editing.view.document;
56
+ const content = dataController.toView(tableSelection.getSelectionAsFragment());
57
+ viewDocument.fire('clipboardOutput', {
58
+ dataTransfer: data.dataTransfer,
59
+ content,
60
+ method: evt.name
61
+ });
62
+ }
63
+ /**
64
+ * Overrides default {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method to handle pasting table inside
65
+ * selected table fragment.
66
+ *
67
+ * Depending on selected table fragment:
68
+ * - If a selected table fragment is smaller than paste table it will crop pasted table to match dimensions.
69
+ * - If dimensions are equal it will replace selected table fragment with a pasted table contents.
70
+ *
71
+ * @param content The content to insert.
72
+ * @param selectable The selection into which the content should be inserted.
73
+ * If not provided the current model document selection will be used.
74
+ */
75
+ _onInsertContent(evt, content, selectable) {
76
+ if (selectable && !selectable.is('documentSelection')) {
77
+ return;
78
+ }
79
+ const model = this.editor.model;
80
+ const tableUtils = this.editor.plugins.get(TableUtils);
81
+ // We might need to crop table before inserting so reference might change.
82
+ let pastedTable = this.getTableIfOnlyTableInContent(content, model);
83
+ if (!pastedTable) {
84
+ return;
85
+ }
86
+ const selectedTableCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
87
+ if (!selectedTableCells.length) {
88
+ removeEmptyRowsColumns(pastedTable, tableUtils);
89
+ return;
90
+ }
91
+ // Override default model.insertContent() handling at this point.
92
+ evt.stop();
93
+ model.change(writer => {
94
+ const pastedDimensions = {
95
+ width: tableUtils.getColumns(pastedTable),
96
+ height: tableUtils.getRows(pastedTable)
97
+ };
98
+ // Prepare the table for pasting.
99
+ const selection = prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils);
100
+ // Beyond this point we operate on a fixed content table with rectangular selection and proper last row/column values.
101
+ const selectionHeight = selection.lastRow - selection.firstRow + 1;
102
+ const selectionWidth = selection.lastColumn - selection.firstColumn + 1;
103
+ // Crop pasted table if:
104
+ // - Pasted table dimensions exceeds selection area.
105
+ // - Pasted table has broken layout (ie some cells sticks out by the table dimensions established by the first and last row).
106
+ //
107
+ // Note: The table dimensions are established by the width of the first row and the total number of rows.
108
+ // It is possible to programmatically create a table that has rows which would have cells anchored beyond first row width but
109
+ // such table will not be created by other editing solutions.
110
+ const cropDimensions = {
111
+ startRow: 0,
112
+ startColumn: 0,
113
+ endRow: Math.min(selectionHeight, pastedDimensions.height) - 1,
114
+ endColumn: Math.min(selectionWidth, pastedDimensions.width) - 1
115
+ };
116
+ pastedTable = cropTableToDimensions(pastedTable, cropDimensions, writer);
117
+ // Content table to which we insert a pasted table.
118
+ const selectedTable = selectedTableCells[0].findAncestor('table');
119
+ const cellsToSelect = this._replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer);
120
+ if (this.editor.plugins.get('TableSelection').isEnabled) {
121
+ // Selection ranges must be sorted because the first and last selection ranges are considered
122
+ // as anchor/focus cell ranges for multi-cell selection.
123
+ const selectionRanges = tableUtils.sortRanges(cellsToSelect.map(cell => writer.createRangeOn(cell)));
124
+ writer.setSelection(selectionRanges);
125
+ }
126
+ else {
127
+ // Set selection inside first cell if multi-cell selection is disabled.
128
+ writer.setSelection(cellsToSelect[0], 0);
129
+ }
130
+ });
131
+ }
132
+ /**
133
+ * Replaces the part of selectedTable with pastedTable.
134
+ */
135
+ _replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer) {
136
+ const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
137
+ // Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
138
+ const pastedTableLocationMap = createLocationMap(pastedTable, pastedWidth, pastedHeight);
139
+ const selectedTableMap = [...new TableWalker(selectedTable, {
140
+ startRow: selection.firstRow,
141
+ endRow: selection.lastRow,
142
+ startColumn: selection.firstColumn,
143
+ endColumn: selection.lastColumn,
144
+ includeAllSlots: true
145
+ })];
146
+ // Selection must be set to pasted cells (some might be removed or new created).
147
+ const cellsToSelect = [];
148
+ // Store next cell insert position.
149
+ let insertPosition;
150
+ // Content table replace cells algorithm iterates over a selected table fragment and:
151
+ //
152
+ // - Removes existing table cells at current slot (location).
153
+ // - Inserts cell from a pasted table for a matched slots.
154
+ //
155
+ // This ensures proper table geometry after the paste
156
+ for (const tableSlot of selectedTableMap) {
157
+ const { row, column } = tableSlot;
158
+ // Save the insert position for current row start.
159
+ if (column === selection.firstColumn) {
160
+ insertPosition = tableSlot.getPositionBefore();
161
+ }
162
+ // Map current table slot location to an pasted table slot location.
163
+ const pastedRow = row - selection.firstRow;
164
+ const pastedColumn = column - selection.firstColumn;
165
+ const pastedCell = pastedTableLocationMap[pastedRow % pastedHeight][pastedColumn % pastedWidth];
166
+ // Clone cell to insert (to duplicate its attributes and children).
167
+ // Cloning is required to support repeating pasted table content when inserting to a bigger selection.
168
+ const cellToInsert = pastedCell ? writer.cloneElement(pastedCell) : null;
169
+ // Replace the cell from the current slot with new table cell.
170
+ const newTableCell = this._replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer);
171
+ // The cell was only removed.
172
+ if (!newTableCell) {
173
+ continue;
174
+ }
175
+ // Trim the cell if it's row/col-spans would exceed selection area.
176
+ trimTableCellIfNeeded(newTableCell, row, column, selection.lastRow, selection.lastColumn, writer);
177
+ cellsToSelect.push(newTableCell);
178
+ insertPosition = writer.createPositionAfter(newTableCell);
179
+ }
180
+ // If there are any headings, all the cells that overlap from heading must be splitted.
181
+ const headingRows = parseInt(selectedTable.getAttribute('headingRows') || '0');
182
+ const headingColumns = parseInt(selectedTable.getAttribute('headingColumns') || '0');
183
+ const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
184
+ const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
185
+ if (areHeadingRowsIntersectingSelection) {
186
+ const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
187
+ const newCells = doHorizontalSplit(selectedTable, headingRows, columnsLimit, writer, selection.firstRow);
188
+ cellsToSelect.push(...newCells);
189
+ }
190
+ if (areHeadingColumnsIntersectingSelection) {
191
+ const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
192
+ const newCells = doVerticalSplit(selectedTable, headingColumns, rowsLimit, writer);
193
+ cellsToSelect.push(...newCells);
194
+ }
195
+ return cellsToSelect;
196
+ }
197
+ /**
198
+ * Replaces a single table slot.
199
+ *
200
+ * @returns Inserted table cell or null if slot should remain empty.
201
+ * @private
202
+ */
203
+ _replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer) {
204
+ const { cell, isAnchor } = tableSlot;
205
+ // If the slot is occupied by a cell in a selected table - remove it.
206
+ // The slot of this cell will be either:
207
+ // - Replaced by a pasted table cell.
208
+ // - Spanned by a previously pasted table cell.
209
+ if (isAnchor) {
210
+ writer.remove(cell);
211
+ }
212
+ // There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
213
+ if (!cellToInsert) {
214
+ return null;
215
+ }
216
+ writer.insert(cellToInsert, insertPosition);
217
+ return cellToInsert;
218
+ }
219
+ /**
220
+ * Extracts the table for pasting into a table.
221
+ *
222
+ * @param content The content to insert.
223
+ * @param model The editor model.
224
+ */
225
+ getTableIfOnlyTableInContent(content, model) {
226
+ if (!content.is('documentFragment') && !content.is('element')) {
227
+ return null;
228
+ }
229
+ // Table passed directly.
230
+ if (content.is('element', 'table')) {
231
+ return content;
232
+ }
233
+ // We do not support mixed content when pasting table into table.
234
+ // See: https://github.com/ckeditor/ckeditor5/issues/6817.
235
+ if (content.childCount == 1 && content.getChild(0).is('element', 'table')) {
236
+ return content.getChild(0);
237
+ }
238
+ // If there are only whitespaces around a table then use that table for pasting.
239
+ const contentRange = model.createRangeIn(content);
240
+ for (const element of contentRange.getItems()) {
241
+ if (element.is('element', 'table')) {
242
+ // Stop checking if there is some content before table.
243
+ const rangeBefore = model.createRange(contentRange.start, model.createPositionBefore(element));
244
+ if (model.hasContent(rangeBefore, { ignoreWhitespaces: true })) {
245
+ return null;
246
+ }
247
+ // Stop checking if there is some content after table.
248
+ const rangeAfter = model.createRange(model.createPositionAfter(element), contentRange.end);
249
+ if (model.hasContent(rangeAfter, { ignoreWhitespaces: true })) {
250
+ return null;
251
+ }
252
+ // There wasn't any content neither before nor after.
253
+ return element;
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+ }
259
+ /**
260
+ * Prepares a table for pasting and returns adjusted selection dimensions.
261
+ */
262
+ function prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils) {
263
+ const selectedTable = selectedTableCells[0].findAncestor('table');
264
+ const columnIndexes = tableUtils.getColumnIndexes(selectedTableCells);
265
+ const rowIndexes = tableUtils.getRowIndexes(selectedTableCells);
266
+ const selection = {
267
+ firstColumn: columnIndexes.first,
268
+ lastColumn: columnIndexes.last,
269
+ firstRow: rowIndexes.first,
270
+ lastRow: rowIndexes.last
271
+ };
272
+ // Single cell selected - expand selection to pasted table dimensions.
273
+ const shouldExpandSelection = selectedTableCells.length === 1;
274
+ if (shouldExpandSelection) {
275
+ selection.lastRow += pastedDimensions.height - 1;
276
+ selection.lastColumn += pastedDimensions.width - 1;
277
+ expandTableSize(selectedTable, selection.lastRow + 1, selection.lastColumn + 1, tableUtils);
278
+ }
279
+ // In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
280
+ // like in the case of a non-rectangular area. This might be fixed by re-setting selected cells array but this shortcut is safe.
281
+ if (shouldExpandSelection || !tableUtils.isSelectionRectangular(selectedTableCells)) {
282
+ // For a non-rectangular selection (ie in which some cells sticks out from a virtual selection rectangle) we need to create
283
+ // a table layout that has a rectangular selection. This will split cells so the selection become rectangular.
284
+ // Beyond this point we will operate on fixed content table.
285
+ splitCellsToRectangularSelection(selectedTable, selection, writer);
286
+ }
287
+ // However a selected table fragment might be invalid if examined alone. Ie such table fragment:
288
+ //
289
+ // +---+---+---+---+
290
+ // 0 | a | b | c | d |
291
+ // + + +---+---+
292
+ // 1 | | e | f | g |
293
+ // + +---+ +---+
294
+ // 2 | | h | | i | <- last row, each cell has rowspan = 2,
295
+ // + + + + + so we need to return 3, not 2
296
+ // 3 | | | | |
297
+ // +---+---+---+---+
298
+ //
299
+ // is invalid as the cells "h" and "i" have rowspans.
300
+ // This case needs only adjusting the selection dimension as the rest of the algorithm operates on empty slots also.
301
+ else {
302
+ selection.lastRow = adjustLastRowIndex(selectedTable, selection);
303
+ selection.lastColumn = adjustLastColumnIndex(selectedTable, selection);
304
+ }
305
+ return selection;
306
+ }
307
+ /**
308
+ * Expand table (in place) to expected size.
309
+ */
310
+ function expandTableSize(table, expectedHeight, expectedWidth, tableUtils) {
311
+ const tableWidth = tableUtils.getColumns(table);
312
+ const tableHeight = tableUtils.getRows(table);
313
+ if (expectedWidth > tableWidth) {
314
+ tableUtils.insertColumns(table, {
315
+ at: tableWidth,
316
+ columns: expectedWidth - tableWidth
317
+ });
318
+ }
319
+ if (expectedHeight > tableHeight) {
320
+ tableUtils.insertRows(table, {
321
+ at: tableHeight,
322
+ rows: expectedHeight - tableHeight
323
+ });
324
+ }
325
+ }
326
+ /**
327
+ * Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
328
+ *
329
+ * At given row & column location it might be one of:
330
+ *
331
+ * * cell - cell from pasted table anchored at this location.
332
+ * * null - if no cell is anchored at this location.
333
+ *
334
+ * For instance, from a table below:
335
+ *
336
+ * +----+----+----+----+
337
+ * | 00 | 01 | 02 | 03 |
338
+ * + +----+----+----+
339
+ * | | 11 | 13 |
340
+ * +----+ +----+
341
+ * | 20 | | 23 |
342
+ * +----+----+----+----+
343
+ *
344
+ * The method will return an array (numbers represents cell element):
345
+ *
346
+ * ```ts
347
+ * const map = [
348
+ * [ '00', '01', '02', '03' ],
349
+ * [ null, '11', null, '13' ],
350
+ * [ '20', null, null, '23' ]
351
+ * ]
352
+ * ```
353
+ *
354
+ * This allows for a quick access to table at give row & column. For instance to access table cell "13" from pasted table call:
355
+ *
356
+ * ```ts
357
+ * const cell = map[ 1 ][ 3 ]
358
+ * ```
359
+ */
360
+ function createLocationMap(table, width, height) {
361
+ // Create height x width (row x column) two-dimensional table to store cells.
362
+ const map = new Array(height).fill(null)
363
+ .map(() => new Array(width).fill(null));
364
+ for (const { column, row, cell } of new TableWalker(table)) {
365
+ map[row][column] = cell;
366
+ }
367
+ return map;
368
+ }
369
+ /**
370
+ * Make selected cells rectangular by splitting the cells that stand out from a rectangular selection.
371
+ *
372
+ * In the table below a selection is shown with "::" and slots with anchor cells are named.
373
+ *
374
+ * +----+----+----+----+----+ +----+----+----+----+----+
375
+ * | 00 | 01 | 02 | 03 | | 00 | 01 | 02 | 03 |
376
+ * + +----+ +----+----+ | ::::::::::::::::----+
377
+ * | | 11 | | 13 | 14 | | ::11 | | 13:: 14 | <- first row
378
+ * +----+----+ + +----+ +----::---| | ::----+
379
+ * | 20 | 21 | | | 24 | select cells: | 20 ::21 | | :: 24 |
380
+ * +----+----+ +----+----+ 11 -> 33 +----::---| |---::----+
381
+ * | 30 | | 33 | 34 | | 30 :: | | 33:: 34 | <- last row
382
+ * + + +----+ + | :::::::::::::::: +
383
+ * | | | 43 | | | | | 43 | |
384
+ * +----+----+----+----+----+ +----+----+----+----+----+
385
+ * ^ ^
386
+ * first & last columns
387
+ *
388
+ * Will update table to:
389
+ *
390
+ * +----+----+----+----+----+
391
+ * | 00 | 01 | 02 | 03 |
392
+ * + +----+----+----+----+
393
+ * | | 11 | | 13 | 14 |
394
+ * +----+----+ + +----+
395
+ * | 20 | 21 | | | 24 |
396
+ * +----+----+ +----+----+
397
+ * | 30 | | | 33 | 34 |
398
+ * + +----+----+----+ +
399
+ * | | | | 43 | |
400
+ * +----+----+----+----+----+
401
+ *
402
+ * In th example above:
403
+ * - Cell "02" which have `rowspan = 4` must be trimmed at first and at after last row.
404
+ * - Cell "03" which have `rowspan = 2` and `colspan = 2` must be trimmed at first column and after last row.
405
+ * - Cells "00", "03" & "30" which cannot be cut by this algorithm as they are outside the trimmed area.
406
+ * - Cell "13" cannot be cut as it is inside the trimmed area.
407
+ */
408
+ function splitCellsToRectangularSelection(table, dimensions, writer) {
409
+ const { firstRow, lastRow, firstColumn, lastColumn } = dimensions;
410
+ const rowIndexes = { first: firstRow, last: lastRow };
411
+ const columnIndexes = { first: firstColumn, last: lastColumn };
412
+ // 1. Split cells vertically in two steps as first step might create cells that needs to split again.
413
+ doVerticalSplit(table, firstColumn, rowIndexes, writer);
414
+ doVerticalSplit(table, lastColumn + 1, rowIndexes, writer);
415
+ // 2. Split cells horizontally in two steps as first step might create cells that needs to split again.
416
+ doHorizontalSplit(table, firstRow, columnIndexes, writer);
417
+ doHorizontalSplit(table, lastRow + 1, columnIndexes, writer, firstRow);
418
+ }
419
+ function doHorizontalSplit(table, splitRow, limitColumns, writer, startRow = 0) {
420
+ // If selection starts at first row then no split is needed.
421
+ if (splitRow < 1) {
422
+ return;
423
+ }
424
+ const overlappingCells = getVerticallyOverlappingCells(table, splitRow, startRow);
425
+ // Filter out cells that are not touching insides of the rectangular selection.
426
+ const cellsToSplit = overlappingCells.filter(({ column, cellWidth }) => isAffectedBySelection(column, cellWidth, limitColumns));
427
+ return cellsToSplit.map(({ cell }) => splitHorizontally(cell, splitRow, writer));
428
+ }
429
+ function doVerticalSplit(table, splitColumn, limitRows, writer) {
430
+ // If selection starts at first column then no split is needed.
431
+ if (splitColumn < 1) {
432
+ return;
433
+ }
434
+ const overlappingCells = getHorizontallyOverlappingCells(table, splitColumn);
435
+ // Filter out cells that are not touching insides of the rectangular selection.
436
+ const cellsToSplit = overlappingCells.filter(({ row, cellHeight }) => isAffectedBySelection(row, cellHeight, limitRows));
437
+ return cellsToSplit.map(({ cell, column }) => splitVertically(cell, column, splitColumn, writer));
438
+ }
439
+ /**
440
+ * Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
441
+ *
442
+ * The same check is used for row as for column.
443
+ */
444
+ function isAffectedBySelection(index, span, limit) {
445
+ const endIndex = index + span - 1;
446
+ const { first, last } = limit;
447
+ const isInsideSelection = index >= first && index <= last;
448
+ const overlapsSelectionFromOutside = index < first && endIndex >= first;
449
+ return isInsideSelection || overlapsSelectionFromOutside;
450
+ }