tablewalker.js 14 KB

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