8
0

tableutils.js 33 KB

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