tablewalker.js 15 KB

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