tableutils.js 15 KB

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