/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import MergeCellsCommand from '../../src/commands/mergecellscommand'; import { modelTable } from '../_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import TableSelection from '../../src/tableselection'; import TableEditing from '../../src/tableediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'MergeCellsCommand', () => { let editor, model, command, root, tableSelection; beforeEach( async () => { editor = await ModelTestEditor.create( { plugins: [ Paragraph, TableEditing, TableSelection ] } ); model = editor.model; root = model.document.getRoot( 'main' ); tableSelection = editor.plugins.get( TableSelection ); command = new MergeCellsCommand( editor ); } ); afterEach( () => { return editor.destroy(); } ); describe( 'isEnabled', () => { it( 'should be false if collapsed selection in table cell', () => { setData( model, modelTable( [ [ '00[]', '01' ] ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if only one table cell is selected', () => { setData( model, modelTable( [ [ '00', '01' ] ] ) ); selectNodes( [ [ 0, 0, 0 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true if at least two adjacent table cells are selected', () => { setData( model, modelTable( [ [ '00', '01' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if many table cells are selected', () => { setData( model, modelTable( [ [ '00', '01', '02', '03' ], [ '10', '11', '12', '13' ], [ '20', '21', '22', '23' ], [ '30', '31', '32', '33' ] ] ) ); selectNodes( [ [ 0, 0, 1 ], [ 0, 0, 2 ], [ 0, 1, 1 ], [ 0, 1, 2 ], [ 0, 2, 1 ], [ 0, 2, 2 ], [ 0, 3, 1 ], [ 0, 3, 2 ] ] ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if at least one table cell is not selected from an area', () => { setData( model, modelTable( [ [ '00', '01', '02', '03' ], [ '10', '11', '12', '13' ], [ '20', '21', '22', '23' ], [ '30', '31', '32', '33' ] ] ) ); selectNodes( [ [ 0, 0, 1 ], [ 0, 0, 2 ], [ 0, 1, 2 ], // one table cell not selected from this row [ 0, 2, 1 ], [ 0, 2, 2 ], [ 0, 3, 1 ], [ 0, 3, 2 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if table cells are not in adjacent rows', () => { setData( model, modelTable( [ [ '00', '01' ], [ '10', '11' ] ] ) ); selectNodes( [ [ 0, 1, 0 ], [ 0, 0, 1 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if table cells are not in adjacent columns', () => { setData( model, modelTable( [ [ '00', '01', '02' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 2 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if any table cell with colspan attribute extends over selection area', () => { setData( model, modelTable( [ [ '00', { colspan: 2, contents: '01' } ], [ '10', '11', '12' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1, 0 ], [ 0, 1, 1 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true if none table cell with colspan attribute extends over selection area', () => { setData( model, modelTable( [ [ '00', { colspan: 2, contents: '01' } ], [ '10', '11', '12' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ] ] ); expect( command.isEnabled ).to.be.true; } ); it( 'should be true if first table cell is inside selection area', () => { setData( model, modelTable( [ [ { colspan: 2, rowspan: 2, contents: '00' }, '02', '03' ], [ '12', '13' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1, 0 ] ] ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if any table cell with rowspan attribute extends over selection area', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true if none table cell with rowspan attribute extends over selection area', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1, 0 ] ] ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if not in a cell', () => { setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection has cells from header and body sections', () => { setData( model, modelTable( [ [ '00[]', '01' ], [ '10', '11' ] ], { headingRows: 1 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 1, 0 ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if more than 10 rows selected and some are in heading section', () => { setData( model, modelTable( [ [ '0' ], [ '1' ], [ '2' ], [ '3' ], [ '4' ], [ '5' ], [ '6' ], [ '7' ], [ '8' ], [ '9' ], [ '10' ], [ '11' ], [ '12' ], [ '13' ], [ '14' ] ], { headingRows: 10 } ) ); const tableSelection = editor.plugins.get( TableSelection ); const modelRoot = model.document.getRoot(); tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 0 ] ), modelRoot.getNodeByPath( [ 0, 12, 0 ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true if selection has cells only from column headers - rows in body section', () => { setData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ '10', '11', '12', '13' ] ], { headingColumns: 2 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 1, 1 ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection has cells from column headers and other cells - rows in body section', () => { setData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ '10', '11', '12', '13' ] ], { headingColumns: 2 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 1, 2 ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be true if selection has cells only from column headers - rows in header section', () => { setData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ '10', '11', '12', '13' ] ], { headingColumns: 2, headingRows: 1 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 0, 1 ] ) ); expect( command.isEnabled ).to.be.true; } ); it( 'should be false if selection has cells only from column headers and other cells - rows in header section', () => { setData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ '10', '11', '12', '13' ] ], { headingColumns: 2, headingRows: 1 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 0, 2 ] ) ); expect( command.isEnabled ).to.be.false; } ); it( 'should be false if selection has cells from column headers, row headers and body sections', () => { setData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ '10', '11', '12', '13' ] ], { headingColumns: 2, headingRows: 1 } ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 1, 2 ] ) ); expect( command.isEnabled ).to.be.false; } ); } ); describe( 'execute()', () => { it( 'should merge simple table cell selection', () => { setData( model, modelTable( [ [ '[]00', '01' ] ] ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 0, 0 ] ), root.getNodeByPath( [ 0, 0, 1 ] ) ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); it( 'should merge selection with a cell with rowspan in the selection', () => { setData( model, modelTable( [ [ '[]00', '01', '02' ], [ '10', { contents: '11', rowspan: 2 }, '12' ], [ '20', '22' ] ] ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 1, 0 ] ), root.getNodeByPath( [ 0, 2, 1 ] ) ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '00', '01', '02' ], [ { colspan: 3, contents: '[101112' + '2022]' } ] ] ) ); } ); it( 'should merge selection with a cell with rowspan in the selection (reverse selection)', () => { setData( model, modelTable( [ [ '[]00', '01', '02' ], [ '10', { contents: '11', rowspan: 2 }, '12' ], [ '20', '22' ] ] ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 2, 1 ] ), root.getNodeByPath( [ 0, 1, 0 ] ) ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '00', '01', '02' ], [ { colspan: 3, contents: '[101112' + '2022]' } ] ] ) ); } ); it( 'should merge selection inside a table (properly calculate target rowspan/colspan)', () => { setData( model, modelTable( [ [ '[]00', '01', '02', '03' ], [ '10', '11', { contents: '12', rowspan: 2 }, '13' ], [ '20', '21', '23' ], [ '30', '31', '32', '33' ] ] ) ); tableSelection.setCellSelection( root.getNodeByPath( [ 0, 2, 1 ] ), root.getNodeByPath( [ 0, 1, 2 ] ) ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '00', '01', '02', '03' ], [ '10', { colspan: 2, rowspan: 2, contents: '[111221]' }, '13' ], [ '20', '23' ], [ '30', '31', '32', '33' ] ] ) ); } ); it( 'should merge table cells - extend colspan attribute', () => { setData( model, modelTable( [ [ { colspan: 2, contents: '00' }, '02', '03' ], [ '10', '11', '12', '13' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ], [ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 3, rowspan: 2, contents: '[00' + '02' + '10' + '11' + '12]' }, '03' ], [ '13' ] ] ) ); } ); it( 'should merge to a single paragraph - every cell is empty', () => { setData( model, modelTable( [ [ '[]', '' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[]' } ] ] ) ); } ); it( 'should merge to a single paragraph - merged cell is empty', () => { setData( model, modelTable( [ [ 'foo', '' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); it( 'should merge to a single paragraph - cell to which others are merged is empty', () => { setData( model, modelTable( [ [ '', 'foo' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); it( 'should not merge empty blocks other then to a single block', () => { model.schema.register( 'block', { allowWhere: '$block', allowContentOf: '$block', isBlock: true } ); setData( model, modelTable( [ [ '[]', '' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[]' } ] ] ) ); } ); describe( 'removing empty row', () => { it( 'should remove empty row if merging all table cells from that row', () => { setData( model, modelTable( [ [ '00' ], [ '10' ], [ '20' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 0, 2, 0 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '[001020]' ] ] ) ); } ); it( 'should decrease heading rows if some heading rows were removed', () => { setData( model, modelTable( [ [ '00' ], [ '10' ], [ '20' ] ], { headingRows: 2 } ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 1, 0 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '[0010]' ], [ '20' ] ], { headingRows: 1 } ) ); } ); it( 'should create one undo step (1 batch)', () => { setData( model, modelTable( [ [ '00' ], [ '10' ], [ '20' ] ], { headingRows: 2 } ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 1, 0 ] ] ); const createdBatches = new Set(); model.on( 'applyOperation', ( evt, [ operation ] ) => { createdBatches.add( operation.batch ); } ); command.execute(); expect( createdBatches.size ).to.equal( 1 ); } ); it( 'should decrease rowspan if cell overlaps removed row', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ], [ '10' ], [ '20', '21' ] ] ) ); selectNodes( [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 0, 2, 0 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ { rowspan: 2, contents: '[001020]' }, '01', { rowspan: 2, contents: '02' } ], [ '21' ] ] ) ); } ); it( 'should not decrease rowspan if cell from previous row does not overlaps removed row', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ], [ '20', '21' ], [ '30', '31' ] ] ) ); selectNodes( [ [ 0, 2, 0 ], [ 0, 2, 1 ], [ 0, 3, 0 ], [ 0, 3, 1 ] ] ); command.execute(); assertEqualMarkup( getData( model ), modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ], [ { colspan: 2, contents: '[2021' + '3031]' } ] ] ) ); } ); } ); } ); function selectNodes( paths ) { model.change( writer => { const ranges = paths.map( path => writer.createRangeOn( root.getNodeByPath( path ) ) ); writer.setSelection( ranges ); } ); } } );