ソースを参照

Other: Basic insert row & insert column commands.

Maciej Gołaszewski 7 年 前
コミット
3f0d2fcfe8

+ 1 - 1
packages/ckeditor5-table/src/converters/downcasttable.js

@@ -154,7 +154,7 @@ function getCellElementName( rowIndex, columnIndex, headingRows, headingColumns
  *
  * @private
  */
-class CellSpans {
+export class CellSpans {
 	/**
 	 * Creates CellSpans instance.
 	 */

+ 122 - 0
packages/ckeditor5-table/src/insertcolumncommand.js

@@ -0,0 +1,122 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/insertcolumncommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { CellSpans } from './converters/downcasttable';
+
+/**
+ * The insert column command.
+ *
+ * @extends module:core/command~Command
+ */
+export default class InsertColumnCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const doc = model.document;
+
+		const tableParent = getValidParent( doc.selection.getFirstPosition() );
+
+		this.isEnabled = !!tableParent;
+	}
+
+	/**
+	 * 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 startingAt = parseInt( options.at ) || 0;
+
+		const table = getValidParent( selection.getFirstPosition() );
+
+		const maxColumns = getColumns( table );
+
+		const cellSpans = new CellSpans();
+
+		model.change( writer => {
+			let rowIndex = 0;
+
+			const headingColumns = table.getAttribute( 'headingColumns' );
+
+			if ( startingAt < headingColumns ) {
+				writer.setAttribute( 'headingColumns', headingColumns + columns, table );
+			}
+
+			for ( const row of table.getChildren() ) {
+				const insertAt = startingAt > maxColumns ? maxColumns : startingAt;
+
+				let columnIndex = 0;
+
+				for ( const tableCell of row.getChildren() ) {
+					columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+
+					while ( columnIndex >= insertAt && columnIndex < insertAt + columns ) {
+						const cell = writer.createElement( 'tableCell' );
+
+						writer.insert( cell, row, insertAt );
+
+						columnIndex++;
+					}
+
+					const colspan = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
+					const rowspan = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
+
+					cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
+
+					columnIndex += colspan;
+				}
+
+				// Insert at the end of column
+				while ( columnIndex >= insertAt && columnIndex < insertAt + columns ) {
+					const cell = writer.createElement( 'tableCell' );
+
+					writer.insert( cell, row, insertAt );
+
+					columnIndex++;
+				}
+
+				rowIndex++;
+			}
+		} );
+	}
+}
+
+function getValidParent( firstPosition ) {
+	let parent = firstPosition.parent;
+
+	while ( parent ) {
+		if ( parent.name === 'table' ) {
+			return parent;
+		}
+
+		parent = parent.parent;
+	}
+}
+
+function getColumns( table ) {
+	const row = table.getChild( 0 );
+
+	return [ ...row.getChildren() ].reduce( ( columns, row ) => {
+		const columnWidth = parseInt( row.getAttribute( 'colspan' ) ) || 1;
+
+		return columns + ( columnWidth );
+	}, 0 );
+}

+ 96 - 0
packages/ckeditor5-table/src/insertrowcommand.js

@@ -0,0 +1,96 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/insertrowcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * The insert row command.
+ *
+ * @extends module:core/command~Command
+ */
+export default class InsertRowCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const doc = model.document;
+
+		const tableParent = getValidParent( doc.selection.getFirstPosition() );
+
+		this.isEnabled = !!tableParent;
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @param {Object} [options] Options for the executed command.
+	 * @param {Number} [options.rows=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 rows = parseInt( options.rows ) || 1;
+		const startingAt = parseInt( options.at ) || 0;
+
+		const table = getValidParent( selection.getFirstPosition() );
+
+		const headingRows = table.getAttribute( 'headingRows' ) || 0;
+
+		const columns = getColumns( table );
+
+		model.change( writer => {
+			let insertAt = startingAt > table.childCount ? table.childCount : startingAt;
+
+			if ( headingRows > insertAt ) {
+				writer.setAttribute( 'headingRows', headingRows + rows, table );
+			}
+
+			for ( let rowIndex = 0; rowIndex < rows; rowIndex++ ) {
+				const row = writer.createElement( 'tableRow' );
+				writer.insert( row, table, insertAt );
+
+				for ( let column = 0; column < columns; column++ ) {
+					const cell = writer.createElement( 'tableCell' );
+
+					writer.insert( cell, row, 'end' );
+				}
+
+				insertAt++;
+			}
+		} );
+	}
+}
+
+function getValidParent( firstPosition ) {
+	let parent = firstPosition.parent;
+
+	while ( parent ) {
+		if ( parent.name === 'table' ) {
+			return parent;
+		}
+
+		parent = parent.parent;
+	}
+}
+
+function getColumns( table ) {
+	const row = table.getChild( 0 );
+
+	return [ ...row.getChildren() ].reduce( ( columns, row ) => {
+		const columnWidth = parseInt( row.getAttribute( 'colspan' ) ) || 1;
+
+		return columns + ( columnWidth );
+	}, 0 );
+}

+ 8 - 0
packages/ckeditor5-table/src/tableediting.js

@@ -9,9 +9,12 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import upcastTable from './converters/upcasttable';
 import downcastTable from './converters/downcasttable';
 import InsertTableCommand from './inserttablecommand';
+import InsertRowCommand from './insertrowcommand';
+import InsertColumnCommand from './insertcolumncommand';
 
 /**
  * The table editing feature.
@@ -53,6 +56,9 @@ export default class TablesEditing extends Plugin {
 		conversion.for( 'upcast' ).add( upcastTable() );
 		conversion.for( 'downcast' ).add( downcastTable() );
 
+		conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
+		conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+
 		// Table cell conversion.
 		conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
 		conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
@@ -61,5 +67,7 @@ export default class TablesEditing extends Plugin {
 		conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
 
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
+		editor.commands.add( 'insertRow', new InsertRowCommand( editor ) );
+		editor.commands.add( 'insertColumn', new InsertColumnCommand( editor ) );
 	}
 }

+ 41 - 0
packages/ckeditor5-table/src/tableui.js

@@ -11,6 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 import icon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+import insertRowIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import insertColumnIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
 
 /**
  * The table UI plugin.
@@ -43,5 +45,44 @@ export default class TableUI extends Plugin {
 
 			return buttonView;
 		} );
+
+		editor.ui.componentFactory.add( 'insertRow', locale => {
+			const command = editor.commands.get( 'insertRow' );
+			const buttonView = new ButtonView( locale );
+
+			buttonView.bind( 'isEnabled' ).to( command );
+
+			buttonView.set( {
+				icon: insertRowIcon,
+				label: 'Insert row',
+				tooltip: true
+			} );
+
+			buttonView.on( 'execute', () => {
+				editor.execute( 'insertRow' );
+				editor.editing.view.focus();
+			} );
+
+			return buttonView;
+		} );
+		editor.ui.componentFactory.add( 'insertColumn', locale => {
+			const command = editor.commands.get( 'insertColumn' );
+			const buttonView = new ButtonView( locale );
+
+			buttonView.bind( 'isEnabled' ).to( command );
+
+			buttonView.set( {
+				icon: insertColumnIcon,
+				label: 'Insert row',
+				tooltip: true
+			} );
+
+			buttonView.on( 'execute', () => {
+				editor.execute( 'insertColumn' );
+				editor.editing.view.focus();
+			} );
+
+			return buttonView;
+		} );
 	}
 }

+ 52 - 0
packages/ckeditor5-table/tests/_utils/utils.js

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @param {Number} columns
+ * @param {Array.<String>} tableData
+ * @param {Object} [attributes]
+ *
+ * @returns {String}
+ */
+export function modelTable( columns, tableData, attributes ) {
+	const tableRows = tableData
+		.map( cellData => `<tableCell>${ cellData }</tableCell>` )
+		.reduce( ( table, tableCell, index ) => {
+			if ( index % columns === 0 ) {
+				table += '<tableRow>';
+			}
+
+			table += tableCell;
+
+			if ( index % columns === columns - 1 ) {
+				table += '</tableRow>';
+			}
+
+			return table;
+		}, '' );
+
+	let attributesString = '';
+
+	if ( attributes ) {
+		const entries = Object.entries( attributes );
+
+		attributesString = ' ' + entries.map( entry => `${ entry[ 0 ] }="${ entry[ 1 ] }"` ).join( ' ' );
+	}
+
+	return `<table${ attributesString }>${ tableRows }</table>`;
+}
+
+export function formatModelTable( tableString ) {
+	return tableString
+		.replace( /<tableRow>/g, '\n<tableRow>\n    ' )
+		.replace( /<\/tableRow>/g, '\n</tableRow>' )
+		.replace( /<\/table>/g, '\n</table>' );
+}
+
+export function formattedModelTable( columns, tableData, attributes ) {
+	const tableString = modelTable( columns, tableData, attributes );
+
+	return formatModelTable( tableString );
+}

+ 147 - 0
packages/ckeditor5-table/tests/insertcolumncommand.js

@@ -0,0 +1,147 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import InsertColumnCommand from '../src/insertcolumncommand';
+import downcastTable from '../src/converters/downcasttable';
+import upcastTable from '../src/converters/upcasttable';
+import { formatModelTable, formattedModelTable, modelTable } from './_utils/utils';
+
+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( downcastTable() );
+
+				// 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', () => {
+			it( 'should be false if wrong node', () => {
+				setData( model, '<p>foo[]</p>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be true if in table', () => {
+				setData( model, modelTable( 1, [ '[]' ] ) );
+				expect( command.isEnabled ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should insert column in given table at given index', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22'
+			] ) );
+
+			command.execute( { at: 1 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 3, [
+				'11[]', '', '12',
+				'21', '', '22'
+			] ) );
+		} );
+
+		it( 'should insert column in given table at default index', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22'
+			] ) );
+
+			command.execute();
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 3, [
+				'', '11[]', '12',
+				'', '21', '22'
+			] ) );
+		} );
+
+		it( 'should update table heading columns attribute when inserting column in headings section', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22',
+				'31', '32'
+			], { headingColumns: 2 } ) );
+
+			command.execute( { at: 1 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 3, [
+				'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( 2, [
+				'11[]', '12',
+				'21', '22',
+				'31', '32'
+			], { headingColumns: 2 } ) );
+
+			command.execute( { at: 2 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 3, [
+				'11[]', '12', '',
+				'21', '22', '',
+				'31', '32', ''
+			], { headingColumns: 2 } ) );
+		} );
+	} );
+} );

