tableutils.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  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/tableutils
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import TableWalker from './tablewalker';
  10. import { createEmptyTableCell, updateNumericAttribute } from './commands/utils';
  11. /**
  12. * The table utilities plugin.
  13. *
  14. * @extends module:core/plugin~Plugin
  15. */
  16. export default class TableUtils extends Plugin {
  17. /**
  18. * @inheritDoc
  19. */
  20. static get pluginName() {
  21. return 'TableUtils';
  22. }
  23. /**
  24. * Returns the table cell location as an object with table row and table column indexes.
  25. *
  26. * For instance, in the table below:
  27. *
  28. * 0 1 2 3
  29. * +---+---+---+---+
  30. * 0 | a | b | c |
  31. * + + +---+
  32. * 1 | | | d |
  33. * +---+---+ +---+
  34. * 2 | e | | f |
  35. * +---+---+---+---+
  36. *
  37. * the method will return:
  38. *
  39. * const cellA = table.getNodeByPath( [ 0, 0 ] );
  40. * editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
  41. * // will return { row: 0, column: 0 }
  42. *
  43. * const cellD = table.getNodeByPath( [ 1, 0 ] );
  44. * editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
  45. * // will return { row: 1, column: 3 }
  46. *
  47. * @param {module:engine/model/element~Element} tableCell
  48. * @returns {Object} Returns a `{row, column}` object.
  49. */
  50. getCellLocation( tableCell ) {
  51. const tableRow = tableCell.parent;
  52. const table = tableRow.parent;
  53. const rowIndex = table.getChildIndex( tableRow );
  54. const tableWalker = new TableWalker( table, { startRow: rowIndex, endRow: rowIndex } );
  55. for ( const { cell, row, column } of tableWalker ) {
  56. if ( cell === tableCell ) {
  57. return { row, column };
  58. }
  59. }
  60. }
  61. /**
  62. * Creates an empty table with a proper structure. The table needs to be inserted into the model,
  63. * for example, by using the {@link module:engine/model/model~Model#insertContent} function.
  64. *
  65. * model.change( ( writer ) => {
  66. * // Create a table of 2 rows and 7 columns:
  67. * const table = tableUtils.createTable( writer, 2, 7);
  68. *
  69. * // Insert a table to the model at the best position taking the current selection:
  70. * model.insertContent( table );
  71. * }
  72. *
  73. * @param {module:engine/model/writer~Writer} writer The model writer.
  74. * @param {Number} rows The number of rows to create.
  75. * @param {Number} columns The number of columns to create.
  76. * @returns {module:engine/model/element~Element} The created table element.
  77. */
  78. createTable( writer, rows, columns ) {
  79. const table = writer.createElement( 'table' );
  80. createEmptyRows( writer, table, 0, rows, columns );
  81. return table;
  82. }
  83. /**
  84. * Inserts rows into a table.
  85. *
  86. * editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
  87. *
  88. * Assuming the table on the left, the above code will transform it to the table on the right:
  89. *
  90. * row index
  91. * 0 +---+---+---+ `at` = 1, +---+---+---+ 0
  92. * | a | b | c | `rows` = 2, | a | b | c |
  93. * 1 + +---+---+ <-- insert here + +---+---+ 1
  94. * | | d | e | | | | |
  95. * 2 + +---+---+ will give: + +---+---+ 2
  96. * | | f | g | | | | |
  97. * 3 +---+---+---+ + +---+---+ 3
  98. * | | d | e |
  99. * + +---+---+ 4
  100. * + + f | g |
  101. * +---+---+---+ 5
  102. *
  103. * @param {module:engine/model/element~Element} table The table model element where the rows will be inserted.
  104. * @param {Object} options
  105. * @param {Number} [options.at=0] The row index at which the rows will be inserted.
  106. * @param {Number} [options.rows=1] The number of rows to insert.
  107. */
  108. insertRows( table, options = {} ) {
  109. const model = this.editor.model;
  110. const insertAt = options.at || 0;
  111. const rowsToInsert = options.rows || 1;
  112. model.change( writer => {
  113. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  114. // Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
  115. if ( headingRows > insertAt ) {
  116. writer.setAttribute( 'headingRows', headingRows + rowsToInsert, table );
  117. }
  118. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  119. if ( insertAt === 0 || insertAt === table.childCount ) {
  120. createEmptyRows( writer, table, insertAt, rowsToInsert, this.getColumns( table ) );
  121. return;
  122. }
  123. // Iterate over all rows above inserted rows in order to check for rowspanned cells.
  124. const tableIterator = new TableWalker( table, { endRow: insertAt } );
  125. // Will hold number of cells needed to insert in created rows.
  126. // The number might be different then table cell width when there are rowspanned cells.
  127. let cellsToInsert = 0;
  128. for ( const { row, rowspan, colspan, cell } of tableIterator ) {
  129. const isBeforeInsertedRow = row < insertAt;
  130. const overlapsInsertedRow = row + rowspan > insertAt;
  131. if ( isBeforeInsertedRow && overlapsInsertedRow ) {
  132. // This cell overlaps inserted rows so we need to expand it further.
  133. writer.setAttribute( 'rowspan', rowspan + rowsToInsert, cell );
  134. }
  135. // Calculate how many cells to insert based on the width of cells in a row at insert position.
  136. // It might be lower then table width as some cells might overlaps inserted row.
  137. // In the table above the cell 'a' overlaps inserted row so only two empty cells are need to be created.
  138. if ( row === insertAt ) {
  139. cellsToInsert += colspan;
  140. }
  141. }
  142. createEmptyRows( writer, table, insertAt, rowsToInsert, cellsToInsert );
  143. } );
  144. }
  145. /**
  146. * Inserts columns into a table.
  147. *
  148. * editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
  149. *
  150. * Assuming the table on the left, the above code will transform it to the table on the right:
  151. *
  152. * 0 1 2 3 0 1 2 3 4 5
  153. * +---+---+---+ +---+---+---+---+---+
  154. * | a | b | | a | b |
  155. * + +---+ + +---+
  156. * | | c | | | c |
  157. * +---+---+---+ will give: +---+---+---+---+---+
  158. * | d | e | f | | d | | | e | f |
  159. * +---+ +---+ +---+---+---+ +---+
  160. * | g | | h | | g | | | | h |
  161. * +---+---+---+ +---+---+---+---+---+
  162. * | i | | i |
  163. * +---+---+---+ +---+---+---+---+---+
  164. * ^---- insert here, `at` = 1, `columns` = 2
  165. *
  166. * @param {module:engine/model/element~Element} table The table model element where the columns will be inserted.
  167. * @param {Object} options
  168. * @param {Number} [options.at=0] The column index at which the columns will be inserted.
  169. * @param {Number} [options.columns=1] The number of columns to insert.
  170. */
  171. insertColumns( table, options = {} ) {
  172. const model = this.editor.model;
  173. const insertAt = options.at || 0;
  174. const columnsToInsert = options.columns || 1;
  175. model.change( writer => {
  176. const headingColumns = table.getAttribute( 'headingColumns' );
  177. // Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
  178. if ( insertAt < headingColumns ) {
  179. writer.setAttribute( 'headingColumns', headingColumns + columnsToInsert, table );
  180. }
  181. const tableColumns = this.getColumns( table );
  182. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  183. if ( insertAt === 0 || tableColumns === insertAt ) {
  184. for ( const tableRow of table.getChildren() ) {
  185. createCells( columnsToInsert, writer, writer.createPositionAt( tableRow, insertAt ? 'end' : 0 ) );
  186. }
  187. return;
  188. }
  189. const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
  190. for ( const { row, cell, cellIndex } of tableWalker ) {
  191. // When iterating over column the table walker outputs either:
  192. // - cells at given column index (cell "e" from method docs),
  193. // - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeSpanned: true`),
  194. // - or a cell from the same row which spans over this column (cell "a").
  195. const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
  196. const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
  197. if ( cellIndex !== insertAt && colspan > 1 ) {
  198. // If column is different than `insertAt`, it is a cell that spans over an inserted column (cell "a" & "i").
  199. // For such cells expand them by a number of columns inserted.
  200. writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
  201. // The `includeSpanned` option will output the "empty"/spanned column so skip this row already.
  202. tableWalker.skipRow( row );
  203. // This cell will overlap cells in rows below so skip them also (because of `includeSpanned` option) - (cell "a")
  204. if ( rowspan > 1 ) {
  205. for ( let i = row + 1; i < row + rowspan; i++ ) {
  206. tableWalker.skipRow( i );
  207. }
  208. }
  209. } else {
  210. // It's either cell at this column index or spanned cell by a rowspanned cell from row above.
  211. // In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
  212. const insertPosition = writer.createPositionAt( table.getChild( row ), cellIndex );
  213. createCells( columnsToInsert, writer, insertPosition );
  214. }
  215. }
  216. } );
  217. }
  218. /**
  219. * Removes rows from the given `table`.
  220. *
  221. * This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
  222. * and table headings values.
  223. *
  224. * editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
  225. *
  226. * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
  227. *
  228. * row index
  229. * ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
  230. * 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
  231. * │ ├───┼───┤ │ ├───┼───┤
  232. * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
  233. * │ │ ├───┤ will give: ├───┼───┼───┤
  234. * 2 │ │ │ f │ │ h │ i │ j │ 2
  235. * │ │ ├───┤ └───┴───┴───┘
  236. * 3 │ │ │ g │
  237. * ├───┼───┼───┤
  238. * 4 │ h │ i │ j │
  239. * └───┴───┴───┘
  240. *
  241. * @param {module:engine/model/element~Element} table
  242. * @param {Object} options
  243. * @param {Number} options.at The row index at which the removing rows will start.
  244. * @param {Number} [options.rows=1] The number of rows to remove.
  245. */
  246. removeRows( table, options ) {
  247. const model = this.editor.model;
  248. const rowsToRemove = options.rows || 1;
  249. const first = options.at;
  250. const last = first + rowsToRemove - 1;
  251. const batch = options.batch || 'default';
  252. model.enqueueChange( batch, writer => {
  253. // Removing rows from the table require that most calculations to be done prior to changing table structure.
  254. // Preparations must be done in the same enqueueChange callback to use the current table structure.
  255. // 1. Preparation - get row-spanned cells that have to be modified after removing rows.
  256. const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
  257. // 2. Execution
  258. // 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
  259. // This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
  260. if ( cellsToMove.size ) {
  261. const rowAfterRemovedSection = last + 1;
  262. moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
  263. }
  264. // 2b. Remove all required rows.
  265. for ( let i = last; i >= first; i-- ) {
  266. writer.remove( table.getChild( i ) );
  267. }
  268. // 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
  269. for ( const { rowspan, cell } of cellsToTrim ) {
  270. updateNumericAttribute( 'rowspan', rowspan, cell, writer );
  271. }
  272. // 2d. Adjust heading rows if removed rows were in a heading section.
  273. updateHeadingRows( table, first, last, model, batch );
  274. } );
  275. }
  276. /**
  277. * Removes columns from the given `table`.
  278. *
  279. * This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
  280. * and table headings values.
  281. *
  282. * editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
  283. *
  284. * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
  285. *
  286. * 0 1 2 3 4 0 1 2
  287. * ┌───────────────┬───┐ ┌───────┬───┐
  288. * │ a │ b │ │ a │ b │
  289. * │ ├───┤ │ ├───┤
  290. * │ │ c │ │ │ c │
  291. * ├───┬───┬───┬───┼───┤ will give: ├───┬───┼───┤
  292. * │ d │ e │ f │ g │ h │ │ d │ g │ h │
  293. * ├───┼───┼───┤ ├───┤ ├───┤ ├───┤
  294. * │ i │ j │ k │ │ l │ │ i │ │ l │
  295. * ├───┴───┴───┴───┴───┤ ├───┴───┴───┤
  296. * │ m │ │ m │
  297. * └───────────────────┘ └───────────┘
  298. * ^---- remove from here, `at` = 1, `columns` = 2
  299. *
  300. * @param {module:engine/model/element~Element} table
  301. * @param {Object} options
  302. * @param {Number} options.at The row index at which the removing columns will start.
  303. * @param {Number} [options.columns=1] The number of columns to remove.
  304. */
  305. removeColumns( table, options ) {
  306. const model = this.editor.model;
  307. const first = options.at;
  308. const columnsToRemove = options.columns || 1;
  309. const last = options.at + columnsToRemove - 1;
  310. model.change( writer => {
  311. adjustHeadingColumns( table, { first, last }, writer );
  312. const emptyRowsIndexes = [];
  313. for ( let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex-- ) {
  314. for ( const { cell, column, colspan } of [ ...new TableWalker( table ) ] ) {
  315. // If colspaned cell overlaps removed column decrease its span.
  316. if ( column <= removedColumnIndex && colspan > 1 && column + colspan > removedColumnIndex ) {
  317. updateNumericAttribute( 'colspan', colspan - 1, cell, writer );
  318. } else if ( column === removedColumnIndex ) {
  319. const cellRow = cell.parent;
  320. // The cell in removed column has colspan of 1.
  321. writer.remove( cell );
  322. // If the cell was the last one in the row, get rid of the entire row.
  323. // https://github.com/ckeditor/ckeditor5/issues/6429
  324. if ( !cellRow.childCount ) {
  325. emptyRowsIndexes.push( cellRow.index );
  326. }
  327. }
  328. }
  329. }
  330. emptyRowsIndexes.reverse().forEach( row => this.removeRows( table, { at: row, batch: writer.batch } ) );
  331. } );
  332. }
  333. /**
  334. * Divides a table cell vertically into several ones.
  335. *
  336. * The cell will be visually split into more cells by updating colspans of other cells in a column
  337. * and inserting cells (columns) after that cell.
  338. *
  339. * In the table below, if cell "a" is split into 3 cells:
  340. *
  341. * +---+---+---+
  342. * | a | b | c |
  343. * +---+---+---+
  344. * | d | e | f |
  345. * +---+---+---+
  346. *
  347. * it will result in the table below:
  348. *
  349. * +---+---+---+---+---+
  350. * | a | | | b | c |
  351. * +---+---+---+---+---+
  352. * | d | e | f |
  353. * +---+---+---+---+---+
  354. *
  355. * So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
  356. *
  357. * Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
  358. * will be left to the original cell:
  359. *
  360. * +---+---+---+
  361. * | a |
  362. * +---+---+---+
  363. * | b | c | d |
  364. * +---+---+---+
  365. *
  366. * Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
  367. *
  368. * +---+---+---+
  369. * | a | |
  370. * +---+---+---+
  371. * | b | c | d |
  372. * +---+---+---+
  373. *
  374. * @param {module:engine/model/element~Element} tableCell
  375. * @param {Number} numberOfCells
  376. */
  377. splitCellVertically( tableCell, numberOfCells = 2 ) {
  378. const model = this.editor.model;
  379. const tableRow = tableCell.parent;
  380. const table = tableRow.parent;
  381. const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
  382. const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
  383. model.change( writer => {
  384. // First check - the cell spans over multiple rows so before doing anything else just split this cell.
  385. if ( colspan > 1 ) {
  386. // Get spans of new (inserted) cells and span to update of split cell.
  387. const { newCellsSpan, updatedSpan } = breakSpanEvenly( colspan, numberOfCells );
  388. updateNumericAttribute( 'colspan', updatedSpan, tableCell, writer );
  389. // Each inserted cell will have the same attributes:
  390. const newCellsAttributes = {};
  391. // Do not store default value in the model.
  392. if ( newCellsSpan > 1 ) {
  393. newCellsAttributes.colspan = newCellsSpan;
  394. }
  395. // Copy rowspan of split cell.
  396. if ( rowspan > 1 ) {
  397. newCellsAttributes.rowspan = rowspan;
  398. }
  399. const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
  400. createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );
  401. }
  402. // Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
  403. if ( colspan < numberOfCells ) {
  404. const cellsToInsert = numberOfCells - colspan;
  405. // First step: expand cells on the same column as split cell.
  406. const tableMap = [ ...new TableWalker( table ) ];
  407. // Get the column index of split cell.
  408. const { column: splitCellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
  409. // Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
  410. const cellsToUpdate = tableMap.filter( ( { cell, colspan, column } ) => {
  411. const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
  412. const spansOverColumn = ( column < splitCellColumn && column + colspan > splitCellColumn );
  413. return isOnSameColumn || spansOverColumn;
  414. } );
  415. // Expand cells vertically.
  416. for ( const { cell, colspan } of cellsToUpdate ) {
  417. writer.setAttribute( 'colspan', colspan + cellsToInsert, cell );
  418. }
  419. // Second step: create columns after split cell.
  420. // Each inserted cell will have the same attributes:
  421. const newCellsAttributes = {};
  422. // Do not store default value in the model.
  423. // Copy rowspan of split cell.
  424. if ( rowspan > 1 ) {
  425. newCellsAttributes.rowspan = rowspan;
  426. }
  427. createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );
  428. const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
  429. // Update heading section if split cell is in heading section.
  430. if ( headingColumns > splitCellColumn ) {
  431. updateNumericAttribute( 'headingColumns', headingColumns + cellsToInsert, table, writer );
  432. }
  433. }
  434. } );
  435. }
  436. /**
  437. * Divides a table cell horizontally into several ones.
  438. *
  439. * The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
  440. * below.
  441. *
  442. * If in the table below cell "b" is split into 3 cells:
  443. *
  444. * +---+---+---+
  445. * | a | b | c |
  446. * +---+---+---+
  447. * | d | e | f |
  448. * +---+---+---+
  449. *
  450. * It will result in the table below:
  451. *
  452. * +---+---+---+
  453. * | a | b | c |
  454. * + +---+ +
  455. * | | | |
  456. * + +---+ +
  457. * | | | |
  458. * +---+---+---+
  459. * | d | e | f |
  460. * +---+---+---+
  461. *
  462. * So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
  463. *
  464. * Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
  465. * will be left to the original cell:
  466. *
  467. * +---+---+---+
  468. * | a | b | c |
  469. * + +---+---+
  470. * | | d | e |
  471. * + +---+---+
  472. * | | f | g |
  473. * + +---+---+
  474. * | | h | i |
  475. * +---+---+---+
  476. *
  477. * Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
  478. *
  479. * +---+---+---+
  480. * | a | b | c |
  481. * + +---+---+
  482. * | | d | e |
  483. * +---+---+---+
  484. * | | f | g |
  485. * +---+---+---+
  486. * | | h | i |
  487. * +---+---+---+
  488. *
  489. * @param {module:engine/model/element~Element} tableCell
  490. * @param {Number} numberOfCells
  491. */
  492. splitCellHorizontally( tableCell, numberOfCells = 2 ) {
  493. const model = this.editor.model;
  494. const tableRow = tableCell.parent;
  495. const table = tableRow.parent;
  496. const splitCellRow = table.getChildIndex( tableRow );
  497. const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
  498. const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
  499. model.change( writer => {
  500. // First check - the cell spans over multiple rows so before doing anything else just split this cell.
  501. if ( rowspan > 1 ) {
  502. // Cache table map before updating table.
  503. const tableMap = [ ...new TableWalker( table, {
  504. startRow: splitCellRow,
  505. endRow: splitCellRow + rowspan - 1,
  506. includeSpanned: true
  507. } ) ];
  508. // Get spans of new (inserted) cells and span to update of split cell.
  509. const { newCellsSpan, updatedSpan } = breakSpanEvenly( rowspan, numberOfCells );
  510. updateNumericAttribute( 'rowspan', updatedSpan, tableCell, writer );
  511. const { column: cellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
  512. // Each inserted cell will have the same attributes:
  513. const newCellsAttributes = {};
  514. // Do not store default value in the model.
  515. if ( newCellsSpan > 1 ) {
  516. newCellsAttributes.rowspan = newCellsSpan;
  517. }
  518. // Copy colspan of split cell.
  519. if ( colspan > 1 ) {
  520. newCellsAttributes.colspan = colspan;
  521. }
  522. for ( const { column, row, cellIndex } of tableMap ) {
  523. // As both newly created cells and the split cell might have rowspan,
  524. // the insertion of new cells must go to appropriate rows:
  525. //
  526. // 1. It's a row after split cell + it's height.
  527. const isAfterSplitCell = row >= splitCellRow + updatedSpan;
  528. // 2. Is on the same column.
  529. const isOnSameColumn = column === cellColumn;
  530. // 3. And it's row index is after previous cell height.
  531. const isInEvenlySplitRow = ( row + splitCellRow + updatedSpan ) % newCellsSpan === 0;
  532. if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
  533. const position = writer.createPositionAt( table.getChild( row ), cellIndex );
  534. createCells( 1, writer, position, newCellsAttributes );
  535. }
  536. }
  537. }
  538. // Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
  539. if ( rowspan < numberOfCells ) {
  540. // We already split the cell in check one so here we split to the remaining number of cells only.
  541. const cellsToInsert = numberOfCells - rowspan;
  542. // This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
  543. const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: splitCellRow } ) ];
  544. // First step: expand cells.
  545. for ( const { cell, rowspan, row } of tableMap ) {
  546. // Expand rowspan of cells that are either:
  547. // - on the same row as current cell,
  548. // - or are below split cell row and overlaps that row.
  549. if ( cell !== tableCell && row + rowspan > splitCellRow ) {
  550. const rowspanToSet = rowspan + cellsToInsert;
  551. writer.setAttribute( 'rowspan', rowspanToSet, cell );
  552. }
  553. }
  554. // Second step: create rows with single cell below split cell.
  555. const newCellsAttributes = {};
  556. // Copy colspan of split cell.
  557. if ( colspan > 1 ) {
  558. newCellsAttributes.colspan = colspan;
  559. }
  560. createEmptyRows( writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes );
  561. // Update heading section if split cell is in heading section.
  562. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  563. if ( headingRows > splitCellRow ) {
  564. updateNumericAttribute( 'headingRows', headingRows + cellsToInsert, table, writer );
  565. }
  566. }
  567. } );
  568. }
  569. /**
  570. * Returns the number of columns for a given table.
  571. *
  572. * editor.plugins.get( 'TableUtils' ).getColumns( table );
  573. *
  574. * @param {module:engine/model/element~Element} table The table to analyze.
  575. * @returns {Number}
  576. */
  577. getColumns( table ) {
  578. // Analyze first row only as all the rows should have the same width.
  579. const row = table.getChild( 0 );
  580. return [ ...row.getChildren() ].reduce( ( columns, row ) => {
  581. const columnWidth = parseInt( row.getAttribute( 'colspan' ) || 1 );
  582. return columns + columnWidth;
  583. }, 0 );
  584. }
  585. /**
  586. * Returns the number of rows for a given table.
  587. *
  588. * editor.plugins.get( 'TableUtils' ).getRows( table );
  589. *
  590. * @param {module:engine/model/element~Element} table The table to analyze.
  591. * @returns {Number}
  592. */
  593. getRows( table ) {
  594. // Simple row counting, not including rowspan due to #6427.
  595. return table.childCount;
  596. }
  597. }
  598. // Creates empty rows at the given index in an existing table.
  599. //
  600. // @param {module:engine/model/writer~Writer} writer
  601. // @param {module:engine/model/element~Element} table
  602. // @param {Number} insertAt The row index of row insertion.
  603. // @param {Number} rows The number of rows to create.
  604. // @param {Number} tableCellToInsert The number of cells to insert in each row.
  605. function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attributes = {} ) {
  606. for ( let i = 0; i < rows; i++ ) {
  607. const tableRow = writer.createElement( 'tableRow' );
  608. writer.insert( tableRow, table, insertAt );
  609. createCells( tableCellToInsert, writer, writer.createPositionAt( tableRow, 'end' ), attributes );
  610. }
  611. }
  612. // Creates cells at a given position.
  613. //
  614. // @param {Number} columns The number of columns to create
  615. // @param {module:engine/model/writer~Writer} writer
  616. // @param {module:engine/model/position~Position} insertPosition
  617. function createCells( cells, writer, insertPosition, attributes = {} ) {
  618. for ( let i = 0; i < cells; i++ ) {
  619. createEmptyTableCell( writer, insertPosition, attributes );
  620. }
  621. }
  622. // Evenly distributes the span of a cell to a number of provided cells.
  623. // The resulting spans will always be integer values.
  624. //
  625. // For instance breaking a span of 7 into 3 cells will return:
  626. //
  627. // { newCellsSpan: 2, updatedSpan: 3 }
  628. //
  629. // as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
  630. //
  631. // @param {Number} span The span value do break.
  632. // @param {Number} numberOfCells The number of resulting spans.
  633. // @returns {{newCellsSpan: Number, updatedSpan: Number}}
  634. function breakSpanEvenly( span, numberOfCells ) {
  635. if ( span < numberOfCells ) {
  636. return { newCellsSpan: 1, updatedSpan: 1 };
  637. }
  638. const newCellsSpan = Math.floor( span / numberOfCells );
  639. const updatedSpan = ( span - newCellsSpan * numberOfCells ) + newCellsSpan;
  640. return { newCellsSpan, updatedSpan };
  641. }
  642. // Updates heading columns attribute if removing a row from head section.
  643. function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
  644. const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
  645. if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
  646. const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
  647. removedColumnIndexes.first + 1;
  648. writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
  649. }
  650. }
  651. // Calculates a new heading rows value for removing rows from heading section.
  652. function updateHeadingRows( table, first, last, model, batch ) {
  653. // Must be done after the changes in table structure (removing rows).
  654. // Otherwise the downcast converter for headingRows attribute will fail.
  655. // See https://github.com/ckeditor/ckeditor5/issues/6391.
  656. //
  657. // Must be completely wrapped in enqueueChange to get the current table state (after applying other enqueued changes).
  658. model.enqueueChange( batch, writer => {
  659. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  660. if ( first < headingRows ) {
  661. const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
  662. updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
  663. }
  664. } );
  665. }
  666. // Finds cells that will be:
  667. // - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
  668. // - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
  669. //
  670. // Sample table with overlapping & sticking out cells:
  671. //
  672. // +----+----+----+----+----+
  673. // | 00 | 01 | 02 | 03 | 04 |
  674. // +----+ + + + +
  675. // | 10 | | | | |
  676. // +----+----+ + + +
  677. // | 20 | 21 | | | | <-- removed row
  678. // + + +----+ + +
  679. // | | | 32 | | | <-- removed row
  680. // +----+ + +----+ +
  681. // | 40 | | | 43 | |
  682. // +----+----+----+----+----+
  683. //
  684. // In a table above:
  685. // - cells to trim: '02', '03' & '04'.
  686. // - cells to move: '21' & '32'.
  687. function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
  688. const cellsToMove = new Map();
  689. const cellsToTrim = [];
  690. for ( const { row, column, rowspan, cell } of new TableWalker( table, { endRow: last } ) ) {
  691. const lastRowOfCell = row + rowspan - 1;
  692. const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
  693. if ( isCellStickingOutFromRemovedRows ) {
  694. const rowspanInRemovedSection = last - row + 1;
  695. const rowSpanToSet = rowspan - rowspanInRemovedSection;
  696. cellsToMove.set( column, {
  697. cell,
  698. rowspan: rowSpanToSet
  699. } );
  700. }
  701. const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
  702. if ( isCellOverlappingRemovedRows ) {
  703. let rowspanAdjustment;
  704. // Cell fully covers removed section - trim it by removed rows count.
  705. if ( lastRowOfCell >= last ) {
  706. rowspanAdjustment = last - first + 1;
  707. }
  708. // Cell partially overlaps removed section - calculate cell's span that is in removed section.
  709. else {
  710. rowspanAdjustment = lastRowOfCell - first + 1;
  711. }
  712. cellsToTrim.push( {
  713. cell,
  714. rowspan: rowspan - rowspanAdjustment
  715. } );
  716. }
  717. }
  718. return { cellsToMove, cellsToTrim };
  719. }
  720. function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
  721. const tableWalker = new TableWalker( table, {
  722. includeSpanned: true,
  723. startRow: targetRowIndex,
  724. endRow: targetRowIndex
  725. } );
  726. const tableRowMap = [ ...tableWalker ];
  727. const row = table.getChild( targetRowIndex );
  728. let previousCell;
  729. for ( const { column, cell, isSpanned } of tableRowMap ) {
  730. if ( cellsToMove.has( column ) ) {
  731. const { cell: cellToMove, rowspan } = cellsToMove.get( column );
  732. const targetPosition = previousCell ?
  733. writer.createPositionAfter( previousCell ) :
  734. writer.createPositionAt( row, 0 );
  735. writer.move( writer.createRangeOn( cellToMove ), targetPosition );
  736. updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );
  737. previousCell = cellToMove;
  738. } else if ( !isSpanned ) {
  739. // If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
  740. previousCell = cell;
  741. }
  742. }
  743. }