removecolumncommand.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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/commands/removecolumncommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import TableWalker from '../tablewalker';
  10. import { findAncestor, updateNumericAttribute } from './utils';
  11. /**
  12. * The remove column command.
  13. *
  14. * The command is registered by {@link module:table/tableediting~TableEditing} as the `'removeTableColumn'` editor command.
  15. *
  16. * To remove the column containing the selected cell, execute the command:
  17. *
  18. * editor.execute( 'removeTableColumn' );
  19. *
  20. * @extends module:core/command~Command
  21. */
  22. export default class RemoveColumnCommand extends Command {
  23. /**
  24. * @inheritDoc
  25. */
  26. refresh() {
  27. const tableUtils = this.editor.plugins.get( 'TableUtils' );
  28. const firstCell = this._getReferenceCells().next().value;
  29. this.isEnabled = !!firstCell && tableUtils.getColumns( firstCell.parent.parent ) > 1;
  30. }
  31. /**
  32. * @inheritDoc
  33. */
  34. execute() {
  35. const model = this.editor.model;
  36. const referenceCells = Array.from( this._getReferenceCells() );
  37. const firstCell = referenceCells[ 0 ];
  38. const lastCell = referenceCells[ referenceCells.length - 1 ];
  39. const tableRow = firstCell.parent;
  40. const table = tableRow.parent;
  41. // Cache the table before removing or updating colspans.
  42. const tableMap = [ ...new TableWalker( table ) ];
  43. // Get column index of removed column.
  44. const firstCellData = tableMap.find( value => value.cell === firstCell );
  45. const cellsToFocus = getCellToFocus( firstCell, lastCell );
  46. const removedColumnIndexes = {
  47. first: firstCellData.column,
  48. last: tableMap.find( value => value.cell === lastCell ).column
  49. };
  50. model.change( writer => {
  51. // A temporary workaround to avoid the "model-selection-range-intersects" error.
  52. writer.setSelection( writer.createSelection( table, 'on' ) );
  53. adjustHeadingColumns( table, removedColumnIndexes, writer );
  54. for (
  55. let removedColumnIndex = removedColumnIndexes.last;
  56. removedColumnIndex >= removedColumnIndexes.first;
  57. removedColumnIndex--
  58. ) {
  59. for ( const { cell, column, colspan } of tableMap ) {
  60. // If colspaned cell overlaps removed column decrease it's span.
  61. if ( column <= removedColumnIndex && colspan > 1 && column + colspan > removedColumnIndex ) {
  62. updateNumericAttribute( 'colspan', colspan - 1, cell, writer );
  63. } else if ( column === removedColumnIndex ) {
  64. // The cell in removed column has colspan of 1.
  65. writer.remove( cell );
  66. }
  67. }
  68. }
  69. writer.setSelection( writer.createPositionAt( cellsToFocus.reverse().filter( item => item != null )[ 0 ], 0 ) );
  70. } );
  71. }
  72. /**
  73. * Returns cells that are selected and are a reference to removing rows.
  74. *
  75. * @private
  76. * @returns {Iterable.<module:engine/model/element~Element>} Generates `tableCell` elements.
  77. */
  78. * _getReferenceCells() {
  79. const plugins = this.editor.plugins;
  80. if ( plugins.has( 'TableSelection' ) && plugins.get( 'TableSelection' ).hasMultiCellSelection ) {
  81. for ( const cell of plugins.get( 'TableSelection' ).getSelectedTableCells() ) {
  82. yield cell;
  83. }
  84. } else {
  85. const selection = this.editor.model.document.selection;
  86. yield findAncestor( 'tableCell', selection.getFirstPosition() );
  87. }
  88. }
  89. }
  90. // Updates heading columns attribute if removing a row from head section.
  91. function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
  92. const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
  93. if ( headingColumns && removedColumnIndexes.first <= headingColumns ) {
  94. const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
  95. removedColumnIndexes.first + 1;
  96. writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
  97. }
  98. }
  99. // Returns a proper table cell to focus after removing a column. It should be a next sibling to selection visually stay in place but:
  100. // - selection is on last table cell it will return previous cell.
  101. // - table cell is spanned over 2+ columns - it will be truncated so the selection should stay in that cell.
  102. function getCellToFocus( firstCell, lastCell ) {
  103. const colspan = parseInt( lastCell.getAttribute( 'colspan' ) || 1 );
  104. if ( colspan > 1 ) {
  105. return [ firstCell, lastCell ];
  106. }
  107. // return lastCell.nextSibling ? lastCell.nextSibling : lastCell.previousSibling;
  108. return [ firstCell.previousSibling, lastCell.nextSibling ];
  109. }