tablewalker.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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` excludes `startRow` and `endRow` (use `row` or `startRow` and `endRow` but never together).
  71. * Option `column` excludes `startColumn` and `endColumn` (use `column` or `startColumn` and `endColumn` but never together).
  72. *
  73. * @constructor
  74. * @param {module:engine/model/element~Element} table A table over which the walker iterates.
  75. * @param {Object} [options={}] An object with configuration.
  76. * @param {Number} [options.row] A row index for which this iterator will output cells.
  77. * Can't be used together with `startRow` and `endRow`.
  78. * @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
  79. * @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
  80. * @param {Number} [options.column] A column index for which this iterator will output cells.
  81. * Can't be used together with `startColumn` and `endColumn`.
  82. * @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
  83. * @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
  84. * @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
  85. */
  86. constructor( table, options = {} ) {
  87. /**
  88. * The walker's table element.
  89. *
  90. * @readonly
  91. * @member {module:engine/model/element~Element}
  92. * @protected
  93. */
  94. this._table = table;
  95. /**
  96. * A row index from which this iterator will start.
  97. *
  98. * @readonly
  99. * @member {Number}
  100. * @private
  101. */
  102. this._startRow = options.row !== undefined ? options.row : options.startRow || 0;
  103. /**
  104. * A row index at which this iterator will end.
  105. *
  106. * @readonly
  107. * @member {Number}
  108. * @private
  109. */
  110. this._endRow = options.row !== undefined ? options.row : options.endRow;
  111. /**
  112. * If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
  113. *
  114. * @readonly
  115. * @member {Number}
  116. * @private
  117. */
  118. this._startColumn = options.column !== undefined ? options.column : options.startColumn || 0;
  119. /**
  120. * If set, the table walker will only output cells up to a given column.
  121. *
  122. * @readonly
  123. * @member {Number}
  124. * @private
  125. */
  126. this._endColumn = options.column !== undefined ? options.column : options.endColumn;
  127. /**
  128. * Enables output of spanned cells that are normally not yielded.
  129. *
  130. * @readonly
  131. * @member {Boolean}
  132. * @private
  133. */
  134. this._includeAllSlots = !!options.includeAllSlots;
  135. /**
  136. * Row indexes to skip from the iteration.
  137. *
  138. * @readonly
  139. * @member {Set<Number>}
  140. * @private
  141. */
  142. this._skipRows = new Set();
  143. /**
  144. * The current row index.
  145. *
  146. * @member {Number}
  147. * @protected
  148. */
  149. this._row = 0;
  150. /**
  151. * The current column index.
  152. *
  153. * @member {Number}
  154. * @protected
  155. */
  156. this._column = 0;
  157. /**
  158. * The cell index in a parent row. For spanned cells when {@link #_includeAllSlots} is set to `true`,
  159. * this represents the index of the next table cell.
  160. *
  161. * @member {Number}
  162. * @protected
  163. */
  164. this._cellIndex = 0;
  165. /**
  166. * Holds a map of spanned cells in a table.
  167. *
  168. * @readonly
  169. * @member {Map.<Number, Map.<Number, Object>>}
  170. * @private
  171. */
  172. this._spannedCells = new Map();
  173. /**
  174. * Index of the next column where a cell is anchored.
  175. *
  176. * @member {Number}
  177. * @private
  178. */
  179. this._nextCellAtColumn = -1;
  180. }
  181. /**
  182. * Iterable interface.
  183. *
  184. * @returns {Iterable.<module:table/tablewalker~TableSlot>}
  185. */
  186. [ Symbol.iterator ]() {
  187. return this;
  188. }
  189. /**
  190. * Gets the next table walker's value.
  191. *
  192. * @returns {module:table/tablewalker~TableSlot} The next table walker's value.
  193. */
  194. next() {
  195. const row = this._table.getChild( this._row );
  196. // Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
  197. if ( !row || this._isOverEndRow() ) {
  198. return { done: true };
  199. }
  200. if ( this._isOverEndColumn() ) {
  201. return this._advanceToNextRow();
  202. }
  203. let outValue = null;
  204. const spanData = this._getSpanned();
  205. if ( spanData ) {
  206. if ( this._includeAllSlots && !this._shouldSkipSlot() ) {
  207. outValue = this._formatOutValue( spanData.cell, spanData.row, spanData.column );
  208. }
  209. } else {
  210. const cell = row.getChild( this._cellIndex );
  211. if ( !cell ) {
  212. // If there are no more cells left in row advance to the next row.
  213. return this._advanceToNextRow();
  214. }
  215. const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
  216. const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
  217. // Record this cell spans if it's not 1x1 cell.
  218. if ( colspan > 1 || rowspan > 1 ) {
  219. this._recordSpans( cell, rowspan, colspan );
  220. }
  221. if ( !this._shouldSkipSlot() ) {
  222. outValue = this._formatOutValue( cell );
  223. }
  224. this._nextCellAtColumn = this._column + colspan;
  225. }
  226. // Advance to the next column before returning value.
  227. this._column++;
  228. if ( this._column == this._nextCellAtColumn ) {
  229. this._cellIndex++;
  230. }
  231. // The current value will be returned only if current row and column are not skipped.
  232. return outValue || this.next();
  233. }
  234. /**
  235. * 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
  236. * to output.
  237. *
  238. * @param {Number} row The row index to skip.
  239. */
  240. skipRow( row ) {
  241. this._skipRows.add( row );
  242. }
  243. /**
  244. * Advances internal cursor to the next row.
  245. *
  246. * @private
  247. * @returns {module:table/tablewalker~TableSlot}
  248. */
  249. _advanceToNextRow() {
  250. this._row++;
  251. this._column = 0;
  252. this._cellIndex = 0;
  253. this._nextCellAtColumn = -1;
  254. return this.next();
  255. }
  256. /**
  257. * Checks if the current row is over {@link #_endRow}.
  258. *
  259. * @private
  260. * @returns {Boolean}
  261. */
  262. _isOverEndRow() {
  263. // If #_endRow is defined skip all rows after it.
  264. return this._endRow !== undefined && this._row > this._endRow;
  265. }
  266. /**
  267. * Checks if the current cell is over {@link #_endColumn}
  268. *
  269. * @private
  270. * @returns {Boolean}
  271. */
  272. _isOverEndColumn() {
  273. // If #_endColumn is defined skip all cells after it.
  274. return this._endColumn !== undefined && this._column > this._endColumn;
  275. }
  276. /**
  277. * A common method for formatting the iterator's output value.
  278. *
  279. * @private
  280. * @param {module:engine/model/element~Element} cell The table cell to output.
  281. * @param {Number} [anchorRow] The row index of a cell anchor slot.
  282. * @param {Number} [anchorColumn] The column index of a cell anchor slot.
  283. * @returns {{done: Boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
  284. */
  285. _formatOutValue( cell, anchorRow = this._row, anchorColumn = this._column ) {
  286. return {
  287. done: false,
  288. value: new TableSlot( this, cell, anchorRow, anchorColumn )
  289. };
  290. }
  291. /**
  292. * Checks if the current slot should be skipped.
  293. *
  294. * @private
  295. * @returns {Boolean}
  296. */
  297. _shouldSkipSlot() {
  298. const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
  299. const rowIsBeforeStartRow = this._row < this._startRow;
  300. const columnIsBeforeStartColumn = this._column < this._startColumn;
  301. const columnIsAfterEndColumn = this._endColumn !== undefined && this._column > this._endColumn;
  302. return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
  303. }
  304. /**
  305. * Returns the cell element that is spanned over the current cell location.
  306. *
  307. * @private
  308. * @returns {module:engine/model/element~Element}
  309. */
  310. _getSpanned() {
  311. const rowMap = this._spannedCells.get( this._row );
  312. // No spans for given row.
  313. if ( !rowMap ) {
  314. return null;
  315. }
  316. // If spans for given rows has entry for column it means that this location if spanned by other cell.
  317. return rowMap.get( this._column ) || null;
  318. }
  319. /**
  320. * Updates spanned cells map relative to the current cell location and its span dimensions.
  321. *
  322. * @private
  323. * @param {module:engine/model/element~Element} cell A cell that is spanned.
  324. * @param {Number} rowspan Cell height.
  325. * @param {Number} colspan Cell width.
  326. */
  327. _recordSpans( cell, rowspan, colspan ) {
  328. const data = {
  329. cell,
  330. row: this._row,
  331. column: this._column
  332. };
  333. for ( let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++ ) {
  334. for ( let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++ ) {
  335. if ( rowToUpdate != this._row || columnToUpdate != this._column ) {
  336. this._markSpannedCell( rowToUpdate, columnToUpdate, data );
  337. }
  338. }
  339. }
  340. }
  341. /**
  342. * Marks the cell location as spanned by another cell.
  343. *
  344. * @private
  345. * @param {Number} row The row index of the cell location.
  346. * @param {Number} column The column index of the cell location.
  347. * @param {Object} data A spanned cell details (cell element, anchor row and column).
  348. */
  349. _markSpannedCell( row, column, data ) {
  350. if ( !this._spannedCells.has( row ) ) {
  351. this._spannedCells.set( row, new Map() );
  352. }
  353. const rowSpans = this._spannedCells.get( row );
  354. rowSpans.set( column, data );
  355. }
  356. }
  357. /**
  358. * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
  359. */
  360. class TableSlot {
  361. /**
  362. * Creates an instance of the table walker value.
  363. *
  364. * @protected
  365. * @param {module:table/tablewalker~TableWalker} tableWalker The table walker instance.
  366. * @param {module:engine/model/element~Element} cell The current table cell.
  367. * @param {Number} anchorRow The row index of a cell anchor slot.
  368. * @param {Number} anchorColumn The column index of a cell anchor slot.
  369. */
  370. constructor( tableWalker, cell, anchorRow, anchorColumn ) {
  371. /**
  372. * The current table cell.
  373. *
  374. * @readonly
  375. * @member {module:engine/model/element~Element}
  376. */
  377. this.cell = cell;
  378. /**
  379. * The row index of a table slot.
  380. *
  381. * @readonly
  382. * @member {Number}
  383. */
  384. this.row = tableWalker._row;
  385. /**
  386. * The column index of a table slot.
  387. *
  388. * @readonly
  389. * @member {Number}
  390. */
  391. this.column = tableWalker._column;
  392. /**
  393. * The row index of a cell anchor slot.
  394. *
  395. * @readonly
  396. * @member {Number}
  397. */
  398. this.cellAnchorRow = anchorRow;
  399. /**
  400. * The column index of a cell anchor slot.
  401. *
  402. * @readonly
  403. * @member {Number}
  404. */
  405. this.cellAnchorColumn = anchorColumn;
  406. /**
  407. * The index of the current cell in the parent row.
  408. *
  409. * @readonly
  410. * @member {Number}
  411. * @private
  412. */
  413. this._cellIndex = tableWalker._cellIndex;
  414. /**
  415. * The table element.
  416. *
  417. * @readonly
  418. * @member {module:engine/model/element~Element}
  419. * @private
  420. */
  421. this._table = tableWalker._table;
  422. }
  423. /**
  424. * Whether the cell is anchored in the current slot.
  425. *
  426. * @readonly
  427. * @returns {Boolean}
  428. */
  429. get isAnchor() {
  430. return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
  431. }
  432. /**
  433. * The `colspan` attribute of a cell. If the model attribute is not present, it is set to `1`.
  434. *
  435. * @readonly
  436. * @returns {Number}
  437. */
  438. get cellWidth() {
  439. return parseInt( this.cell.getAttribute( 'colspan' ) || 1 );
  440. }
  441. /**
  442. * The `rowspan` attribute of a cell. If the model attribute is not present, it is set to `1`.
  443. *
  444. * @readonly
  445. * @returns {Number}
  446. */
  447. get cellHeight() {
  448. return parseInt( this.cell.getAttribute( 'rowspan' ) || 1 );
  449. }
  450. /**
  451. * Returns the {@link module:engine/model/position~Position} before the table slot.
  452. *
  453. * @returns {module:engine/model/position~Position}
  454. */
  455. getPositionBefore() {
  456. const model = this._table.root.document.model;
  457. return model.createPositionAt( this._table.getChild( this.row ), this._cellIndex );
  458. }
  459. // @if CK_DEBUG // get isSpanned() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
  460. // @if CK_DEBUG // get colspan() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
  461. // @if CK_DEBUG // get rowspan() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
  462. // @if CK_DEBUG // get cellIndex() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
  463. }