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 | * The table iterator class. It allows to iterate over table cells. For each cell the iterator yields
|
7 | * {@link module:table/tablewalker~TableSlot} with proper table cell attributes.
|
8 | */
|
9 | export default class TableWalker {
|
10 | /**
|
11 | * Creates an instance of the table walker.
|
12 | *
|
13 | * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
|
14 | * It walks row by row and column by column in order to output values defined in the constructor.
|
15 | * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
|
16 | * pass the `includeAllSlots` option to the constructor.
|
17 | *
|
18 | * The most important values of the iterator are column and row indexes of a cell.
|
19 | *
|
20 | * See {@link module:table/tablewalker~TableSlot} what values are returned by the table walker.
|
21 | *
|
22 | * To iterate over a given row:
|
23 | *
|
24 | * ```ts
|
25 | * const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
|
26 | *
|
27 | * for ( const tableSlot of tableWalker ) {
|
28 | * console.log( 'A cell at row', tableSlot.row, 'and column', tableSlot.column );
|
29 | * }
|
30 | * ```
|
31 | *
|
32 | * For instance the code above for the following table:
|
33 | *
|
34 | * +----+----+----+----+----+----+
|
35 | * | 00 | 02 | 03 | 04 | 05 |
|
36 | * | +----+----+----+----+
|
37 | * | | 12 | 14 | 15 |
|
38 | * | +----+----+----+ +
|
39 | * | | 22 | |
|
40 | * |----+----+----+----+----+ +
|
41 | * | 30 | 31 | 32 | 33 | 34 | |
|
42 | * +----+----+----+----+----+----+
|
43 | *
|
44 | * will log in the console:
|
45 | *
|
46 | * 'A cell at row 1 and column 2'
|
47 | * 'A cell at row 1 and column 4'
|
48 | * 'A cell at row 1 and column 5'
|
49 | * 'A cell at row 2 and column 2'
|
50 | *
|
51 | * To also iterate over spanned cells:
|
52 | *
|
53 | * ```ts
|
54 | * const tableWalker = new TableWalker( table, { row: 1, includeAllSlots: true } );
|
55 | *
|
56 | * for ( const tableSlot of tableWalker ) {
|
57 | * console.log( 'Slot at', tableSlot.row, 'x', tableSlot.column, ':', tableSlot.isAnchor ? 'is anchored' : 'is spanned' );
|
58 | * }
|
59 | * ```
|
60 | *
|
61 | * will log in the console for the table from the previous example:
|
62 | *
|
63 | * 'Cell at 1 x 0 : is spanned'
|
64 | * 'Cell at 1 x 1 : is spanned'
|
65 | * 'Cell at 1 x 2 : is anchored'
|
66 | * 'Cell at 1 x 3 : is spanned'
|
67 | * 'Cell at 1 x 4 : is anchored'
|
68 | * 'Cell at 1 x 5 : is anchored'
|
69 | *
|
70 | * **Note**: Option `row` is a shortcut that sets both `startRow` and `endRow` to the same row.
|
71 | * (Use either `row` or `startRow` and `endRow` but never together). Similarly the `column` option sets both `startColumn`
|
72 | * and `endColumn` to the same column (Use either `column` or `startColumn` and `endColumn` but never together).
|
73 | *
|
74 | * @param table A table over which the walker iterates.
|
75 | * @param options An object with configuration.
|
76 | * @param options.row A row index for which this iterator will output cells. Can't be used together with `startRow` and `endRow`.
|
77 | * @param options.startRow A row index from which this iterator should start. Can't be used together with `row`. Default value is 0.
|
78 | * @param options.endRow A row index at which this iterator should end. Can't be used together with `row`.
|
79 | * @param options.column A column index for which this iterator will output cells.
|
80 | * Can't be used together with `startColumn` and `endColumn`.
|
81 | * @param options.startColumn A column index from which this iterator should start.
|
82 | * Can't be used together with `column`. Default value is 0.
|
83 | * @param options.endColumn A column index at which this iterator should end. Can't be used together with `column`.
|
84 | * @param options.includeAllSlots Also return values for spanned cells. Default value is "false".
|
85 | */
|
86 | constructor(table, options = {}) {
|
87 | this._table = table;
|
88 | this._startRow = options.row !== undefined ? options.row : options.startRow || 0;
|
89 | this._endRow = options.row !== undefined ? options.row : options.endRow;
|
90 | this._startColumn = options.column !== undefined ? options.column : options.startColumn || 0;
|
91 | this._endColumn = options.column !== undefined ? options.column : options.endColumn;
|
92 | this._includeAllSlots = !!options.includeAllSlots;
|
93 | this._skipRows = new Set();
|
94 | this._row = 0;
|
95 | this._rowIndex = 0;
|
96 | this._column = 0;
|
97 | this._cellIndex = 0;
|
98 | this._spannedCells = new Map();
|
99 | this._nextCellAtColumn = -1;
|
100 | }
|
101 | /**
|
102 | * Iterable interface.
|
103 | */
|
104 | [Symbol.iterator]() {
|
105 | return this;
|
106 | }
|
107 | /**
|
108 | * Gets the next table walker's value.
|
109 | *
|
110 | * @returns The next table walker's value.
|
111 | */
|
112 | next() {
|
113 | const row = this._table.getChild(this._rowIndex);
|
114 | // Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
|
115 | if (!row || this._isOverEndRow()) {
|
116 | return { done: true, value: undefined };
|
117 | }
|
118 | // We step over current element when it is not a tableRow instance.
|
119 | if (!row.is('element', 'tableRow')) {
|
120 | this._rowIndex++;
|
121 | return this.next();
|
122 | }
|
123 | if (this._isOverEndColumn()) {
|
124 | return this._advanceToNextRow();
|
125 | }
|
126 | let outValue = null;
|
127 | const spanData = this._getSpanned();
|
128 | if (spanData) {
|
129 | if (this._includeAllSlots && !this._shouldSkipSlot()) {
|
130 | outValue = this._formatOutValue(spanData.cell, spanData.row, spanData.column);
|
131 | }
|
132 | }
|
133 | else {
|
134 | const cell = row.getChild(this._cellIndex);
|
135 | if (!cell) {
|
136 | // If there are no more cells left in row advance to the next row.
|
137 | return this._advanceToNextRow();
|
138 | }
|
139 | const colspan = parseInt(cell.getAttribute('colspan') || '1');
|
140 | const rowspan = parseInt(cell.getAttribute('rowspan') || '1');
|
141 | // Record this cell spans if it's not 1x1 cell.
|
142 | if (colspan > 1 || rowspan > 1) {
|
143 | this._recordSpans(cell, rowspan, colspan);
|
144 | }
|
145 | if (!this._shouldSkipSlot()) {
|
146 | outValue = this._formatOutValue(cell);
|
147 | }
|
148 | this._nextCellAtColumn = this._column + colspan;
|
149 | }
|
150 | // Advance to the next column before returning value.
|
151 | this._column++;
|
152 | if (this._column == this._nextCellAtColumn) {
|
153 | this._cellIndex++;
|
154 | }
|
155 | // The current value will be returned only if current row and column are not skipped.
|
156 | return outValue || this.next();
|
157 | }
|
158 | /**
|
159 | * Marks a row to skip in the next iteration. It will also skip cells from the current row if there are any cells from the current row
|
160 | * to output.
|
161 | *
|
162 | * @param row The row index to skip.
|
163 | */
|
164 | skipRow(row) {
|
165 | this._skipRows.add(row);
|
166 | }
|
167 | /**
|
168 | * Advances internal cursor to the next row.
|
169 | */
|
170 | _advanceToNextRow() {
|
171 | this._row++;
|
172 | this._rowIndex++;
|
173 | this._column = 0;
|
174 | this._cellIndex = 0;
|
175 | this._nextCellAtColumn = -1;
|
176 | return this.next();
|
177 | }
|
178 | /**
|
179 | * Checks if the current row is over {@link #_endRow}.
|
180 | */
|
181 | _isOverEndRow() {
|
182 | // If #_endRow is defined skip all rows after it.
|
183 | return this._endRow !== undefined && this._row > this._endRow;
|
184 | }
|
185 | /**
|
186 | * Checks if the current cell is over {@link #_endColumn}
|
187 | */
|
188 | _isOverEndColumn() {
|
189 | // If #_endColumn is defined skip all cells after it.
|
190 | return this._endColumn !== undefined && this._column > this._endColumn;
|
191 | }
|
192 | /**
|
193 | * A common method for formatting the iterator's output value.
|
194 | *
|
195 | * @param cell The table cell to output.
|
196 | * @param anchorRow The row index of a cell anchor slot.
|
197 | * @param anchorColumn The column index of a cell anchor slot.
|
198 | */
|
199 | _formatOutValue(cell, anchorRow = this._row, anchorColumn = this._column) {
|
200 | return {
|
201 | done: false,
|
202 | value: new TableSlot(this, cell, anchorRow, anchorColumn)
|
203 | };
|
204 | }
|
205 | /**
|
206 | * Checks if the current slot should be skipped.
|
207 | */
|
208 | _shouldSkipSlot() {
|
209 | const rowIsMarkedAsSkipped = this._skipRows.has(this._row);
|
210 | const rowIsBeforeStartRow = this._row < this._startRow;
|
211 | const columnIsBeforeStartColumn = this._column < this._startColumn;
|
212 | const columnIsAfterEndColumn = this._endColumn !== undefined && this._column > this._endColumn;
|
213 | return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
|
214 | }
|
215 | /**
|
216 | * Returns the cell element that is spanned over the current cell location.
|
217 | */
|
218 | _getSpanned() {
|
219 | const rowMap = this._spannedCells.get(this._row);
|
220 | // No spans for given row.
|
221 | if (!rowMap) {
|
222 | return null;
|
223 | }
|
224 | // If spans for given rows has entry for column it means that this location if spanned by other cell.
|
225 | return rowMap.get(this._column) || null;
|
226 | }
|
227 | /**
|
228 | * Updates spanned cells map relative to the current cell location and its span dimensions.
|
229 | *
|
230 | * @param cell A cell that is spanned.
|
231 | * @param rowspan Cell height.
|
232 | * @param colspan Cell width.
|
233 | */
|
234 | _recordSpans(cell, rowspan, colspan) {
|
235 | const data = {
|
236 | cell,
|
237 | row: this._row,
|
238 | column: this._column
|
239 | };
|
240 | for (let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++) {
|
241 | for (let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++) {
|
242 | if (rowToUpdate != this._row || columnToUpdate != this._column) {
|
243 | this._markSpannedCell(rowToUpdate, columnToUpdate, data);
|
244 | }
|
245 | }
|
246 | }
|
247 | }
|
248 | /**
|
249 | * Marks the cell location as spanned by another cell.
|
250 | *
|
251 | * @param row The row index of the cell location.
|
252 | * @param column The column index of the cell location.
|
253 | * @param data A spanned cell details (cell element, anchor row and column).
|
254 | */
|
255 | _markSpannedCell(row, column, data) {
|
256 | if (!this._spannedCells.has(row)) {
|
257 | this._spannedCells.set(row, new Map());
|
258 | }
|
259 | const rowSpans = this._spannedCells.get(row);
|
260 | rowSpans.set(column, data);
|
261 | }
|
262 | }
|
263 | /**
|
264 | * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
|
265 | */
|
266 | class TableSlot {
|
267 | /**
|
268 | * Creates an instance of the table walker value.
|
269 | *
|
270 | * @param tableWalker The table walker instance.
|
271 | * @param cell The current table cell.
|
272 | * @param anchorRow The row index of a cell anchor slot.
|
273 | * @param anchorColumn The column index of a cell anchor slot.
|
274 | */
|
275 | constructor(tableWalker, cell, anchorRow, anchorColumn) {
|
276 | this.cell = cell;
|
277 | this.row = tableWalker._row;
|
278 | this.column = tableWalker._column;
|
279 | this.cellAnchorRow = anchorRow;
|
280 | this.cellAnchorColumn = anchorColumn;
|
281 | this._cellIndex = tableWalker._cellIndex;
|
282 | this._rowIndex = tableWalker._rowIndex;
|
283 | this._table = tableWalker._table;
|
284 | }
|
285 | // @if CK_DEBUG // public get isSpanned(): unknown { return throwMissingGetterError( 'isSpanned' ); }
|
286 | // @if CK_DEBUG // public get colspan(): unknown { return throwMissingGetterError( 'colspan' ); }
|
287 | // @if CK_DEBUG // public get rowspan(): unknown { return throwMissingGetterError( 'rowspan' ); }
|
288 | // @if CK_DEBUG // public get cellIndex(): unknown { return throwMissingGetterError( 'cellIndex' ); }
|
289 | /**
|
290 | * Whether the cell is anchored in the current slot.
|
291 | */
|
292 | get isAnchor() {
|
293 | return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
|
294 | }
|
295 | /**
|
296 | * The width of a cell defined by a `colspan` attribute. If the model attribute is not present, it is set to `1`.
|
297 | */
|
298 | get cellWidth() {
|
299 | return parseInt(this.cell.getAttribute('colspan') || '1');
|
300 | }
|
301 | /**
|
302 | * The height of a cell defined by a `rowspan` attribute. If the model attribute is not present, it is set to `1`.
|
303 | */
|
304 | get cellHeight() {
|
305 | return parseInt(this.cell.getAttribute('rowspan') || '1');
|
306 | }
|
307 | /**
|
308 | * The index of the current row element in the table.
|
309 | */
|
310 | get rowIndex() {
|
311 | return this._rowIndex;
|
312 | }
|
313 | /**
|
314 | * Returns the {@link module:engine/model/position~Position} before the table slot.
|
315 | */
|
316 | getPositionBefore() {
|
317 | const model = this._table.root.document.model;
|
318 | return model.createPositionAt(this._table.getChild(this.row), this._cellIndex);
|
319 | }
|
320 | }
|
321 | /**
|
322 | * This `TableSlot`'s getter (property) was removed in CKEditor 5 v20.0.0.
|
323 | *
|
324 | * Check out the new `TableWalker`'s API in the documentation.
|
325 | *
|
326 | * @error tableslot-getter-removed
|
327 | * @param getterName
|
328 | */
|
329 | // @if CK_DEBUG // function throwMissingGetterError( getterName: string ): void {
|
330 | // @if CK_DEBUG // throw new CKEditorError( 'tableslot-getter-removed', null, {
|
331 | // @if CK_DEBUG // getterName
|
332 | // @if CK_DEBUG // } );
|
333 | // @if CK_DEBUG // }
|