Procházet zdrojové kódy

Changed: Create `TableUtils#insertColumns()` and create 'insertColumnBefore' and 'insertColumnAfter' commands.

Maciej Gołaszewski před 7 roky
rodič
revize
ce6affda39

+ 30 - 70
packages/ckeditor5-table/src/commands/insertcolumncommand.js

@@ -9,8 +9,8 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableWalker from '../tablewalker';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import { getColumns, getParentTable } from './utils';
+import { getParentTable } from './utils';
+import TableUtils from '../tableutils';
 
 /**
  * The insert column command.
@@ -19,6 +19,19 @@ import { getColumns, getParentTable } from './utils';
  */
 export default class InsertColumnCommand extends Command {
 	/**
+	 * Creates a new `InsertRowCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 * @param {Object} options
+	 * @param {String} [options.location="after"] Where to insert new row - relative to current row. Possible values: "after", "before".
+	 */
+	constructor( editor, options = {} ) {
+		super( editor );
+
+		this.direction = options.location || 'after';
+	}
+
+	/**
 	 * @inheritDoc
 	 */
 	refresh() {
@@ -33,84 +46,31 @@ export default class InsertColumnCommand extends Command {
 	/**
 	 * Executes the command.
 	 *
-	 * @param {Object} [options] Options for the executed command.
-	 * @param {Number} [options.columns=1] Number of rows to insert.
-	 * @param {Number} [options.at=0] Row index to insert at.
-	 *
 	 * @fires execute
 	 */
-	execute( options = {} ) {
-		const model = this.editor.model;
-		const document = model.document;
-		const selection = document.selection;
-
-		const columns = parseInt( options.columns ) || 1;
-		const insertAt = parseInt( options.at ) || 0;
+	execute() {
+		const editor = this.editor;
+		const model = editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
 
 		const table = getParentTable( selection.getFirstPosition() );
 
-		model.change( writer => {
-			const tableColumns = getColumns( table );
-
-			// Inserting at the end of a table
-			if ( tableColumns <= insertAt ) {
-				for ( const tableRow of table.getChildren() ) {
-					createCells( columns, writer, Position.createAt( tableRow, 'end' ) );
-				}
-
-				return;
-			}
+		const element = doc.selection.getFirstPosition().parent;
+		const rowIndex = table.getChildIndex( element.parent );
 
-			const headingColumns = table.getAttribute( 'headingColumns' );
+		let columnIndex;
 
-			if ( insertAt < headingColumns ) {
-				writer.setAttribute( 'headingColumns', headingColumns + columns, table );
+		for ( const tableWalkerValue of new TableWalker( table, { startRow: rowIndex, endRow: rowIndex } ) ) {
+			if ( tableWalkerValue.cell === element ) {
+				columnIndex = tableWalkerValue.column;
 			}
+		}
 
-			const tableIterator = new TableWalker( table );
-
-			let currentRow = -1;
-			let currentRowInserted = false;
-
-			for ( const tableCellInfo of tableIterator ) {
-				const { row, column, cell: tableCell, colspan } = tableCellInfo;
-
-				if ( currentRow !== row ) {
-					currentRow = row;
-					currentRowInserted = false;
-				}
-
-				const shouldExpandSpan = colspan > 1 &&
-					( column !== insertAt ) &&
-					( column <= insertAt ) &&
-					( column <= insertAt + columns ) &&
-					( column + colspan > insertAt );
-
-				if ( shouldExpandSpan ) {
-					writer.setAttribute( 'colspan', colspan + columns, tableCell );
-				}
-
-				if ( column === insertAt || ( column < insertAt + columns && column > insertAt && !currentRowInserted ) ) {
-					const insertPosition = Position.createBefore( tableCell );
-
-					createCells( columns, writer, insertPosition );
-
-					currentRowInserted = true;
-				}
-			}
-		} );
-	}
-}
+		const insertAt = this.direction === 'after' ? columnIndex + 1 : columnIndex;
 
-// Creates cells at given position.
-//
-// @param {Number} columns Number of columns to create
-// @param {module:engine/model/writer} writer
-// @param {module:engine/model/position} insertPosition
-function createCells( columns, writer, insertPosition ) {
-	for ( let i = 0; i < columns; i++ ) {
-		const cell = writer.createElement( 'tableCell' );
+		const tableUtils = editor.plugins.get( TableUtils );
 
-		writer.insert( cell, insertPosition );
+		tableUtils.insertColumns( table, { columns: 1, at: insertAt } );
 	}
 }

+ 2 - 1
packages/ckeditor5-table/src/tableediting.js

@@ -99,7 +99,8 @@ export default class TablesEditing extends Plugin {
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		editor.commands.add( 'insertRowAbove', new InsertRowCommand( editor, { location: 'above' } ) );
 		editor.commands.add( 'insertRowBelow', new InsertRowCommand( editor, { location: 'below' } ) );
-		editor.commands.add( 'insertColumn', new InsertColumnCommand( editor ) );
+		editor.commands.add( 'insertColumnBefore', new InsertColumnCommand( editor, { location: 'before' } ) );
+		editor.commands.add( 'insertColumnAfter', new InsertColumnCommand( editor, { location: 'after' } ) );
 		editor.commands.add( 'splitCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) );
 		editor.commands.add( 'splitCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) );
 		editor.commands.add( 'removeRow', new RemoveRowCommand( editor ) );

+ 3 - 3
packages/ckeditor5-table/src/tableui.js

@@ -66,8 +66,8 @@ export default class TableUI extends Plugin {
 			return buttonView;
 		} );
 
-		editor.ui.componentFactory.add( 'insertColumn', locale => {
-			const command = editor.commands.get( 'insertColumn' );
+		editor.ui.componentFactory.add( 'insertColumnAfter', locale => {
+			const command = editor.commands.get( 'insertColumnAfter' );
 			const buttonView = new ButtonView( locale );
 
 			buttonView.bind( 'isEnabled' ).to( command );
@@ -79,7 +79,7 @@ export default class TableUI extends Plugin {
 			} );
 
 			buttonView.on( 'execute', () => {
-				editor.execute( 'insertColumn' );
+				editor.execute( 'insertColumnAfter' );
 				editor.editing.view.focus();
 			} );
 

+ 71 - 0
packages/ckeditor5-table/src/tableutils.js

@@ -60,6 +60,64 @@ export default class TableUtils extends Plugin {
 		} );
 	}
 
+	insertColumns( table, options = {} ) {
+		const model = this.editor.model;
+
+		const columns = parseInt( options.columns ) || 1;
+		const insertAt = parseInt( options.at ) || 0;
+
+		model.change( writer => {
+			const tableColumns = getColumns( table );
+
+			// Inserting at the end of a table
+			if ( tableColumns <= insertAt ) {
+				for ( const tableRow of table.getChildren() ) {
+					createCells( columns, writer, Position.createAt( tableRow, 'end' ) );
+				}
+
+				return;
+			}
+
+			const headingColumns = table.getAttribute( 'headingColumns' );
+
+			if ( insertAt < headingColumns ) {
+				writer.setAttribute( 'headingColumns', headingColumns + columns, table );
+			}
+
+			const tableIterator = new TableWalker( table );
+
+			let currentRow = -1;
+			let currentRowInserted = false;
+
+			for ( const tableCellInfo of tableIterator ) {
+				const { row, column, cell: tableCell, colspan } = tableCellInfo;
+
+				if ( currentRow !== row ) {
+					currentRow = row;
+					currentRowInserted = false;
+				}
+
+				const shouldExpandSpan = colspan > 1 &&
+					( column !== insertAt ) &&
+					( column <= insertAt ) &&
+					( column <= insertAt + columns ) &&
+					( column + colspan > insertAt );
+
+				if ( shouldExpandSpan ) {
+					writer.setAttribute( 'colspan', colspan + columns, tableCell );
+				}
+
+				if ( column === insertAt || ( column < insertAt + columns && column > insertAt && !currentRowInserted ) ) {
+					const insertPosition = Position.createBefore( tableCell );
+
+					createCells( columns, writer, insertPosition );
+
+					currentRowInserted = true;
+				}
+			}
+		} );
+	}
+
 	splitCellHorizontally( tableCell, cellNumber = 2 ) {
 		const model = this.editor.model;
 
@@ -162,3 +220,16 @@ function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert ) {
 		}
 	}
 }
+
+// Creates cells at given position.
+//
+// @param {Number} columns Number of columns to create
+// @param {module:engine/model/writer} writer
+// @param {module:engine/model/position} insertPosition
+function createCells( columns, writer, insertPosition ) {
+	for ( let i = 0; i < columns; i++ ) {
+		const cell = writer.createElement( 'tableCell' );
+
+		writer.insert( cell, insertPosition );
+	}
+}

+ 232 - 144
packages/ckeditor5-table/tests/commands/insertcolumncommand.js

@@ -11,66 +11,71 @@ import InsertColumnCommand from '../../src/commands/insertcolumncommand';
 import { downcastInsertTable } from '../../src/converters/downcast';
 import upcastTable from '../../src/converters/upcasttable';
 import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import TableUtils from '../../src/tableutils';
 
 describe( 'InsertColumnCommand', () => {
 	let editor, model, command;
 
 	beforeEach( () => {
-		return ModelTestEditor.create()
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				command = new InsertColumnCommand( editor );
-
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isBlock: true,
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', {
-					allowIn: 'table',
-					allowAttributes: [],
-					isBlock: true,
-					isLimit: true
-				} );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isBlock: true,
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Table row upcast only since downcast conversion is done in `downcastTable()`.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
-
-				// Table cell conversion.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+		return ModelTestEditor.create( {
+			plugins: [ TableUtils ]
+		} ).then( newEditor => {
+			editor = newEditor;
+			model = editor.model;
+
+			const conversion = editor.conversion;
+			const schema = model.schema;
+
+			schema.register( 'table', {
+				allowWhere: '$block',
+				allowAttributes: [ 'headingRows' ],
+				isBlock: true,
+				isObject: true
 			} );
+
+			schema.register( 'tableRow', {
+				allowIn: 'table',
+				allowAttributes: [],
+				isBlock: true,
+				isLimit: true
+			} );
+
+			schema.register( 'tableCell', {
+				allowIn: 'tableRow',
+				allowContentOf: '$block',
+				allowAttributes: [ 'colspan', 'rowspan' ],
+				isBlock: true,
+				isLimit: true
+			} );
+
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+
+			// Table conversion.
+			conversion.for( 'upcast' ).add( upcastTable() );
+			conversion.for( 'downcast' ).add( downcastInsertTable() );
+
+			// Table row upcast only since downcast conversion is done in `downcastTable()`.
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
+
+			// Table cell conversion.
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
+
+			conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+			conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+		} );
 	} );
 
 	afterEach( () => {
 		return editor.destroy();
 	} );
 
-	describe( 'isEnabled', () => {
-		describe( 'when selection is collapsed', () => {
+	describe( 'location=after', () => {
+		beforeEach( () => {
+			command = new InsertColumnCommand( editor );
+		} );
+
+		describe( 'isEnabled', () => {
 			it( 'should be false if wrong node', () => {
 				setData( model, '<p>foo[]</p>' );
 				expect( command.isEnabled ).to.be.false;
@@ -81,128 +86,211 @@ describe( 'InsertColumnCommand', () => {
 				expect( command.isEnabled ).to.be.true;
 			} );
 		} );
-	} );
 
-	describe( 'execute()', () => {
-		it( 'should insert column in given table at given index', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ '21', '22' ]
-			] ) );
+		describe( 'execute()', () => {
+			it( 'should insert column in given table at given index', () => {
+				setData( model, modelTable( [
+					[ '11[]', '12' ],
+					[ '21', '22' ]
+				] ) );
 
-			command.execute( { at: 1 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '', '12' ],
-				[ '21', '', '22' ]
-			] ) );
-		} );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11[]', '', '12' ],
+					[ '21', '', '22' ]
+				] ) );
+			} );
 
-		it( 'should insert column in given table at default index', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ '21', '22' ]
-			] ) );
+			it( 'should insert columns at table end', () => {
+				setData( model, modelTable( [
+					[ '11', '12' ],
+					[ '21', '22[]' ]
+				] ) );
 
-			command.execute();
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '', '11[]', '12' ],
-				[ '', '21', '22' ]
-			] ) );
-		} );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '12', '' ],
+					[ '21', '22[]', '' ]
+				] ) );
+			} );
 
