tableutils.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module table/tableutils
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  10. import TableWalker from './tablewalker';
  11. import { createEmptyTableCell, updateNumericAttribute } from './commands/utils';
  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. * Returns the table cell location as an object with table row and table column indexes.
  26. *
  27. * For instance in the table below:
  28. *
  29. * 0 1 2 3
  30. * +---+---+---+---+
  31. * 0 | a | b | c |
  32. * + + +---+
  33. * 1 | | | d |
  34. * +---+---+ +---+
  35. * 2 | e | | f |
  36. * +---+---+---+---+
  37. *
  38. * the method will return:
  39. *
  40. * const cellA = table.getNodeByPath( [ 0, 0 ] );
  41. * editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
  42. * // will return { row: 0, column: 0 }
  43. *
  44. * const cellD = table.getNodeByPath( [ 1, 0 ] );
  45. * editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
  46. * // will return { row: 1, column: 3 }
  47. *
  48. * @param {module:engine/model/element~Element} tableCell
  49. * @returns {Object} Returns a `{row, column}` object.
  50. */
  51. getCellLocation( tableCell ) {
  52. const tableRow = tableCell.parent;
  53. const table = tableRow.parent;
  54. const rowIndex = table.getChildIndex( tableRow );
  55. const tableWalker = new TableWalker( table, { startRow: rowIndex, endRow: rowIndex } );
  56. for ( const { cell, row, column } of tableWalker ) {
  57. if ( cell === tableCell ) {
  58. return { row, column };
  59. }
  60. }
  61. }
  62. /**
  63. * Creates an empty table with proper structure. The table needs to be inserted into the model,
  64. * ie. using {@link module:engine/model/model~Model#insertContent} function.
  65. *
  66. * model.change( ( writer ) => {
  67. * // Create a table of 2 rows and 7 columns:
  68. * const table = tableUtils.createTable( writer, 2, 7);
  69. *
  70. * // Insert table to the model at the best position taking current selection:
  71. * model.insertContent( table );
  72. * }
  73. *
  74. * @param {module:engine/model/writer~Writer} writer The model writer.
  75. * @param {Number} rows The number of rows to create.
  76. * @param {Number} columns The number of columns to create.
  77. * @returns {module:engine/model/element~Element} The created table element.
  78. */
  79. createTable( writer, rows, columns ) {
  80. const table = writer.createElement( 'table' );
  81. createEmptyRows( writer, table, 0, rows, columns );
  82. return table;
  83. }
  84. /**
  85. * Inserts rows into a table.
  86. *
  87. * editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
  88. *
  89. * Assuming the table on the left, the above code will transform it to the table on the right:
  90. *
  91. * row index
  92. * 0 +---+---+---+ `at` = 1, +---+---+---+ 0
  93. * | a | b | c | `rows` = 2, | a | b | c |
  94. * 1 + +---+---+ <-- insert here + +---+---+ 1
  95. * | | d | e | | | | |
  96. * 2 + +---+---+ will give: + +---+---+ 2
  97. * | | f | g | | | | |
  98. * 3 +---+---+---+ + +---+---+ 3
  99. * | | d | e |
  100. * +---+---+---+ 4
  101. * + + f | g |
  102. * +---+---+---+ 5
  103. *
  104. * @param {module:engine/model/element~Element} table The table model element where the rows will be inserted.
  105. * @param {Object} options
  106. * @param {Number} [options.at=0] Row index at which the rows will be inserted.
  107. * @param {Number} [options.rows=1] The number of rows to insert.
  108. */
  109. insertRows( table, options = {} ) {
  110. const model = this.editor.model;
  111. const insertAt = options.at || 0;
  112. const rowsToInsert = options.rows || 1;
  113. model.change( writer => {
  114. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  115. // Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
  116. if ( headingRows > insertAt ) {
  117. writer.setAttribute( 'headingRows', headingRows + rowsToInsert, table );
  118. }
  119. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  120. if ( insertAt === 0 || insertAt === table.childCount ) {
  121. createEmptyRows( writer, table, insertAt, rowsToInsert, this.getColumns( table ) );
  122. return;
  123. }
  124. // Iterate over all rows above inserted rows in order to check for rowspanned cells.
  125. const tableIterator = new TableWalker( table, { endRow: insertAt } );
  126. // Will hold number of cells needed to insert in created rows.
  127. // The number might be different then table cell width when there are rowspanned cells.
  128. let cellsToInsert = 0;
  129. for ( const { row, rowspan, colspan, cell } of tableIterator ) {
  130. const isBeforeInsertedRow = row < insertAt;
  131. const overlapsInsertedRow = row + rowspan > insertAt;
  132. if ( isBeforeInsertedRow && overlapsInsertedRow ) {
  133. // This cell overlaps inserted rows so we need to expand it further.
  134. writer.setAttribute( 'rowspan', rowspan + rowsToInsert, cell );
  135. }
  136. // Calculate how many cells to insert based on the width of cells in a row at insert position.
  137. // It might be lower then table width as some cells might overlaps inserted row.
  138. // In the table above the cell 'a' overlaps inserted row so only two empty cells are need to be created.
  139. if ( row === insertAt ) {
  140. cellsToInsert += colspan;
  141. }
  142. }
  143. createEmptyRows( writer, table, insertAt, rowsToInsert, cellsToInsert );
  144. } );
  145. }
  146. /**
  147. * Inserts columns into a table.
  148. *
  149. * editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
  150. *
  151. * Assuming the table on the left, the above code will transform it to the table on the right:
  152. *
  153. * 0 1 2 3 0 1 2 3 4 5
  154. * +---+---+---+ +---+---+---+---+---+
  155. * | a | b | | a | b |
  156. * + +---+ + +---+
  157. * | | c | | | c |
  158. * +---+---+---+ will give: +---+---+---+---+---+
  159. * | d | e | f | | d | | | e | f |
  160. * +---+ +---+ +---+---+---+ +---+
  161. * | g | | h | | g | | | | h |
  162. * +---+---+---+ +---+---+---+---+---+
  163. * | i | | i |
  164. * +---+---+---+ +---+---+---+---+---+
  165. * ^---- insert here, `at` = 1, `columns` = 2
  166. *
  167. * @param {module:engine/model/element~Element} table The table model element where the columns will be inserted.
  168. * @param {Object} options
  169. * @param {Number} [options.at=0] Column index at which the columns will be inserted.
  170. * @param {Number} [options.columns=1] The number of columns to insert.
  171. */
  172. insertColumns( table, options = {} ) {
  173. const model = this.editor.model;
  174. const insertAt = options.at || 0;
  175. const columnsToInsert = options.columns || 1;
  176. model.change( writer => {
  177. const headingColumns = table.getAttribute( 'headingColumns' );
  178. // Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
  179. if ( insertAt < headingColumns ) {
  180. writer.setAttribute( 'headingColumns', headingColumns + columnsToInsert, table );
  181. }
  182. const tableColumns = this.getColumns( table );
  183. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  184. if ( insertAt === 0 || tableColumns === insertAt ) {
  185. for ( const tableRow of table.getChildren() ) {
  186. createCells( columnsToInsert, writer, Position.createAt( tableRow, insertAt ? 'end' : 0 ) );
  187. }
  188. return;
  189. }
  190. const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
  191. for ( const { row, column, cell, colspan, rowspan, cellIndex } of tableWalker ) {
  192. // When iterating over column the table walker outputs either:
  193. // - cells at given column index (cell "e" from method docs),
  194. // - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeSpanned: true`),
  195. // - or a cell from the same row which spans over this column (cell "a").
  196. if ( column !== insertAt ) {
  197. // If column is different than `insertAt`, it is a cell that spans over an inserted column (cell "a" & "i").
  198. // For such cells expand them by a number of columns inserted.
  199. writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
  200. // The `includeSpanned` option will output the "empty"/spanned column so skip this row already.
  201. tableWalker.skipRow( row );
  202. // This cell will overlap cells in rows below so skip them also (because of `includeSpanned` option) - (cell "a")
  203. if ( rowspan > 1 ) {
  204. for ( let i = row + 1; i < row + rowspan; i++ ) {
  205. tableWalker.skipRow( i );
  206. }
  207. }
  208. } else {
  209. // It's either cell at this column index or spanned cell by a rowspanned cell from row above.
  210. // In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
  211. const insertPosition = Position.createFromParentAndOffset( table.getChild( row ), cellIndex );
  212. createCells( columnsToInsert, writer, insertPosition );
  213. }
  214. }
  215. } );
  216. }
  217. /**
  218. * Divides a table cell vertically into several ones.
  219. *
  220. * The cell will be visually split into more cells by updating colspans of other cells in a column
  221. * and inserting cells (columns) after that cell.
  222. *
  223. * In the table below, if cell "a" is split to 3 cells:
  224. *
  225. * +---+---+---+
  226. * | a | b | c |
  227. * +---+---+---+
  228. * | d | e | f |
  229. * +---+---+---+
  230. *
  231. * it will result in the table below:
  232. *
  233. * +---+---+---+---+---+
  234. * | a | | | b | c |
  235. * +---+---+---+---+---+
  236. * | d | e | f |
  237. * +---+---+---+---+---+
  238. *
  239. * So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
  240. *
  241. * Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
  242. * will be left to the original cell:
  243. *
  244. * +---+---+---+
  245. * | a |
  246. * +---+---+---+
  247. * | b | c | d |
  248. * +---+---+---+
  249. *
  250. * Splitting cell "a" with `colspan=3` to 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
  251. *
  252. * +---+---+---+
  253. * | a | |
  254. * +---+---+---+
  255. * | b | c | d |
  256. * +---+---+---+
  257. *
  258. * @param {module:engine/model/element~Element} tableCell
  259. * @param {Number} numberOfCells
  260. */
  261. splitCellVertically( tableCell, numberOfCells = 2 ) {
  262. const model = this.editor.model;
  263. const tableRow = tableCell.parent;
  264. const table = tableRow.parent;
  265. const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
  266. const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
  267. model.change( writer => {
  268. // First check - the cell spans over multiple rows so before doing anything else just split this cell.
  269. if ( colspan > 1 ) {
  270. // Get spans of new (inserted) cells and span to update of split cell.
  271. const { newCellsSpan, updatedSpan } = breakSpanEvenly( colspan, numberOfCells );
  272. updateNumericAttribute( 'colspan', updatedSpan, tableCell, writer );
  273. // Each inserted cell will have the same attributes:
  274. const newCellsAttributes = {};
  275. // Do not store default value in the model.
  276. if ( newCellsSpan > 1 ) {
  277. newCellsAttributes.colspan = newCellsSpan;
  278. }
  279. // Copy rowspan of split cell.
  280. if ( rowspan > 1 ) {
  281. newCellsAttributes.rowspan = rowspan;
  282. }
  283. const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
  284. createCells( cellsToInsert, writer, Position.createAfter( tableCell ), newCellsAttributes );
  285. }
  286. // Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
  287. if ( colspan < numberOfCells ) {
  288. const cellsToInsert = numberOfCells - colspan;
  289. // First step: expand cells on the same column as split cell.
  290. const tableMap = [ ...new TableWalker( table ) ];
  291. // Get the column index of split cell.
  292. const { column: splitCellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
  293. // Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
  294. const cellsToUpdate = tableMap.filter( ( { cell, colspan, column } ) => {
  295. const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
  296. const spansOverColumn = ( column < splitCellColumn && column + colspan > splitCellColumn );
  297. return isOnSameColumn || spansOverColumn;
  298. } );
  299. // Expand cells vertically.
  300. for ( const { cell, colspan } of cellsToUpdate ) {
  301. writer.setAttribute( 'colspan', colspan + cellsToInsert, cell );
  302. }
  303. // Second step: create columns after split cell.
  304. // Each inserted cell will have the same attributes:
  305. const newCellsAttributes = {};
  306. // Do not store default value in the model.
  307. // Copy rowspan of split cell.
  308. if ( rowspan > 1 ) {
  309. newCellsAttributes.rowspan = rowspan;
  310. }
  311. createCells( cellsToInsert, writer, Position.createAfter( tableCell ), newCellsAttributes );
  312. const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
  313. // Update heading section if split cell is in heading section.
  314. if ( headingColumns > splitCellColumn ) {
  315. updateNumericAttribute( 'headingColumns', headingColumns + cellsToInsert, table, writer );
  316. }
  317. }
  318. } );
  319. }
  320. /**
  321. * Divides a table cell horizontally into several ones.
  322. *
  323. * 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
  324. * below.
  325. *
  326. * If in the table below cell "b" is split to 3 cells:
  327. *
  328. * +---+---+---+
  329. * | a | b | c |
  330. * +---+---+---+
  331. * | d | e | f |
  332. * +---+---+---+
  333. *
  334. * It will result in the table below:
  335. *
  336. * +---+---+---+
  337. * | a | b | c |
  338. * + +---+ +
  339. * | | | |
  340. * + +---+ +
  341. * | | | |
  342. * +---+---+---+
  343. * | d | e | f |
  344. * +---+---+---+
  345. *
  346. * So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
  347. *
  348. * Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
  349. * will be left to the original cell:
  350. *
  351. * +---+---+---+
  352. * | a | b | c |
  353. * + +---+---+
  354. * | | d | e |
  355. * + +---+---+
  356. * | | f | g |
  357. * + +---+---+
  358. * | | h | i |
  359. * +---+---+---+
  360. *
  361. * Splitting cell "a" with `rowspan=4` to 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
  362. *
  363. * +---+---+---+
  364. * | a | b | c |
  365. * + +---+---+
  366. * | | d | e |
  367. * +---+---+---+
  368. * | | f | g |
  369. * +---+---+---+
  370. * | | h | i |
  371. * +---+---+---+
  372. *
  373. * @param {module:engine/model/element~Element} tableCell
  374. * @param {Number} numberOfCells
  375. */
  376. splitCellHorizontally( tableCell, numberOfCells = 2 ) {
  377. const model = this.editor.model;
  378. const tableRow = tableCell.parent;
  379. const table = tableRow.parent;
  380. const splitCellRow = table.getChildIndex( tableRow );
  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 ( rowspan > 1 ) {
  386. // Cache table map before updating table.
  387. const tableMap = [ ...new TableWalker( table, {
  388. startRow: splitCellRow,
  389. endRow: splitCellRow + rowspan - 1,
  390. includeSpanned: true
  391. } ) ];
  392. // Get spans of new (inserted) cells and span to update of split cell.
  393. const { newCellsSpan, updatedSpan } = breakSpanEvenly( rowspan, numberOfCells );
  394. updateNumericAttribute( 'rowspan', updatedSpan, tableCell, writer );
  395. const { column: cellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
  396. // Each inserted cell will have the same attributes:
  397. const newCellsAttributes = {};
  398. // Do not store default value in the model.
  399. if ( newCellsSpan > 1 ) {
  400. newCellsAttributes.rowspan = newCellsSpan;
  401. }
  402. // Copy colspan of split cell.
  403. if ( colspan > 1 ) {
  404. newCellsAttributes.colspan = colspan;
  405. }
  406. for ( const { column, row, cellIndex } of tableMap ) {
  407. // As both newly created cells and the split cell might have rowspan,
  408. // the insertion of new cells must go to appropriate rows:
  409. //
  410. // 1. It's a row after split cell + it's height.
  411. const isAfterSplitCell = row >= splitCellRow + updatedSpan;
  412. // 2. Is on the same column.
  413. const isOnSameColumn = column === cellColumn;
  414. // 3. And it's row index is after previous cell height.
  415. const isInEvenlySplitRow = ( row + splitCellRow + updatedSpan ) % newCellsSpan === 0;
  416. if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
  417. const position = Position.createFromParentAndOffset( table.getChild( row ), cellIndex );
  418. createCells( 1, writer, position, newCellsAttributes );
  419. }
  420. }
  421. }
  422. // Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
  423. if ( rowspan < numberOfCells ) {
  424. // We already split the cell in check one so here we split to the remaining number of cells only.
  425. const cellsToInsert = numberOfCells - rowspan;
  426. // This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
  427. const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: splitCellRow } ) ];
  428. // First step: expand cells.
  429. for ( const { cell, rowspan, row } of tableMap ) {
  430. // Expand rowspan of cells that are either:
  431. // - on the same row as current cell,
  432. // - or are below split cell row and overlaps that row.
  433. if ( cell !== tableCell && row + rowspan > splitCellRow ) {
  434. const rowspanToSet = rowspan + cellsToInsert;
  435. writer.setAttribute( 'rowspan', rowspanToSet, cell );
  436. }
  437. }
  438. // Second step: create rows with single cell below split cell.
  439. const newCellsAttributes = {};
  440. // Copy colspan of split cell.
  441. if ( colspan > 1 ) {
  442. newCellsAttributes.colspan = colspan;
  443. }
  444. createEmptyRows( writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes );
  445. // Update heading section if split cell is in heading section.
  446. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  447. if ( headingRows > splitCellRow ) {
  448. updateNumericAttribute( 'headingRows', headingRows + cellsToInsert, table, writer );
  449. }
  450. }
  451. } );
  452. }
  453. /**
  454. * Returns the number of columns for a given table.
  455. *
  456. * editor.plugins.get( 'TableUtils' ).getColumns( table );
  457. *
  458. * @param {module:engine/model/element~Element} table The table to analyze.
  459. * @returns {Number}
  460. */
  461. getColumns( table ) {
  462. // Analyze first row only as all the rows should have the same width.
  463. const row = table.getChild( 0 );
  464. return [ ...row.getChildren() ].reduce( ( columns, row ) => {
  465. const columnWidth = parseInt( row.getAttribute( 'colspan' ) || 1 );
  466. return columns + columnWidth;
  467. }, 0 );
  468. }
  469. }
  470. // Creates empty rows at the given index in an existing table.
  471. //
  472. // @param {module:engine/model/writer~Writer} writer
  473. // @param {module:engine/model/element~Element} table
  474. // @param {Number} insertAt Row index of row insertion.
  475. // @param {Number} rows Number of rows to create.
  476. // @param {Number} tableCellToInsert Number of cells to insert in each row.
  477. function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attributes = {} ) {
  478. for ( let i = 0; i < rows; i++ ) {
  479. const tableRow = writer.createElement( 'tableRow' );
  480. writer.insert( tableRow, table, insertAt );
  481. createCells( tableCellToInsert, writer, Position.createAt( tableRow, 'end' ), attributes );
  482. }
  483. }
  484. // Creates cells at a given position.
  485. //
  486. // @param {Number} columns Number of columns to create
  487. // @param {module:engine/model/writer~Writer} writer
  488. // @param {module:engine/model/position~Position} insertPosition
  489. function createCells( cells, writer, insertPosition, attributes = {} ) {
  490. for ( let i = 0; i < cells; i++ ) {
  491. createEmptyTableCell( writer, insertPosition, attributes );
  492. }
  493. }
  494. // Evenly distributes the span of a cell to a number of provided cells.
  495. // The resulting spans will always be integer values.
  496. //
  497. // For instance breaking a span of 7 into 3 cells will return:
  498. //
  499. // { newCellsSpan: 2, updatedSpan: 3 }
  500. //
  501. // as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
  502. //
  503. // @param {Number} span Span value do break.
  504. // @param {Number} numberOfCells Number of resulting spans.
  505. // @returns {{newCellsSpan: Number, updatedSpan: Number}}
  506. function breakSpanEvenly( span, numberOfCells ) {
  507. if ( span < numberOfCells ) {
  508. return { newCellsSpan: 1, updatedSpan: 1 };
  509. }
  510. const newCellsSpan = Math.floor( span / numberOfCells );
  511. const updatedSpan = ( span - newCellsSpan * numberOfCells ) + newCellsSpan;
  512. return { newCellsSpan, updatedSpan };
  513. }