8
0
Просмотр исходного кода

Merge pull request #8286 from ckeditor/cf/3466

Internal (table): Exposed API for integration with `TableClipboard`.
Maciej 5 лет назад
Родитель
Сommit
f60cbd5c22

+ 186 - 148
packages/ckeditor5-table/src/tableclipboard.js

@@ -56,6 +56,8 @@ export default class TableClipboard extends Plugin {
 		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) );
 		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) );
 		this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } );
+
+		this.decorate( '_replaceTableSlotCell' );
 	}
 
 	/**
@@ -164,7 +166,7 @@ export default class TableClipboard extends Plugin {
 			// Content table to which we insert a pasted table.
 			const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );
 
-			const cellsToSelect = replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
+			const cellsToSelect = this._replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
 
 			if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) {
 				// Selection ranges must be sorted because the first and last selection ranges are considered
@@ -178,6 +180,189 @@ export default class TableClipboard extends Plugin {
 			}
 		} );
 	}
+
+	/**
+	 * Replaces the part of selectedTable with pastedTable.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} pastedTable
+	 * @param {Object} pastedDimensions
+	 * @param {Number} pastedDimensions.height
+	 * @param {Number} pastedDimensions.width
+	 * @param {module:engine/model/element~Element} selectedTable
+	 * @param {Object} selection
+	 * @param {Number} selection.firstColumn
+	 * @param {Number} selection.firstRow
+	 * @param {Number} selection.lastColumn
+	 * @param {Number} selection.lastRow
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @returns {Array.<module:engine/model/element~Element>}
+	 */
+	_replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
+		const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
+
+		// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
+		const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
+
+		const selectedTableMap = [ ...new TableWalker( selectedTable, {
+			startRow: selection.firstRow,
+			endRow: selection.lastRow,
+			startColumn: selection.firstColumn,
+			endColumn: selection.lastColumn,
+			includeAllSlots: true
+		} ) ];
+
+		// Selection must be set to pasted cells (some might be removed or new created).
+		const cellsToSelect = [];
+
+		// Store next cell insert position.
+		let insertPosition;
+
+		// Content table replace cells algorithm iterates over a selected table fragment and:
+		//
+		// - Removes existing table cells at current slot (location).
+		// - Inserts cell from a pasted table for a matched slots.
+		//
+		// This ensures proper table geometry after the paste
+		for ( const tableSlot of selectedTableMap ) {
+			const { row, column } = tableSlot;
+
+			// Save the insert position for current row start.
+			if ( column === selection.firstColumn ) {
+				insertPosition = tableSlot.getPositionBefore();
+			}
+
+			// Map current table slot location to an pasted table slot location.
+			const pastedRow = row - selection.firstRow;
+			const pastedColumn = column - selection.firstColumn;
+			const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
+
+			// Clone cell to insert (to duplicate its attributes and children).
+			// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
+			const cellToInsert = pastedCell ? writer.cloneElement( pastedCell ) : null;
+
+			// Replace the cell from the current slot with new table cell.
+			const newTableCell = this._replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer );
+
+			// The cell was only removed.
+			if ( !newTableCell ) {
+				continue;
+			}
+
+			// Trim the cell if it's row/col-spans would exceed selection area.
+			trimTableCellIfNeeded( newTableCell, row, column, selection.lastRow, selection.lastColumn, writer );
+
+			cellsToSelect.push( newTableCell );
+
+			insertPosition = writer.createPositionAfter( newTableCell );
+		}
+
+		// If there are any headings, all the cells that overlap from heading must be splitted.
+		const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
+		const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
+
+		const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
+		const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
+
+		if ( areHeadingRowsIntersectingSelection ) {
+			const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
+			const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
+
+			cellsToSelect.push( ...newCells );
+		}
+
+		if ( areHeadingColumnsIntersectingSelection ) {
+			const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
+			const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
+
+			cellsToSelect.push( ...newCells );
+		}
+
+		return cellsToSelect;
+	}
+
+	/**
+	 * Replaces a single table slot.
+	 *
+	 * @private
+	 * @param {module:table/tablewalker~TableSlot} tableSlot
+	 * @param {module:engine/model/element~Element} cellToInsert
+	 * @param {module:engine/model/position~Position} insertPosition
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @returns {module:engine/model/element~Element|null} Inserted table cell or null if slot should remain empty.
+	 */
+	_replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer ) {
+		const { cell, isAnchor } = tableSlot;
+
+		// If the slot is occupied by a cell in a selected table - remove it.
+		// The slot of this cell will be either:
+		// - Replaced by a pasted table cell.
+		// - Spanned by a previously pasted table cell.
+		if ( isAnchor ) {
+			writer.remove( cell );
+		}
+
+		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
+		if ( !cellToInsert ) {
+			return null;
+		}
+
+		writer.insert( cellToInsert, insertPosition );
+
+		return cellToInsert;
+	}
+}
+
+/**
+ * Extract table for pasting into table.
+ *
+ * @private
+ * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
+ * @param {module:engine/model/model~Model} model The editor model.
+ * @returns {module:engine/model/element~Element|null}
+ */
+export function getTableIfOnlyTableInContent( content, model ) {
+	if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
+		return null;
+	}
+
+	// Table passed directly.
+	if ( content.is( 'element', 'table' ) ) {
+		return content;
+	}
+
+	// We do not support mixed content when pasting table into table.
+	// See: https://github.com/ckeditor/ckeditor5/issues/6817.
+	if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
+		return content.getChild( 0 );
+	}
+
+	// If there are only whitespaces around a table then use that table for pasting.
+
+	const contentRange = model.createRangeIn( content );
+
+	for ( const element of contentRange.getItems() ) {
+		if ( element.is( 'element', 'table' ) ) {
+			// Stop checking if there is some content before table.
+			const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
+
+			if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
+				return null;
+			}
+
+			// Stop checking if there is some content after table.
+			const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
+
+			if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
+				return null;
+			}
+
+			// There wasn't any content neither before nor after.
+			return element;
+		}
+	}
+
+	return null;
 }
 
 // Prepares a table for pasting and returns adjusted selection dimensions.