-		it( 'should insert columns at table end', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ '21', '22' ]
-			] ) );
+			it( 'should update table heading columns attribute when inserting column in headings section', () => {
+				setData( model, modelTable( [
+					[ '11[]', '12' ],
+					[ '21', '22' ],
+					[ '31', '32' ]
+				], { headingColumns: 2 } ) );
 
-			command.execute( { at: 2, columns: 2 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '12', '', '' ],
-				[ '21', '22', '', '' ]
-			] ) );
-		} );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11[]', '', '12' ],
+					[ '21', '', '22' ],
+					[ '31', '', '32' ]
+				], { headingColumns: 3 } ) );
+			} );
 
-		it( 'should update table heading columns attribute when inserting column in headings section', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ '21', '22' ],
-				[ '31', '32' ]
-			], { headingColumns: 2 } ) );
+			it( 'should not update table heading columns attribute when inserting column after headings section', () => {
+				setData( model, modelTable( [
+					[ '11', '12[]', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				], { headingColumns: 2 } ) );
 
-			command.execute( { at: 1 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '', '12' ],
-				[ '21', '', '22' ],
-				[ '31', '', '32' ]
-			], { headingColumns: 3 } ) );
-		} );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '12[]', '', '13' ],
+					[ '21', '22', '', '23' ],
+					[ '31', '32', '', '33' ]
+				], { headingColumns: 2 } ) );
+			} );
 
