UNPKG

13.5 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
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 */
9export 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 */
266class 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 // }