setheaderrowcommand.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module table/commands/setheaderrowcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  10. import { getParentTable, updateNumericAttribute } from './utils';
  11. import TableWalker from '../tablewalker';
  12. /**
  13. * The header row command.
  14. *
  15. * @extends module:core/command~Command
  16. */
  17. export default class SetHeaderRowCommand extends Command {
  18. /**
  19. * @inheritDoc
  20. */
  21. refresh() {
  22. const model = this.editor.model;
  23. const doc = model.document;
  24. const selection = doc.selection;
  25. const position = selection.getFirstPosition();
  26. const tableParent = getParentTable( position );
  27. const isInTable = !!tableParent;
  28. this.isEnabled = isInTable;
  29. /**
  30. * Flag indicating whether the command is active. The command is active when the
  31. * {@link module:engine/model/selection~Selection} is in a header row.
  32. *
  33. * @observable
  34. * @readonly
  35. * @member {Boolean} #value
  36. */
  37. this.value = isInTable && this._isInHeading( position.parent, tableParent );
  38. }
  39. /**
  40. * Executes the command.
  41. *
  42. * When the selection is non-header row, the command will set `headingRows` table's attribute to cover that row.
  43. *
  44. * When selection is already in a header row then it will set `headingRows` so the heading section will end before that row.
  45. *
  46. * @fires execute
  47. */
  48. execute() {
  49. const model = this.editor.model;
  50. const doc = model.document;
  51. const selection = doc.selection;
  52. const position = selection.getFirstPosition();
  53. const tableCell = position.parent;
  54. const tableRow = tableCell.parent;
  55. const table = tableRow.parent;
  56. const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
  57. let rowIndex = tableRow.index;
  58. if ( rowIndex + 1 !== currentHeadingRows ) {
  59. rowIndex++;
  60. }
  61. model.change( writer => {
  62. if ( rowIndex ) {
  63. // Changing heading rows requires to check if any of a heading cell is overlapping vertically the table head.
  64. // Any table cell that has a rowspan attribute > 1 will not exceed the table head so we need to fix it in rows below.
  65. const cellsToSplit = getOverlappingCells( table, rowIndex, currentHeadingRows );
  66. for ( const cell of cellsToSplit ) {
  67. splitHorizontally( cell, rowIndex, writer );
  68. }
  69. }
  70. updateNumericAttribute( 'headingRows', rowIndex, table, writer, 0 );
  71. } );
  72. }
  73. /**
  74. * Checks if table cell is in heading section.
  75. *
  76. * @param {module:engine/model/element~Element} tableCell
  77. * @param {module:engine/model/element~Element} table
  78. * @returns {Boolean}
  79. * @private
  80. */
  81. _isInHeading( tableCell, table ) {
  82. const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
  83. return !!headingRows && tableCell.parent.index < headingRows;
  84. }
  85. }
  86. // Returns cells that span beyond new heading section.
  87. //
  88. // @param {module:engine/model/element~Element} table Table to check
  89. // @param {Number} headingRowsToSet New heading rows attribute.
  90. // @param {Number} currentHeadingRows Current heading rows attribute.
  91. // @returns {Array.<module:engine/model/element~Element>}
  92. function getOverlappingCells( table, headingRowsToSet, currentHeadingRows ) {
  93. const cellsToSplit = [];
  94. const startAnalysisRow = headingRowsToSet > currentHeadingRows ? currentHeadingRows : 0;
  95. const tableWalker = new TableWalker( table, { startRow: startAnalysisRow, endRow: headingRowsToSet } );
  96. for ( const { row, rowspan, cell } of tableWalker ) {
  97. if ( rowspan > 1 && row + rowspan > headingRowsToSet ) {
  98. cellsToSplit.push( cell );
  99. }
  100. }
  101. return cellsToSplit;
  102. }
  103. // Splits table cell horizontally.
  104. //
  105. // @param {module:engine/model/element~Element} tableCell
  106. // @param {Number} headingRows
  107. // @param {module:engine/model/writer~Writer} writer
  108. function splitHorizontally( tableCell, headingRows, writer ) {
  109. const tableRow = tableCell.parent;
  110. const table = tableRow.parent;
  111. const rowIndex = tableRow.index;
  112. const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) );
  113. const newRowspan = headingRows - rowIndex;
  114. const attributes = {};
  115. const spanToSet = rowspan - newRowspan;
  116. if ( spanToSet > 1 ) {
  117. attributes.rowspan = spanToSet;
  118. }
  119. const startRow = table.getChildIndex( tableRow );
  120. const endRow = startRow + newRowspan;
  121. const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
  122. let columnIndex;
  123. for ( const { row, column, cell, colspan, cellIndex } of tableMap ) {
  124. if ( cell === tableCell ) {
  125. columnIndex = column;
  126. if ( colspan > 1 ) {
  127. attributes.colspan = colspan;
  128. }
  129. }
  130. if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
  131. const tableRow = table.getChild( row );
  132. writer.insertElement( 'tableCell', attributes, Position.createFromParentAndOffset( tableRow, cellIndex ) );
  133. }
  134. }
  135. // Update the rowspan attribute after updating table.
  136. updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
  137. }