+ 151 - 0
packages/ckeditor5-table/tests/insertrowcommand.js

@@ -0,0 +1,151 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import InsertRowCommand from '../src/insertrowcommand';
+import downcastTable from '../src/converters/downcasttable';
+import upcastTable from '../src/converters/upcasttable';
+import { formatModelTable, formattedModelTable, modelTable } from './_utils/utils';
+
+describe( 'InsertRowCommand', () => {
+	let editor, model, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				command = new InsertRowCommand( 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( downcastTable() );
+
+				// 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', () => {
+			it( 'should be false if wrong node', () => {
+				setData( model, '<p>foo[]</p>' );
+				expect( command.isEnabled ).to.be.false;
+			} );
+
+			it( 'should be true if in table', () => {
+				setData( model, modelTable( 1, [ '[]' ] ) );
+				expect( command.isEnabled ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should insert row in given table at given index', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22'
+			] ) );
+
+			command.execute( { at: 1 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 2, [
+				'11[]', '12',
+				'', '',
+				'21', '22'
+			] ) );
+		} );
+
+		it( 'should insert row in given table at default index', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22'
+			] ) );
+
+			command.execute();
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 2, [
+				'', '',
+				'11[]', '12',
+				'21', '22'
+			] ) );
+		} );
+
+		it( 'should update table heading rows attribute when inserting row in headings section', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22',
+				'31', '32'
+			], { headingRows: 2 } ) );
+
+			command.execute( { at: 1 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 2, [
+				'11[]', '12',
+				'', '',
+				'21', '22',
+				'31', '32'
+			], { headingRows: 3 } ) );
+		} );
+
+		it( 'should not update table heading rows attribute when inserting row after headings section', () => {
+			setData( model, modelTable( 2, [
+				'11[]', '12',
+				'21', '22',
+				'31', '32'
+			], { headingRows: 2 } ) );
+
+			command.execute( { at: 2 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( 2, [
+				'11[]', '12',
+				'21', '22',
+				'', '',
+				'31', '32'
+			], { headingRows: 2 } ) );
+		} );
+	} );
+} );

+ 1 - 0
packages/ckeditor5-table/tests/inserttablecommand.js

@@ -96,6 +96,7 @@ describe( 'InsertTableCommand', () => {
 					'</table>[]'
 				);
 			} );
+
 			it( 'should insert table with two rows and two columns after non-empty paragraph', () => {
 				setData( model, '<p>foo[]</p>' );
 

+ 2 - 1
packages/ckeditor5-table/tests/manual/table.js

@@ -13,7 +13,8 @@ ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ ArticlePluginSet, Table ],
 		toolbar: [
-			'heading', '|', 'insertTable', 'insertRow', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
+			'heading', '|', 'insertTable', 'insertRow', 'insertColumn',
+			'|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
 		]
 	} )
 	.then( editor => {