-		it( 'should not update table heading columns attribute when inserting column after headings section', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12', '13' ],
-				[ '21', '22', '23' ],
-				[ '31', '32', '33' ]
-			], { headingColumns: 2 } ) );
+			it( 'should skip spanned columns', () => {
+				setData( model, modelTable( [
+					[ '11[]', '12' ],
+					[ { colspan: 2, contents: '21' } ],
+					[ '31', '32' ]
+				], { headingColumns: 2 } ) );
 
-			command.execute( { at: 2 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '12', '', '13' ],
-				[ '21', '22', '', '23' ],
-				[ '31', '32', '', '33' ]
-			], { headingColumns: 2 } ) );
-		} );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11[]', '', '12' ],
+					[ { colspan: 3, contents: '21' } ],
+					[ '31', '', '32' ]
+				], { headingColumns: 3 } ) );
+			} );
 
-		it( 'should skip spanned columns', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ { colspan: 2, contents: '21' } ],
-				[ '31', '32' ]
-			], { headingColumns: 2 } ) );
+			it( 'should skip wide spanned columns', () => {
+				setData( model, modelTable( [
+					[ '11', '12[]', '13', '14', '15' ],
+					[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
+					[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+				], { headingColumns: 4 } ) );
 
-			command.execute( { at: 1 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '', '12' ],
-				[ { colspan: 3, contents: '21' } ],
-				[ '31', '', '32' ]
-			], { headingColumns: 3 } ) );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '12[]', '', '13', '14', '15' ],
+					[ '21', '22', '', { colspan: 2, contents: '23' }, '25' ],
+					[ { colspan: 5, contents: '31' }, { colspan: 2, contents: '34' } ]
+				], { headingColumns: 5 } ) );
+			} );
 		} );