@@ -246,109 +431,6 @@ function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, t
 	return selection;
 }
 
-// Replaces the part of selectedTable with pastedTable.
-//
-// @param {module:engine/model/element~Element} pastedTable
-// @param {Object} pastedDimensions
-// @param {Number} pastedDimensions.height
-// @param {Number} pastedDimensions.width
-// @param {module:engine/model/element~Element} selectedTable
-// @param {Object} selection
-// @param {Number} selection.firstColumn
-// @param {Number} selection.firstRow
-// @param {Number} selection.lastColumn
-// @param {Number} selection.lastRow
-// @param {module:engine/model/writer~Writer} writer
-// @returns {Array.<module:engine/model/element~Element>}
-function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
-	const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
-
-	// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
-	const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
-
-	const selectedTableMap = [ ...new TableWalker( selectedTable, {
-		startRow: selection.firstRow,
-		endRow: selection.lastRow,
-		startColumn: selection.firstColumn,
-		endColumn: selection.lastColumn,
-		includeAllSlots: true
-	} ) ];
-
-	// Selection must be set to pasted cells (some might be removed or new created).
-	const cellsToSelect = [];
-
-	// Store next cell insert position.
-	let insertPosition;
-
-	// Content table replace cells algorithm iterates over a selected table fragment and:
-	//
-	// - Removes existing table cells at current slot (location).
-	// - Inserts cell from a pasted table for a matched slots.
-	//
-	// This ensures proper table geometry after the paste
-	for ( const tableSlot of selectedTableMap ) {
-		const { row, column, cell, isAnchor } = tableSlot;
-
-		// Save the insert position for current row start.
-		if ( column === selection.firstColumn ) {
-			insertPosition = tableSlot.getPositionBefore();
-		}
-
-		// If the slot is occupied by a cell in a selected table - remove it.
-		// The slot of this cell will be either:
-		// - Replaced by a pasted table cell.
-		// - Spanned by a previously pasted table cell.
-		if ( isAnchor ) {
-			writer.remove( cell );
-		}
-
-		// Map current table slot location to an pasted table slot location.
-		const pastedRow = row - selection.firstRow;
-		const pastedColumn = column - selection.firstColumn;
-		const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
-
-		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
-		if ( !pastedCell ) {
-			continue;
-		}
-
-		// Clone cell to insert (to duplicate its attributes and children).
-		// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
-		const cellToInsert = writer.cloneElement( pastedCell );
-
-		// Trim the cell if it's row/col-spans would exceed selection area.
-		trimTableCellIfNeeded( cellToInsert, row, column, selection.lastRow, selection.lastColumn, writer );
-
-		writer.insert( cellToInsert, insertPosition );
-		cellsToSelect.push( cellToInsert );
-
-		insertPosition = writer.createPositionAfter( cellToInsert );
-	}
-
-	// If there are any headings, all the cells that overlap from heading must be splitted.
-	const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
-	const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
-
-	const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
-	const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
-
-	if ( areHeadingRowsIntersectingSelection ) {
-		const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
-		const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
-
-		cellsToSelect.push( ...newCells );
-	}
-
-	if ( areHeadingColumnsIntersectingSelection ) {
-		const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
-		const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
-
-		cellsToSelect.push( ...newCells );
-	}
-
-	return cellsToSelect;
-}
-
 // Expand table (in place) to expected size.
 function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
 	const tableWidth = tableUtils.getColumns( table );
@@ -369,50 +451,6 @@ function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
 	}
 }
 
-function getTableIfOnlyTableInContent( content, model ) {
-	if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
-		return null;
-	}
-
-	// Table passed directly.
-	if ( content.is( 'element', 'table' ) ) {
-		return content;
-	}
-
-	// We do not support mixed content when pasting table into table.
-	// See: https://github.com/ckeditor/ckeditor5/issues/6817.
-	if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
-		return content.getChild( 0 );
-	}
-
-	// If there are only whitespaces around a table then use that table for pasting.
-
-	const contentRange = model.createRangeIn( content );
-
-	for ( const element of contentRange.getItems() ) {
-		if ( element.is( 'element', 'table' ) ) {
-			// Stop checking if there is some content before table.
-			const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
-
-			if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
-				return null;
-			}
-
-			// Stop checking if there is some content after table.
-			const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
-
-			if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
-				return null;
-			}
-
-			// There wasn't any content neither before nor after.
-			return element;
-		}
-	}
-
-	return null;
-}
-
 // Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
 //
 // At given row & column location it might be one of:

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

