/
tablewalker.js
538 lines (477 loc) · 14.8 KB
/
tablewalker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module table/tablewalker
*/
// @if CK_DEBUG // import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
/**
* The table iterator class. It allows to iterate over table cells. For each cell the iterator yields
* {@link module:table/tablewalker~TableSlot} with proper table cell attributes.
*/
export default class TableWalker {
/**
* Creates an instance of the table walker.
*
* The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
* It walks row by row and column by column in order to output values defined in the constructor.
* By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
* pass the `includeAllSlots` option to the constructor.
*
* The most important values of the iterator are column and row indexes of a cell.
*
* See {@link module:table/tablewalker~TableSlot} what values are returned by the table walker.
*
* To iterate over a given row:
*
* const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
*
* for ( const tableSlot of tableWalker ) {
* console.log( 'A cell at row', tableSlot.row, 'and column', tableSlot.column );
* }
*
* For instance the code above for the following table:
*
* +----+----+----+----+----+----+
* | 00 | 02 | 03 | 04 | 05 |
* | +----+----+----+----+
* | | 12 | 14 | 15 |
* | +----+----+----+ +
* | | 22 | |
* |----+----+----+----+----+ +
* | 30 | 31 | 32 | 33 | 34 | |
* +----+----+----+----+----+----+
*
* will log in the console:
*
* 'A cell at row 1 and column 2'
* 'A cell at row 1 and column 4'
* 'A cell at row 1 and column 5'
* 'A cell at row 2 and column 2'
*
* To also iterate over spanned cells:
*
* const tableWalker = new TableWalker( table, { row: 1, includeAllSlots: true } );
*
* for ( const tableSlot of tableWalker ) {
* console.log( 'Slot at', tableSlot.row, 'x', tableSlot.column, ':', tableSlot.isAnchor ? 'is anchored' : 'is spanned' );
* }
*
* will log in the console for the table from the previous example:
*
* 'Cell at 1 x 0 : is spanned'
* 'Cell at 1 x 1 : is spanned'
* 'Cell at 1 x 2 : is anchored'
* 'Cell at 1 x 3 : is spanned'
* 'Cell at 1 x 4 : is anchored'
* 'Cell at 1 x 5 : is anchored'
*
* **Note**: Option `row` is a shortcut that sets both `startRow` and `endRow` to the same row.
* (Use either `row` or `startRow` and `endRow` but never together). Similarly the `column` option sets both `startColumn`
* and `endColumn` to the same column (Use either `column` or `startColumn` and `endColumn` but never together).
*
* @constructor
* @param {module:engine/model/element~Element} table A table over which the walker iterates.
* @param {Object} [options={}] An object with configuration.
* @param {Number} [options.row] A row index for which this iterator will output cells.
* Can't be used together with `startRow` and `endRow`.
* @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
* @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
* @param {Number} [options.column] A column index for which this iterator will output cells.
* Can't be used together with `startColumn` and `endColumn`.
* @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
* @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
* @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
*/
constructor( table, options = {} ) {
/**
* The walker's table element.
*
* @readonly
* @member {module:engine/model/element~Element}
* @protected
*/
this._table = table;
/**
* A row index from which this iterator will start.
*
* @readonly
* @member {Number}
* @private
*/
this._startRow = options.row !== undefined ? options.row : options.startRow || 0;
/**
* A row index at which this iterator will end.
*
* @readonly
* @member {Number}
* @private
*/
this._endRow = options.row !== undefined ? options.row : options.endRow;
/**
* If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
*
* @readonly
* @member {Number}
* @private
*/
this._startColumn = options.column !== undefined ? options.column : options.startColumn || 0;
/**
* If set, the table walker will only output cells up to a given column.
*
* @readonly
* @member {Number}
* @private
*/
this._endColumn = options.column !== undefined ? options.column : options.endColumn;
/**
* Enables output of spanned cells that are normally not yielded.
*
* @readonly
* @member {Boolean}
* @private
*/
this._includeAllSlots = !!options.includeAllSlots;
/**
* Row indexes to skip from the iteration.
*
* @readonly
* @member {Set<Number>}
* @private
*/
this._skipRows = new Set();
/**
* The current row index.
*
* @member {Number}
* @protected
*/
this._row = 0;
/**
* The current column index.
*
* @member {Number}
* @protected
*/
this._column = 0;
/**
* The cell index in a parent row. For spanned cells when {@link #_includeAllSlots} is set to `true`,
* this represents the index of the next table cell.
*
* @member {Number}
* @protected
*/
this._cellIndex = 0;
/**
* Holds a map of spanned cells in a table.
*
* @readonly
* @member {Map.<Number, Map.<Number, Object>>}
* @private
*/
this._spannedCells = new Map();
/**
* Index of the next column where a cell is anchored.
*
* @member {Number}
* @private
*/
this._nextCellAtColumn = -1;
}
/**
* Iterable interface.
*
* @returns {Iterable.<module:table/tablewalker~TableSlot>}
*/
[ Symbol.iterator ]() {
return this;
}
/**
* Gets the next table walker's value.
*
* @returns {module:table/tablewalker~TableSlot} The next table walker's value.
*/
next() {
const row = this._table.getChild( this._row );
// Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
if ( !row || this._isOverEndRow() ) {
return { done: true };
}
if ( this._isOverEndColumn() ) {
return this._advanceToNextRow();
}
let outValue = null;
const spanData = this._getSpanned();
if ( spanData ) {
if ( this._includeAllSlots && !this._shouldSkipSlot() ) {
outValue = this._formatOutValue( spanData.cell, spanData.row, spanData.column );
}
} else {
const cell = row.getChild( this._cellIndex );
if ( !cell ) {
// If there are no more cells left in row advance to the next row.
return this._advanceToNextRow();
}
const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
// Record this cell spans if it's not 1x1 cell.
if ( colspan > 1 || rowspan > 1 ) {
this._recordSpans( cell, rowspan, colspan );
}
if ( !this._shouldSkipSlot() ) {
outValue = this._formatOutValue( cell );
}
this._nextCellAtColumn = this._column + colspan;
}
// Advance to the next column before returning value.
this._column++;
if ( this._column == this._nextCellAtColumn ) {
this._cellIndex++;
}
// The current value will be returned only if current row and column are not skipped.
return outValue || this.next();
}
/**
* 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
* to output.
*
* @param {Number} row The row index to skip.
*/
skipRow( row ) {
this._skipRows.add( row );
}
/**
* Advances internal cursor to the next row.
*
* @private
* @returns {module:table/tablewalker~TableSlot}
*/
_advanceToNextRow() {
this._row++;
this._column = 0;
this._cellIndex = 0;
this._nextCellAtColumn = -1;
return this.next();
}
/**
* Checks if the current row is over {@link #_endRow}.
*
* @private
* @returns {Boolean}
*/
_isOverEndRow() {
// If #_endRow is defined skip all rows after it.
return this._endRow !== undefined && this._row > this._endRow;
}
/**
* Checks if the current cell is over {@link #_endColumn}
*
* @private
* @returns {Boolean}
*/
_isOverEndColumn() {
// If #_endColumn is defined skip all cells after it.
return this._endColumn !== undefined && this._column > this._endColumn;
}
/**
* A common method for formatting the iterator's output value.
*
* @private
* @param {module:engine/model/element~Element} cell The table cell to output.
* @param {Number} [anchorRow] The row index of a cell anchor slot.
* @param {Number} [anchorColumn] The column index of a cell anchor slot.
* @returns {{done: Boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
*/
_formatOutValue( cell, anchorRow = this._row, anchorColumn = this._column ) {
return {
done: false,
value: new TableSlot( this, cell, anchorRow, anchorColumn )
};
}
/**
* Checks if the current slot should be skipped.
*
* @private
* @returns {Boolean}
*/
_shouldSkipSlot() {
const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
const rowIsBeforeStartRow = this._row < this._startRow;
const columnIsBeforeStartColumn = this._column < this._startColumn;
const columnIsAfterEndColumn = this._endColumn !== undefined && this._column > this._endColumn;
return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
}
/**
* Returns the cell element that is spanned over the current cell location.
*
* @private
* @returns {module:engine/model/element~Element}
*/
_getSpanned() {
const rowMap = this._spannedCells.get( this._row );
// No spans for given row.
if ( !rowMap ) {
return null;
}
// If spans for given rows has entry for column it means that this location if spanned by other cell.
return rowMap.get( this._column ) || null;
}
/**
* Updates spanned cells map relative to the current cell location and its span dimensions.
*
* @private
* @param {module:engine/model/element~Element} cell A cell that is spanned.
* @param {Number} rowspan Cell height.
* @param {Number} colspan Cell width.
*/
_recordSpans( cell, rowspan, colspan ) {
const data = {
cell,
row: this._row,
column: this._column
};
for ( let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++ ) {
for ( let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++ ) {
if ( rowToUpdate != this._row || columnToUpdate != this._column ) {
this._markSpannedCell( rowToUpdate, columnToUpdate, data );
}
}
}
}
/**
* Marks the cell location as spanned by another cell.
*
* @private
* @param {Number} row The row index of the cell location.
* @param {Number} column The column index of the cell location.
* @param {Object} data A spanned cell details (cell element, anchor row and column).
*/
_markSpannedCell( row, column, data ) {
if ( !this._spannedCells.has( row ) ) {
this._spannedCells.set( row, new Map() );
}
const rowSpans = this._spannedCells.get( row );
rowSpans.set( column, data );
}
}
/**
* An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
*/
class TableSlot {
/**
* Creates an instance of the table walker value.
*
* @protected
* @param {module:table/tablewalker~TableWalker} tableWalker The table walker instance.
* @param {module:engine/model/element~Element} cell The current table cell.
* @param {Number} anchorRow The row index of a cell anchor slot.
* @param {Number} anchorColumn The column index of a cell anchor slot.
*/
constructor( tableWalker, cell, anchorRow, anchorColumn ) {
/**
* The current table cell.
*
* @readonly
* @member {module:engine/model/element~Element}
*/
this.cell = cell;
/**
* The row index of a table slot.
*
* @readonly
* @member {Number}
*/
this.row = tableWalker._row;
/**
* The column index of a table slot.
*
* @readonly
* @member {Number}
*/
this.column = tableWalker._column;
/**
* The row index of a cell anchor slot.
*
* @readonly
* @member {Number}
*/
this.cellAnchorRow = anchorRow;
/**
* The column index of a cell anchor slot.
*
* @readonly
* @member {Number}
*/
this.cellAnchorColumn = anchorColumn;
/**
* The index of the current cell in the parent row.
*
* @readonly
* @member {Number}
* @private
*/
this._cellIndex = tableWalker._cellIndex;
/**
* The table element.
*
* @readonly
* @member {module:engine/model/element~Element}
* @private
*/
this._table = tableWalker._table;
}
/**
* Whether the cell is anchored in the current slot.
*
* @readonly
* @returns {Boolean}
*/
get isAnchor() {
return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
}
/**
* The width of a cell defined by a `colspan` attribute. If the model attribute is not present, it is set to `1`.
*
* @readonly
* @returns {Number}
*/
get cellWidth() {
return parseInt( this.cell.getAttribute( 'colspan' ) || 1 );
}
/**
* The height of a cell defined by a `rowspan` attribute. If the model attribute is not present, it is set to `1`.
*
* @readonly
* @returns {Number}
*/
get cellHeight() {
return parseInt( this.cell.getAttribute( 'rowspan' ) || 1 );
}
/**
* Returns the {@link module:engine/model/position~Position} before the table slot.
*
* @returns {module:engine/model/position~Position}
*/
getPositionBefore() {
const model = this._table.root.document.model;
return model.createPositionAt( this._table.getChild( this.row ), this._cellIndex );
}
// @if CK_DEBUG // get isSpanned() { throwMissingGetterError( 'isSpanned' ); }
// @if CK_DEBUG // get colspan() { throwMissingGetterError( 'colspan' ); }
// @if CK_DEBUG // get rowspan() { throwMissingGetterError( 'rowspan' ); }
// @if CK_DEBUG // get cellIndex() { throwMissingGetterError( 'cellIndex' ); }
}
/**
* This `TableSlot`'s getter (property) was removed in CKEditor 5 v20.0.0.
*
* Check out the new `TableWalker`'s API in the documentation.
*
* @error tableslot-getter-removed
* @param {String} getterName
*/
// @if CK_DEBUG // function throwMissingGetterError( getterName ) {
// @if CK_DEBUG // throw new CKEditorError( 'tableslot-getter-removed', this, {
// @if CK_DEBUG // getterName
// @if CK_DEBUG // } );
// @if CK_DEBUG // }