+	} );
 
-		it( 'should skip wide spanned columns', () => {
-			setData( model, modelTable( [
-				[ '11[]', '12', '13', '14', '15' ],
-				[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
-				[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
-			], { headingColumns: 4 } ) );
+	describe( 'location=before', () => {
+		beforeEach( () => {
+			command = new InsertColumnCommand( editor, { location: 'before' } );
+		} );
 
-			command.execute( { at: 2, columns: 2 } );
+		describe( 'isEnabled', () => {
+			it( 'should be false if wrong node', () => {
+				setData( model, '<p>foo[]</p>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '12', '', '', '13', '14', '15' ],
-				[ '21', '22', '', '', { colspan: 2, contents: '23' }, '25' ],
-				[ { colspan: 6, contents: '31' }, { colspan: 2, contents: '34' } ]
-			], { headingColumns: 6 } ) );
+			it( 'should be true if in table', () => {
+				setData( model, modelTable( [ [ '[]' ] ] ) );
+				expect( command.isEnabled ).to.be.true;
+			} );
 		} );
 
-		// TODO fix me
-		it.skip( 'should skip row spanned cells', () => {
-			setData( model, modelTable( [
-				[ { colspan: 2, rowspan: 2, contents: '11[]' }, '13' ],
-				[ '23' ]
-			], { headingColumns: 2 } ) );
+		describe( 'execute()', () => {
+			it( 'should insert column in given table at given index', () => {
+				setData( model, modelTable( [
+					[ '11', '12[]' ],
+					[ '21', '22' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '', '12[]' ],
+					[ '21', '', '22' ]
+				] ) );
+			} );
+
+			it( 'should insert columns at the table start', () => {
+				setData( model, modelTable( [
+					[ '11', '12' ],
+					[ '[]21', '22' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '', '11', '12' ],
+					[ '', '[]21', '22' ]
+				] ) );
+			} );
+
+			it( 'should update table heading columns attribute when inserting column in headings section', () => {
+				setData( model, modelTable( [
+					[ '11', '12[]' ],
+					[ '21', '22' ],
+					[ '31', '32' ]
+				], { headingColumns: 2 } ) );
 
-			command.execute( { at: 1, columns: 2 } );
+				command.execute();
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ { colspan: 4, rowspan: 2, contents: '11[]' }, '13' ],
-				[ '23' ]
-			], { headingColumns: 4 } ) );
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '', '12[]' ],
+					[ '21', '', '22' ],
+					[ '31', '', '32' ]
+				], { headingColumns: 3 } ) );
+			} );
+
+			it( 'should not update table heading columns attribute when inserting column after headings section', () => {
+				setData( model, modelTable( [
+					[ '11', '12', '13[]' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				], { headingColumns: 2 } ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '12', '', '13[]' ],
+					[ '21', '22', '', '23' ],
+					[ '31', '32', '', '33' ]
+				], { headingColumns: 2 } ) );
+			} );
+
+			it( 'should skip spanned columns', () => {
+				setData( model, modelTable( [
+					[ '11', '12[]' ],
+					[ { colspan: 2, contents: '21' } ],
+					[ '31', '32' ]
+				], { headingColumns: 2 } ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '', '12[]' ],
+					[ { colspan: 3, contents: '21' } ],
+					[ '31', '', '32' ]
+				], { headingColumns: 3 } ) );
+			} );
+
+			it( 'should skip wide spanned columns', () => {
+				setData( model, modelTable( [
+					[ '11', '12', '13[]', '14', '15' ],
+					[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
+					[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+				], { headingColumns: 4 } ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '12', '', '13[]', '14', '15' ],
+					[ '21', '22', '', { colspan: 2, contents: '23' }, '25' ],
+					[ { colspan: 5, contents: '31' }, { colspan: 2, contents: '34' } ]
+				], { headingColumns: 5 } ) );
+			} );
 		} );
 	} );
 } );