@@ -26,6 +26,14 @@ export default class TableUtils extends Plugin {
 		return 'TableUtils';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.decorate( 'insertColumns' );
+		this.decorate( 'insertRows' );
+	}
+
 	/**
 	 * Returns the table cell location as an object with table row and table column indexes.
 	 *

+ 146 - 1
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -23,7 +23,7 @@ import TableEditing from '../src/tableediting';
 import TableCellPropertiesEditing from '../src/tablecellproperties/tablecellpropertiesediting';
 import TableWalker from '../src/tablewalker';
 
-import TableClipboard from '../src/tableclipboard';
+import TableClipboard, { getTableIfOnlyTableInContent } from '../src/tableclipboard';
 
 describe( 'table clipboard', () => {
 	let editor, model, modelRoot, tableSelection, viewDocument, element;
@@ -478,6 +478,30 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
+		it( '#_replaceTableSlotCell() should be overridable', () => {
+			const tableClipboard = editor.plugins.get( 'TableClipboard' );
+
+			tableClipboard.on( '_replaceTableSlotCell', ( evt, args ) => {
+				const [ /* tableSlot */, cellToInsert, /* insertPosition */, writer ] = args;
+
+				if ( cellToInsert ) {
+					writer.setAttribute( 'foo', 'bar', cellToInsert );
+				}
+			}, { priority: 'high' } );
+
+			pasteTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ { contents: 'aa', foo: 'bar' }, { contents: 'ab', foo: 'bar' }, '02', '03' ],
+				[ { contents: 'ba', foo: 'bar' }, { contents: 'bb', foo: 'bar' }, '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
 		describe( 'single cell selected', () => {
 			beforeEach( () => {
 				setModelData( model, modelTable( [
@@ -3949,6 +3973,127 @@ describe( 'table clipboard', () => {
 		} );
 	} );
 
+	describe( 'getTableIfOnlyTableInContent helper', () => {
+		beforeEach( async () => {
+			await createEditor();
+		} );
+
+		it( 'should return null for no table provided', () => {
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const content = modelRoot.getChild( 0 );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for a text node provided', async () => {
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const content = modelRoot.getNodeByPath( [ 0, 0 ] );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for mixed content provided (table + paragraph)', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph>foo</paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for mixed content provided (paragraph + table)', () => {
+			setModelData( model,
+				'<paragraph>foo</paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return table element for mixed content provided (table + empty paragraph)', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph></paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for mixed content provided (table + empty paragraph)', () => {
+			setModelData( model,
+				'<paragraph></paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for mixed content provided (p + p + table + p)', () => {
+			setModelData( model,
+				'<paragraph></paragraph>' +
+				'<paragraph></paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph></paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for if table is the only element provided in document fragment', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for if table is the only element provided directly', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = modelRoot.getChild( 0 );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		function documentFragmentFromChildren( element ) {
+			return model.change( writer => {
+				const documentFragment = writer.createDocumentFragment();
+
+				for ( const child of element.getChildren() ) {
+					writer.insert( writer.cloneElement( child ), documentFragment, 'end' );
+				}
+
+				return documentFragment;
+			} );
+		}
+	} );
+
 	async function createEditor( extraPlugins = [] ) {
 		editor = await ClassicTestEditor.create( element, {
 			plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard, ...extraPlugins ]

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

@@ -5,6 +5,7 @@
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
@@ -16,6 +17,8 @@ import TableUtils from '../src/tableutils';
 describe( 'TableUtils', () => {
 	let editor, model, root, tableUtils;
 
+	testUtils.createSinonSandbox();
+
 	beforeEach( () => {
 		return ModelTestEditor.create( {
 			plugins: [ Paragraph, TableEditing, TableUtils ]
@@ -51,6 +54,27 @@ describe( 'TableUtils', () => {
 	} );
 
 	describe( 'insertRows()', () => {
+		it( 'should be decorated', () => {
+			const spy = sinon.spy();
+
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.on( 'insertRows', spy );
+
+			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '11[]', '12' ],
+				[ '', '' ],
+				[ '21', '22' ]
+			] ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
 		it( 'should insert row in given table at given index', () => {
 			setData( model, modelTable( [
 				[ '11[]', '12' ],
@@ -335,6 +359,26 @@ describe( 'TableUtils', () => {
 	} );
 
 	describe( 'insertColumns()', () => {
+		it( 'should be decorated', () => {
+			const spy = sinon.spy();
+
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.on( 'insertColumns', spy );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '11[]', '', '12' ],
+				[ '21', '', '22' ]
+			] ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
 		it( 'should insert column in given table at given index', () => {
 			setData( model, modelTable( [
 				[ '11[]', '12' ],