tableutils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 { getParentTable, updateNumericAttribute } from './commands/utils';
  12. /**
  13. * The table utils 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 table cell location as in table row and column indexes.
  26. *
  27. * For instance in a 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 {{row, column}}
  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 at given position.
  64. *
  65. * @param {module:engine/model/position~Position} position Position at which insert a table.
  66. * @param {Number} rows Number of rows to create.
  67. * @param {Number} columns Number of columns to create.
  68. */
  69. createTable( position, rows, columns ) {
  70. const model = this.editor.model;
  71. model.change( writer => {
  72. const table = writer.createElement( 'table' );
  73. writer.insert( table, position );
  74. createEmptyRows( writer, table, 0, rows, columns );
  75. } );
  76. }
  77. /**
  78. * Insert rows into a table.
  79. *
  80. * editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
  81. *
  82. * For the table below this code
  83. *
  84. * row index
  85. * 0 +---+---+---+ +---+---+---+ 0
  86. * | a | b | c | | a | b | c |
  87. * 1 + +---+---+ <-- insert here at=1 + +---+---+ 1
  88. * | | d | e | | | | |
  89. * 2 + +---+---+ should give: + +---+---+ 2
  90. * | | f | g | | | | |
  91. * 3 +---+---+---+ + +---+---+ 3
  92. * | | d | e |
  93. * +---+---+---+ 4
  94. * + + f | g |
  95. * +---+---+---+ 5
  96. *
  97. * @param {module:engine/model/element~Element} table Table model element to which insert rows.
  98. * @param {Object} options
  99. * @param {Number} [options.at=0] Row index at which insert rows.
  100. * @param {Number} [options.rows=1] Number of rows to insert.
  101. */
  102. insertRows( table, options = {} ) {
  103. const model = this.editor.model;
  104. const insertAt = options.at || 0;
  105. const rowsToInsert = options.rows || 1;
  106. model.change( writer => {
  107. const headingRows = table.getAttribute( 'headingRows' ) || 0;
  108. // Inserting rows inside heading section requires to update table's headingRows attribute as the heading section will grow.
  109. if ( headingRows > insertAt ) {
  110. writer.setAttribute( 'headingRows', headingRows + rowsToInsert, table );
  111. }
  112. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  113. if ( insertAt === 0 || insertAt === table.childCount ) {
  114. createEmptyRows( writer, table, insertAt, rowsToInsert, this.getColumns( table ) );
  115. return;
  116. }
  117. // Iterate over all rows below inserted rows in order to check for rowspanned cells.
  118. const tableIterator = new TableWalker( table, { endRow: insertAt } );
  119. // Will hold number of cells needed to insert in created rows.
  120. // The number might be different then table cell width when there are rowspanned cells.
  121. let cellsToInsert = 0;
  122. for ( const { row, rowspan, colspan, cell } of tableIterator ) {
  123. const isBeforeInsertedRow = row < insertAt;
  124. const overlapsInsertedRow = row + rowspan > insertAt;
  125. if ( isBeforeInsertedRow && overlapsInsertedRow ) {
  126. // This cell overlaps inserted rows so we need to expand it further.
  127. writer.setAttribute( 'rowspan', rowspan + rowsToInsert, cell );
  128. }
  129. // Calculate how many cells to insert based on the width of cells in a row at insert position.
  130. // It might be lower then table width as some cells might overlaps inserted row.
  131. // In the table above the cell 'a' overlaps inserted row so only two empty cells are need to be created.
  132. if ( row === insertAt ) {
  133. cellsToInsert += colspan;
  134. }
  135. }
  136. createEmptyRows( writer, table, insertAt, rowsToInsert, cellsToInsert );
  137. } );
  138. }
  139. /**
  140. * Inserts columns into a table.
  141. *
  142. * editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
  143. *
  144. * For the table below this code
  145. *
  146. * 0 1 2 3 0 1 2 3 4 5
  147. * +---+---+---+ +---+---+---+---+---+
  148. * | a | b | | a | b |
  149. * + +---+ + +---+
  150. * | | c | | | c |
  151. * +---+---+---+ should give: +---+---+---+---+---+
  152. * | d | e | f | | d | | | e | f |
  153. * +---+ +---+ +---+---+---+ +---+
  154. * | g | | h | | g | | | | h |
  155. * +---+---+---+ +---+---+---+---+---+
  156. * | i | | i |
  157. * +---+---+---+ +---+---+---+---+---+
  158. * ^________ insert here at=1
  159. *
  160. * @param {module:engine/model/element~Element} table Table model element to which insert columns.
  161. * @param {Object} options
  162. * @param {Number} [options.at=0] Column index at which insert columns.
  163. * @param {Number} [options.columns=1] Number of columns to insert.
  164. */
  165. insertColumns( table, options = {} ) {
  166. const model = this.editor.model;
  167. const insertAt = options.at || 0;
  168. const columnsToInsert = options.columns || 1;
  169. model.change( writer => {
  170. const headingColumns = table.getAttribute( 'headingColumns' );
  171. // Inserting rows inside heading section requires to update table's headingRows attribute as the heading section will grow.
  172. if ( insertAt < headingColumns ) {
  173. writer.setAttribute( 'headingColumns', headingColumns + columnsToInsert, table );
  174. }
  175. const tableColumns = this.getColumns( table );
  176. // Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
  177. if ( insertAt === 0 || tableColumns === insertAt ) {
  178. for ( const tableRow of table.getChildren() ) {
  179. createCells( columnsToInsert, writer, Position.createAt( tableRow, insertAt ? 'end' : 0 ) );
  180. }
  181. return;
  182. }
  183. const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
  184. for ( const { row, column, cell, colspan, rowspan, cellIndex } of tableWalker ) {
  185. // When iterating over column the table walker outputs either:
  186. // - cells at given column index (cell "e" from method docs),
  187. // - spanned columns (includeSpanned option) (spanned cell from row between cells "g" and "h" - spanned by "e"),
  188. // - or a cell from the same row which spans over this column (cell "a").
  189. if ( column !== insertAt ) {
  190. // If column is different then insertAt it is a cell that spans over an inserted column (cell "a" & "i").
  191. // For such cells expand them of number of columns inserted.
  192. writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
  193. // The includeSpanned option will output the "empty"/spanned column so skip this row already.
  194. tableWalker.skipRow( row );
  195. // This cell will overlap cells in rows below so skip them also (because of includeSpanned option) - (cell "a")
  196. if ( rowspan > 1 ) {
  197. for ( let i = row + 1; i < row + rowspan; i++ ) {
  198. tableWalker.skipRow( i );
  199. }
  200. }
  201. } else {
  202. // It's either cell at this column index or spanned cell by a rowspanned cell from row above.
  203. // In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
  204. const insertPosition = Position.createFromParentAndOffset( table.getChild( row ), cellIndex );
  205. createCells( columnsToInsert, writer, insertPosition );
  206. }
  207. }
  208. } );
  209. }
  210. /**
  211. * Divides table cell vertically into several ones.
  212. *
  213. * @param {module:engine/model/element~Element} tableCell
  214. * @param {Number} numberOfCells
  215. */
  216. splitCellVertically( tableCell, numberOfCells = 2 ) {
  217. const model = this.editor.model;
  218. const table = getParentTable( tableCell );
  219. model.change( writer => {
  220. const tableMap = [ ...new TableWalker( table ) ];
  221. const cellData = tableMap.find( value => value.cell === tableCell );
  222. const cellColspan = cellData.colspan;
  223. const cellsToInsert = numberOfCells - 1;
  224. const attributes = {};
  225. if ( cellColspan >= numberOfCells ) {
  226. // If the colspan is bigger than or equal to required cells to create we don't need to update colspan on
  227. // cells from the same column. The colspan will be equally divided for newly created cells and a current one.
  228. const colspanOfInsertedCells = Math.floor( cellColspan / numberOfCells );
  229. const newColspan = ( cellColspan - colspanOfInsertedCells * numberOfCells ) + colspanOfInsertedCells;
  230. if ( colspanOfInsertedCells > 1 ) {
  231. attributes.colspan = colspanOfInsertedCells;
  232. }
  233. updateNumericAttribute( 'colspan', newColspan, tableCell, writer );
  234. const cellRowspan = cellData.rowspan;
  235. if ( cellRowspan > 1 ) {
  236. attributes.rowspan = cellRowspan;
  237. }
  238. } else {
  239. const cellColumn = cellData.column;
  240. const cellsToUpdate = tableMap.filter( ( { cell, colspan, column } ) => {
  241. const isOnSameColumn = cell !== tableCell && column === cellColumn;
  242. const spansOverColumn = ( column < cellColumn && column + colspan - 1 >= cellColumn );
  243. return isOnSameColumn || spansOverColumn;
  244. } );
  245. for ( const { cell, colspan } of cellsToUpdate ) {
  246. writer.setAttribute( 'colspan', colspan + numberOfCells - 1, cell );
  247. }
  248. }
  249. createCells( cellsToInsert, writer, Position.createAfter( tableCell ), attributes );
  250. } );
  251. }
  252. /**
  253. * Divides table cell horizontally into several ones.
  254. *
  255. * @param {module:engine/model/element~Element} tableCell
  256. * @param {Number} numberOfCells
  257. */
  258. splitCellHorizontally( tableCell, numberOfCells = 2 ) {
  259. const model = this.editor.model;
  260. const table = getParentTable( tableCell );
  261. const rowIndex = table.getChildIndex( tableCell.parent );
  262. const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
  263. const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
  264. model.change( writer => {
  265. // First check - the cell spans over multiple rows so before doing anything else just split this cell.
  266. if ( rowspan > 1 ) {
  267. let newRowspan;
  268. let rowspanOfCellsToInsert;
  269. if ( rowspan < numberOfCells ) {
  270. // Split cell completely (remove rowspan) - the reminder of cells will be added in the second check.
  271. newRowspan = 1;
  272. rowspanOfCellsToInsert = 1;
  273. } else {
  274. // Split cell's rowspan evenly. Example: having a cell with rowspan of 7 and splitting it to 3 cells:
  275. // - distribute spans evenly for needed two cells (2 cells - each with rowspan of 2).
  276. // - the remaining span goes to current cell (3).
  277. rowspanOfCellsToInsert = Math.floor( rowspan / numberOfCells );
  278. const cellsToInsert = numberOfCells - 1;
  279. newRowspan = rowspan - cellsToInsert * rowspanOfCellsToInsert;
  280. }
  281. const tableMap = [ ...new TableWalker( table, {
  282. startRow: rowIndex,
  283. endRow: rowIndex + rowspan - 1,
  284. includeSpanned: true
  285. } ) ];
  286. updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
  287. let cellColumn = 0;
  288. const attributes = {};
  289. if ( rowspanOfCellsToInsert > 1 ) {
  290. attributes.rowspan = rowspanOfCellsToInsert;
  291. }
  292. if ( colspan > 1 ) {
  293. attributes.colspan = colspan;
  294. }
  295. for ( const { cell, column, row, cellIndex } of tableMap ) {
  296. if ( cell === tableCell ) {
  297. cellColumn = column;
  298. }
  299. const isAfterSplitCell = row >= rowIndex + newRowspan;
  300. const isOnSameColumn = column === cellColumn;
  301. const isInEvenlySplitRow = ( row + rowIndex + newRowspan ) % rowspanOfCellsToInsert === 0;
  302. if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
  303. const position = Position.createFromParentAndOffset( table.getChild( row ), cellIndex );
  304. writer.insertElement( 'tableCell', attributes, position );
  305. }
  306. }
  307. }
  308. // Second check - the cell has rowspan of 1 or we need to create more cells the the currently one spans over.
  309. if ( rowspan < numberOfCells ) {
  310. // We already split the cell in check one so here we split to the remaining number of cells only.
  311. const remaingingRowspan = numberOfCells - rowspan;
  312. // This check is needed since we need to check if there are any cells from previous rows thatn spans over this cell's row.
  313. const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: rowIndex } ) ];
  314. for ( const { cell, rowspan, row } of tableMap ) {
  315. if ( cell !== tableCell && row + rowspan > rowIndex ) {
  316. const rowspanToSet = rowspan + remaingingRowspan;
  317. writer.setAttribute( 'rowspan', rowspanToSet, cell );
  318. }
  319. }
  320. const attributes = {};
  321. if ( colspan > 1 ) {
  322. attributes.colspan = colspan;
  323. }
  324. createEmptyRows( writer, table, rowIndex + 1, remaingingRowspan, 1, attributes );
  325. const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
  326. if ( headingRows > rowIndex ) {
  327. updateNumericAttribute( 'headingRows', headingRows + 1, table, writer );
  328. }
  329. }
  330. } );
  331. }
  332. /**
  333. * Returns number of columns for given table.
  334. *
  335. * editor.plugins.get( 'TableUtils' ).getColumns( table );
  336. *
  337. * @param {module:engine/model/element~Element} table Table to analyze.
  338. * @returns {Number}
  339. */
  340. getColumns( table ) {
  341. // Analyze first row only as all the rows should have the same width.
  342. const row = table.getChild( 0 );
  343. return [ ...row.getChildren() ].reduce( ( columns, row ) => {
  344. const columnWidth = parseInt( row.getAttribute( 'colspan' ) || 1 );
  345. return columns + columnWidth;
  346. }, 0 );
  347. }
  348. }
  349. // Creates empty rows at given index in an existing table.
  350. //
  351. // @param {module:engine/model/writer~Writer} writer
  352. // @param {module:engine/model/element~Element} table
  353. // @param {Number} insertAt Row index of row insertion.
  354. // @param {Number} rows Number of rows to create.
  355. // @param {Number} tableCellToInsert Number of cells to insert in each row.
  356. function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attributes = {} ) {
  357. for ( let i = 0; i < rows; i++ ) {
  358. const tableRow = writer.createElement( 'tableRow' );
  359. writer.insert( tableRow, table, insertAt );
  360. for ( let columnIndex = 0; columnIndex < tableCellToInsert; columnIndex++ ) {
  361. const cell = writer.createElement( 'tableCell', attributes );
  362. writer.insert( cell, tableRow, 'end' );
  363. }
  364. }
  365. }
  366. // Creates cells at given position.
  367. //
  368. // @param {Number} columns Number of columns to create
  369. // @param {module:engine/model/writer~Writer} writer
  370. // @param {module:engine/model/position~Position} insertPosition
  371. function createCells( cells, writer, insertPosition, attributes = {} ) {
  372. for ( let i = 0; i < cells; i++ ) {
  373. writer.insertElement( 'tableCell', attributes, insertPosition );
  374. }
  375. }