+ 4 - 4
packages/ckeditor5-table/tests/tableui.js

@@ -116,11 +116,11 @@ describe( 'TableUI', () => {
 		} );
 	} );
 
-	describe( 'insertColumn button', () => {
+	describe( 'insertColumnAfter button', () => {
 		let insertColumn;
 
 		beforeEach( () => {
-			insertColumn = editor.ui.componentFactory.create( 'insertColumn' );
+			insertColumn = editor.ui.componentFactory.create( 'insertColumnAfter' );
 		} );
 
 		it( 'should register insertColumn buton', () => {
@@ -131,7 +131,7 @@ describe( 'TableUI', () => {
 		} );
 
 		it( 'should bind to insertColumn command', () => {
-			const command = editor.commands.get( 'insertColumn' );
+			const command = editor.commands.get( 'insertColumnAfter' );
 
 			command.isEnabled = true;
 			expect( insertColumn.isOn ).to.be.false;
@@ -147,7 +147,7 @@ describe( 'TableUI', () => {
 			insertColumn.fire( 'execute' );
 
 			sinon.assert.calledOnce( executeSpy );
-			sinon.assert.calledWithExactly( executeSpy, 'insertColumn' );
+			sinon.assert.calledWithExactly( executeSpy, 'insertColumnAfter' );
 		} );
 	} );
 } );

+ 136 - 0
packages/ckeditor5-table/tests/tableutils.js

@@ -211,6 +211,142 @@ describe( 'TableUtils', () => {
 		} );
 	} );
 
+	describe( 'insertColumns()', () => {
+		it( 'should insert column in given table at given index', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '', '12' ],
+				[ '21', '', '22' ]
+			] ) );
+		} );
+		it( 'should insert column in given table with default values', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ) );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '', '11[]', '12' ],
+				[ '', '21', '22' ]
+			] ) );
+		} );
+
+		it( 'should insert column in given table at default index', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ) );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '', '11[]', '12' ],
+				[ '', '21', '22' ]
+			] ) );
+		} );
+
+		it( 'should insert columns at table end', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '12', '', '' ],
+				[ '21', '22', '', '' ]
+			] ) );
+		} );
+
+		it( 'should update table heading columns attribute when inserting column in headings section', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ],
+				[ '31', '32' ]
+			], { headingColumns: 2 } ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '', '12' ],
+				[ '21', '', '22' ],
+				[ '31', '', '32' ]
+			], { headingColumns: 3 } ) );
+		} );
+
+		it( 'should not update table heading columns attribute when inserting column after headings section', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			], { headingColumns: 2 } ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '12', '', '13' ],
+				[ '21', '22', '', '23' ],
+				[ '31', '32', '', '33' ]
+			], { headingColumns: 2 } ) );
+		} );
+
+		it( 'should skip spanned columns', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ { colspan: 2, contents: '21' } ],
+				[ '31', '32' ]
+			], { headingColumns: 2 } ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '', '12' ],
+				[ { colspan: 3, contents: '21' } ],
+				[ '31', '', '32' ]
+			], { headingColumns: 3 } ) );
+		} );
+
+		it( 'should skip wide spanned columns', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12', '13', '14', '15' ],
+				[ '21', '22', { colspan: 2, contents: '23' }, '25' ],
+				[ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
+			], { headingColumns: 4 } ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '12', '', '', '13', '14', '15' ],
+				[ '21', '22', '', '', { colspan: 2, contents: '23' }, '25' ],
+				[ { colspan: 6, contents: '31' }, { colspan: 2, contents: '34' } ]
+			], { headingColumns: 6 } ) );
+		} );
+
+		// TODO fix me
+		it.skip( 'should skip row spanned cells', () => {
+			setData( model, modelTable( [
+				[ { colspan: 2, rowspan: 2, contents: '11[]' }, '13' ],
+				[ '23' ]
+			], { headingColumns: 2 } ) );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1, columns: 2 } );
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 4, rowspan: 2, contents: '11[]' }, '13' ],
+				[ '23' ]
+			], { headingColumns: 4 } ) );
+		} );
+	} );
+
 	describe( 'splitCellHorizontally()', () => {
 		it( 'should split table cell to given table cells number', () => {
 			setData( model, modelTable( [