From 9ac915b2b447feddbf700da3c4e33ea4128ac46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Mar 2020 16:21:05 +0200 Subject: [PATCH 01/71] Add tests for pasting scenarios. --- .../ckeditor5-table/tests/tableclipboard.js | 145 +++++++++++++++++- 1 file changed, 139 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 39110d703be..5a66d3f3ccf 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -3,21 +3,27 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import TableEditing from '../src/tableediting'; -import { modelTable, viewTable } from './_utils/utils'; -import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; -import TableClipboard from '../src/tableclipboard'; +import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import TableClipboard from '../src/tableclipboard'; + describe( 'table clipboard', () => { - let editor, model, modelRoot, tableSelection, viewDocument; + let editor, model, modelRoot, tableSelection, viewDocument, element; beforeEach( async () => { - editor = await VirtualTestEditor.create( { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ] } ); @@ -35,6 +41,8 @@ describe( 'table clipboard', () => { afterEach( async () => { await editor.destroy(); + + element.remove(); } ); describe( 'Clipboard integration', () => { @@ -389,6 +397,121 @@ describe( 'table clipboard', () => { sinon.assert.calledOnce( preventDefaultStub ); } ); } ); + + describe( 'paste', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + describe( 'pasted table is equal ot selected area', () => { + it( 'pastes simple table to a simple table fragment - at the beginning of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02', '03' ], + [ 'ba', 'bb', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + assertSelectedCells( model, [ + [ 1, 1, 0, 0 ], + [ 1, 1, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'pastes simple table to a simple table fragment - at the end of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', 'aa', 'ab' ], + [ '30', '31', 'ba', 'bb' ] + ] ) ); + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 1, 1 ], + [ 0, 0, 1, 1 ] + ] ); + } ); + + it( 'pastes simple table to a simple table fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'pastes simple table to a simple table fragment - whole table selected', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ) ); + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); + } ); } ); function assertClipboardContentOnMethod( method, expectedViewTable ) { @@ -401,6 +524,16 @@ describe( 'table clipboard', () => { expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( expectedViewTable ); } + function pasteTable( tableData ) { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', viewTable( tableData ) ); + viewDocument.fire( 'paste', data ); + } + function createDataTransfer() { const store = new Map(); From 6f7ba10a5117c2266feedf3e7e50a36d37816d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Apr 2020 10:01:30 +0200 Subject: [PATCH 02/71] Add paste handler stub to TableClipboard plugin. --- .../ckeditor5-table/src/tableclipboard.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 8486da2e058..7257cded6ab 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -40,6 +40,7 @@ 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( viewDocument, 'paste', ( evt, data ) => this._onPaste( evt, data ) ); } /** @@ -74,4 +75,26 @@ export default class TableClipboard extends Plugin { method: evt.name } ); } + + /** + * Handles "paste" event over a table. + * + * @private + * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event. + * @param {Object} data Clipboard event data. + */ + _onPaste( evt, data ) { + if ( this.editor.isReadOnly ) { + return; + } + + const tableSelection = this.editor.plugins.get( 'TableSelection' ); + + if ( !tableSelection.getSelectedTableCells() ) { + return; + } + + data.preventDefault(); + evt.stop(); + } } From 791649f75241055d59d937940b876e17787325e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Apr 2020 10:04:42 +0200 Subject: [PATCH 03/71] Add test for preventing paste in read only mode. --- .../ckeditor5-table/tests/tableclipboard.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 5a66d3f3ccf..4ecd94e5dcb 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -409,6 +409,31 @@ describe( 'table clipboard', () => { } ); describe( 'pasted table is equal ot selected area', () => { + it( 'should be disabled in a readonly mode', () => { + editor.isReadOnly = true; + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const data = pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + editor.isReadOnly = false; + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + sinon.assert.calledOnce( data.preventDefault ); + } ); + it( 'pastes simple table to a simple table fragment - at the beginning of a table', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), @@ -532,6 +557,8 @@ describe( 'table clipboard', () => { }; data.dataTransfer.setData( 'text/html', viewTable( tableData ) ); viewDocument.fire( 'paste', data ); + + return data; } function createDataTransfer() { From 8b90a88304d6ccd6c772a02fa7092d51378b5b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Apr 2020 10:12:49 +0200 Subject: [PATCH 04/71] Add test for pasting when no table cells are selected. --- .../ckeditor5-table/tests/tableclipboard.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 4ecd94e5dcb..f76ffce3ff0 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -434,6 +434,25 @@ describe( 'table clipboard', () => { sinon.assert.calledOnce( data.preventDefault ); } ); + it( 'should allow normal paste if no table cells are selected', () => { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', '

foo

' ); + viewDocument.fire( 'paste', data ); + + editor.isReadOnly = false; + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00foo[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + it( 'pastes simple table to a simple table fragment - at the beginning of a table', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), From c7b28a26a0780ba9ca439014e2e22b689848c4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Apr 2020 11:20:56 +0200 Subject: [PATCH 05/71] Listen to Clipboard plugin's "inputTransformation" event. --- packages/ckeditor5-table/src/tableclipboard.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 7257cded6ab..ce6bd98ca91 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -38,9 +38,11 @@ export default class TableClipboard extends Plugin { const editor = this.editor; const viewDocument = editor.editing.view.document; + const clipboardPlugin = editor.plugins.get( 'Clipboard' ); + this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) ); this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) ); - this.listenTo( viewDocument, 'paste', ( evt, data ) => this._onPaste( evt, data ) ); + this.listenTo( clipboardPlugin, 'inputTransformation', ( evt, data ) => this._onPaste( evt, data ) ); } /** @@ -89,12 +91,15 @@ export default class TableClipboard extends Plugin { } const tableSelection = this.editor.plugins.get( 'TableSelection' ); + const selectedTableCells = tableSelection.getSelectedTableCells(); - if ( !tableSelection.getSelectedTableCells() ) { + if ( !selectedTableCells ) { return; } data.preventDefault(); evt.stop(); + + // const pastedTable = } } From 7b0e49c0486f9e545ae75f8566d2f3fe42d2a532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 12:07:01 +0200 Subject: [PATCH 06/71] Handle insertContent overriding in a base paste scenario. --- .../ckeditor5-table/src/tableclipboard.js | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index ce6bd98ca91..ffda7e941f9 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -38,11 +38,9 @@ export default class TableClipboard extends Plugin { const editor = this.editor; const viewDocument = editor.editing.view.document; - const clipboardPlugin = editor.plugins.get( 'Clipboard' ); - this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) ); this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) ); - this.listenTo( clipboardPlugin, 'inputTransformation', ( evt, data ) => this._onPaste( evt, data ) ); + this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } ); } /** @@ -79,13 +77,17 @@ export default class TableClipboard extends Plugin { } /** - * Handles "paste" event over a table. + * Handles... * * @private - * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event. - * @param {Object} data Clipboard event data. + * @param evt + * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. + * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection] + * The selection into which the content should be inserted. If not provided the current model document selection will be used. + * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`. + * This param defines a position in relation to that item. */ - _onPaste( evt, data ) { + _onInsertContent( evt, content ) { if ( this.editor.isReadOnly ) { return; } @@ -97,9 +99,20 @@ export default class TableClipboard extends Plugin { return; } - data.preventDefault(); - evt.stop(); + if ( containsTable( content ) ) { + // console.log( 'contains table' ); - // const pastedTable = + evt.stop(); + } } } + +function containsTable( content ) { + for ( const child of content ) { + if ( child.is( 'table' ) ) { + return true; + } + } + + return false; +} From 65225630f4d6f9352618c6f84c1325f8303e842a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 13:34:24 +0200 Subject: [PATCH 07/71] Calculate and compare selection and inserted table areas. --- .../ckeditor5-table/src/tableclipboard.js | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index ffda7e941f9..fc02b033498 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -9,6 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import TableSelection from './tableselection'; +import { getColumnIndexes, getRowIndexes } from './utils'; /** * This plugin adds support for copying/cutting/pasting fragments of tables. @@ -99,20 +100,47 @@ export default class TableClipboard extends Plugin { return; } - if ( containsTable( content ) ) { - // console.log( 'contains table' ); + const table = getTable( content ); + if ( table ) { evt.stop(); + + if ( selectedTableCells.length === 1 ) { + // @if CK_DEBUG // console.log( 'Single table cell is selected. Not handled.' ); + + return; + } + + const tableUtils = this.editor.plugins.get( 'TableUtils' ); + + const rowIndexes = getRowIndexes( selectedTableCells ); + const columnIndexes = getColumnIndexes( selectedTableCells ); + const selectionHeight = rowIndexes.last - rowIndexes.first + 1; + const selectionWidth = columnIndexes.last - columnIndexes.first + 1; + const insertHeight = tableUtils.getRows( table ); + const insertWidth = tableUtils.getColumns( table ); + + if ( selectionHeight === insertHeight && selectionWidth === insertHeight ) { + // @if CK_DEBUG // console.log( 'Pasted table and selection area are the same.' ); + + return; + } + + if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); + } else { + // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); + } } } } -function containsTable( content ) { +function getTable( content ) { for ( const child of content ) { if ( child.is( 'table' ) ) { - return true; + return child; } } - return false; + return null; } From 149bb66fbfd65178de4ce576007d5094000c3f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 14:40:12 +0200 Subject: [PATCH 08/71] Handle simple table paste over simple selection of same size. --- .../ckeditor5-table/src/tableclipboard.js | 35 +++++++++++++++++-- .../ckeditor5-table/tests/tableclipboard.js | 11 +++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index fc02b033498..569836f18b0 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -10,6 +10,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import TableSelection from './tableselection'; import { getColumnIndexes, getRowIndexes } from './utils'; +import TableWalker from './tablewalker'; +import { findAncestor } from './commands/utils'; /** * This plugin adds support for copying/cutting/pasting fragments of tables. @@ -120,9 +122,36 @@ export default class TableClipboard extends Plugin { const insertHeight = tableUtils.getRows( table ); const insertWidth = tableUtils.getColumns( table ); - if ( selectionHeight === insertHeight && selectionWidth === insertHeight ) { - // @if CK_DEBUG // console.log( 'Pasted table and selection area are the same.' ); + const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + if ( selectionHeight === insertHeight && selectionWidth === insertWidth ) { + const model = this.editor.model; + + model.change( writer => { + const insertionMap = new Map(); + + for ( const { column, row, cell } of new TableWalker( table ) ) { + insertionMap.set( `${ row }x${ column }`, cell ); + } + + for ( const { column, row, cell } of new TableWalker( contentTable, { + startRow: rowIndexes.first, + endRow: rowIndexes.last + } ) ) { + if ( column < columnIndexes.first || column > columnIndexes.last ) { + continue; + } + + const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; + + const cellToInsert = insertionMap.get( toGet ); + writer.remove( writer.createRangeIn( cell ) ); + + for ( const child of cellToInsert.getChildren() ) { + writer.insert( child, cell, 'end' ); + } + } + } ); return; } @@ -136,7 +165,7 @@ export default class TableClipboard extends Plugin { } function getTable( content ) { - for ( const child of content ) { + for ( const child of Array.from( content ) ) { if ( child.is( 'table' ) ) { return child; } diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index f76ffce3ff0..da7628f74e9 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -408,7 +408,7 @@ describe( 'table clipboard', () => { ] ) ); } ); - describe( 'pasted table is equal ot selected area', () => { + describe( 'pasted table is equal to the selected area', () => { it( 'should be disabled in a readonly mode', () => { editor.isReadOnly = true; @@ -453,7 +453,7 @@ describe( 'table clipboard', () => { ] ) ); } ); - it( 'pastes simple table to a simple table fragment - at the beginning of a table', () => { + it( 'inserts simple table to a simple table fragment - at the beginning of a table', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 1, 1 ] ) @@ -470,6 +470,7 @@ describe( 'table clipboard', () => { [ '20', '21', '22', '23' ], [ '30', '31', '32', '33' ] ] ) ); + assertSelectedCells( model, [ [ 1, 1, 0, 0 ], [ 1, 1, 0, 0 ], @@ -478,7 +479,7 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'pastes simple table to a simple table fragment - at the end of a table', () => { + it( 'inserts simple table to a simple table fragment - at the end of a table', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 2, 2 ] ), modelRoot.getNodeByPath( [ 0, 3, 3 ] ) @@ -503,7 +504,7 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'pastes simple table to a simple table fragment - in the middle of a table', () => { + it( 'inserts simple table to a simple table fragment - in the middle of a table', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 2 ] ) @@ -528,7 +529,7 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'pastes simple table to a simple table fragment - whole table selected', () => { + it( 'inserts simple table to a simple table fragment - whole table selected', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 3 ] ) From f6e0c2fae9c088ad38b81e8c64dd86c749219f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 14:44:55 +0200 Subject: [PATCH 09/71] Add column/row tests for simple paste scenarios. --- .../ckeditor5-table/src/tableclipboard.js | 1 + .../ckeditor5-table/tests/tableclipboard.js | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 569836f18b0..72143120993 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -152,6 +152,7 @@ export default class TableClipboard extends Plugin { } } } ); + return; } diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index da7628f74e9..7e3b0d1fcc8 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -496,6 +496,7 @@ describe( 'table clipboard', () => { [ '20', '21', 'aa', 'ab' ], [ '30', '31', 'ba', 'bb' ] ] ) ); + assertSelectedCells( model, [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], @@ -521,6 +522,7 @@ describe( 'table clipboard', () => { [ '20', 'ba', 'bb', '23' ], [ '30', '31', '32', '33' ] ] ) ); + assertSelectedCells( model, [ [ 0, 0, 0, 0 ], [ 0, 1, 1, 0 ], @@ -529,6 +531,57 @@ describe( 'table clipboard', () => { ] ); } ); + it( 'inserts simple row to a simple row fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'inserts simple column to a simple column fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa' ], + [ 'ba' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', '12', '13' ], + [ '20', 'ba', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + it( 'inserts simple table to a simple table fragment - whole table selected', () => { tableSelection._setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), @@ -548,6 +601,7 @@ describe( 'table clipboard', () => { [ 'ca', 'cb', 'cc', 'cd' ], [ 'da', 'db', 'dc', 'dd' ] ] ) ); + assertSelectedCells( model, [ [ 1, 1, 1, 1 ], [ 1, 1, 1, 1 ], From 07a5e42df89dab0f591614831c261cb5d48f1f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 15:10:12 +0200 Subject: [PATCH 10/71] Handle case of pasted table that has has colspans. --- .../ckeditor5-table/src/tableclipboard.js | 20 +- .../ckeditor5-table/tests/tableclipboard.js | 368 ++++++++++-------- 2 files changed, 229 insertions(+), 159 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 72143120993..2b773895e48 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import TableSelection from './tableselection'; import { getColumnIndexes, getRowIndexes } from './utils'; import TableWalker from './tablewalker'; -import { findAncestor } from './commands/utils'; +import { findAncestor, updateNumericAttribute } from './commands/utils'; /** * This plugin adds support for copying/cutting/pasting fragments of tables. @@ -134,19 +134,27 @@ export default class TableClipboard extends Plugin { insertionMap.set( `${ row }x${ column }`, cell ); } - for ( const { column, row, cell } of new TableWalker( contentTable, { - startRow: rowIndexes.first, - endRow: rowIndexes.last - } ) ) { + const tableMap = [ ...new TableWalker( contentTable, { startRow: rowIndexes.first, endRow: rowIndexes.last } ) ]; + + for ( const { column, row, cell } of tableMap ) { + // TODO: crete issue for table walker startColumn, endColumn. if ( column < columnIndexes.first || column > columnIndexes.last ) { continue; } const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; - const cellToInsert = insertionMap.get( toGet ); + + if ( !cellToInsert ) { + writer.remove( writer.createRangeOn( cell ) ); + + continue; + } + writer.remove( writer.createRangeIn( cell ) ); + updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), cell, writer, 1 ); + for ( const child of cellToInsert.getChildren() ) { writer.insert( child, cell, 'end' ); } diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 7e3b0d1fcc8..04d78fe3078 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -453,161 +453,223 @@ describe( 'table clipboard', () => { ] ) ); } ); - it( 'inserts simple table to a simple table fragment - at the beginning of a table', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', '02', '03' ], - [ 'ba', 'bb', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 0, 0 ], - [ 1, 1, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'inserts simple table to a simple table fragment - at the end of a table', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 2, 2 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', 'aa', 'ab' ], - [ '30', '31', 'ba', 'bb' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 1, 1 ], - [ 0, 0, 1, 1 ] - ] ); - } ); - - it( 'inserts simple table to a simple table fragment - in the middle of a table', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); + describe( 'no spans', () => { + it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02', '03' ], + [ 'ba', 'bb', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 0, 0 ], + [ 1, 1, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', 'aa', 'ab' ], + [ '30', '31', 'ba', 'bb' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 1, 1 ], + [ 0, 0, 1, 1 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa' ], + [ 'ba' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', '12', '13' ], + [ '20', 'ba', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - whole table selected', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); } ); - it( 'inserts simple row to a simple row fragment - in the middle of a table', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'inserts simple column to a simple column fragment - in the middle of a table', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa' ], - [ 'ba' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', '12', '13' ], - [ '20', 'ba', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'inserts simple table to a simple table fragment - whole table selected', () => { - tableSelection._setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ] - ] ); + describe( 'pasted table has spans', () => { + it( 'handles pasting table that has cell with colspan', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { colspan: 2, contents: 'aa' } ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various colspan', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ], + [ { colspan: 2, contents: 'da' }, 'dc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ { colspan: 2, contents: 'da' }, 'dc', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); } ); } ); From 06ac3f03128a0830e68f1550e7fe9dd495e8ebc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 16:16:37 +0200 Subject: [PATCH 11/71] Handle pasting over table with colspans. --- .../ckeditor5-table/src/tableclipboard.js | 36 +++++++-- .../ckeditor5-table/tests/tableclipboard.js | 76 +++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 2b773895e48..74fe40f5e8f 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -134,11 +134,21 @@ export default class TableClipboard extends Plugin { insertionMap.set( `${ row }x${ column }`, cell ); } - const tableMap = [ ...new TableWalker( contentTable, { startRow: rowIndexes.first, endRow: rowIndexes.last } ) ]; + const tableMap = [ ...new TableWalker( contentTable, { + startRow: rowIndexes.first, + endRow: rowIndexes.last, + includeSpanned: true + } ) ]; - for ( const { column, row, cell } of tableMap ) { + let previousCell; + + const cellsToSelect = []; + + for ( const { column, row, cell, isSpanned } of tableMap ) { // TODO: crete issue for table walker startColumn, endColumn. if ( column < columnIndexes.first || column > columnIndexes.last ) { + previousCell = cell; + continue; } @@ -146,19 +156,33 @@ export default class TableClipboard extends Plugin { const cellToInsert = insertionMap.get( toGet ); if ( !cellToInsert ) { - writer.remove( writer.createRangeOn( cell ) ); + if ( !isSpanned ) { + writer.remove( writer.createRangeOn( cell ) ); + } continue; } - writer.remove( writer.createRangeIn( cell ) ); + let targetCell = cell; - updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), cell, writer, 1 ); + if ( isSpanned ) { + targetCell = writer.createElement( 'tableCell' ); + writer.insert( targetCell, writer.createPositionAfter( previousCell ) ); + } else { + writer.remove( writer.createRangeIn( cell ) ); + } + + updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); for ( const child of cellToInsert.getChildren() ) { - writer.insert( child, cell, 'end' ); + writer.insert( child, targetCell, 'end' ); } + + cellsToSelect.push( targetCell ); + previousCell = targetCell; } + + writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); } ); return; diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 04d78fe3078..8296e3cda64 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -671,6 +671,82 @@ describe( 'table clipboard', () => { /* eslint-enable no-multi-spaces */ } ); } ); + + describe( 'content table has spans', () => { + it( 'handles pasting simple table over table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + + describe( 'content and paste tables have spans', () => { + it( 'handles pasting simple table over table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); } ); } ); } ); From b9adc75dd5990aff96af891babd39043ea9b1cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 6 May 2020 17:17:47 +0200 Subject: [PATCH 12/71] Handle various rowspan scenarios in tests. --- .../ckeditor5-table/src/tableclipboard.js | 12 +- .../ckeditor5-table/tests/tableclipboard.js | 133 +++++++++++++++++- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 74fe40f5e8f..d8190fb5b5d 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -166,13 +166,23 @@ export default class TableClipboard extends Plugin { let targetCell = cell; if ( isSpanned ) { + let insertPosition; + + if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { + insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); + } else { + insertPosition = writer.createPositionAfter( previousCell ); + } + targetCell = writer.createElement( 'tableCell' ); - writer.insert( targetCell, writer.createPositionAfter( previousCell ) ); + + writer.insert( targetCell, insertPosition ); } else { writer.remove( writer.createRangeIn( cell ) ); } updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); + updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); for ( const child of cellToInsert.getChildren() ) { writer.insert( child, targetCell, 'end' ); diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 8296e3cda64..868a248b314 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -670,10 +670,67 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting table that has cell with rowspan', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { rowspan: 2, contents: 'aa' }, 'ab' ], + [ 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], + [ '20', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various rowspan', () => { + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 3 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); describe( 'content table has spans', () => { - it( 'handles pasting simple table over table with colspans (no colspan exceeds selection)', () => { + it( 'handles pasting simple table over a table with colspans (no colspan exceeds selection)', () => { setModelData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ { colspan: 3, contents: '10' }, '13' ], @@ -708,10 +765,46 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting simple table over a table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00', { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], + [ { rowspan: 2, contents: '10' }, '13' ], + [ '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); describe( 'content and paste tables have spans', () => { - it( 'handles pasting simple table over table with colspans (no colspan exceeds selection)', () => { + it( 'handles pasting colspanned table over table with colspans (no colspan exceeds selection)', () => { setModelData( model, modelTable( [ [ '00[]', '01', '02', '03' ], [ { colspan: 3, contents: '10' }, '13' ], @@ -746,6 +839,42 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting rowspanned table over table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02', '03' ], + [ { rowspan: 2, contents: '12' }, '13' ], + [ '21', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); } ); } ); From 220e9bf504ce5461d3e143f21881ed5f2f3b0436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 14:34:19 +0200 Subject: [PATCH 13/71] Add tests for mixed col- and row-spans. --- .../ckeditor5-table/tests/tableclipboard.js | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 868a248b314..e7bab7114e5 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -727,6 +727,66 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting multi-spanned table', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ '10', '11', '12', '13', '14', '15' ], + [ '20', '21', '22', '23', '24', '25' ], + [ '30', '31', '32', '33', '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 4 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); describe( 'content table has spans', () => { @@ -801,6 +861,62 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae' ], + [ 'ba', 'bb', 'bc', 'bd', 'be' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd', 'de' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae', '05' ], + [ 'ba', 'bb', 'bc', 'bd', 'be', '15' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce', '25' ], + [ 'da', 'db', 'dc', 'dd', 'de', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); describe( 'content and paste tables have spans', () => { @@ -875,6 +991,82 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting multi-spanned table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); } ); } ); From 682f949c78deb8f43b281aef7d4f68ebc61f8bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 14:50:52 +0200 Subject: [PATCH 14/71] Add option to renumber table cells using letters. --- .../ckeditor5-table/tests/manual/tablemocking.html | 1 + .../ckeditor5-table/tests/manual/tablemocking.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/tests/manual/tablemocking.html b/packages/ckeditor5-table/tests/manual/tablemocking.html index 5ad6dc06011..5d3f483a534 100644 --- a/packages/ckeditor5-table/tests/manual/tablemocking.html +++ b/packages/ckeditor5-table/tests/manual/tablemocking.html @@ -43,6 +43,7 @@ + diff --git a/packages/ckeditor5-table/tests/manual/tablemocking.js b/packages/ckeditor5-table/tests/manual/tablemocking.js index 89b613e6b70..0f155516e27 100644 --- a/packages/ckeditor5-table/tests/manual/tablemocking.js +++ b/packages/ckeditor5-table/tests/manual/tablemocking.js @@ -57,10 +57,13 @@ ClassicEditor return; } + const useLetters = document.getElementById( 'use-letters' ).checked; + editor.model.change( writer => { for ( const { row, column, cell } of new TableWalker( table ) ) { const selection = editor.model.createSelection( cell, 'in' ); - editor.model.insertContent( writer.createText( `${ row }${ column }` ), selection ); + + editor.model.insertContent( writer.createText( createCellText( row, column, useLetters ) ), selection ); } } ); @@ -126,6 +129,13 @@ ClassicEditor function updateInputStatus( message = '' ) { document.getElementById( 'input-status' ).innerText = message; } + + function createCellText( row, column, useLetters ) { + const rowLabel = useLetters ? String.fromCharCode( row + 'a'.charCodeAt( 0 ) ) : row; + const columnLabel = useLetters ? String.fromCharCode( column + 'a'.charCodeAt( 0 ) ) : column; + + return `${ rowLabel }${ columnLabel }`; + } } ) .catch( err => { console.error( err.stack ); From 1af43b2d207acaddfcb995aad1bfa80e499a429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 15:03:21 +0200 Subject: [PATCH 15/71] Add tests for special selection cases (wrong area calculation). --- .../ckeditor5-table/tests/tableclipboard.js | 103 +++++++++++++++++- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 03a12603ce2..1946dcf19a5 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -634,7 +634,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -852,14 +852,12 @@ describe( 'table clipboard', () => { [ '30', '31', '32', '33' ] ] ) ); - /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 1, 1, 1, 0 ], [ 1, 1, 1, 0 ], [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] + [ 0, 0, 0, 0 ] ] ); - /* eslint-enable no-multi-spaces */ } ); it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { @@ -907,7 +905,6 @@ describe( 'table clipboard', () => { [ '40', '41', '42', '43', '44', '45' ] ] ) ); - /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 1, 1, 1, 1, 1, 0 ], [ 1, 1, 1, 1, 1, 0 ], @@ -915,7 +912,49 @@ describe( 'table clipboard', () => { [ 1, 1, 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] ); - /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); } ); } ); @@ -1067,6 +1106,58 @@ describe( 'table clipboard', () => { ] ); /* eslint-enable no-multi-spaces */ } ); + + it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + // +----+----+----+ + // | aa | ab | ac | + // +----+----+----+ + // | ba | bc | + // + +----+ + // | | cc | + // +----+----+----+ + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ], + [ 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); } ); } ); } ); From 5b7fe359aee5bd5987bb882ca946918eec400370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 15:03:51 +0200 Subject: [PATCH 16/71] Align code to the changes in table selection API. --- .../ckeditor5-table/tests/tableclipboard.js | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 1946dcf19a5..22893f09ae8 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -412,7 +412,7 @@ describe( 'table clipboard', () => { it( 'should be disabled in a readonly mode', () => { editor.isReadOnly = true; - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); @@ -455,7 +455,7 @@ describe( 'table clipboard', () => { describe( 'no spans', () => { it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); @@ -481,7 +481,7 @@ describe( 'table clipboard', () => { } ); it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 2, 2 ] ), modelRoot.getNodeByPath( [ 0, 3, 3 ] ) ); @@ -507,7 +507,7 @@ describe( 'table clipboard', () => { } ); it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); @@ -533,7 +533,7 @@ describe( 'table clipboard', () => { } ); it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); @@ -558,7 +558,7 @@ describe( 'table clipboard', () => { } ); it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); @@ -584,7 +584,7 @@ describe( 'table clipboard', () => { } ); it( 'handles simple table paste to a simple table fragment - whole table selected', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 3 ] ) ); @@ -614,7 +614,7 @@ describe( 'table clipboard', () => { describe( 'pasted table has spans', () => { it( 'handles pasting table that has cell with colspan', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); @@ -642,7 +642,7 @@ describe( 'table clipboard', () => { } ); it( 'handles pasting table that has many cells with various colspan', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 2 ] ) ); @@ -672,7 +672,7 @@ describe( 'table clipboard', () => { } ); it( 'handles pasting table that has cell with rowspan', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); @@ -700,7 +700,7 @@ describe( 'table clipboard', () => { } ); it( 'handles pasting table that has many cells with various rowspan', () => { - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 2, 3 ] ) ); @@ -737,7 +737,7 @@ describe( 'table clipboard', () => { [ '40', '41', '42', '43', '44', '45' ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 4 ] ) ); @@ -798,7 +798,7 @@ describe( 'table clipboard', () => { [ '30', '31', { colspan: 2, contents: '31' } ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); @@ -834,7 +834,7 @@ describe( 'table clipboard', () => { [ '30', '31', '32', '33' ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 2, 0 ] ) ); @@ -885,7 +885,7 @@ describe( 'table clipboard', () => { [ '40', '41', '42', '43', '44', '45' ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 2 ] ) ); @@ -931,7 +931,7 @@ describe( 'table clipboard', () => { [ '30', '31', '32', '33' ] ] ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 2 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); @@ -967,7 +967,7 @@ describe( 'table clipboard', () => { [ '30', '31', { colspan: 2, contents: '31' } ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); @@ -1003,7 +1003,7 @@ describe( 'table clipboard', () => { [ '30', '31', '32', '33' ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); @@ -1056,7 +1056,7 @@ describe( 'table clipboard', () => { [ '40', '41', '42', '43', '44', '45' ] ] ) ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 3, 2 ] ) ); @@ -1124,7 +1124,7 @@ describe( 'table clipboard', () => { [ '30', '31', '32', '33' ] ] ); - tableSelection._setCellSelection( + tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 2 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); From b9d1a8428f56609918cec99b8f236a35fb85729b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 16:00:56 +0200 Subject: [PATCH 17/71] Skip edge cases for handling paste. --- packages/ckeditor5-table/tests/tableclipboard.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 22893f09ae8..d9b666c2665 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -914,7 +914,8 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { // +----+----+----+----+ // | 00 | 01 | 02 | 03 | // +----+----+----+----+ @@ -924,12 +925,12 @@ describe( 'table clipboard', () => { // +----+----+----+----+ // | 30 | 31 | 32 | 33 | // +----+----+----+----+ - setModelData( model, [ + setModelData( model, modelTable( [ [ '00', '01', '02', '03' ], [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], [ '23' ], [ '30', '31', '32', '33' ] - ] ); + ] ) ); tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 2 ] ), @@ -1107,7 +1108,8 @@ describe( 'table clipboard', () => { /* eslint-enable no-multi-spaces */ } ); - it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { // +----+----+----+----+ // | 00 | 01 | 02 | 03 | // +----+----+----+----+ @@ -1117,12 +1119,12 @@ describe( 'table clipboard', () => { // +----+----+----+----+ // | 30 | 31 | 32 | 33 | // +----+----+----+----+ - setModelData( model, [ + setModelData( model, modelTable( [ [ '00', '01', '02', '03' ], [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], [ '23' ], [ '30', '31', '32', '33' ] - ] ); + ] ) ); tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 2 ] ), From 8bc3650ad23265aef9d5c2d32f702f60f110bf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 7 May 2020 16:12:16 +0200 Subject: [PATCH 18/71] Disable non-rectangular selection from paste handling. --- .../src/commands/mergecellscommand.js | 132 +----------------- .../ckeditor5-table/src/tableclipboard.js | 9 +- packages/ckeditor5-table/src/utils.js | 126 +++++++++++++++++ 3 files changed, 137 insertions(+), 130 deletions(-) diff --git a/packages/ckeditor5-table/src/commands/mergecellscommand.js b/packages/ckeditor5-table/src/commands/mergecellscommand.js index c42982acff3..d7821d90d5f 100644 --- a/packages/ckeditor5-table/src/commands/mergecellscommand.js +++ b/packages/ckeditor5-table/src/commands/mergecellscommand.js @@ -9,9 +9,9 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; -import { findAncestor, updateNumericAttribute } from './utils'; +import { updateNumericAttribute } from './utils'; import TableUtils from '../tableutils'; -import { getColumnIndexes, getRowIndexes, getSelectedTableCells } from '../utils'; +import { isSelectionRectangular, getSelectedTableCells } from '../utils'; /** * The merge cells command. @@ -29,7 +29,7 @@ export default class MergeCellsCommand extends Command { * @inheritDoc */ refresh() { - this.isEnabled = canMergeCells( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) ); + this.isEnabled = isSelectionRectangular( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) ); } /** @@ -119,132 +119,6 @@ function isEmpty( tableCell ) { return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty; } -// Checks if the selection contains cells that can be merged. -// -// In a table below: -// -// ┌───┬───┬───┬───┐ -// │ a │ b │ c │ d │ -// ├───┴───┼───┤ │ -// │ e │ f │ │ -// ├ ├───┼───┤ -// │ │ g │ h │ -// └───────┴───┴───┘ -// -// Valid selections are these which create a solid rectangle (without gaps), such as: -// - a, b (two horizontal cells) -// - c, f (two vertical cells) -// - a, b, e (cell "e" spans over four cells) -// - c, d, f (cell d spans over a cell in the row below) -// -// While an invalid selection would be: -// - a, c (the unselected cell "b" creates a gap) -// - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap) -// -// @param {module:engine/model/selection~Selection} selection -// @param {module:table/tableUtils~TableUtils} tableUtils -// @returns {boolean} -function canMergeCells( selection, tableUtils ) { - const selectedTableCells = getSelectedTableCells( selection ); - - if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) { - return false; - } - - // A valid selection is a fully occupied rectangle composed of table cells. - // Below we will calculate the area of a selected table cells and the area of valid selection. - // The area of a valid selection is defined by top-left and bottom-right cells. - const rows = new Set(); - const columns = new Set(); - - let areaOfSelectedCells = 0; - - for ( const tableCell of selectedTableCells ) { - const { row, column } = tableUtils.getCellLocation( tableCell ); - const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); - - // Record row & column indexes of current cell. - rows.add( row ); - columns.add( column ); - - // For cells that spans over multiple rows add also the last row that this cell spans over. - if ( rowspan > 1 ) { - rows.add( row + rowspan - 1 ); - } - - // For cells that spans over multiple columns add also the last column that this cell spans over. - if ( colspan > 1 ) { - columns.add( column + colspan - 1 ); - } - - areaOfSelectedCells += ( rowspan * colspan ); - } - - // We can only merge table cells that are in adjacent rows... - const areaOfValidSelection = getBiggestRectangleArea( rows, columns ); - - return areaOfValidSelection == areaOfSelectedCells; -} - -// Calculates the area of a maximum rectangle that can span over the provided row & column indexes. -// -// @param {Array.} rows -// @param {Array.} columns -// @returns {Number} -function getBiggestRectangleArea( rows, columns ) { - const rowsIndexes = Array.from( rows.values() ); - const columnIndexes = Array.from( columns.values() ); - - const lastRow = Math.max( ...rowsIndexes ); - const firstRow = Math.min( ...rowsIndexes ); - const lastColumn = Math.max( ...columnIndexes ); - const firstColumn = Math.min( ...columnIndexes ); - - return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 ); -} - -// Checks if the selection does not mix a header (column or row) with other cells. -// -// For instance, in the table below valid selections consist of cells with the same letter only. -// So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not. -// -// header columns -// ↓ ↓ -// ┌───┬───┬───┬───┐ -// │ a │ a │ b │ b │ ← header row -// ├───┼───┼───┼───┤ -// │ c │ c │ d │ d │ -// ├───┼───┼───┼───┤ -// │ c │ c │ d │ d │ -// └───┴───┴───┴───┘ -// -function areCellInTheSameTableSection( tableCells ) { - const table = findAncestor( 'table', tableCells[ 0 ] ); - - const rowIndexes = getRowIndexes( tableCells ); - const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 ); - - // Calculating row indexes is a bit cheaper so if this check fails we can't merge. - if ( !areIndexesInSameSection( rowIndexes, headingRows ) ) { - return false; - } - - const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 ); - const columnIndexes = getColumnIndexes( tableCells ); - - // Similarly cells must be in same column section. - return areIndexesInSameSection( columnIndexes, headingColumns ); -} - -// Unified check if table rows/columns indexes are in the same heading/body section. -function areIndexesInSameSection( { first, last }, headingSectionSize ) { - const firstCellIsInHeading = first < headingSectionSize; - const lastCellIsInHeading = last < headingSectionSize; - - return firstCellIsInHeading === lastCellIsInHeading; -} - function getMergeDimensions( firstTableCell, selectedTableCells, tableUtils ) { let maxWidthOffset = 0; let maxHeightOffset = 0; diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index d8190fb5b5d..94ac01de183 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import TableSelection from './tableselection'; -import { getColumnIndexes, getRowIndexes } from './utils'; +import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import TableWalker from './tablewalker'; import { findAncestor, updateNumericAttribute } from './commands/utils'; @@ -124,6 +124,13 @@ export default class TableClipboard extends Plugin { const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + // TODO: Temporally block non-rectangular selection. + if ( isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { + // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); + + return; + } + if ( selectionHeight === insertHeight && selectionWidth === insertWidth ) { const model = this.editor.model; diff --git a/packages/ckeditor5-table/src/utils.js b/packages/ckeditor5-table/src/utils.js index 2fefc1e2d7f..10cec18dfd1 100644 --- a/packages/ckeditor5-table/src/utils.js +++ b/packages/ckeditor5-table/src/utils.js @@ -179,6 +179,74 @@ export function getColumnIndexes( tableCells ) { return getFirstLastIndexesObject( indexes ); } +// Checks if the selection contains cells that do not exceed rectangular selection. +// +// In a table below: +// +// ┌───┬───┬───┬───┐ +// │ a │ b │ c │ d │ +// ├───┴───┼───┤ │ +// │ e │ f │ │ +// ├ ├───┼───┤ +// │ │ g │ h │ +// └───────┴───┴───┘ +// +// Valid selections are these which create a solid rectangle (without gaps), such as: +// - a, b (two horizontal cells) +// - c, f (two vertical cells) +// - a, b, e (cell "e" spans over four cells) +// - c, d, f (cell d spans over a cell in the row below) +// +// While an invalid selection would be: +// - a, c (the unselected cell "b" creates a gap) +// - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap) +// +// @param {module:engine/model/selection~Selection} selection +// @param {module:table/tableUtils~TableUtils} tableUtils +// @returns {boolean} +export function isSelectionRectangular( selection, tableUtils ) { + const selectedTableCells = getSelectedTableCells( selection ); + + if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) { + return false; + } + + // A valid selection is a fully occupied rectangle composed of table cells. + // Below we will calculate the area of a selected table cells and the area of valid selection. + // The area of a valid selection is defined by top-left and bottom-right cells. + const rows = new Set(); + const columns = new Set(); + + let areaOfSelectedCells = 0; + + for ( const tableCell of selectedTableCells ) { + const { row, column } = tableUtils.getCellLocation( tableCell ); + const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); + const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + + // Record row & column indexes of current cell. + rows.add( row ); + columns.add( column ); + + // For cells that spans over multiple rows add also the last row that this cell spans over. + if ( rowspan > 1 ) { + rows.add( row + rowspan - 1 ); + } + + // For cells that spans over multiple columns add also the last column that this cell spans over. + if ( colspan > 1 ) { + columns.add( column + colspan - 1 ); + } + + areaOfSelectedCells += ( rowspan * colspan ); + } + + // We can only merge table cells that are in adjacent rows... + const areaOfValidSelection = getBiggestRectangleArea( rows, columns ); + + return areaOfValidSelection == areaOfSelectedCells; +} + // Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes. function getFirstLastIndexesObject( indexes ) { const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB ); @@ -203,3 +271,61 @@ function compareRangeOrder( rangeA, rangeB ) { // b. Collapsed range on the same position (allowed by model but should not happen). return posA.isBefore( posB ) ? -1 : 1; } + +// Calculates the area of a maximum rectangle that can span over the provided row & column indexes. +// +// @param {Array.} rows +// @param {Array.} columns +// @returns {Number} +function getBiggestRectangleArea( rows, columns ) { + const rowsIndexes = Array.from( rows.values() ); + const columnIndexes = Array.from( columns.values() ); + + const lastRow = Math.max( ...rowsIndexes ); + const firstRow = Math.min( ...rowsIndexes ); + const lastColumn = Math.max( ...columnIndexes ); + const firstColumn = Math.min( ...columnIndexes ); + + return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 ); +} + +// Checks if the selection does not mix a header (column or row) with other cells. +// +// For instance, in the table below valid selections consist of cells with the same letter only. +// So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not. +// +// header columns +// ↓ ↓ +// ┌───┬───┬───┬───┐ +// │ a │ a │ b │ b │ ← header row +// ├───┼───┼───┼───┤ +// │ c │ c │ d │ d │ +// ├───┼───┼───┼───┤ +// │ c │ c │ d │ d │ +// └───┴───┴───┴───┘ +// +function areCellInTheSameTableSection( tableCells ) { + const table = findAncestor( 'table', tableCells[ 0 ] ); + + const rowIndexes = getRowIndexes( tableCells ); + const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 ); + + // Calculating row indexes is a bit cheaper so if this check fails we can't merge. + if ( !areIndexesInSameSection( rowIndexes, headingRows ) ) { + return false; + } + + const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 ); + const columnIndexes = getColumnIndexes( tableCells ); + + // Similarly cells must be in same column section. + return areIndexesInSameSection( columnIndexes, headingColumns ); +} + +// Unified check if table rows/columns indexes are in the same heading/body section. +function areIndexesInSameSection( { first, last }, headingSectionSize ) { + const firstCellIsInHeading = first < headingSectionSize; + const lastCellIsInHeading = last < headingSectionSize; + + return firstCellIsInHeading === lastCellIsInHeading; +} From 432cc1b41839db450ac3289fb88a8ef14f3d8b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 09:20:01 +0200 Subject: [PATCH 19/71] Fix if statement that disables non-rectangular selection. --- packages/ckeditor5-table/src/tableclipboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 94ac01de183..1659164ef87 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -125,7 +125,7 @@ export default class TableClipboard extends Plugin { const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); // TODO: Temporally block non-rectangular selection. - if ( isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { + if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); return; From ec300ea045a99c6f997ab0fd661308960d516755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 09:27:51 +0200 Subject: [PATCH 20/71] Re-organize code to anticipate table cropping. --- .../ckeditor5-table/src/tableclipboard.js | 115 +-- .../ckeditor5-table/tests/tableclipboard.js | 788 +++++++++++++++++- 2 files changed, 809 insertions(+), 94 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 1659164ef87..36a8d42bdf8 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -131,85 +131,88 @@ export default class TableClipboard extends Plugin { return; } - if ( selectionHeight === insertHeight && selectionWidth === insertWidth ) { - const model = this.editor.model; + // TODO: Temporally block block tables that are smaller than selection area. + if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); - model.change( writer => { - const insertionMap = new Map(); + return; + } - for ( const { column, row, cell } of new TableWalker( table ) ) { - insertionMap.set( `${ row }x${ column }`, cell ); - } + if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); - const tableMap = [ ...new TableWalker( contentTable, { - startRow: rowIndexes.first, - endRow: rowIndexes.last, - includeSpanned: true - } ) ]; + return; + } - let previousCell; + const model = this.editor.model; - const cellsToSelect = []; + model.change( writer => { + const insertionMap = new Map(); - for ( const { column, row, cell, isSpanned } of tableMap ) { - // TODO: crete issue for table walker startColumn, endColumn. - if ( column < columnIndexes.first || column > columnIndexes.last ) { - previousCell = cell; + for ( const { column, row, cell } of new TableWalker( table ) ) { + insertionMap.set( `${ row }x${ column }`, cell ); + } - continue; - } + const tableMap = [ ...new TableWalker( contentTable, { + startRow: rowIndexes.first, + endRow: rowIndexes.last, + includeSpanned: true + } ) ]; - const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; - const cellToInsert = insertionMap.get( toGet ); + let previousCell; - if ( !cellToInsert ) { - if ( !isSpanned ) { - writer.remove( writer.createRangeOn( cell ) ); - } + const cellsToSelect = []; - continue; - } + for ( const { column, row, cell, isSpanned } of tableMap ) { + // TODO: crete issue for table walker startColumn, endColumn. + if ( column < columnIndexes.first || column > columnIndexes.last ) { + previousCell = cell; - let targetCell = cell; + continue; + } - if ( isSpanned ) { - let insertPosition; + const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; + const cellToInsert = insertionMap.get( toGet ); - if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { - insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); - } else { - insertPosition = writer.createPositionAfter( previousCell ); - } + if ( !cellToInsert ) { + if ( !isSpanned ) { + writer.remove( writer.createRangeOn( cell ) ); + } - targetCell = writer.createElement( 'tableCell' ); + continue; + } - writer.insert( targetCell, insertPosition ); - } else { - writer.remove( writer.createRangeIn( cell ) ); - } + let targetCell = cell; - updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); - updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); + if ( isSpanned ) { + let insertPosition; - for ( const child of cellToInsert.getChildren() ) { - writer.insert( child, targetCell, 'end' ); + if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { + insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); + } else { + insertPosition = writer.createPositionAfter( previousCell ); } - cellsToSelect.push( targetCell ); - previousCell = targetCell; + targetCell = writer.createElement( 'tableCell' ); + + writer.insert( targetCell, insertPosition ); + } else { + writer.remove( writer.createRangeIn( cell ) ); } - writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); - } ); + updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); + updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); - return; - } + for ( const child of cellToInsert.getChildren() ) { + writer.insert( child, targetCell, 'end' ); + } - if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); - } else { - // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); - } + cellsToSelect.push( targetCell ); + previousCell = targetCell; + } + + writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); + } ); } } } diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index d9b666c2665..f9cf8f2a49f 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -408,51 +408,51 @@ describe( 'table clipboard', () => { ] ) ); } ); - describe( 'pasted table is equal to the selected area', () => { - it( 'should be disabled in a readonly mode', () => { - editor.isReadOnly = true; + it( 'should be disabled in a readonly mode', () => { + editor.isReadOnly = true; - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); - const data = pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); + const data = pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); - editor.isReadOnly = false; + editor.isReadOnly = false; - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); - sinon.assert.calledOnce( data.preventDefault ); - } ); + sinon.assert.calledOnce( data.preventDefault ); + } ); - it( 'should allow normal paste if no table cells are selected', () => { - const data = { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - data.dataTransfer.setData( 'text/html', '

foo

' ); - viewDocument.fire( 'paste', data ); - - editor.isReadOnly = false; - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '00foo[]', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - } ); + it( 'should allow normal paste if no table cells are selected', () => { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', '

foo

' ); + viewDocument.fire( 'paste', data ); + + editor.isReadOnly = false; + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00foo[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + describe( 'pasted table is equal to the selected area', () => { describe( 'no spans', () => { it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { tableSelection.setCellSelection( @@ -1162,6 +1162,718 @@ describe( 'table clipboard', () => { } ); } ); } ); + + describe( 'pasted table is smaller than the selected area', () => { + describe( 'no spans', () => { + it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02', '03' ], + [ 'ba', 'bb', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 0, 0 ], + [ 1, 1, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', 'aa', 'ab' ], + [ '30', '31', 'ba', 'bb' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 1, 1 ], + [ 0, 0, 1, 1 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa' ], + [ 'ba' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', '12', '13' ], + [ '20', 'ba', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - whole table selected', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); + + describe.skip( 'pasted table has spans', () => { + it( 'handles pasting table that has cell with colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { colspan: 2, contents: 'aa' } ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ], + [ { colspan: 2, contents: 'da' }, 'dc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ { colspan: 2, contents: 'da' }, 'dc', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has cell with rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { rowspan: 2, contents: 'aa' }, 'ab' ], + [ 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], + [ '20', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 3 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting multi-spanned table', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ '10', '11', '12', '13', '14', '15' ], + [ '20', '21', '22', '23', '24', '25' ], + [ '30', '31', '32', '33', '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 4 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + + describe.skip( 'content table has spans', () => { + it( 'handles pasting simple table over a table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting simple table over a table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00', { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], + [ { rowspan: 2, contents: '10' }, '13' ], + [ '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae' ], + [ 'ba', 'bb', 'bc', 'bd', 'be' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd', 'de' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae', '05' ], + [ 'ba', 'bb', 'bc', 'bd', 'be', '15' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce', '25' ], + [ 'da', 'db', 'dc', 'dd', 'de', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + } ); + + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + } ); + + describe.skip( 'content and paste tables have spans', () => { + it( 'handles pasting colspanned table over table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting rowspanned table over table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02', '03' ], + [ { rowspan: 2, contents: '12' }, '13' ], + [ '21', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting multi-spanned table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + // +----+----+----+ + // | aa | ab | ac | + // +----+----+----+ + // | ba | bc | + // + +----+ + // | | cc | + // +----+----+----+ + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ], + [ 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + } ); } ); } ); From 9d156f803be1f715b66898e073d14f14c1eb3d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 09:29:24 +0200 Subject: [PATCH 21/71] Add early return when no table is found on paste. --- .../ckeditor5-table/src/tableclipboard.js | 155 +++++++++--------- 1 file changed, 76 insertions(+), 79 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 36a8d42bdf8..026df3ab2d1 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -104,116 +104,113 @@ export default class TableClipboard extends Plugin { const table = getTable( content ); - if ( table ) { - evt.stop(); - - if ( selectedTableCells.length === 1 ) { - // @if CK_DEBUG // console.log( 'Single table cell is selected. Not handled.' ); - - return; - } - - const tableUtils = this.editor.plugins.get( 'TableUtils' ); + if ( !table ) { + return; + } - const rowIndexes = getRowIndexes( selectedTableCells ); - const columnIndexes = getColumnIndexes( selectedTableCells ); - const selectionHeight = rowIndexes.last - rowIndexes.first + 1; - const selectionWidth = columnIndexes.last - columnIndexes.first + 1; - const insertHeight = tableUtils.getRows( table ); - const insertWidth = tableUtils.getColumns( table ); + evt.stop(); - const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + if ( selectedTableCells.length === 1 ) { + // @if CK_DEBUG // console.log( 'Single table cell is selected. Not handled.' ); - // TODO: Temporally block non-rectangular selection. - if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { - // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); + return; + } - return; - } + const tableUtils = this.editor.plugins.get( 'TableUtils' ); + const rowIndexes = getRowIndexes( selectedTableCells ); + const columnIndexes = getColumnIndexes( selectedTableCells ); + const selectionHeight = rowIndexes.last - rowIndexes.first + 1; + const selectionWidth = columnIndexes.last - columnIndexes.first + 1; + const insertHeight = tableUtils.getRows( table ); + const insertWidth = tableUtils.getColumns( table ); + const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { + // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); - // TODO: Temporally block block tables that are smaller than selection area. - if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); + return; + } - return; - } + if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); - if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); + return; + } - return; - } + if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); - const model = this.editor.model; + return; + } - model.change( writer => { - const insertionMap = new Map(); + const model = this.editor.model; - for ( const { column, row, cell } of new TableWalker( table ) ) { - insertionMap.set( `${ row }x${ column }`, cell ); - } + model.change( writer => { + const insertionMap = new Map(); - const tableMap = [ ...new TableWalker( contentTable, { - startRow: rowIndexes.first, - endRow: rowIndexes.last, - includeSpanned: true - } ) ]; + for ( const { column, row, cell } of new TableWalker( table ) ) { + insertionMap.set( `${ row }x${ column }`, cell ); + } - let previousCell; + const tableMap = [ ...new TableWalker( contentTable, { + startRow: rowIndexes.first, + endRow: rowIndexes.last, + includeSpanned: true + } ) ]; - const cellsToSelect = []; + let previousCell; - for ( const { column, row, cell, isSpanned } of tableMap ) { - // TODO: crete issue for table walker startColumn, endColumn. - if ( column < columnIndexes.first || column > columnIndexes.last ) { - previousCell = cell; + const cellsToSelect = []; - continue; - } + for ( const { column, row, cell, isSpanned } of tableMap ) { + // TODO: crete issue for table walker startColumn, endColumn. + if ( column < columnIndexes.first || column > columnIndexes.last ) { + previousCell = cell; - const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; - const cellToInsert = insertionMap.get( toGet ); + continue; + } - if ( !cellToInsert ) { - if ( !isSpanned ) { - writer.remove( writer.createRangeOn( cell ) ); - } + const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; + const cellToInsert = insertionMap.get( toGet ); - continue; + if ( !cellToInsert ) { + if ( !isSpanned ) { + writer.remove( writer.createRangeOn( cell ) ); } - let targetCell = cell; - - if ( isSpanned ) { - let insertPosition; + continue; + } - if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { - insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); - } else { - insertPosition = writer.createPositionAfter( previousCell ); - } + let targetCell = cell; - targetCell = writer.createElement( 'tableCell' ); + if ( isSpanned ) { + let insertPosition; - writer.insert( targetCell, insertPosition ); + if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { + insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); } else { - writer.remove( writer.createRangeIn( cell ) ); + insertPosition = writer.createPositionAfter( previousCell ); } - updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); - updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); + targetCell = writer.createElement( 'tableCell' ); - for ( const child of cellToInsert.getChildren() ) { - writer.insert( child, targetCell, 'end' ); - } + writer.insert( targetCell, insertPosition ); + } else { + writer.remove( writer.createRangeIn( cell ) ); + } - cellsToSelect.push( targetCell ); - previousCell = targetCell; + updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); + updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); + + for ( const child of cellToInsert.getChildren() ) { + writer.insert( child, targetCell, 'end' ); } - writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); - } ); - } + cellsToSelect.push( targetCell ); + previousCell = targetCell; + } + + writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); + } ); } } From 7a370154fe07b521213e1cf689ce81bd52ee31d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 10:22:13 +0200 Subject: [PATCH 22/71] Refactor table cropping utilities to make them reusable. --- .../ckeditor5-table/src/tableselection.js | 4 +- .../src/tableselection/croptable.js | 51 ++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection.js b/packages/ckeditor5-table/src/tableselection.js index 23d3087a27e..2c94a202647 100644 --- a/packages/ckeditor5-table/src/tableselection.js +++ b/packages/ckeditor5-table/src/tableselection.js @@ -18,7 +18,7 @@ import { getTableCellsContainingSelection } from './utils'; import { findAncestor } from './commands/utils'; -import cropTable from './tableselection/croptable'; +import { cropTableToSelection } from './tableselection/croptable'; import '../theme/tableselection.css'; @@ -99,7 +99,7 @@ export default class TableSelection extends Plugin { return this.editor.model.change( writer => { const documentFragment = writer.createDocumentFragment(); - const table = cropTable( selectedCells, this.editor.plugins.get( 'TableUtils' ), writer ); + const table = cropTableToSelection( selectedCells, this.editor.plugins.get( 'TableUtils' ), writer ); writer.insert( table, documentFragment, 0 ); diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 656c9b13402..150e2af5a6c 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -8,6 +8,36 @@ */ import { findAncestor } from '../commands/utils'; +import TableWalker from '../tablewalker'; + +/** + * Returns a cropped table according to given dimensions. + * + * This function is to be used with the table selection. + * + * const croppedTable = cropTable( table, 1, 1, 3, 3, tableUtils, writer ); + * + * @param {Number} sourceTable + * @param {Number} startRow + * @param {Number} startColumn + * @param {Number} endRow + * @param {Number} endColumn + * @param {module:table/tableutils~TableUtils} tableUtils + * @param {module:engine/model/writer~Writer} writer + * @returns {module:engine/model/element~Element} + */ +export function cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ) { + const tableCopy = makeTableCopy( sourceTable, startRow, startColumn, endRow, endColumn, writer, tableUtils ); + + const selectionWidth = endColumn - startColumn + 1; + const selectionHeight = endRow - startRow + 1; + + trimTable( tableCopy, selectionWidth, selectionHeight, writer, tableUtils ); + + addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer ); + + return tableCopy; +} /** * Returns a cropped table from the selected table cells. @@ -26,37 +56,33 @@ import { findAncestor } from '../commands/utils'; * @param {module:engine/model/writer~Writer} writer * @returns {module:engine/model/element~Element} */ -export default function cropTable( selectedTableCellsIterator, tableUtils, writer ) { +export function cropTableToSelection( selectedTableCellsIterator, tableUtils, writer ) { const selectedTableCells = Array.from( selectedTableCellsIterator ); const startElement = selectedTableCells[ 0 ]; const endElement = selectedTableCells[ selectedTableCells.length - 1 ]; const { row: startRow, column: startColumn } = tableUtils.getCellLocation( startElement ); - - const tableCopy = makeTableCopy( selectedTableCells, startColumn, writer, tableUtils ); - const { row: endRow, column: endColumn } = tableUtils.getCellLocation( endElement ); - const selectionWidth = endColumn - startColumn + 1; - const selectionHeight = endRow - startRow + 1; - - trimTable( tableCopy, selectionWidth, selectionHeight, writer, tableUtils ); const sourceTable = findAncestor( 'table', startElement ); - addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer ); - return tableCopy; + return cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ); } // Creates a table copy from a selected table cells. // // It fills "gaps" in copied table - ie when cell outside copied range was spanning over selection. -function makeTableCopy( selectedTableCells, startColumn, writer, tableUtils ) { +function makeTableCopy( sourceTable, startRow, startColumn, endRow, endColumn, writer, tableUtils ) { const tableCopy = writer.createElement( 'table' ); const rowToCopyMap = new Map(); const copyToOriginalColumnMap = new Map(); - for ( const tableCell of selectedTableCells ) { + for ( const { column, cell: tableCell } of [ ...new TableWalker( sourceTable, { startRow, endRow } ) ] ) { + if ( column < startColumn || column > endColumn ) { + continue; + } + const row = findAncestor( 'tableRow', tableCell ); if ( !rowToCopyMap.has( row ) ) { @@ -66,7 +92,6 @@ function makeTableCopy( selectedTableCells, startColumn, writer, tableUtils ) { } const tableCellCopy = tableCell._clone( true ); - const { column } = tableUtils.getCellLocation( tableCell ); copyToOriginalColumnMap.set( tableCellCopy, column ); From 67761ce1092a5838a8fe8c006fb630661604a379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 10:30:12 +0200 Subject: [PATCH 23/71] Add tests for simple pasting scenarios when pasted table exceeds selection. --- .../ckeditor5-table/src/tableclipboard.js | 17 ++++++---- .../ckeditor5-table/tests/tableclipboard.js | 32 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 026df3ab2d1..1424df06a04 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -12,6 +12,7 @@ import TableSelection from './tableselection'; import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import TableWalker from './tablewalker'; import { findAncestor, updateNumericAttribute } from './commands/utils'; +import { cropTableToDimensions } from './tableselection/croptable'; /** * This plugin adds support for copying/cutting/pasting fragments of tables. @@ -102,7 +103,8 @@ export default class TableClipboard extends Plugin { return; } - const table = getTable( content ); + // We might need to crop table before inserting. + let table = getTable( content ); if ( !table ) { return; @@ -124,6 +126,7 @@ export default class TableClipboard extends Plugin { const insertHeight = tableUtils.getRows( table ); const insertWidth = tableUtils.getColumns( table ); const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); @@ -136,15 +139,15 @@ export default class TableClipboard extends Plugin { return; } - if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); - - return; - } - const model = this.editor.model; model.change( writer => { + if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { + // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); + + table = cropTableToDimensions( table, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); + } + const insertionMap = new Map(); for ( const { column, row, cell } of new TableWalker( table ) ) { diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index f9cf8f2a49f..a9e1d71559b 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -1199,8 +1199,9 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1225,8 +1226,9 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1244,14 +1246,16 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { + it( 'handles paste to a simple row fragment - in the middle of a table', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); pasteTable( [ - [ 'aa', 'ab' ] + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1269,15 +1273,16 @@ describe( 'table clipboard', () => { ] ); } ); - it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { + it( 'handles paste to a simple column fragment - in the middle of a table', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); pasteTable( [ - [ 'aa' ], - [ 'ba' ] + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1302,10 +1307,11 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] + [ 'aa', 'ab', 'ac', 'ad', 'ae' ], + [ 'ba', 'bb', 'bc', 'bd', 'be' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd', 'de' ], + [ 'ea', 'eb', 'ec', 'ed', 'ee' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ From f5ef5c991fd8a34173faee6ee4039c5597174319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 13:49:39 +0200 Subject: [PATCH 24/71] Update docs. --- packages/ckeditor5-table/src/tableselection/croptable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 150e2af5a6c..917f982c790 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -47,7 +47,7 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo * tableSelection.startSelectingFrom( startCell ) * tableSelection.setSelectingFrom( endCell ) * - * const croppedTable = cropTable( tableSelection.getSelectedTableCells() ); + * const croppedTable = cropTable( tableSelection.getSelectedTableCells(), tableUtils, writer ); * * **Note**: This function is also used by {@link module:table/tableselection~TableSelection#getSelectionAsFragment}. * From 32ed5df477b00afc4711a3b456df5722ec6734f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 13:54:00 +0200 Subject: [PATCH 25/71] Add stub test for bigger selection than pasted table case. --- .../ckeditor5-table/tests/tableclipboard.js | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index a9e1d71559b..8dc4eb2348a 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -1154,7 +1154,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -1163,7 +1163,7 @@ describe( 'table clipboard', () => { } ); } ); - describe( 'pasted table is smaller than the selected area', () => { + describe( 'pasted table is bigger than the selected area', () => { describe( 'no spans', () => { it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { tableSelection.setCellSelection( @@ -1872,7 +1872,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -1880,6 +1880,34 @@ describe( 'table clipboard', () => { } ); } ); } ); + + describe( 'pasted table is smaller than the selected area', () => { + it( 'blocks this case', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); } ); } ); From de13de5c96901a7c96cd73b3e4c3c3c5d3c16ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 13:59:37 +0200 Subject: [PATCH 26/71] Add stub test for non-rectangular selection is pasted. --- .../ckeditor5-table/tests/tableclipboard.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 8dc4eb2348a..a989d96a2d6 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -452,6 +452,30 @@ describe( 'table clipboard', () => { ] ) ); } ); + it( 'should block non-rectangular selection', () => { + setModelData( model, modelTable( [ + [ { contents: '00', colspan: 3 } ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: '00', colspan: 3 } ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + describe( 'pasted table is equal to the selected area', () => { describe( 'no spans', () => { it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { From 448e61c54f8a7ec5eb1b3446ce9ff628b42d1a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 14:26:20 +0200 Subject: [PATCH 27/71] Add stub test for non-rectangular selection is pasted. --- .../ckeditor5-table/src/tableclipboard.js | 4 --- .../ckeditor5-table/tests/tableclipboard.js | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 1424df06a04..278f684ac96 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -92,10 +92,6 @@ export default class TableClipboard extends Plugin { * This param defines a position in relation to that item. */ _onInsertContent( evt, content ) { - if ( this.editor.isReadOnly ) { - return; - } - const tableSelection = this.editor.plugins.get( 'TableSelection' ); const selectedTableCells = tableSelection.getSelectedTableCells(); diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index a989d96a2d6..833c697a105 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -476,6 +476,32 @@ describe( 'table clipboard', () => { ] ) ); } ); + describe( 'single cell selected', () => { + it( 'blocks this case', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 0, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + describe( 'pasted table is equal to the selected area', () => { describe( 'no spans', () => { it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { From cddb8081311d10e501e7c76cbbe9d68328575420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 14:38:12 +0200 Subject: [PATCH 28/71] Fix cases for "pasted table has spans" and bigger pasted table. --- .../ckeditor5-table/tests/tableclipboard.js | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 833c697a105..77ece7f6f95 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -1380,7 +1380,7 @@ describe( 'table clipboard', () => { } ); } ); - describe.skip( 'pasted table has spans', () => { + describe( 'pasted table has spans', () => { it( 'handles pasting table that has cell with colspan', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 1 ] ), @@ -1388,8 +1388,8 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ { colspan: 2, contents: 'aa' } ], - [ 'ba', 'bb' ] + [ { colspan: 3, contents: 'aa' } ], + [ 'ba', 'bb', 'bc' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1416,10 +1416,10 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ 'aa', { colspan: 2, contents: 'ab' } ], - [ { colspan: 3, contents: 'ba' } ], - [ 'ca', 'cb', 'cc' ], - [ { colspan: 2, contents: 'da' }, 'dc' ] + [ 'aa', { colspan: 3, contents: 'ab' } ], + [ { colspan: 4, contents: 'ba' } ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ { colspan: 2, contents: 'da' }, 'dc', 'dd' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1446,8 +1446,9 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ { rowspan: 2, contents: 'aa' }, 'ab' ], - [ 'bb' ] + [ { rowspan: 3, contents: 'aa' }, 'ab' ], + [ 'bb' ], + [ 'cb' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1474,9 +1475,10 @@ describe( 'table clipboard', () => { ); pasteTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ] + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad', 'ae' ], + [ { rowspan: 3, contents: 'ba' }, 'bd', 'be' ], + [ 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd' ] ] ); assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ @@ -1506,8 +1508,8 @@ describe( 'table clipboard', () => { ] ) ); tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 4 ] ) + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) ); // +----+----+----+----+----+ @@ -1527,30 +1529,30 @@ describe( 'table clipboard', () => { ] ); // +----+----+----+----+----+----+ - // | aa | ac | ad | ae | 05 | - // +----+----+----+----+ +----+ - // | ba | bb | | 15 | - // +----+ +----+----+ - // | ca | | ce | 25 | - // + +----+----+----+----+----+ - // | | db | dc | dd | 35 | + // | 00 | 01 | 02 | 03 | 04 | 05 | + // +----+----+----+----+----+----+ + // | 10 | aa | ac | 14 | 15 | + // +----+----+----+----+----+----+ + // | 20 | ba | bb | 24 | 25 | + // +----+----+ +----+----+ + // | 30 | ca | | 34 | 35 | // +----+----+----+----+----+----+ // | 40 | 41 | 42 | 43 | 44 | 45 | // +----+----+----+----+----+----+ assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], - [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '00', '01', '02', '03', '04', '05' ], + [ '10', { contents: 'aa', colspan: 2 }, 'ac', '14', '15' ], + [ '20', 'ba', { contents: 'bb', colspan: 2, rowspan: 2 }, '24', '25' ], + [ '30', 'ca', '34', '35' ], [ '40', '41', '42', '43', '44', '45' ] ] ) ); /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ From 04a01eed2193e6e6ec733d5d7a9bd0512a4042e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 8 May 2020 14:39:56 +0200 Subject: [PATCH 29/71] Remove special test cases. --- .../ckeditor5-table/tests/tableclipboard.js | 373 ------------------ 1 file changed, 373 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js index 77ece7f6f95..f5ce92d21fe 100644 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -1558,379 +1558,6 @@ describe( 'table clipboard', () => { /* eslint-enable no-multi-spaces */ } ); } ); - - describe.skip( 'content table has spans', () => { - it( 'handles pasting simple table over a table with colspans (no colspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ { colspan: 3, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20' }, '22', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting simple table over a table with rowspans (no rowspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00', { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], - [ { rowspan: 2, contents: '10' }, '13' ], - [ '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 0 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { - // +----+----+----+----+----+----+ - // | 00 | 02 | 03 | 05 | - // + + + +----+ - // | | | | 15 | - // +----+----+----+ +----+ - // | 20 | 21 | | 25 | - // + +----+----+----+----+----+ - // | | 31 | 32 | 34 | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - setModelData( model, modelTable( [ - [ - { contents: '00', colspan: 2, rowspan: 2 }, - { contents: '02', rowspan: 2 }, - { contents: '03', colspan: 2, rowspan: 3 }, - '05' - ], - [ '15' ], - [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], - [ '31', { contents: '32', colspan: 2 }, '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad', 'ae' ], - [ 'ba', 'bb', 'bc', 'bd', 'be' ], - [ 'ca', 'cb', 'cc', 'cd', 'ce' ], - [ 'da', 'db', 'dc', 'dd', 'de' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', 'ad', 'ae', '05' ], - [ 'ba', 'bb', 'bc', 'bd', 'be', '15' ], - [ 'ca', 'cb', 'cc', 'cd', 'ce', '25' ], - [ 'da', 'db', 'dc', 'dd', 'de', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - } ); - - // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). - it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { - // +----+----+----+----+ - // | 00 | 01 | 02 | 03 | - // +----+----+----+----+ - // | 10 | 11 | 13 | - // + + +----+ - // | | | 23 | - // +----+----+----+----+ - // | 30 | 31 | 32 | 33 | - // +----+----+----+----+ - setModelData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], - [ '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 2 ] ), - modelRoot.getNodeByPath( [ 0, 1, 0 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - } ); - - describe.skip( 'content and paste tables have spans', () => { - it( 'handles pasting colspanned table over table with colspans (no colspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ { colspan: 3, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20' }, '22', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', { colspan: 2, contents: 'ab' } ], - [ { colspan: 3, contents: 'ba' } ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], - [ { colspan: 3, contents: 'ba' }, '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting rowspanned table over table with rowspans (no rowspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02', '03' ], - [ { rowspan: 2, contents: '12' }, '13' ], - [ '21', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting multi-spanned table over table with multi-spans (no span exceeds selection)', () => { - // +----+----+----+----+----+----+ - // | 00 | 02 | 03 | 05 | - // + + + +----+ - // | | | | 15 | - // +----+----+----+ +----+ - // | 20 | 21 | | 25 | - // + +----+----+----+----+----+ - // | | 31 | 32 | 34 | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - setModelData( model, modelTable( [ - [ - { contents: '00', colspan: 2, rowspan: 2 }, - { contents: '02', rowspan: 2 }, - { contents: '03', colspan: 2, rowspan: 3 }, - '05' - ], - [ '15' ], - [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], - [ '31', { contents: '32', colspan: 2 }, '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - // +----+----+----+----+----+ - // | aa | ac | ad | ae | - // +----+----+----+----+ + - // | ba | bb | | - // +----+ +----+ - // | ca | | ce | - // + +----+----+----+----+ - // | | db | dc | dd | - // +----+----+----+----+----+ - pasteTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], - [ { contents: 'ca', rowspan: 2 }, 'ce' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] - ] ); - - // +----+----+----+----+----+----+ - // | aa | ac | ad | ae | 05 | - // +----+----+----+----+ +----+ - // | ba | bb | | 15 | - // +----+ +----+----+ - // | ca | | ce | 25 | - // + +----+----+----+----+----+ - // | | db | dc | dd | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], - [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). - it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { - // +----+----+----+----+ - // | 00 | 01 | 02 | 03 | - // +----+----+----+----+ - // | 10 | 11 | 13 | - // + + +----+ - // | | | 23 | - // +----+----+----+----+ - // | 30 | 31 | 32 | 33 | - // +----+----+----+----+ - setModelData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], - [ '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 2 ] ), - modelRoot.getNodeByPath( [ 0, 1, 0 ] ) - ); - - // +----+----+----+ - // | aa | ab | ac | - // +----+----+----+ - // | ba | bc | - // + +----+ - // | | cc | - // +----+----+----+ - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ], - [ 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { colspan: 2, contents: 'aa' }, '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - } ); } ); describe( 'pasted table is smaller than the selected area', () => { From 5117d98455f4e8db9cfc7dcc11bbe650587ad52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 09:05:20 +0200 Subject: [PATCH 30/71] Split table clipboard tests. --- .../tests/tableclipboard-copy-cut.js | 423 +++++ .../tests/tableclipboard-paste.js | 1256 +++++++++++++ .../ckeditor5-table/tests/tableclipboard.js | 1628 ----------------- 3 files changed, 1679 insertions(+), 1628 deletions(-) create mode 100644 packages/ckeditor5-table/tests/tableclipboard-copy-cut.js create mode 100644 packages/ckeditor5-table/tests/tableclipboard-paste.js delete mode 100644 packages/ckeditor5-table/tests/tableclipboard.js diff --git a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js new file mode 100644 index 00000000000..fb8fc873844 --- /dev/null +++ b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js @@ -0,0 +1,423 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import TableEditing from '../src/tableediting'; +import { modelTable, viewTable } from './_utils/utils'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +import TableClipboard from '../src/tableclipboard'; + +describe( 'table clipboard', () => { + let editor, model, modelRoot, tableSelection, viewDocument, element; + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + viewDocument = editor.editing.view.document; + tableSelection = editor.plugins.get( 'TableSelection' ); + + setModelData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + afterEach( async () => { + await editor.destroy(); + + element.remove(); + } ); + + describe( 'Clipboard integration - copy', () => { + it( 'should do nothing for normal selection in table', () => { + const dataTransferMock = createDataTransfer(); + const spy = sinon.spy(); + + viewDocument.on( 'clipboardOutput', spy ); + + viewDocument.fire( 'copy', { + dataTransfer: dataTransferMock, + preventDefault: sinon.spy() + } ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should copy selected table cells as a standalone table', () => { + const preventDefaultSpy = sinon.spy(); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: preventDefaultSpy + }; + viewDocument.fire( 'copy', data ); + + sinon.assert.calledOnce( preventDefaultSpy ); + expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [ + [ '01', '02' ], + [ '11', '12' ] + ] ) ); + } ); + + it( 'should trim selected table to a selection rectangle (inner cell with colspan, no colspan after trim)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', { contents: '11', colspan: 2 } ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ] ) ); + } ); + + it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ { contents: '10', colspan: 3 } ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '00', '01' ], + [ { contents: '10', colspan: 2 } ], + [ '20', '21' ] + ] ) ); + } ); + + it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', { contents: '11', rowspan: 2 }, '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + } ); + + it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', () => { + setModelData( model, modelTable( [ + [ '00[]', { contents: '01', rowspan: 3 }, '02' ], + [ '10', '12' ], + [ '20', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '00', { contents: '01', rowspan: 2 }, '02' ], + [ '10', '12' ] + ] ) ); + } ); + + it( 'should prepend spanned columns with empty cells (outside cell with colspan)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ { contents: '10', colspan: 2 }, '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '01', '02' ], + [ ' ', '12' ], + [ '21', '22' ] + ] ) ); + } ); + + it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', () => { + setModelData( model, modelTable( [ + [ '00[]', { contents: '01', rowspan: 2 }, '02' ], + [ '10', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '10', ' ', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should fix selected table to a selection rectangle (hardcore case)', () => { + // This test check how previous simple rules run together (mixed prepending and trimming). + // In the example below a selection is set from cell "32" to "88" + // + // Input table: Copied table: + // + // +----+----+----+----+----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | 06 | 07 | 08 | + // +----+----+ +----+ +----+----+----+ + // | 10 | 11 | | 13 | | 16 | 17 | 18 | + // +----+----+ +----+ +----+----+----+ +----+----+----+---------+----+----+ + // | 20 | 21 | | 23 | | 26 | | 21 | | 23 | | | 26 | | + // +----+----+ +----+ +----+----+----+ +----+----+----+----+----+----+----+ + // | 30 | 31 | | 33 | | 36 | 37 | | 31 | | 33 | | | 36 | 37 | + // +----+----+----+----+ +----+----+----+ +----+----+----+----+----+----+----+ + // | 40 | | 46 | 47 | 48 | | | | | | | 46 | 47 | + // +----+----+----+----+ +----+----+----+ ==> +----+----+----+----+----+----+----+ + // | 50 | 51 | 52 | 53 | | 56 | 57 | 58 | | 51 | 52 | 53 | | | 56 | 57 | + // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ + // | 60 | 61 | 64 | 65 | | 67 | 68 | | 61 | | | 64 | 65 | | 67 | + // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ + // | 70 | 71 | 72 | 73 | 74 | 75 | | 77 | 78 | | 71 | 72 | 73 | 74 | 75 | | 77 | + // +----+ +----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ + // | 80 | | 82 | 83 | 84 | 85 | | 87 | 88 | + // +----+----+----+----+----+----+----+----+----+ + // + setModelData( model, modelTable( [ + [ '00', '01', { contents: '02', rowspan: 4 }, '03', { contents: '04', colspan: 2, rowspan: 7 }, '07', '07', '08' ], + [ '10', '11', '13', '17', '17', '18' ], + [ '20', '21', '23', { contents: '27', colspan: 3 } ], + [ '30', '31', '33', '37', { contents: '37', colspan: 2 } ], + [ { contents: '40', colspan: 4 }, '47', '47', '48' ], + [ '50', '51', '52', '53', { contents: '57', rowspan: 4 }, '57', '58' ], + [ '60', { contents: '61', colspan: 3 }, '67', '68' ], + [ '70', { contents: '71', rowspan: 2 }, '72', '73', '74', '75', '77', '78' ], + [ '80', '82', '83', '84', '85', '87', '88' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 1 ] ), + modelRoot.getNodeByPath( [ 0, 7, 6 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '21', ' ', '23', ' ', ' ', { contents: '27', colspan: 2 } ], + [ '31', ' ', '33', ' ', ' ', '37', '37' ], + [ ' ', ' ', ' ', ' ', ' ', '47', '47' ], + [ '51', '52', '53', ' ', ' ', { contents: '57', rowspan: 3 }, '57' ], + [ { contents: '61', colspan: 3 }, ' ', ' ', ' ', '67' ], + [ '71', '72', '73', '74', '75', '77' ] + ] ) ); + } ); + + it( 'should update table heading attributes (selection with headings)', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', '11', '12', '13', '14' ], + [ '20', '21', '22', '23', '24' ], + [ '30', '31', '32', '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ], { headingRows: 3, headingColumns: 2 } ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '11', '12', '13' ], + [ '21', '22', '23' ], + [ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable + ], { headingRows: 2, headingColumns: 1 } ) ); + } ); + + it( 'should update table heading attributes (selection without headings)', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', '11', '12', '13', '14' ], + [ '20', '21', '22', '23', '24' ], + [ '30', '31', '32', '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ], { headingRows: 3, headingColumns: 2 } ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 3, 2 ] ), + modelRoot.getNodeByPath( [ 0, 4, 4 ] ) + ); + + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '32', '33', '34' ], + [ '42', '43', '44' ] + ] ) ); + } ); + } ); + + describe( 'Clipboard integration - cut', () => { + it( 'should not block clipboardOutput if no multi-cell selection', () => { + setModelData( model, modelTable( [ + [ '[00]', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + const dataTransferMock = createDataTransfer(); + + viewDocument.fire( 'cut', { + dataTransfer: dataTransferMock, + preventDefault: sinon.spy() + } ); + + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '00' ); + } ); + + it( 'should be preventable', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + viewDocument.on( 'clipboardOutput', evt => evt.stop(), { priority: 'high' } ); + + viewDocument.fire( 'cut', { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy() + } ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'is clears selected table cells', () => { + const spy = sinon.spy(); + + viewDocument.on( 'clipboardOutput', spy ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + viewDocument.fire( 'cut', { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy() + } ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '', '', '02' ], + [ '', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should copy selected table cells as a standalone table', () => { + const preventDefaultSpy = sinon.spy(); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: preventDefaultSpy + }; + viewDocument.fire( 'cut', data ); + + sinon.assert.calledOnce( preventDefaultSpy ); + expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [ + [ '01', '02' ], + [ '11', '12' ] + ] ) ); + } ); + + it( 'should be disabled in a readonly mode', () => { + const preventDefaultStub = sinon.stub(); + + editor.isReadOnly = true; + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: preventDefaultStub + }; + viewDocument.fire( 'cut', data ); + + editor.isReadOnly = false; + + expect( data.dataTransfer.getData( 'text/html' ) ).to.be.undefined; + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + sinon.assert.calledOnce( preventDefaultStub ); + } ); + } ); + + function assertClipboardContentOnMethod( method, expectedViewTable ) { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy() + }; + viewDocument.fire( method, data ); + + expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( expectedViewTable ); + } + + function createDataTransfer() { + const store = new Map(); + + return { + setData( type, data ) { + store.set( type, data ); + }, + + getData( type ) { + return store.get( type ); + } + }; + } +} ); diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js new file mode 100644 index 00000000000..d0262030c3a --- /dev/null +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -0,0 +1,1256 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import TableEditing from '../src/tableediting'; +import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +import TableClipboard from '../src/tableclipboard'; + +describe( 'table clipboard', () => { + let editor, model, modelRoot, tableSelection, viewDocument, element; + + describe( 'Clipboard integration - paste (selection scenarios)', () => { + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + viewDocument = editor.editing.view.document; + tableSelection = editor.plugins.get( 'TableSelection' ); + + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + afterEach( async () => { + await editor.destroy(); + + element.remove(); + } ); + + it( 'should be disabled in a readonly mode', () => { + editor.isReadOnly = true; + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const data = pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + editor.isReadOnly = false; + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + sinon.assert.calledOnce( data.preventDefault ); + } ); + + it( 'should allow normal paste if no table cells are selected', () => { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', '

foo

' ); + viewDocument.fire( 'paste', data ); + + editor.isReadOnly = false; + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00foo[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should block non-rectangular selection', () => { + setModelData( model, modelTable( [ + [ { contents: '00', colspan: 3 } ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: '00', colspan: 3 } ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + describe( 'single cell selected', () => { + it( 'blocks this case', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 0, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + + describe( 'pasted table is equal to the selected area', () => { + describe( 'no spans', () => { + it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02', '03' ], + [ 'ba', 'bb', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 0, 0 ], + [ 1, 1, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', 'aa', 'ab' ], + [ '30', '31', 'ba', 'bb' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 1, 1 ], + [ 0, 0, 1, 1 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa' ], + [ 'ba' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', '12', '13' ], + [ '20', 'ba', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - whole table selected', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); + + describe( 'pasted table has spans', () => { + it( 'handles pasting table that has cell with colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { colspan: 2, contents: 'aa' } ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ], + [ { colspan: 2, contents: 'da' }, 'dc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ { colspan: 2, contents: 'da' }, 'dc', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has cell with rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { rowspan: 2, contents: 'aa' }, 'ab' ], + [ 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], + [ '20', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 3 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting multi-spanned table', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ '10', '11', '12', '13', '14', '15' ], + [ '20', '21', '22', '23', '24', '25' ], + [ '30', '31', '32', '33', '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 4 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + + describe( 'content table has spans', () => { + it( 'handles pasting simple table over a table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting simple table over a table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00', { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], + [ { rowspan: 2, contents: '10' }, '13' ], + [ '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae' ], + [ 'ba', 'bb', 'bc', 'bd', 'be' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd', 'de' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae', '05' ], + [ 'ba', 'bb', 'bc', 'bd', 'be', '15' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce', '25' ], + [ 'da', 'db', 'dc', 'dd', 'de', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + } ); + + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', '03' ], + [ 'ba', 'bb', 'bc', '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + } ); + + describe( 'content and paste tables have spans', () => { + it( 'handles pasting colspanned table over table with colspans (no colspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { colspan: 3, contents: '10' }, '13' ], + [ { colspan: 2, contents: '20' }, '22', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 2, contents: 'ab' } ], + [ { colspan: 3, contents: 'ba' } ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ '30', '31', { colspan: 2, contents: '31' } ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting rowspanned table over table with rowspans (no rowspan exceeds selection)', () => { + setModelData( model, modelTable( [ + [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02', '03' ], + [ { rowspan: 2, contents: '12' }, '13' ], + [ '21', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting multi-spanned table over table with multi-spans (no span exceeds selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 02 | 03 | 05 | + // + + + +----+ + // | | | | 15 | + // +----+----+----+ +----+ + // | 20 | 21 | | 25 | + // + +----+----+----+----+----+ + // | | 31 | 32 | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ + { contents: '00', colspan: 2, rowspan: 2 }, + { contents: '02', rowspan: 2 }, + { contents: '03', colspan: 2, rowspan: 3 }, + '05' + ], + [ '15' ], + [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], + [ '31', { contents: '32', colspan: 2 }, '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | aa | ac | ad | ae | 05 | + // +----+----+----+----+ +----+ + // | ba | bb | | 15 | + // +----+ +----+----+ + // | ca | | ce | 25 | + // + +----+----+----+----+----+ + // | | db | dc | dd | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], + [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). + it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { + // +----+----+----+----+ + // | 00 | 01 | 02 | 03 | + // +----+----+----+----+ + // | 10 | 11 | 13 | + // + + +----+ + // | | | 23 | + // +----+----+----+----+ + // | 30 | 31 | 32 | 33 | + // +----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], + [ '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 0 ] ) + ); + + // +----+----+----+ + // | aa | ab | ac | + // +----+----+----+ + // | ba | bc | + // + +----+ + // | | cc | + // +----+----+----+ + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ], + [ 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + } ); + + describe( 'pasted table is bigger than the selected area', () => { + describe( 'no spans', () => { + it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02', '03' ], + [ 'ba', 'bb', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 0, 0 ], + [ 1, 1, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', 'aa', 'ab' ], + [ '30', '31', 'ba', 'bb' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 1, 1 ], + [ 0, 0, 1, 1 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles paste to a simple row fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', 'ab', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles paste to a simple column fragment - in the middle of a table', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac' ], + [ 'ba', 'bb', 'bc' ], + [ 'ca', 'cb', 'cc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', 'aa', '12', '13' ], + [ '20', 'ba', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 0, 0, 0 ] + ] ); + } ); + + it( 'handles simple table paste to a simple table fragment - whole table selected', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab', 'ac', 'ad', 'ae' ], + [ 'ba', 'bb', 'bc', 'bd', 'be' ], + [ 'ca', 'cb', 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd', 'de' ], + [ 'ea', 'eb', 'ec', 'ed', 'ee' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', 'ac', 'ad' ], + [ 'ba', 'bb', 'bc', 'bd' ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); + + describe( 'pasted table has spans', () => { + it( 'handles pasting table that has cell with colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { colspan: 3, contents: 'aa' } ], + [ 'ba', 'bb', 'bc' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { colspan: 2, contents: 'aa' }, '13' ], + [ '20', 'ba', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various colspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', { colspan: 3, contents: 'ab' } ], + [ { colspan: 4, contents: 'ba' } ], + [ 'ca', 'cb', 'cc', 'cd' ], + [ { colspan: 2, contents: 'da' }, 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], + [ { colspan: 3, contents: 'ba' }, '13' ], + [ 'ca', 'cb', 'cc', '23' ], + [ { colspan: 2, contents: 'da' }, 'dc', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 0 ], + [ 1, 0 ], + [ 1, 1, 1, 0 ], + [ 1, 1, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has cell with rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ { rowspan: 3, contents: 'aa' }, 'ab' ], + [ 'bb' ], + [ 'cb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], + [ '20', 'bb', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting table that has many cells with various rowspan', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 3 ] ) + ); + + pasteTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad', 'ae' ], + [ { rowspan: 3, contents: 'ba' }, 'bd', 'be' ], + [ 'cc', 'cd', 'ce' ], + [ 'da', 'db', 'dc', 'dd' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], + [ { rowspan: 2, contents: 'ba' }, 'bd' ], + [ 'cc', 'cd' ], + [ '30', '31', '32', '33' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting multi-spanned table', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ '10', '11', '12', '13', '14', '15' ], + [ '20', '21', '22', '23', '24', '25' ], + [ '30', '31', '32', '33', '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + // +----+----+----+----+----+ + // | aa | ac | ad | ae | + // +----+----+----+----+ + + // | ba | bb | | + // +----+ +----+ + // | ca | | ce | + // + +----+----+----+----+ + // | | db | dc | dd | + // +----+----+----+----+----+ + pasteTable( [ + [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], + [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], + [ { contents: 'ca', rowspan: 2 }, 'ce' ], + [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] + ] ); + + // +----+----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | 05 | + // +----+----+----+----+----+----+ + // | 10 | aa | ac | 14 | 15 | + // +----+----+----+----+----+----+ + // | 20 | ba | bb | 24 | 25 | + // +----+----+ +----+----+ + // | 30 | ca | | 34 | 35 | + // +----+----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | 45 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ '10', { contents: 'aa', colspan: 2 }, 'ac', '14', '15' ], + [ '20', 'ba', { contents: 'bb', colspan: 2, rowspan: 2 }, '24', '25' ], + [ '30', 'ca', '34', '35' ], + [ '40', '41', '42', '43', '44', '45' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0, 0, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + } ); + } ); + + describe( 'pasted table is smaller than the selected area', () => { + it( 'blocks this case', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 3, 3 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + assertSelectedCells( model, [ + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ], + [ 1, 1, 1, 1 ] + ] ); + } ); + } ); + } ); + + function pasteTable( tableData ) { + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', viewTable( tableData ) ); + viewDocument.fire( 'paste', data ); + + return data; + } + + function createDataTransfer() { + const store = new Map(); + + return { + setData( type, data ) { + store.set( type, data ); + }, + + getData( type ) { + return store.get( type ); + } + }; + } +} ); diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js deleted file mode 100644 index f5ce92d21fe..00000000000 --- a/packages/ckeditor5-table/tests/tableclipboard.js +++ /dev/null @@ -1,1628 +0,0 @@ -/** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals document */ - -import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; -import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import TableEditing from '../src/tableediting'; -import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; -import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; - -import TableClipboard from '../src/tableclipboard'; - -describe( 'table clipboard', () => { - let editor, model, modelRoot, tableSelection, viewDocument, element; - - beforeEach( async () => { - element = document.createElement( 'div' ); - document.body.appendChild( element ); - - editor = await ClassicTestEditor.create( element, { - plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ] - } ); - - model = editor.model; - modelRoot = model.document.getRoot(); - viewDocument = editor.editing.view.document; - tableSelection = editor.plugins.get( 'TableSelection' ); - - setModelData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - - afterEach( async () => { - await editor.destroy(); - - element.remove(); - } ); - - describe( 'Clipboard integration', () => { - describe( 'copy', () => { - it( 'should do nothing for normal selection in table', () => { - const dataTransferMock = createDataTransfer(); - const spy = sinon.spy(); - - viewDocument.on( 'clipboardOutput', spy ); - - viewDocument.fire( 'copy', { - dataTransfer: dataTransferMock, - preventDefault: sinon.spy() - } ); - - sinon.assert.calledOnce( spy ); - } ); - - it( 'should copy selected table cells as a standalone table', () => { - const preventDefaultSpy = sinon.spy(); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - const data = { - dataTransfer: createDataTransfer(), - preventDefault: preventDefaultSpy - }; - viewDocument.fire( 'copy', data ); - - sinon.assert.calledOnce( preventDefaultSpy ); - expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [ - [ '01', '02' ], - [ '11', '12' ] - ] ) ); - } ); - - it( 'should trim selected table to a selection rectangle (inner cell with colspan, no colspan after trim)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', { contents: '11', colspan: 2 } ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ] ) ); - } ); - - it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ { contents: '10', colspan: 3 } ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '00', '01' ], - [ { contents: '10', colspan: 2 } ], - [ '20', '21' ] - ] ) ); - } ); - - it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', { contents: '11', rowspan: 2 }, '12' ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '00', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - } ); - - it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', () => { - setModelData( model, modelTable( [ - [ '00[]', { contents: '01', rowspan: 3 }, '02' ], - [ '10', '12' ], - [ '20', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '00', { contents: '01', rowspan: 2 }, '02' ], - [ '10', '12' ] - ] ) ); - } ); - - it( 'should prepend spanned columns with empty cells (outside cell with colspan)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ { contents: '10', colspan: 2 }, '12' ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '01', '02' ], - [ ' ', '12' ], - [ '21', '22' ] - ] ) ); - } ); - - it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', () => { - setModelData( model, modelTable( [ - [ '00[]', { contents: '01', rowspan: 2 }, '02' ], - [ '10', '12' ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '10', ' ', '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - - it( 'should fix selected table to a selection rectangle (hardcore case)', () => { - // This test check how previous simple rules run together (mixed prepending and trimming). - // In the example below a selection is set from cell "32" to "88" - // - // Input table: Copied table: - // - // +----+----+----+----+----+----+----+----+----+ - // | 00 | 01 | 02 | 03 | 04 | 06 | 07 | 08 | - // +----+----+ +----+ +----+----+----+ - // | 10 | 11 | | 13 | | 16 | 17 | 18 | - // +----+----+ +----+ +----+----+----+ +----+----+----+---------+----+----+ - // | 20 | 21 | | 23 | | 26 | | 21 | | 23 | | | 26 | | - // +----+----+ +----+ +----+----+----+ +----+----+----+----+----+----+----+ - // | 30 | 31 | | 33 | | 36 | 37 | | 31 | | 33 | | | 36 | 37 | - // +----+----+----+----+ +----+----+----+ +----+----+----+----+----+----+----+ - // | 40 | | 46 | 47 | 48 | | | | | | | 46 | 47 | - // +----+----+----+----+ +----+----+----+ ==> +----+----+----+----+----+----+----+ - // | 50 | 51 | 52 | 53 | | 56 | 57 | 58 | | 51 | 52 | 53 | | | 56 | 57 | - // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ - // | 60 | 61 | 64 | 65 | | 67 | 68 | | 61 | | | 64 | 65 | | 67 | - // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ - // | 70 | 71 | 72 | 73 | 74 | 75 | | 77 | 78 | | 71 | 72 | 73 | 74 | 75 | | 77 | - // +----+ +----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ - // | 80 | | 82 | 83 | 84 | 85 | | 87 | 88 | - // +----+----+----+----+----+----+----+----+----+ - // - setModelData( model, modelTable( [ - [ '00', '01', { contents: '02', rowspan: 4 }, '03', { contents: '04', colspan: 2, rowspan: 7 }, '07', '07', '08' ], - [ '10', '11', '13', '17', '17', '18' ], - [ '20', '21', '23', { contents: '27', colspan: 3 } ], - [ '30', '31', '33', '37', { contents: '37', colspan: 2 } ], - [ { contents: '40', colspan: 4 }, '47', '47', '48' ], - [ '50', '51', '52', '53', { contents: '57', rowspan: 4 }, '57', '58' ], - [ '60', { contents: '61', colspan: 3 }, '67', '68' ], - [ '70', { contents: '71', rowspan: 2 }, '72', '73', '74', '75', '77', '78' ], - [ '80', '82', '83', '84', '85', '87', '88' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 2, 1 ] ), - modelRoot.getNodeByPath( [ 0, 7, 6 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '21', ' ', '23', ' ', ' ', { contents: '27', colspan: 2 } ], - [ '31', ' ', '33', ' ', ' ', '37', '37' ], - [ ' ', ' ', ' ', ' ', ' ', '47', '47' ], - [ '51', '52', '53', ' ', ' ', { contents: '57', rowspan: 3 }, '57' ], - [ { contents: '61', colspan: 3 }, ' ', ' ', ' ', '67' ], - [ '71', '72', '73', '74', '75', '77' ] - ] ) ); - } ); - - it( 'should update table heading attributes (selection with headings)', () => { - setModelData( model, modelTable( [ - [ '00', '01', '02', '03', '04' ], - [ '10', '11', '12', '13', '14' ], - [ '20', '21', '22', '23', '24' ], - [ '30', '31', '32', '33', '34' ], - [ '40', '41', '42', '43', '44' ] - ], { headingRows: 3, headingColumns: 2 } ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '11', '12', '13' ], - [ '21', '22', '23' ], - [ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable - ], { headingRows: 2, headingColumns: 1 } ) ); - } ); - - it( 'should update table heading attributes (selection without headings)', () => { - setModelData( model, modelTable( [ - [ '00', '01', '02', '03', '04' ], - [ '10', '11', '12', '13', '14' ], - [ '20', '21', '22', '23', '24' ], - [ '30', '31', '32', '33', '34' ], - [ '40', '41', '42', '43', '44' ] - ], { headingRows: 3, headingColumns: 2 } ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 3, 2 ] ), - modelRoot.getNodeByPath( [ 0, 4, 4 ] ) - ); - - assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '32', '33', '34' ], - [ '42', '43', '44' ] - ] ) ); - } ); - } ); - - describe( 'cut', () => { - it( 'should not block clipboardOutput if no multi-cell selection', () => { - setModelData( model, modelTable( [ - [ '[00]', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - const dataTransferMock = createDataTransfer(); - - viewDocument.fire( 'cut', { - dataTransfer: dataTransferMock, - preventDefault: sinon.spy() - } ); - - expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '00' ); - } ); - - it( 'should be preventable', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - viewDocument.on( 'clipboardOutput', evt => evt.stop(), { priority: 'high' } ); - - viewDocument.fire( 'cut', { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy() - } ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - - it( 'is clears selected table cells', () => { - const spy = sinon.spy(); - - viewDocument.on( 'clipboardOutput', spy ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - viewDocument.fire( 'cut', { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy() - } ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '', '', '02' ], - [ '', '[]', '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - - it( 'should copy selected table cells as a standalone table', () => { - const preventDefaultSpy = sinon.spy(); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - const data = { - dataTransfer: createDataTransfer(), - preventDefault: preventDefaultSpy - }; - viewDocument.fire( 'cut', data ); - - sinon.assert.calledOnce( preventDefaultSpy ); - expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [ - [ '01', '02' ], - [ '11', '12' ] - ] ) ); - } ); - - it( 'should be disabled in a readonly mode', () => { - const preventDefaultStub = sinon.stub(); - - editor.isReadOnly = true; - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - const data = { - dataTransfer: createDataTransfer(), - preventDefault: preventDefaultStub - }; - viewDocument.fire( 'cut', data ); - - editor.isReadOnly = false; - - expect( data.dataTransfer.getData( 'text/html' ) ).to.be.undefined; - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - sinon.assert.calledOnce( preventDefaultStub ); - } ); - } ); - - describe( 'paste', () => { - beforeEach( () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - } ); - - it( 'should be disabled in a readonly mode', () => { - editor.isReadOnly = true; - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - const data = pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - editor.isReadOnly = false; - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - sinon.assert.calledOnce( data.preventDefault ); - } ); - - it( 'should allow normal paste if no table cells are selected', () => { - const data = { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - data.dataTransfer.setData( 'text/html', '

foo

' ); - viewDocument.fire( 'paste', data ); - - editor.isReadOnly = false; - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '00foo[]', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - } ); - - it( 'should block non-rectangular selection', () => { - setModelData( model, modelTable( [ - [ { contents: '00', colspan: 3 } ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ { contents: '00', colspan: 3 } ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - - describe( 'single cell selected', () => { - it( 'blocks this case', () => { - setModelData( model, modelTable( [ - [ '00', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 0, 0 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - } ); - } ); - - describe( 'pasted table is equal to the selected area', () => { - describe( 'no spans', () => { - it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', '02', '03' ], - [ 'ba', 'bb', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 0, 0 ], - [ 1, 1, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 2, 2 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', 'aa', 'ab' ], - [ '30', '31', 'ba', 'bb' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 1, 1 ], - [ 0, 0, 1, 1 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple row paste to a simple row fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple column paste to a simple column fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa' ], - [ 'ba' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', '12', '13' ], - [ '20', 'ba', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - whole table selected', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ] - ] ); - } ); - } ); - - describe( 'pasted table has spans', () => { - it( 'handles pasting table that has cell with colspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ { colspan: 2, contents: 'aa' } ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { colspan: 2, contents: 'aa' }, '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has many cells with various colspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - pasteTable( [ - [ 'aa', { colspan: 2, contents: 'ab' } ], - [ { colspan: 3, contents: 'ba' } ], - [ 'ca', 'cb', 'cc' ], - [ { colspan: 2, contents: 'da' }, 'dc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], - [ { colspan: 3, contents: 'ba' }, '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ { colspan: 2, contents: 'da' }, 'dc', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has cell with rowspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ { rowspan: 2, contents: 'aa' }, 'ab' ], - [ 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], - [ '20', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has many cells with various rowspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 3 ] ) - ); - - pasteTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting multi-spanned table', () => { - setModelData( model, modelTable( [ - [ '00', '01', '02', '03', '04', '05' ], - [ '10', '11', '12', '13', '14', '15' ], - [ '20', '21', '22', '23', '24', '25' ], - [ '30', '31', '32', '33', '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 4 ] ) - ); - - // +----+----+----+----+----+ - // | aa | ac | ad | ae | - // +----+----+----+----+ + - // | ba | bb | | - // +----+ +----+ - // | ca | | ce | - // + +----+----+----+----+ - // | | db | dc | dd | - // +----+----+----+----+----+ - pasteTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], - [ { contents: 'ca', rowspan: 2 }, 'ce' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] - ] ); - - // +----+----+----+----+----+----+ - // | aa | ac | ad | ae | 05 | - // +----+----+----+----+ +----+ - // | ba | bb | | 15 | - // +----+ +----+----+ - // | ca | | ce | 25 | - // + +----+----+----+----+----+ - // | | db | dc | dd | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], - [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - } ); - - describe( 'content table has spans', () => { - it( 'handles pasting simple table over a table with colspans (no colspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ { colspan: 3, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20' }, '22', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting simple table over a table with rowspans (no rowspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00', { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], - [ { rowspan: 2, contents: '10' }, '13' ], - [ '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 0 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles pasting simple table over table with multi-spans (no span exceeds selection)', () => { - // +----+----+----+----+----+----+ - // | 00 | 02 | 03 | 05 | - // + + + +----+ - // | | | | 15 | - // +----+----+----+ +----+ - // | 20 | 21 | | 25 | - // + +----+----+----+----+----+ - // | | 31 | 32 | 34 | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - setModelData( model, modelTable( [ - [ - { contents: '00', colspan: 2, rowspan: 2 }, - { contents: '02', rowspan: 2 }, - { contents: '03', colspan: 2, rowspan: 3 }, - '05' - ], - [ '15' ], - [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], - [ '31', { contents: '32', colspan: 2 }, '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad', 'ae' ], - [ 'ba', 'bb', 'bc', 'bd', 'be' ], - [ 'ca', 'cb', 'cc', 'cd', 'ce' ], - [ 'da', 'db', 'dc', 'dd', 'de' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', 'ad', 'ae', '05' ], - [ 'ba', 'bb', 'bc', 'bd', 'be', '15' ], - [ 'ca', 'cb', 'cc', 'cd', 'ce', '25' ], - [ 'da', 'db', 'dc', 'dd', 'de', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 1, 1, 1, 1, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - } ); - - // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). - it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { - // +----+----+----+----+ - // | 00 | 01 | 02 | 03 | - // +----+----+----+----+ - // | 10 | 11 | 13 | - // + + +----+ - // | | | 23 | - // +----+----+----+----+ - // | 30 | 31 | 32 | 33 | - // +----+----+----+----+ - setModelData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], - [ '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 2 ] ), - modelRoot.getNodeByPath( [ 0, 1, 0 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', '03' ], - [ 'ba', 'bb', 'bc', '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - } ); - - describe( 'content and paste tables have spans', () => { - it( 'handles pasting colspanned table over table with colspans (no colspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ { colspan: 3, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20' }, '22', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', { colspan: 2, contents: 'ab' } ], - [ { colspan: 3, contents: 'ba' } ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], - [ { colspan: 3, contents: 'ba' }, '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ '30', '31', { colspan: 2, contents: '31' } ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting rowspanned table over table with rowspans (no rowspan exceeds selection)', () => { - setModelData( model, modelTable( [ - [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02', '03' ], - [ { rowspan: 2, contents: '12' }, '13' ], - [ '21', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting multi-spanned table over table with multi-spans (no span exceeds selection)', () => { - // +----+----+----+----+----+----+ - // | 00 | 02 | 03 | 05 | - // + + + +----+ - // | | | | 15 | - // +----+----+----+ +----+ - // | 20 | 21 | | 25 | - // + +----+----+----+----+----+ - // | | 31 | 32 | 34 | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - setModelData( model, modelTable( [ - [ - { contents: '00', colspan: 2, rowspan: 2 }, - { contents: '02', rowspan: 2 }, - { contents: '03', colspan: 2, rowspan: 3 }, - '05' - ], - [ '15' ], - [ { contents: '20', rowspan: 2 }, { contents: '21', colspan: 2 }, '25' ], - [ '31', { contents: '32', colspan: 2 }, '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - // +----+----+----+----+----+ - // | aa | ac | ad | ae | - // +----+----+----+----+ + - // | ba | bb | | - // +----+ +----+ - // | ca | | ce | - // + +----+----+----+----+ - // | | db | dc | dd | - // +----+----+----+----+----+ - pasteTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], - [ { contents: 'ca', rowspan: 2 }, 'ce' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] - ] ); - - // +----+----+----+----+----+----+ - // | aa | ac | ad | ae | 05 | - // +----+----+----+----+ +----+ - // | ba | bb | | 15 | - // +----+ +----+----+ - // | ca | | ce | 25 | - // + +----+----+----+----+----+ - // | | db | dc | dd | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 }, '05' ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 }, '15' ], - [ { contents: 'ca', rowspan: 2 }, 'ce', '25' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 }, '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). - it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { - // +----+----+----+----+ - // | 00 | 01 | 02 | 03 | - // +----+----+----+----+ - // | 10 | 11 | 13 | - // + + +----+ - // | | | 23 | - // +----+----+----+----+ - // | 30 | 31 | 32 | 33 | - // +----+----+----+----+ - setModelData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ { contents: '10', rowspan: 2 }, { contents: '11', colspan: 2, rowspan: 2 }, '13' ], - [ '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 2 ] ), - modelRoot.getNodeByPath( [ 0, 1, 0 ] ) - ); - - // +----+----+----+ - // | aa | ab | ac | - // +----+----+----+ - // | ba | bc | - // + +----+ - // | | cc | - // +----+----+----+ - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ], - [ 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { colspan: 2, contents: 'aa' }, '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - } ); - } ); - - describe( 'pasted table is bigger than the selected area', () => { - describe( 'no spans', () => { - it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 1, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', '02', '03' ], - [ 'ba', 'bb', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 0, 0 ], - [ 1, 1, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - at the end of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 2, 2 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', 'aa', 'ab' ], - [ '30', '31', 'ba', 'bb' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 1, 1 ], - [ 0, 0, 1, 1 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles paste to a simple row fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 1, 2 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', 'ab', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles paste to a simple column fragment - in the middle of a table', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 1 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac' ], - [ 'ba', 'bb', 'bc' ], - [ 'ca', 'cb', 'cc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', 'aa', '12', '13' ], - [ '20', 'ba', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 1, 0, 0 ], - [ 0, 0, 0, 0 ] - ] ); - } ); - - it( 'handles simple table paste to a simple table fragment - whole table selected', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab', 'ac', 'ad', 'ae' ], - [ 'ba', 'bb', 'bc', 'bd', 'be' ], - [ 'ca', 'cb', 'cc', 'cd', 'ce' ], - [ 'da', 'db', 'dc', 'dd', 'de' ], - [ 'ea', 'eb', 'ec', 'ed', 'ee' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', 'ab', 'ac', 'ad' ], - [ 'ba', 'bb', 'bc', 'bd' ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ] - ] ); - } ); - } ); - - describe( 'pasted table has spans', () => { - it( 'handles pasting table that has cell with colspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ { colspan: 3, contents: 'aa' } ], - [ 'ba', 'bb', 'bc' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { colspan: 2, contents: 'aa' }, '13' ], - [ '20', 'ba', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has many cells with various colspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) - ); - - pasteTable( [ - [ 'aa', { colspan: 3, contents: 'ab' } ], - [ { colspan: 4, contents: 'ba' } ], - [ 'ca', 'cb', 'cc', 'cd' ], - [ { colspan: 2, contents: 'da' }, 'dc', 'dd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { colspan: 2, contents: 'ab' }, '03' ], - [ { colspan: 3, contents: 'ba' }, '13' ], - [ 'ca', 'cb', 'cc', '23' ], - [ { colspan: 2, contents: 'da' }, 'dc', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], - [ 1, 1, 1, 0 ], - [ 1, 1, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has cell with rowspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) - ); - - pasteTable( [ - [ { rowspan: 3, contents: 'aa' }, 'ab' ], - [ 'bb' ], - [ 'cb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', { rowspan: 2, contents: 'aa' }, 'ab', '13' ], - [ '20', 'bb', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], - [ 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting table that has many cells with various rowspan', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 2, 3 ] ) - ); - - pasteTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad', 'ae' ], - [ { rowspan: 3, contents: 'ba' }, 'bd', 'be' ], - [ 'cc', 'cd', 'ce' ], - [ 'da', 'db', 'dc', 'dd' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ 'aa', { rowspan: 3, contents: 'ab' }, { rowspan: 2, contents: 'ac' }, 'ad' ], - [ { rowspan: 2, contents: 'ba' }, 'bd' ], - [ 'cc', 'cd' ], - [ '30', '31', '32', '33' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - - it( 'handles pasting multi-spanned table', () => { - setModelData( model, modelTable( [ - [ '00', '01', '02', '03', '04', '05' ], - [ '10', '11', '12', '13', '14', '15' ], - [ '20', '21', '22', '23', '24', '25' ], - [ '30', '31', '32', '33', '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 1 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - // +----+----+----+----+----+ - // | aa | ac | ad | ae | - // +----+----+----+----+ + - // | ba | bb | | - // +----+ +----+ - // | ca | | ce | - // + +----+----+----+----+ - // | | db | dc | dd | - // +----+----+----+----+----+ - pasteTable( [ - [ { contents: 'aa', colspan: 2 }, 'ac', 'ad', { contents: 'ae', rowspan: 2 } ], - [ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ], - [ { contents: 'ca', rowspan: 2 }, 'ce' ], - [ 'db', 'dc', { contents: 'dd', colspan: 2 } ] - ] ); - - // +----+----+----+----+----+----+ - // | 00 | 01 | 02 | 03 | 04 | 05 | - // +----+----+----+----+----+----+ - // | 10 | aa | ac | 14 | 15 | - // +----+----+----+----+----+----+ - // | 20 | ba | bb | 24 | 25 | - // +----+----+ +----+----+ - // | 30 | ca | | 34 | 35 | - // +----+----+----+----+----+----+ - // | 40 | 41 | 42 | 43 | 44 | 45 | - // +----+----+----+----+----+----+ - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03', '04', '05' ], - [ '10', { contents: 'aa', colspan: 2 }, 'ac', '14', '15' ], - [ '20', 'ba', { contents: 'bb', colspan: 2, rowspan: 2 }, '24', '25' ], - [ '30', 'ca', '34', '35' ], - [ '40', '41', '42', '43', '44', '45' ] - ] ) ); - - /* eslint-disable no-multi-spaces */ - assertSelectedCells( model, [ - [ 0, 0, 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], - [ 0, 0, 0, 0, 0, 0 ] - ] ); - /* eslint-enable no-multi-spaces */ - } ); - } ); - } ); - - describe( 'pasted table is smaller than the selected area', () => { - it( 'blocks this case', () => { - tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 3 ] ) - ); - - pasteTable( [ - [ 'aa', 'ab' ], - [ 'ba', 'bb' ] - ] ); - - assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - assertSelectedCells( model, [ - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ], - [ 1, 1, 1, 1 ] - ] ); - } ); - } ); - } ); - } ); - - function assertClipboardContentOnMethod( method, expectedViewTable ) { - const data = { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy() - }; - viewDocument.fire( method, data ); - - expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( expectedViewTable ); - } - - function pasteTable( tableData ) { - const data = { - dataTransfer: createDataTransfer(), - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - data.dataTransfer.setData( 'text/html', viewTable( tableData ) ); - viewDocument.fire( 'paste', data ); - - return data; - } - - function createDataTransfer() { - const store = new Map(); - - return { - setData( type, data ) { - store.set( type, data ); - }, - - getData( type ) { - return store.get( type ); - } - }; - } -} ); From be31d7204d7fdf797225da059d7a88ea49b355b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 09:26:12 +0200 Subject: [PATCH 31/71] Handle block content in pasted table cells. --- .../ckeditor5-table/src/tableclipboard.js | 6 +- .../tests/tableclipboard-paste.js | 96 +++++++++++++++---- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 278f684ac96..5741c2d3617 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -138,9 +138,8 @@ export default class TableClipboard extends Plugin { const model = this.editor.model; model.change( writer => { + // Pasted table extends selection area. if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table extends selection area.' ); - table = cropTableToDimensions( table, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); } @@ -200,7 +199,8 @@ export default class TableClipboard extends Plugin { updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); - for ( const child of cellToInsert.getChildren() ) { + // TODO: use Element._clone to copy full structure. + for ( const child of Array.from( cellToInsert.getChildren() ) ) { writer.insert( child, targetCell, 'end' ); } diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index d0262030c3a..076022c2b4c 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -15,23 +15,26 @@ import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import TableClipboard from '../src/tableclipboard'; +import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; +import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; describe( 'table clipboard', () => { let editor, model, modelRoot, tableSelection, viewDocument, element; - describe( 'Clipboard integration - paste (selection scenarios)', () => { - beforeEach( async () => { - element = document.createElement( 'div' ); - document.body.appendChild( element ); + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + } ); - editor = await ClassicTestEditor.create( element, { - plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard ] - } ); + afterEach( async () => { + await editor.destroy(); - model = editor.model; - modelRoot = model.document.getRoot(); - viewDocument = editor.editing.view.document; - tableSelection = editor.plugins.get( 'TableSelection' ); + element.remove(); + } ); + + describe( 'Clipboard integration - paste (selection scenarios)', () => { + beforeEach( async () => { + await createEditor(); setModelData( model, modelTable( [ [ '00[]', '01', '02', '03' ], @@ -41,12 +44,6 @@ describe( 'table clipboard', () => { ] ) ); } ); - afterEach( async () => { - await editor.destroy(); - - element.remove(); - } ); - it( 'should be disabled in a readonly mode', () => { editor.isReadOnly = true; @@ -1228,6 +1225,71 @@ describe( 'table clipboard', () => { } ); } ); + describe( 'Clipboard integration - paste (content scenarios)', () => { + it( 'handles multiple paragraphs', async () => { + await createEditor(); + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '01', '11', '12' ], + [ '02', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ '

a

a

a

', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aaa', 'ab', '02' ], + [ 'ba', 'bb', '12' ], + [ '02', '21', '22' ] + ] ) ); + } ); + + it( 'handles image in table cell', async () => { + await createEditor( [ ImageEditing, ImageCaptionEditing ] ); + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '01', '11', '12' ], + [ '02', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ '', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '', 'ab', '02' ], + [ 'ba', 'bb', '12' ], + [ '02', '21', '22' ] + ] ) ); + } ); + } ); + + async function createEditor( extraPlugins = [] ) { + editor = await ClassicTestEditor.create( element, { + plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard, ...extraPlugins ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + viewDocument = editor.editing.view.document; + tableSelection = editor.plugins.get( 'TableSelection' ); + } + function pasteTable( tableData ) { const data = { dataTransfer: createDataTransfer(), From f36df6a5d7c6c8c28c54e335536c0722c979ca04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 10:46:51 +0200 Subject: [PATCH 32/71] Unify not implemented debug console logs. --- packages/ckeditor5-table/src/tableclipboard.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 5741c2d3617..ab9b07f05d1 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -109,7 +109,7 @@ export default class TableClipboard extends Plugin { evt.stop(); if ( selectedTableCells.length === 1 ) { - // @if CK_DEBUG // console.log( 'Single table cell is selected. Not handled.' ); + // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Single table cell is selected.' ); return; } @@ -124,13 +124,13 @@ export default class TableClipboard extends Plugin { const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { - // @if CK_DEBUG // console.log( 'Selection is not rectangular (non-mergeable) - pasting disabled.' ); + // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Selection is not rectangular (non-mergeable).' ); return; } if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { - // @if CK_DEBUG // console.log( 'Pasted table is smaller than selection area.' ); + // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Pasted table is smaller than selection area.' ); return; } From efc0f7f63d60efca7434cc3d178b61d9f588ae73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 10:47:27 +0200 Subject: [PATCH 33/71] Add tests cases for block content in pasted tables. --- .../tests/tableclipboard-paste.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 076022c2b4c..fae4046da32 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -17,6 +17,9 @@ import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils' import TableClipboard from '../src/tableclipboard'; import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; +import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting'; +import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; describe( 'table clipboard', () => { let editor, model, modelRoot, tableSelection, viewDocument, element; @@ -1277,6 +1280,46 @@ describe( 'table clipboard', () => { [ '02', '21', '22' ] ] ) ); } ); + + it( 'handles mixed nested content in table cell', async () => { + await createEditor( [ ImageEditing, ImageCaptionEditing, BlockQuoteEditing, HorizontalLineEditing, ListEditing ] ); + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '01', '11', '12' ], + [ '02', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const img = ''; + const list = '
  • foo
  • bar
'; + const blockquote = `

baz

${ list }
`; + + pasteTable( [ + [ `${ img }${ list }${ blockquote }`, 'ab' ], + [ 'ba', 'bb' ] + ] ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ + '' + + 'foo' + + 'bar' + + '
' + + 'baz' + + 'foo' + + 'bar' + + '
', + 'ab', + '02' ], + [ 'ba', 'bb', '12' ], + [ '02', '21', '22' ] + ] ) ); + } ); } ); async function createEditor( extraPlugins = [] ) { From ac826a564dd5303728ee1e93860e62372d7078ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 10:48:51 +0200 Subject: [PATCH 34/71] Sort imports in tests. --- .../tests/tableclipboard-copy-cut.js | 7 +++---- .../tests/tableclipboard-paste.js | 17 ++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js index fb8fc873844..1ebd680df5f 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js +++ b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js @@ -6,14 +6,13 @@ /* globals document */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import TableEditing from '../src/tableediting'; -import { modelTable, viewTable } from './_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import { modelTable, viewTable } from './_utils/utils'; +import TableEditing from '../src/tableediting'; import TableClipboard from '../src/tableclipboard'; describe( 'table clipboard', () => { diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index fae4046da32..c962c19ab23 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -6,20 +6,19 @@ /* globals document */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting'; +import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; +import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; +import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import TableEditing from '../src/tableediting'; -import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; +import TableEditing from '../src/tableediting'; import TableClipboard from '../src/tableclipboard'; -import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; -import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; -import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; -import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting'; -import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; describe( 'table clipboard', () => { let editor, model, modelRoot, tableSelection, viewDocument, element; From 11393522f51f676578f9b0c28337af4a57aab9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 11:02:05 +0200 Subject: [PATCH 35/71] Handle table cell properties in pasted table. --- .../ckeditor5-table/src/tableclipboard.js | 7 +++ .../tests/tableclipboard-paste.js | 50 +++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index ab9b07f05d1..08c8cfb709f 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -199,6 +199,13 @@ export default class TableClipboard extends Plugin { updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); + const attributesToCopy = Array.from( cellToInsert.getAttributeKeys() ) + .filter( attribute => attribute !== 'colspan' || attribute !== 'rowspan' ); + + for ( const attribute of attributesToCopy ) { + writer.setAttribute( attribute, cellToInsert.getAttribute( attribute ), targetCell ); + } + // TODO: use Element._clone to copy full structure. for ( const child of Array.from( cellToInsert.getChildren() ) ) { writer.insert( child, targetCell, 'end' ); diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index c962c19ab23..228b31ddb68 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -18,6 +18,7 @@ import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils' import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import TableEditing from '../src/tableediting'; +import TableCellPropertiesEditing from '../src/tablecellproperties/tablecellpropertiesediting'; import TableClipboard from '../src/tableclipboard'; describe( 'table clipboard', () => { @@ -1309,9 +1310,9 @@ describe( 'table clipboard', () => { 'foo' + 'bar' + '
' + - 'baz' + - 'foo' + - 'bar' + + 'baz' + + 'foo' + + 'bar' + '
', 'ab', '02' ], @@ -1319,6 +1320,49 @@ describe( 'table clipboard', () => { [ '02', '21', '22' ] ] ) ); } ); + + it( 'handles table cell properties handling', async () => { + await createEditor( [ TableCellPropertiesEditing ] ); + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '01', '11', '12' ], + [ '02', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + pasteTable( [ + [ { contents: 'aa', style: 'border:1px solid #f00;background:#ba7;width:1337px' }, 'ab' ], + [ 'ba', 'bb' ] + ] ); + + const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] ); + + expect( tableCell.getAttribute( 'borderColor' ) ).to.deep.equal( { + top: '#f00', + right: '#f00', + bottom: '#f00', + left: '#f00' + } ); + expect( tableCell.getAttribute( 'borderStyle' ) ).to.deep.equal( { + top: 'solid', + right: 'solid', + bottom: 'solid', + left: 'solid' + } ); + expect( tableCell.getAttribute( 'borderWidth' ) ).to.deep.equal( { + top: '1px', + right: '1px', + bottom: '1px', + left: '1px' + } ); + expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#ba7' ); + expect( tableCell.getAttribute( 'width' ) ).to.equal( '1337px' ); + } ); } ); async function createEditor( extraPlugins = [] ) { From b20b4e2c85044442833c21b27d04b49603b32c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 11:09:03 +0200 Subject: [PATCH 36/71] Add test case for table properties in paste scenarios. --- .../tests/tableclipboard-paste.js | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 228b31ddb68..e24443821eb 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -1321,7 +1321,7 @@ describe( 'table clipboard', () => { ] ) ); } ); - it( 'handles table cell properties handling', async () => { + it( 'handles table cell properties', async () => { await createEditor( [ TableCellPropertiesEditing ] ); setModelData( model, modelTable( [ @@ -1363,6 +1363,37 @@ describe( 'table clipboard', () => { expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#ba7' ); expect( tableCell.getAttribute( 'width' ) ).to.equal( '1337px' ); } ); + + it( 'discards table properties', async () => { + await createEditor( [ TableCellPropertiesEditing ] ); + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '01', '11', '12' ], + [ '02', '21', '22' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const tableStyle = 'border:1px solid #f00;background:#ba7;width:1337px'; + const pastedTable = `
aaab
babb
`; + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', pastedTable ); + viewDocument.fire( 'paste', data ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'aa', 'ab', '02' ], + [ 'ba', 'bb', '12' ], + [ '02', '21', '22' ] + ] ) ); + } ); } ); async function createEditor( extraPlugins = [] ) { From fb21bb05a97edd4ce3b3acda9c354c832da5cf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 12:29:33 +0200 Subject: [PATCH 37/71] Refactor _onInsertContent() logic. --- .../ckeditor5-table/src/tableclipboard.js | 112 ++++++++++-------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 08c8cfb709f..d76be9c09ae 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import TableSelection from './tableselection'; import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import TableWalker from './tablewalker'; -import { findAncestor, updateNumericAttribute } from './commands/utils'; +import { findAncestor } from './commands/utils'; import { cropTableToDimensions } from './tableselection/croptable'; /** @@ -99,15 +99,16 @@ export default class TableClipboard extends Plugin { return; } - // We might need to crop table before inserting. - let table = getTable( content ); + // We might need to crop table before inserting so reference might change. + let insertedTable = getTableFromContent( content ); - if ( !table ) { + if ( !insertedTable ) { return; } evt.stop(); + // Currently not handled. See: https://github.com/ckeditor/ckeditor5/issues/6121. if ( selectedTableCells.length === 1 ) { // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Single table cell is selected.' ); @@ -115,20 +116,26 @@ export default class TableClipboard extends Plugin { } const tableUtils = this.editor.plugins.get( 'TableUtils' ); - const rowIndexes = getRowIndexes( selectedTableCells ); - const columnIndexes = getColumnIndexes( selectedTableCells ); - const selectionHeight = rowIndexes.last - rowIndexes.first + 1; - const selectionWidth = columnIndexes.last - columnIndexes.first + 1; - const insertHeight = tableUtils.getRows( table ); - const insertWidth = tableUtils.getColumns( table ); - const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + // Currently not handled. The selected table content should be trimmed to a rectangular selection. + // See: https://github.com/ckeditor/ckeditor5/issues/6122. if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Selection is not rectangular (non-mergeable).' ); return; } + const { last: lastColumnOfSelection, first: firstColumnOfSelection } = getColumnIndexes( selectedTableCells ); + const { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells ); + + const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1; + const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1; + + const insertHeight = tableUtils.getRows( insertedTable ); + const insertWidth = tableUtils.getColumns( insertedTable ); + + // The if below is temporal and will be removed when handling this case. + // See: https://github.com/ckeditor/ckeditor5/issues/6769. if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Pasted table is smaller than selection area.' ); @@ -138,81 +145,82 @@ export default class TableClipboard extends Plugin { const model = this.editor.model; model.change( writer => { - // Pasted table extends selection area. + // Crop pasted table if it extends selection area. if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { - table = cropTableToDimensions( table, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); + insertedTable = cropTableToDimensions( insertedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); } + // Stores cells anchors map of inserted table cell as '"row"x"column"' index. const insertionMap = new Map(); - for ( const { column, row, cell } of new TableWalker( table ) ) { + for ( const { column, row, cell } of new TableWalker( insertedTable ) ) { insertionMap.set( `${ row }x${ column }`, cell ); } + // Content table to which we insert a table. + const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + + // Selection must be set to pasted cells (some might be removed or new created). + const cellsToSelect = []; + + // Store previous cell in order to insert a new table cells after it if required. + let previousCellInRow; + const tableMap = [ ...new TableWalker( contentTable, { - startRow: rowIndexes.first, - endRow: rowIndexes.last, + startRow: firstRowOfSelection, + endRow: lastRowOfSelection, includeSpanned: true } ) ]; - let previousCell; - - const cellsToSelect = []; - for ( const { column, row, cell, isSpanned } of tableMap ) { - // TODO: crete issue for table walker startColumn, endColumn. - if ( column < columnIndexes.first || column > columnIndexes.last ) { - previousCell = cell; + if ( column === 0 ) { + previousCellInRow = null; + } + + // Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785. + if ( column < firstColumnOfSelection || column > lastColumnOfSelection ) { + previousCellInRow = cell; continue; } - const toGet = `${ row - rowIndexes.first }x${ column - columnIndexes.first }`; - const cellToInsert = insertionMap.get( toGet ); + // Map current table location to inserted table location. + const cellLocationToInsert = `${ row - firstRowOfSelection }x${ column - firstColumnOfSelection }`; + const cellToInsert = insertionMap.get( cellLocationToInsert ); + // There is no cell to insert (might be spanned by other cell in a pasted table) so... if ( !cellToInsert ) { + // ...if the cell is anchored in current location (not-spanned slot) then remove that cell from content table... if ( !isSpanned ) { writer.remove( writer.createRangeOn( cell ) ); } + // ...and advance to next content table slot. continue; } let targetCell = cell; - if ( isSpanned ) { - let insertPosition; - - if ( column === 0 || !previousCell || previousCell.parent.index !== row ) { - insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); - } else { - insertPosition = writer.createPositionAfter( previousCell ); - } - - targetCell = writer.createElement( 'tableCell' ); - - writer.insert( targetCell, insertPosition ); - } else { - writer.remove( writer.createRangeIn( cell ) ); + // Remove cells from anchor slots (not spanned by other cells). + if ( !isSpanned ) { + writer.remove( writer.createRangeOn( cell ) ); } - updateNumericAttribute( 'colspan', cellToInsert.getAttribute( 'colspan' ), targetCell, writer, 1 ); - updateNumericAttribute( 'rowspan', cellToInsert.getAttribute( 'rowspan' ), targetCell, writer, 1 ); + // 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. + targetCell = cellToInsert._clone( true ); - const attributesToCopy = Array.from( cellToInsert.getAttributeKeys() ) - .filter( attribute => attribute !== 'colspan' || attribute !== 'rowspan' ); + let insertPosition; - for ( const attribute of attributesToCopy ) { - writer.setAttribute( attribute, cellToInsert.getAttribute( attribute ), targetCell ); - } - - // TODO: use Element._clone to copy full structure. - for ( const child of Array.from( cellToInsert.getChildren() ) ) { - writer.insert( child, targetCell, 'end' ); + if ( !previousCellInRow ) { + insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); + } else { + insertPosition = writer.createPositionAfter( previousCellInRow ); } + writer.insert( targetCell, insertPosition ); cellsToSelect.push( targetCell ); - previousCell = targetCell; + previousCellInRow = targetCell; } writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); @@ -220,7 +228,7 @@ export default class TableClipboard extends Plugin { } } -function getTable( content ) { +function getTableFromContent( content ) { for ( const child of Array.from( content ) ) { if ( child.is( 'table' ) ) { return child; From 58523f4c39dc2abbb6f77e3eca4def263497887e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 12:30:07 +0200 Subject: [PATCH 38/71] Organize imports in table clipboard. --- packages/ckeditor5-table/src/tableclipboard.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index d76be9c09ae..03c172da080 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -8,9 +8,10 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + import TableSelection from './tableselection'; -import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import TableWalker from './tablewalker'; +import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import { findAncestor } from './commands/utils'; import { cropTableToDimensions } from './tableselection/croptable'; From 09c03758f641939f1d5d2e567ee18a41be5ccf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 12:35:00 +0200 Subject: [PATCH 39/71] Update table clipboard docs. --- packages/ckeditor5-table/src/tableclipboard.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 03c172da080..a17cfd30a26 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -82,16 +82,17 @@ export default class TableClipboard extends Plugin { } /** - * Handles... + * Overrides default {@link module:engine/model~Model.insertContent `model.insertContent()`} method to handle pasting table inside + * selected table fragment. + * + * Depending on selected table fragment: + * - If a selected table fragment is smaller than paste table it will crop pasted table to match dimensions. + * - If dimensions are equal it will replace selected table fragment with a pasted table contents. * * @private * @param evt * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. - * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection] - * The selection into which the content should be inserted. If not provided the current model document selection will be used. - * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`. - * This param defines a position in relation to that item. - */ +s */ _onInsertContent( evt, content ) { const tableSelection = this.editor.plugins.get( 'TableSelection' ); const selectedTableCells = tableSelection.getSelectedTableCells(); @@ -107,6 +108,7 @@ export default class TableClipboard extends Plugin { return; } + // Override default model.insertContent() handling at this point. evt.stop(); // Currently not handled. See: https://github.com/ckeditor/ckeditor5/issues/6121. From 1912c431eba5f7a41c7baef03ded811040ff095d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 12:52:10 +0200 Subject: [PATCH 40/71] Fix jsdoc comments. --- packages/ckeditor5-table/src/tableclipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index a17cfd30a26..3dd7ca6b544 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -82,7 +82,7 @@ export default class TableClipboard extends Plugin { } /** - * Overrides default {@link module:engine/model~Model.insertContent `model.insertContent()`} method to handle pasting table inside + * Overrides default {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method to handle pasting table inside * selected table fragment. * * Depending on selected table fragment: @@ -92,7 +92,7 @@ export default class TableClipboard extends Plugin { * @private * @param evt * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. -s */ + */ _onInsertContent( evt, content ) { const tableSelection = this.editor.plugins.get( 'TableSelection' ); const selectedTableCells = tableSelection.getSelectedTableCells(); From b6fe69c23545b2d97203e53c5f3b10cd944b7eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 11 May 2020 12:57:02 +0200 Subject: [PATCH 41/71] Anticipate temporary console.log in table clipboard paste tests. --- .../ckeditor5-table/tests/tableclipboard-paste.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index e24443821eb..95c54da26bd 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document */ +/* globals document console */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; @@ -13,6 +13,7 @@ import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imag import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; @@ -24,6 +25,8 @@ import TableClipboard from '../src/tableclipboard'; describe( 'table clipboard', () => { let editor, model, modelRoot, tableSelection, viewDocument, element; + testUtils.createSinonSandbox(); + beforeEach( () => { element = document.createElement( 'div' ); document.body.appendChild( element ); @@ -103,6 +106,9 @@ describe( 'table clipboard', () => { modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + // Catches the temporary console log in the CK_DEBUG mode. + sinon.stub( console, 'log' ); + pasteTable( [ [ 'aa', 'ab' ], [ 'ba', 'bb' ] @@ -128,6 +134,9 @@ describe( 'table clipboard', () => { modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + // Catches the temporary console log in the CK_DEBUG mode. + sinon.stub( console, 'log' ); + pasteTable( [ [ 'aa', 'ab' ], [ 'ba', 'bb' ] @@ -1206,6 +1215,9 @@ describe( 'table clipboard', () => { modelRoot.getNodeByPath( [ 0, 3, 3 ] ) ); + // Catches the temporary console log in the CK_DEBUG mode. + sinon.stub( console, 'log' ); + pasteTable( [ [ 'aa', 'ab' ], [ 'ba', 'bb' ] From af0fac6466c7bd223bd0636a041c1302470682c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 12 May 2020 15:23:43 +0200 Subject: [PATCH 42/71] Add table clipboard tests and update its requires call. --- .../ckeditor5-table/src/tableclipboard.js | 9 ++--- .../ckeditor5-table/tests/tableclipboard.js | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 packages/ckeditor5-table/tests/tableclipboard.js diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 3dd7ca6b544..16ea736f06b 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -14,6 +14,7 @@ import TableWalker from './tablewalker'; import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils'; import { findAncestor } from './commands/utils'; import { cropTableToDimensions } from './tableselection/croptable'; +import TableUtils from './tableutils'; /** * This plugin adds support for copying/cutting/pasting fragments of tables. @@ -33,7 +34,7 @@ export default class TableClipboard extends Plugin { * @inheritDoc */ static get requires() { - return [ TableSelection ]; + return [ TableSelection, TableUtils ]; } /** @@ -56,7 +57,7 @@ export default class TableClipboard extends Plugin { * @param {Object} data Clipboard event data. */ _onCopyCut( evt, data ) { - const tableSelection = this.editor.plugins.get( 'TableSelection' ); + const tableSelection = this.editor.plugins.get( TableSelection ); if ( !tableSelection.getSelectedTableCells() ) { return; @@ -94,7 +95,7 @@ export default class TableClipboard extends Plugin { * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. */ _onInsertContent( evt, content ) { - const tableSelection = this.editor.plugins.get( 'TableSelection' ); + const tableSelection = this.editor.plugins.get( TableSelection ); const selectedTableCells = tableSelection.getSelectedTableCells(); if ( !selectedTableCells ) { @@ -118,7 +119,7 @@ export default class TableClipboard extends Plugin { return; } - const tableUtils = this.editor.plugins.get( 'TableUtils' ); + const tableUtils = this.editor.plugins.get( TableUtils ); // Currently not handled. The selected table content should be trimmed to a rectangular selection. // See: https://github.com/ckeditor/ckeditor5/issues/6122. diff --git a/packages/ckeditor5-table/tests/tableclipboard.js b/packages/ckeditor5-table/tests/tableclipboard.js new file mode 100644 index 00000000000..9b736a15b81 --- /dev/null +++ b/packages/ckeditor5-table/tests/tableclipboard.js @@ -0,0 +1,36 @@ +/** + * @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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +import TableSelection from '../src/tableselection'; +import TableUtils from '../src/tableutils'; + +import TableClipboard from '../src/tableclipboard'; + +describe( 'table clipboard', () => { + let editor; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ TableClipboard, Paragraph ] + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'TableClipboard', () => { + it( 'should have pluginName', () => { + expect( TableClipboard.pluginName ).to.equal( 'TableClipboard' ); + } ); + + it( 'requires TableSelection and TableUtils ', () => { + expect( TableClipboard.requires ).to.deep.equal( [ TableSelection, TableUtils ] ); + } ); + } ); +} ); From 3ac58ce999a0cd28cee5ae32b0f431a2989410ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 12 May 2020 16:39:23 +0200 Subject: [PATCH 43/71] Refactor cropTableToDimensions(). --- .../src/tableselection/croptable.js | 138 +++++++----------- 1 file changed, 50 insertions(+), 88 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 917f982c790..22a373b1c8d 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -7,7 +7,7 @@ * @module table/tableselection/croptable */ -import { findAncestor } from '../commands/utils'; +import { findAncestor, updateNumericAttribute } from '../commands/utils'; import TableWalker from '../tablewalker'; /** @@ -27,16 +27,48 @@ import TableWalker from '../tablewalker'; * @returns {module:engine/model/element~Element} */ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ) { - const tableCopy = makeTableCopy( sourceTable, startRow, startColumn, endRow, endColumn, writer, tableUtils ); + const croppedTable = writer.createElement( 'table' ); - const selectionWidth = endColumn - startColumn + 1; - const selectionHeight = endRow - startRow + 1; + // Create needed rows. + for ( let i = 0; i < endRow - startRow + 1; i++ ) { + writer.insertElement( 'tableRow', croppedTable, 'end' ); + } + + const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, includeSpanned: true } ) ]; + + for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isSpanned } of tableMap ) { + if ( sourceColumn < startColumn || sourceColumn > endColumn ) { + continue; + } + + const insertRow = sourceRow - startRow; + const insertColumn = sourceColumn - startColumn; + + const row = croppedTable.getChild( insertRow ); + + if ( isSpanned ) { + const { row: anchorRow, column: anchorColumn } = tableUtils.getCellLocation( tableCell ); + + if ( anchorRow < startRow || anchorColumn < startColumn ) { + const tableCell = writer.createElement( 'tableCell' ); + const paragraph = writer.createElement( 'paragraph' ); + + writer.insert( paragraph, tableCell, 0 ); + writer.insertText( '', paragraph, 0 ); - trimTable( tableCopy, selectionWidth, selectionHeight, writer, tableUtils ); + writer.append( tableCell, row ); + } + } else { + const tableCellCopy = tableCell._clone( true ); - addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer ); + writer.append( tableCellCopy, row ); - return tableCopy; + trimTableCell( tableCellCopy, tableUtils, writer, insertRow, insertColumn, startRow, startColumn, endRow, endColumn ); + } + } + addHeadingsToTableCopy( croppedTable, sourceTable, startRow, startColumn, writer ); + + return croppedTable; } /** @@ -69,93 +101,23 @@ export function cropTableToSelection( selectedTableCellsIterator, tableUtils, wr return cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ); } -// Creates a table copy from a selected table cells. -// -// It fills "gaps" in copied table - ie when cell outside copied range was spanning over selection. -function makeTableCopy( sourceTable, startRow, startColumn, endRow, endColumn, writer, tableUtils ) { - const tableCopy = writer.createElement( 'table' ); - - const rowToCopyMap = new Map(); - const copyToOriginalColumnMap = new Map(); - - for ( const { column, cell: tableCell } of [ ...new TableWalker( sourceTable, { startRow, endRow } ) ] ) { - if ( column < startColumn || column > endColumn ) { - continue; - } - - const row = findAncestor( 'tableRow', tableCell ); - - if ( !rowToCopyMap.has( row ) ) { - const rowCopy = row._clone(); - writer.append( rowCopy, tableCopy ); - rowToCopyMap.set( row, rowCopy ); - } - - const tableCellCopy = tableCell._clone( true ); - - copyToOriginalColumnMap.set( tableCellCopy, column ); - - writer.append( tableCellCopy, rowToCopyMap.get( row ) ); - } - - addMissingTableCells( tableCopy, startColumn, copyToOriginalColumnMap, writer, tableUtils ); - - return tableCopy; -} - -// Fills gaps for spanned cell from outside the selection range. -function addMissingTableCells( tableCopy, startColumn, copyToOriginalColumnMap, writer, tableUtils ) { - for ( const row of tableCopy.getChildren() ) { - for ( const tableCell of Array.from( row.getChildren() ) ) { - const { column } = tableUtils.getCellLocation( tableCell ); - - const originalColumn = copyToOriginalColumnMap.get( tableCell ); - const shiftedColumn = originalColumn - startColumn; +function trimTableCell( tableCell, tableUtils, writer, row, column, startRow, startColumn, endRow, endColumn ) { + const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - if ( column !== shiftedColumn ) { - for ( let i = 0; i < shiftedColumn - column; i++ ) { - const prepCell = writer.createElement( 'tableCell' ); - writer.insert( prepCell, writer.createPositionBefore( tableCell ) ); + const width = endColumn - startColumn + 1; + const height = endRow - startRow + 1; - const paragraph = writer.createElement( 'paragraph' ); + if ( column + colspan > width ) { + const newSpan = width - column; - writer.insert( paragraph, prepCell, 0 ); - writer.insertText( '', paragraph, 0 ); - } - } - } + updateNumericAttribute( 'colspan', newSpan, tableCell, writer, 1 ); } -} - -// Trims table to a given dimensions. -function trimTable( table, width, height, writer, tableUtils ) { - for ( const row of table.getChildren() ) { - for ( const tableCell of row.getChildren() ) { - const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); - const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - - const { row, column } = tableUtils.getCellLocation( tableCell ); - if ( column + colspan > width ) { - const newSpan = width - column; + if ( row + rowspan > height ) { + const newSpan = height - row; - if ( newSpan > 1 ) { - writer.setAttribute( 'colspan', newSpan, tableCell ); - } else { - writer.removeAttribute( 'colspan', tableCell ); - } - } - - if ( row + rowspan > height ) { - const newSpan = height - row; - - if ( newSpan > 1 ) { - writer.setAttribute( 'rowspan', newSpan, tableCell ); - } else { - writer.removeAttribute( 'rowspan', tableCell ); - } - } - } + updateNumericAttribute( 'rowspan', newSpan, tableCell, writer, 1 ); } } From 85290c71690e8109a97c7ab7879fb10090e3095c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 08:49:05 +0200 Subject: [PATCH 44/71] Update documentation of cropTableToDimensions() util. --- .../src/tableselection/croptable.js | 95 ++++++++++++------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 22a373b1c8d..1a3a6dd5283 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -7,16 +7,31 @@ * @module table/tableselection/croptable */ -import { findAncestor, updateNumericAttribute } from '../commands/utils'; +import { createEmptyTableCell, findAncestor, updateNumericAttribute } from '../commands/utils'; import TableWalker from '../tablewalker'; /** * Returns a cropped table according to given dimensions. - * - * This function is to be used with the table selection. + + * To return a cropped table that starts at first row and first column and end in third row and column: * * const croppedTable = cropTable( table, 1, 1, 3, 3, tableUtils, writer ); * + * Calling the code above for the table below: + * + * 0 1 2 3 4 0 1 2 + * ┌───┬───┬───┬───┬───┐ + * 0 │ a │ b │ c │ d │ e │ + * ├───┴───┤ ├───┴───┤ ┌───┬───┬───┐ + * 1 │ f │ │ g │ │ │ │ g │ 0 + * ├───┬───┴───┼───┬───┤ will return: ├───┴───┼───┤ + * 2 │ h │ i │ j │ k │ │ i │ j │ 1 + * ├───┤ ├───┤ │ │ ├───┤ + * 3 │ l │ │ m │ │ │ │ m │ 2 + * ├───┼───┬───┤ ├───┤ └───────┴───┘ + * 4 │ n │ o │ p │ │ q │ + * └───┴───┴───┴───┴───┘ + * * @param {Number} sourceTable * @param {Number} startRow * @param {Number} startColumn @@ -27,46 +42,58 @@ import TableWalker from '../tablewalker'; * @returns {module:engine/model/element~Element} */ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ) { + // Create empty table with empty rows equal to crop height. const croppedTable = writer.createElement( 'table' ); + const cropHeight = endRow - startRow + 1; - // Create needed rows. - for ( let i = 0; i < endRow - startRow + 1; i++ ) { + for ( let i = 0; i < cropHeight; i++ ) { writer.insertElement( 'tableRow', croppedTable, 'end' ); } const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, includeSpanned: true } ) ]; + // Iterate over source table slots (including empty - spanned - ones). for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isSpanned } of tableMap ) { + // Skip slots outside the cropped area. if ( sourceColumn < startColumn || sourceColumn > endColumn ) { continue; } - const insertRow = sourceRow - startRow; - const insertColumn = sourceColumn - startColumn; - - const row = croppedTable.getChild( insertRow ); + // Row index in cropped table. + const cropRow = sourceRow - startRow; + const row = croppedTable.getChild( cropRow ); + // For empty slots: fill the gap with empty table cell. if ( isSpanned ) { const { row: anchorRow, column: anchorColumn } = tableUtils.getCellLocation( tableCell ); + // But fill the gap only if the spanning cell is anchored outside cropped area. + // In the table from method jsdoc those cells are: "c" & "f". if ( anchorRow < startRow || anchorColumn < startColumn ) { - const tableCell = writer.createElement( 'tableCell' ); - const paragraph = writer.createElement( 'paragraph' ); - - writer.insert( paragraph, tableCell, 0 ); - writer.insertText( '', paragraph, 0 ); - - writer.append( tableCell, row ); + createEmptyTableCell( writer, writer.createPositionAt( row, 'end' ) ); } - } else { + } + // Otherwise clone the cell with all children and trim if it exceeds cropped area. + else { const tableCellCopy = tableCell._clone( true ); writer.append( tableCellCopy, row ); - trimTableCell( tableCellCopy, tableUtils, writer, insertRow, insertColumn, startRow, startColumn, endRow, endColumn ); + // Crop end column/row is equal to crop width/height. + const cropEndRow = endColumn - startColumn + 1; + const cropEndColumn = cropHeight; + + // Column index in cropped table. + const cropColumn = sourceColumn - startColumn; + + // Trim table if it exceeds cropped area. + // In the table from method jsdoc those cells are: "g" & "m". + trimTableCellIfNeeded( tableCellCopy, cropRow, cropColumn, cropEndRow, cropEndColumn, tableUtils, writer ); } } - addHeadingsToTableCopy( croppedTable, sourceTable, startRow, startColumn, writer ); + + // Adjust heading rows & columns in cropped table if crop selection includes headings parts. + addHeadingsToCroppedTable( croppedTable, sourceTable, startRow, startColumn, writer ); return croppedTable; } @@ -101,39 +128,37 @@ export function cropTableToSelection( selectedTableCellsIterator, tableUtils, wr return cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ); } -function trimTableCell( tableCell, tableUtils, writer, row, column, startRow, startColumn, endRow, endColumn ) { +// Adjusts table cell dimensions to not exceed last row and last column. +function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, lastRow, lastColumn, tableUtils, writer ) { const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - const width = endColumn - startColumn + 1; - const height = endRow - startRow + 1; - - if ( column + colspan > width ) { - const newSpan = width - column; + if ( cellColumn + colspan > lastRow ) { + const trimmedSpan = lastRow - cellColumn; - updateNumericAttribute( 'colspan', newSpan, tableCell, writer, 1 ); + updateNumericAttribute( 'colspan', trimmedSpan, tableCell, writer, 1 ); } - if ( row + rowspan > height ) { - const newSpan = height - row; + if ( cellRow + rowspan > lastColumn ) { + const trimmedSpan = lastColumn - cellRow; - updateNumericAttribute( 'rowspan', newSpan, tableCell, writer, 1 ); + updateNumericAttribute( 'rowspan', trimmedSpan, tableCell, writer, 1 ); } } -// Sets proper heading attributes to copied table. -function addHeadingsToTableCopy( tableCopy, sourceTable, startRow, startColumn, writer ) { +// Sets proper heading attributes to a cropped table. +function addHeadingsToCroppedTable( croppedTable, sourceTable, startRow, startColumn, writer ) { const headingRows = parseInt( sourceTable.getAttribute( 'headingRows' ) || 0 ); if ( headingRows > 0 ) { - const copiedRows = headingRows - startRow; - writer.setAttribute( 'headingRows', copiedRows, tableCopy ); + const headingRowsInCrop = headingRows - startRow; + updateNumericAttribute( 'headingRows', headingRowsInCrop, croppedTable, writer, 0 ); } const headingColumns = parseInt( sourceTable.getAttribute( 'headingColumns' ) || 0 ); if ( headingColumns > 0 ) { - const copiedColumns = headingColumns - startColumn; - writer.setAttribute( 'headingColumns', copiedColumns, tableCopy ); + const headingColumnsInCrop = headingColumns - startColumn; + updateNumericAttribute( 'headingColumns', headingColumnsInCrop, croppedTable, writer, 0 ); } } From d01555db8355f433d1ad1d43e3ae7a78edbffcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 08:51:25 +0200 Subject: [PATCH 45/71] Fix copy table tests. --- packages/ckeditor5-table/tests/tableclipboard-copy-cut.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js index 1ebd680df5f..a7b5b53881f 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js +++ b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js @@ -175,7 +175,7 @@ describe( 'table clipboard', () => { it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', () => { setModelData( model, modelTable( [ - [ '00[]', { contents: '01', rowspan: 2 }, '02' ], + [ '00', { contents: '01', rowspan: 2 }, '02' ], [ '10', '12' ], [ '20', '21', '22' ] ] ) ); @@ -239,7 +239,7 @@ describe( 'table clipboard', () => { [ '31', ' ', '33', ' ', ' ', '37', '37' ], [ ' ', ' ', ' ', ' ', ' ', '47', '47' ], [ '51', '52', '53', ' ', ' ', { contents: '57', rowspan: 3 }, '57' ], - [ { contents: '61', colspan: 3 }, ' ', ' ', ' ', '67' ], + [ { contents: '61', colspan: 3 }, ' ', ' ', '67' ], [ '71', '72', '73', '74', '75', '77' ] ] ) ); } ); From 07500b6522934931397819757f669c96abf714ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 09:01:31 +0200 Subject: [PATCH 46/71] Refactor variable names in insert content method. --- packages/ckeditor5-table/src/tableclipboard.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 16ea736f06b..35d0f1dd428 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -190,29 +190,27 @@ export default class TableClipboard extends Plugin { // Map current table location to inserted table location. const cellLocationToInsert = `${ row - firstRowOfSelection }x${ column - firstColumnOfSelection }`; - const cellToInsert = insertionMap.get( cellLocationToInsert ); + const pastedCell = insertionMap.get( cellLocationToInsert ); // There is no cell to insert (might be spanned by other cell in a pasted table) so... - if ( !cellToInsert ) { + if ( !pastedCell ) { // ...if the cell is anchored in current location (not-spanned slot) then remove that cell from content table... if ( !isSpanned ) { - writer.remove( writer.createRangeOn( cell ) ); + writer.remove( cell ); } // ...and advance to next content table slot. continue; } - let targetCell = cell; - // Remove cells from anchor slots (not spanned by other cells). if ( !isSpanned ) { - writer.remove( writer.createRangeOn( cell ) ); + writer.remove( cell ); } // 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. - targetCell = cellToInsert._clone( true ); + const cellToInsert = pastedCell._clone( true ); let insertPosition; @@ -222,9 +220,9 @@ export default class TableClipboard extends Plugin { insertPosition = writer.createPositionAfter( previousCellInRow ); } - writer.insert( targetCell, insertPosition ); - cellsToSelect.push( targetCell ); - previousCellInRow = targetCell; + writer.insert( cellToInsert, insertPosition ); + cellsToSelect.push( cellToInsert ); + previousCellInRow = cellToInsert; } writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) ); From 42cc9753044abc76e4e3d3d09451f59791e72870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 09:12:05 +0200 Subject: [PATCH 47/71] Refactor insertionMap to use an array. --- packages/ckeditor5-table/src/tableclipboard.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 35d0f1dd428..c29619712bb 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -154,11 +154,11 @@ export default class TableClipboard extends Plugin { insertedTable = cropTableToDimensions( insertedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); } - // Stores cells anchors map of inserted table cell as '"row"x"column"' index. - const insertionMap = new Map(); + // Stores cells as a map of inserted table cell as 'row * column' index. + const insertionMap = new Array( selectionHeight * selectionWidth ).fill( null ); for ( const { column, row, cell } of new TableWalker( insertedTable ) ) { - insertionMap.set( `${ row }x${ column }`, cell ); + insertionMap[ row * insertWidth + column ] = cell; } // Content table to which we insert a table. @@ -189,8 +189,8 @@ export default class TableClipboard extends Plugin { } // Map current table location to inserted table location. - const cellLocationToInsert = `${ row - firstRowOfSelection }x${ column - firstColumnOfSelection }`; - const pastedCell = insertionMap.get( cellLocationToInsert ); + const cellLocationToInsert = ( row - firstRowOfSelection ) * insertWidth + ( column - firstColumnOfSelection ); + const pastedCell = insertionMap[ cellLocationToInsert ]; // There is no cell to insert (might be spanned by other cell in a pasted table) so... if ( !pastedCell ) { From 80022e043d5859660c77b1331b95241b1c58352c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 09:49:20 +0200 Subject: [PATCH 48/71] Refactor insertion map to a two dimensional location map. The access by [ row ][ column ] is self explanatory. No need to calculate location by the table width. --- .../ckeditor5-table/src/tableclipboard.js | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index c29619712bb..b46c506ce0b 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -155,11 +155,7 @@ export default class TableClipboard extends Plugin { } // Stores cells as a map of inserted table cell as 'row * column' index. - const insertionMap = new Array( selectionHeight * selectionWidth ).fill( null ); - - for ( const { column, row, cell } of new TableWalker( insertedTable ) ) { - insertionMap[ row * insertWidth + column ] = cell; - } + const pastedTableMap = createLocationMap( insertedTable, selectionWidth, selectionHeight ); // Content table to which we insert a table. const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); @@ -189,8 +185,7 @@ export default class TableClipboard extends Plugin { } // Map current table location to inserted table location. - const cellLocationToInsert = ( row - firstRowOfSelection ) * insertWidth + ( column - firstColumnOfSelection ); - const pastedCell = insertionMap[ cellLocationToInsert ]; + const pastedCell = pastedTableMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ]; // There is no cell to insert (might be spanned by other cell in a pasted table) so... if ( !pastedCell ) { @@ -239,3 +234,46 @@ function getTableFromContent( content ) { 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: +// +// * cell - cell from pasted table anchored at this location. +// * null - if no cell is anchored at this location. +// +// For instance, from a table below: +// +// +----+----+----+----+ +// | 00 | 01 | 02 | 03 | +// + +----+----+----+ +// | | 11 | 13 | +// +----+ +----+ +// | 20 | | 23 | +// +----+----+----+----+ +// +// The method will return an array (numbers represents cell element): +// +// const map = [ +// [ '00', '01', '02', '03' ], +// [ null, '11', null, '13' ], +// [ '20', null, null, '23' ] +// ] +// +// This allows for a quick access to table at give row & column. For instance to access table cell "13" from pasted table call: +// +// const cell = map[ 1 ][ 3 ] +// +function createLocationMap( table, width, height ) { + // Create height x width (row x column) two-dimensional table to store cells. + const map = new Array( height ).fill( null ) + .map( () => { + return new Array( width ).fill( null ); + } ); + + for ( const { column, row, cell } of new TableWalker( table ) ) { + map[ row ][ column ] = cell; + } + + return map; +} From 4926e94d86d34fd273c1a7cfdd72e516ea27e556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 10:08:04 +0200 Subject: [PATCH 49/71] Make table paste work only for document selection. --- .../ckeditor5-table/src/tableclipboard.js | 12 ++++- .../tests/tableclipboard-paste.js | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index b46c506ce0b..e3e4e9d49fb 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -93,8 +93,14 @@ export default class TableClipboard extends Plugin { * @private * @param evt * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert. + * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection] + * The selection into which the content should be inserted. If not provided the current model document selection will be used. */ - _onInsertContent( evt, content ) { + _onInsertContent( evt, content, selectable ) { + if ( selectable && !selectable.is( 'documentSelection' ) ) { + return; + } + const tableSelection = this.editor.plugins.get( TableSelection ); const selectedTableCells = tableSelection.getSelectedTableCells(); @@ -226,6 +232,10 @@ export default class TableClipboard extends Plugin { } function getTableFromContent( content ) { + if ( content.is( 'table' ) ) { + return content; + } + for ( const child of Array.from( content ) ) { if ( child.is( 'table' ) ) { return child; diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 95c54da26bd..76b8293c86d 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -20,6 +20,8 @@ import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import TableEditing from '../src/tableediting'; import TableCellPropertiesEditing from '../src/tablecellproperties/tablecellpropertiesediting'; +import TableWalker from '../src/tablewalker'; + import TableClipboard from '../src/tableclipboard'; describe( 'table clipboard', () => { @@ -94,6 +96,58 @@ describe( 'table clipboard', () => { ] ) ); } ); + it( 'should not alter model.insertContent if selectable is different from document selection', () => { + model.change( writer => { + writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), 0 ); + + const selectedTableCells = model.createSelection( [ + model.createRangeOn( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ), + model.createRangeOn( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ), + model.createRangeOn( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ), + model.createRangeOn( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ) + ] ); + + const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, 2, 2 ); + + for ( const { cell } of new TableWalker( tableToInsert ) ) { + writer.insertText( 'foo', cell.getChild( 0 ), 0 ); + } + + model.insertContent( tableToInsert, selectedTableCells ); + } ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '', '', '02', '03' ], + [ '', '', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should alter model.insertContent if selectable is document selection', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + model.change( writer => { + const tableToInsert = editor.plugins.get( 'TableUtils' ).createTable( writer, 2, 2 ); + + for ( const { cell } of new TableWalker( tableToInsert ) ) { + writer.insertText( 'foo', cell.getChild( 0 ), 0 ); + } + + model.insertContent( tableToInsert, editor.model.document.selection ); + } ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'foo', 'foo', '02', '03' ], + [ 'foo', 'foo', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + it( 'should block non-rectangular selection', () => { setModelData( model, modelTable( [ [ { contents: '00', colspan: 3 } ], From 70b23ae7bcc6db5d20c7ed15f6c6e2deb9b606b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 10:25:09 +0200 Subject: [PATCH 50/71] Refactor cell replace algorithm to make more clear. --- .../ckeditor5-table/src/tableclipboard.js | 25 ++++++++----------- .../src/tableselection/croptable.js | 1 + 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index e3e4e9d49fb..32d5eda3d0e 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -178,7 +178,7 @@ export default class TableClipboard extends Plugin { includeSpanned: true } ) ]; - for ( const { column, row, cell, isSpanned } of tableMap ) { + for ( const { row, column, cell, isSpanned } of tableMap ) { if ( column === 0 ) { previousCellInRow = null; } @@ -190,25 +190,22 @@ export default class TableClipboard extends Plugin { continue; } - // Map current table location to inserted table location. + // 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 ( !isSpanned ) { + writer.remove( cell ); + } + + // Map current table slot location to an inserted table slot location. const pastedCell = pastedTableMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ]; - // There is no cell to insert (might be spanned by other cell in a pasted table) so... + // 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 ) { - // ...if the cell is anchored in current location (not-spanned slot) then remove that cell from content table... - if ( !isSpanned ) { - writer.remove( cell ); - } - - // ...and advance to next content table slot. continue; } - // Remove cells from anchor slots (not spanned by other cells). - if ( !isSpanned ) { - writer.remove( cell ); - } - // 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._clone( true ); diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 1a3a6dd5283..d206d1b7acf 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -55,6 +55,7 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo // Iterate over source table slots (including empty - spanned - ones). for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isSpanned } of tableMap ) { // Skip slots outside the cropped area. + // Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785. if ( sourceColumn < startColumn || sourceColumn > endColumn ) { continue; } From afde3cfaf2464e0644277711423e94bcbcba8763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 10:31:43 +0200 Subject: [PATCH 51/71] Add information about content table replacing goals. --- packages/ckeditor5-table/src/tableclipboard.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 32d5eda3d0e..0340604ff95 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -166,18 +166,24 @@ export default class TableClipboard extends Plugin { // Content table to which we insert a table. const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); - // Selection must be set to pasted cells (some might be removed or new created). - const cellsToSelect = []; - - // Store previous cell in order to insert a new table cells after it if required. - let previousCellInRow; - const tableMap = [ ...new TableWalker( contentTable, { startRow: firstRowOfSelection, endRow: lastRowOfSelection, includeSpanned: true } ) ]; + // Selection must be set to pasted cells (some might be removed or new created). + const cellsToSelect = []; + + // Store previous cell in order to insert a new table cells after it if required. + let previousCellInRow; + + // 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 { row, column, cell, isSpanned } of tableMap ) { if ( column === 0 ) { previousCellInRow = null; From 1228069819eb2261192bfd25518360d7c635b8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 11:12:23 +0200 Subject: [PATCH 52/71] Fix: Pasted table after spanned slots will not break table geometry. --- .../ckeditor5-table/src/tableclipboard.js | 5 +- .../tests/tableclipboard-paste.js | 114 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 0340604ff95..98900ac1176 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -191,7 +191,10 @@ export default class TableClipboard extends Plugin { // Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785. if ( column < firstColumnOfSelection || column > lastColumnOfSelection ) { - previousCellInRow = cell; + // Only update the previousCellInRow for non-spanned slots. + if ( !isSpanned ) { + previousCellInRow = cell; + } continue; } diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 76b8293c86d..2d992233a59 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -666,6 +666,120 @@ describe( 'table clipboard', () => { ] ); } ); + it( 'handles pasting simple table over a table with rowspan (rowspan before selection)', () => { + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+----+----+----+----+ + // | 10 | 11 | 12 | 13 | 14 | + // +----+ +----+----+----+ + // | 20 | | 22 | 23 | 24 | + // +----+ +----+----+----+ + // | 30 | | 32 | 33 | 34 | + // +----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | + // +----+----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', rowspan: 3 }, '12', '13', '14' ], + [ '20', '22', '23', '24' ], + [ '30', '32', '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 2 ] ), + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ], + [ 'ca', 'cb' ] + ] ); + + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+----+----+----+----+ + // | 10 | 11 | aa | ab | 14 | + // +----+ +----+----+----+ + // | 20 | | ba | bb | 24 | + // +----+ +----+----+----+ + // | 30 | | ca | cb | 34 | + // +----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | + // +----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', rowspan: 3 }, 'aa', 'ab', '14' ], + [ '20', 'ba', 'bb', '24' ], + [ '30', 'ca', 'cb', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0, 0 ], + [ 0, 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + + it( 'handles pasting simple table over a table with rowspans (rowspan before selection)', () => { + // +----+----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | 05 | + // +----+----+----+----+----+----+ + // | 10 | 12 | 13 | 14 | 15 | + // +----+----+----+----+----+----+ + // | 20 | 23 | 24 | 25 | + // +----+----+----+----+----+----+ + // | 30 | 31 | 32 | 33 | 34 | 35 | + // +----+----+----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ { contents: '10', colspan: 2 }, '12', '13', '14', '15' ], + [ { contents: '20', colspan: 3 }, '23', '24', '25' ], + [ '30', '31', '32', '33', '34', '35' ] + ] ) ); + + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 2 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + + pasteTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] + ] ); + + // +----+----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | 05 | + // +----+----+----+----+----+----+ + // | 10 | 12 | aa | ab | 15 | + // +----+----+----+----+----+----+ + // | 20 | ba | bb | 25 | + // +----+----+----+----+----+----+ + // | 30 | 31 | 32 | 33 | 34 | 35 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03', '04', '05' ], + [ { contents: '10', colspan: 2 }, '12', 'aa', 'ab', '15' ], + [ { contents: '20', colspan: 3 }, 'ba', 'bb', '25' ], + [ '30', '31', '32', '33', '34', '35' ] + ] ) ); + + /* eslint-disable no-multi-spaces */ + assertSelectedCells( model, [ + [ 0, 0, 0, 0, 0, 0 ], + [ 0, 0, 1, 1, 0 ], + [ 0, 1, 1, 0 ], + [ 0, 0, 0, 0, 0, 0 ] + ] ); + /* eslint-enable no-multi-spaces */ + } ); + // TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand). it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => { // +----+----+----+----+ From 098b7632e74d369779daa099bfe5319118f41f86 Mon Sep 17 00:00:00 2001 From: Maciej Date: Wed, 13 May 2020 12:14:04 +0200 Subject: [PATCH 53/71] Code style - inline simple arrow function. Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- packages/ckeditor5-table/src/tableclipboard.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 98900ac1176..c610c5fd10e 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -283,9 +283,7 @@ function getTableFromContent( content ) { function createLocationMap( table, width, height ) { // Create height x width (row x column) two-dimensional table to store cells. const map = new Array( height ).fill( null ) - .map( () => { - return new Array( width ).fill( null ); - } ); + .map( () => new Array( width ).fill( null ) ); for ( const { column, row, cell } of new TableWalker( table ) ) { map[ row ][ column ] = cell; From 22ca037f0ddbc991106cf3cf9fdfbf236949cddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 12:44:45 +0200 Subject: [PATCH 54/71] Improve pasted/selected nomenclature. --- .../ckeditor5-table/src/tableclipboard.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index c610c5fd10e..7a11fd525a6 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -109,9 +109,9 @@ export default class TableClipboard extends Plugin { } // We might need to crop table before inserting so reference might change. - let insertedTable = getTableFromContent( content ); + let pastedTable = getTableFromContent( content ); - if ( !insertedTable ) { + if ( !pastedTable ) { return; } @@ -141,12 +141,12 @@ export default class TableClipboard extends Plugin { const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1; const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1; - const insertHeight = tableUtils.getRows( insertedTable ); - const insertWidth = tableUtils.getColumns( insertedTable ); + const pasteHeight = tableUtils.getRows( pastedTable ); + const pasteWidth = tableUtils.getColumns( pastedTable ); // The if below is temporal and will be removed when handling this case. // See: https://github.com/ckeditor/ckeditor5/issues/6769. - if ( selectionHeight > insertHeight || selectionWidth > insertWidth ) { + if ( selectionHeight > pasteHeight || selectionWidth > pasteWidth ) { // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Pasted table is smaller than selection area.' ); return; @@ -156,17 +156,17 @@ export default class TableClipboard extends Plugin { model.change( writer => { // Crop pasted table if it extends selection area. - if ( selectionHeight < insertHeight || selectionWidth < insertWidth ) { - insertedTable = cropTableToDimensions( insertedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); + if ( selectionHeight < pasteHeight || selectionWidth < pasteWidth ) { + pastedTable = cropTableToDimensions( pastedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); } - // Stores cells as a map of inserted table cell as 'row * column' index. - const pastedTableMap = createLocationMap( insertedTable, selectionWidth, selectionHeight ); + // Stores cells as a map of pasted table cell as 'row * column' index. + const pastedTableMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); - // Content table to which we insert a table. - const contentTable = findAncestor( 'table', selectedTableCells[ 0 ] ); + // Content table to which we insert a pasted table. + const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] ); - const tableMap = [ ...new TableWalker( contentTable, { + const tableMap = [ ...new TableWalker( selectedTable, { startRow: firstRowOfSelection, endRow: lastRowOfSelection, includeSpanned: true @@ -175,7 +175,7 @@ export default class TableClipboard extends Plugin { // Selection must be set to pasted cells (some might be removed or new created). const cellsToSelect = []; - // Store previous cell in order to insert a new table cells after it if required. + // Store previous cell in order to insert a new table cells after it (if required). let previousCellInRow; // Content table replace cells algorithm iterates over a selected table fragment and: @@ -207,7 +207,7 @@ export default class TableClipboard extends Plugin { writer.remove( cell ); } - // Map current table slot location to an inserted table slot location. + // Map current table slot location to an pasted table slot location. const pastedCell = pastedTableMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ]; // There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot. @@ -222,7 +222,7 @@ export default class TableClipboard extends Plugin { let insertPosition; if ( !previousCellInRow ) { - insertPosition = writer.createPositionAt( contentTable.getChild( row ), 0 ); + insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 ); } else { insertPosition = writer.createPositionAfter( previousCellInRow ); } From 0c09a2e47d98bf5b00456939e837277eaf0d1f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 12:51:02 +0200 Subject: [PATCH 55/71] Use selected table cells in isSelectionRectangular(). --- .../src/commands/mergecellscommand.js | 3 +- .../ckeditor5-table/src/tableclipboard.js | 2 +- packages/ckeditor5-table/src/utils.js | 56 +++++++++---------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/ckeditor5-table/src/commands/mergecellscommand.js b/packages/ckeditor5-table/src/commands/mergecellscommand.js index d7821d90d5f..691e689b741 100644 --- a/packages/ckeditor5-table/src/commands/mergecellscommand.js +++ b/packages/ckeditor5-table/src/commands/mergecellscommand.js @@ -29,7 +29,8 @@ export default class MergeCellsCommand extends Command { * @inheritDoc */ refresh() { - this.isEnabled = isSelectionRectangular( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) ); + const selectedTableCells = getSelectedTableCells( this.editor.model.document.selection ); + this.isEnabled = isSelectionRectangular( selectedTableCells, this.editor.plugins.get( TableUtils ) ); } /** diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 7a11fd525a6..3552f12e37c 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -129,7 +129,7 @@ export default class TableClipboard extends Plugin { // Currently not handled. The selected table content should be trimmed to a rectangular selection. // See: https://github.com/ckeditor/ckeditor5/issues/6122. - if ( !isSelectionRectangular( this.editor.model.document.selection, tableUtils ) ) { + if ( !isSelectionRectangular( selectedTableCells, tableUtils ) ) { // @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Selection is not rectangular (non-mergeable).' ); return; diff --git a/packages/ckeditor5-table/src/utils.js b/packages/ckeditor5-table/src/utils.js index 10cec18dfd1..6818f83f6fc 100644 --- a/packages/ckeditor5-table/src/utils.js +++ b/packages/ckeditor5-table/src/utils.js @@ -179,34 +179,34 @@ export function getColumnIndexes( tableCells ) { return getFirstLastIndexesObject( indexes ); } -// Checks if the selection contains cells that do not exceed rectangular selection. -// -// In a table below: -// -// ┌───┬───┬───┬───┐ -// │ a │ b │ c │ d │ -// ├───┴───┼───┤ │ -// │ e │ f │ │ -// ├ ├───┼───┤ -// │ │ g │ h │ -// └───────┴───┴───┘ -// -// Valid selections are these which create a solid rectangle (without gaps), such as: -// - a, b (two horizontal cells) -// - c, f (two vertical cells) -// - a, b, e (cell "e" spans over four cells) -// - c, d, f (cell d spans over a cell in the row below) -// -// While an invalid selection would be: -// - a, c (the unselected cell "b" creates a gap) -// - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap) -// -// @param {module:engine/model/selection~Selection} selection -// @param {module:table/tableUtils~TableUtils} tableUtils -// @returns {boolean} -export function isSelectionRectangular( selection, tableUtils ) { - const selectedTableCells = getSelectedTableCells( selection ); - +/** + * Checks if the selection contains cells that do not exceed rectangular selection. + * + * In a table below: + * + * ┌───┬───┬───┬───┐ + * │ a │ b │ c │ d │ + * ├───┴───┼───┤ │ + * │ e │ f │ │ + * ├ ├───┼───┤ + * │ │ g │ h │ + * └───────┴───┴───┘ + * + * Valid selections are these which create a solid rectangle (without gaps), such as: + * - a, b (two horizontal cells) + * - c, f (two vertical cells) + * - a, b, e (cell "e" spans over four cells) + * - c, d, f (cell d spans over a cell in the row below) + * + * While an invalid selection would be: + * - a, c (the unselected cell "b" creates a gap) + * - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap) + * + * @param {Array.} selectedTableCells + * @param {module:table/tableUtils~TableUtils} tableUtils + * @returns {boolean} + */ +export function isSelectionRectangular( selectedTableCells, tableUtils ) { if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) { return false; } From 3e723af522b443286bdcd6eb6e26d1f51d0f1920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 12:58:47 +0200 Subject: [PATCH 56/71] Use getChildren() to iterate over document fragment. --- packages/ckeditor5-table/src/tableclipboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 3552f12e37c..e7bbbe2f714 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -242,7 +242,7 @@ function getTableFromContent( content ) { return content; } - for ( const child of Array.from( content ) ) { + for ( const child of content.getChildren() ) { if ( child.is( 'table' ) ) { return child; } From 7f5451feae4bc4a9dda0c4b265d4f3b680dc229a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:06:42 +0200 Subject: [PATCH 57/71] Remove confusing comment. --- packages/ckeditor5-table/src/tableclipboard.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index e7bbbe2f714..ee212a0b097 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -160,7 +160,6 @@ export default class TableClipboard extends Plugin { pastedTable = cropTableToDimensions( pastedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); } - // Stores cells as a map of pasted table cell as 'row * column' index. const pastedTableMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); // Content table to which we insert a pasted table. From c51cd4d64b75ce194dc7a1db3edc5b60c3ead9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:08:33 +0200 Subject: [PATCH 58/71] Rename tableMap to selectedTableMap. --- packages/ckeditor5-table/src/tableclipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index ee212a0b097..084c498ee11 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -165,7 +165,7 @@ export default class TableClipboard extends Plugin { // Content table to which we insert a pasted table. const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] ); - const tableMap = [ ...new TableWalker( selectedTable, { + const selectedTableMap = [ ...new TableWalker( selectedTable, { startRow: firstRowOfSelection, endRow: lastRowOfSelection, includeSpanned: true @@ -183,7 +183,7 @@ export default class TableClipboard extends Plugin { // - Inserts cell from a pasted table for a matched slots. // // This ensures proper table geometry after the paste - for ( const { row, column, cell, isSpanned } of tableMap ) { + for ( const { row, column, cell, isSpanned } of selectedTableMap ) { if ( column === 0 ) { previousCellInRow = null; } From 5d2c1e9bb6953498888bab3bf5656bb70e574123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:35:25 +0200 Subject: [PATCH 59/71] Refactor crop table utilities to use object parameter for crop dimensions. --- .../ckeditor5-table/src/tableclipboard.js | 9 ++- .../src/tableselection/croptable.js | 55 +++++++++++-------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 084c498ee11..1519ffd450b 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -157,7 +157,14 @@ export default class TableClipboard extends Plugin { model.change( writer => { // Crop pasted table if it extends selection area. if ( selectionHeight < pasteHeight || selectionWidth < pasteWidth ) { - pastedTable = cropTableToDimensions( pastedTable, 0, 0, selectionHeight - 1, selectionWidth - 1, tableUtils, writer ); + const cropDimensions = { + startRow: 0, + startColumn: 0, + endRow: selectionHeight - 1, + endColumn: selectionWidth - 1 + }; + + pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils ); } const pastedTableMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index d206d1b7acf..a0dc79c8db7 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -9,6 +9,7 @@ import { createEmptyTableCell, findAncestor, updateNumericAttribute } from '../commands/utils'; import TableWalker from '../tablewalker'; +import { getColumnIndexes, getRowIndexes } from '../utils'; /** * Returns a cropped table according to given dimensions. @@ -32,16 +33,19 @@ import TableWalker from '../tablewalker'; * 4 │ n │ o │ p │ │ q │ * └───┴───┴───┴───┴───┘ * - * @param {Number} sourceTable - * @param {Number} startRow - * @param {Number} startColumn - * @param {Number} endRow - * @param {Number} endColumn - * @param {module:table/tableutils~TableUtils} tableUtils + * @param {module:engine/model/element~Element} sourceTable + * @param {Object} cropDimensions + * @param {Number} cropDimensions.startRow + * @param {Number} cropDimensions.startColumn + * @param {Number} cropDimensions.endRow + * @param {Number} cropDimensions.endColumn * @param {module:engine/model/writer~Writer} writer + * @param {module:table/tableutils~TableUtils} tableUtils * @returns {module:engine/model/element~Element} */ -export function cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ) { +export function cropTableToDimensions( sourceTable, cropDimensions, writer, tableUtils ) { + const { startRow, startColumn, endRow, endColumn } = cropDimensions; + // Create empty table with empty rows equal to crop height. const croppedTable = writer.createElement( 'table' ); const cropHeight = endRow - startRow + 1; @@ -66,6 +70,7 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo // For empty slots: fill the gap with empty table cell. if ( isSpanned ) { + // TODO: Remove table utils usage. See: https://github.com/ckeditor/ckeditor5/issues/6785. const { row: anchorRow, column: anchorColumn } = tableUtils.getCellLocation( tableCell ); // But fill the gap only if the spanning cell is anchored outside cropped area. @@ -81,15 +86,15 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo writer.append( tableCellCopy, row ); // Crop end column/row is equal to crop width/height. - const cropEndRow = endColumn - startColumn + 1; - const cropEndColumn = cropHeight; + const cropEndColumn = endColumn - startColumn + 1; + const cropEndRow = cropHeight; // Column index in cropped table. const cropColumn = sourceColumn - startColumn; // Trim table if it exceeds cropped area. // In the table from method jsdoc those cells are: "g" & "m". - trimTableCellIfNeeded( tableCellCopy, cropRow, cropColumn, cropEndRow, cropEndColumn, tableUtils, writer ); + trimTableCellIfNeeded( tableCellCopy, cropRow, cropColumn, cropEndColumn, cropEndRow, writer ); } } @@ -104,8 +109,7 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo * * This function is to be used with the table selection. * - * tableSelection.startSelectingFrom( startCell ) - * tableSelection.setSelectingFrom( endCell ) + * tableSelection.setCellSelection( startCell, endCell ); * * const croppedTable = cropTable( tableSelection.getSelectedTableCells(), tableUtils, writer ); * @@ -118,30 +122,35 @@ export function cropTableToDimensions( sourceTable, startRow, startColumn, endRo */ export function cropTableToSelection( selectedTableCellsIterator, tableUtils, writer ) { const selectedTableCells = Array.from( selectedTableCellsIterator ); - const startElement = selectedTableCells[ 0 ]; - const endElement = selectedTableCells[ selectedTableCells.length - 1 ]; - const { row: startRow, column: startColumn } = tableUtils.getCellLocation( startElement ); - const { row: endRow, column: endColumn } = tableUtils.getCellLocation( endElement ); + const { first: startColumn, last: endColumn } = getColumnIndexes( selectedTableCells ); + const { first: startRow, last: endRow } = getRowIndexes( selectedTableCells ); + + const sourceTable = findAncestor( 'table', selectedTableCells[ 0 ] ); - const sourceTable = findAncestor( 'table', startElement ); + const cropDimensions = { + startRow, + startColumn, + endRow, + endColumn + }; - return cropTableToDimensions( sourceTable, startRow, startColumn, endRow, endColumn, tableUtils, writer ); + return cropTableToDimensions( sourceTable, cropDimensions, writer, tableUtils ); } // Adjusts table cell dimensions to not exceed last row and last column. -function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, lastRow, lastColumn, tableUtils, writer ) { +function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, lastColumn, lastRow, writer ) { const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - if ( cellColumn + colspan > lastRow ) { - const trimmedSpan = lastRow - cellColumn; + if ( cellColumn + colspan > lastColumn ) { + const trimmedSpan = lastColumn - cellColumn; updateNumericAttribute( 'colspan', trimmedSpan, tableCell, writer, 1 ); } - if ( cellRow + rowspan > lastColumn ) { - const trimmedSpan = lastColumn - cellRow; + if ( cellRow + rowspan > lastRow ) { + const trimmedSpan = lastRow - cellRow; updateNumericAttribute( 'rowspan', trimmedSpan, tableCell, writer, 1 ); } From 9b46958c83aa7b53c36628deae74a89ddd910262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:37:44 +0200 Subject: [PATCH 60/71] Fix jsdoc errors. --- packages/ckeditor5-table/src/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/src/utils.js b/packages/ckeditor5-table/src/utils.js index 6818f83f6fc..d6cdae0e439 100644 --- a/packages/ckeditor5-table/src/utils.js +++ b/packages/ckeditor5-table/src/utils.js @@ -203,8 +203,8 @@ export function getColumnIndexes( tableCells ) { * - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap) * * @param {Array.} selectedTableCells - * @param {module:table/tableUtils~TableUtils} tableUtils - * @returns {boolean} + * @param {module:table/tableutils~TableUtils} tableUtils + * @returns {Boolean} */ export function isSelectionRectangular( selectedTableCells, tableUtils ) { if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) { From 4c0bd107c6ee145937808ec892671fc36cb29222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:39:34 +0200 Subject: [PATCH 61/71] Minor refactor. --- packages/ckeditor5-table/src/tableclipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 1519ffd450b..6f889b43165 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -167,7 +167,7 @@ export default class TableClipboard extends Plugin { pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils ); } - const pastedTableMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); + const pastedTableLocationMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); // Content table to which we insert a pasted table. const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] ); @@ -214,7 +214,7 @@ export default class TableClipboard extends Plugin { } // Map current table slot location to an pasted table slot location. - const pastedCell = pastedTableMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ]; + const pastedCell = pastedTableLocationMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ]; // 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 ) { From 92083f87c7e35dfe9d6be57ba33f072b68832e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:41:39 +0200 Subject: [PATCH 62/71] Restore comment about location map. --- packages/ckeditor5-table/src/tableclipboard.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 6f889b43165..c301785a9ac 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -167,6 +167,7 @@ export default class TableClipboard extends Plugin { pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils ); } + // Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location. const pastedTableLocationMap = createLocationMap( pastedTable, selectionWidth, selectionHeight ); // Content table to which we insert a pasted table. From 3b03eb6c05be4b068ceee7b4991b068865e127f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 13:44:13 +0200 Subject: [PATCH 63/71] Change params order for cropTableToSelection. --- packages/ckeditor5-table/src/tableselection.js | 2 +- packages/ckeditor5-table/src/tableselection/croptable.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection.js b/packages/ckeditor5-table/src/tableselection.js index 6c9588927f4..84e32f6bba0 100644 --- a/packages/ckeditor5-table/src/tableselection.js +++ b/packages/ckeditor5-table/src/tableselection.js @@ -99,7 +99,7 @@ export default class TableSelection extends Plugin { return this.editor.model.change( writer => { const documentFragment = writer.createDocumentFragment(); - const table = cropTableToSelection( selectedCells, this.editor.plugins.get( 'TableUtils' ), writer ); + const table = cropTableToSelection( selectedCells, writer, this.editor.plugins.get( 'TableUtils' ) ); writer.insert( table, documentFragment, 0 ); diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index a0dc79c8db7..e013108e5b6 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -116,11 +116,11 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl * **Note**: This function is also used by {@link module:table/tableselection~TableSelection#getSelectionAsFragment}. * * @param {Iterable.} selectedTableCellsIterator - * @param {module:table/tableutils~TableUtils} tableUtils * @param {module:engine/model/writer~Writer} writer + * @param {module:table/tableutils~TableUtils} tableUtils * @returns {module:engine/model/element~Element} */ -export function cropTableToSelection( selectedTableCellsIterator, tableUtils, writer ) { +export function cropTableToSelection( selectedTableCellsIterator, writer, tableUtils ) { const selectedTableCells = Array.from( selectedTableCellsIterator ); const { first: startColumn, last: endColumn } = getColumnIndexes( selectedTableCells ); From 932cd633a2b9a7e684c3c17eb3689ff7d5dfe8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 13 May 2020 14:15:37 +0200 Subject: [PATCH 64/71] Adjust trimTableCellIfNeeded parameter names and logic to the function purpose. --- .../src/tableselection/croptable.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index e013108e5b6..915b4cd4f12 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -65,8 +65,8 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl } // Row index in cropped table. - const cropRow = sourceRow - startRow; - const row = croppedTable.getChild( cropRow ); + const rowInCroppedTable = sourceRow - startRow; + const row = croppedTable.getChild( rowInCroppedTable ); // For empty slots: fill the gap with empty table cell. if ( isSpanned ) { @@ -85,16 +85,9 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl writer.append( tableCellCopy, row ); - // Crop end column/row is equal to crop width/height. - const cropEndColumn = endColumn - startColumn + 1; - const cropEndRow = cropHeight; - - // Column index in cropped table. - const cropColumn = sourceColumn - startColumn; - // Trim table if it exceeds cropped area. // In the table from method jsdoc those cells are: "g" & "m". - trimTableCellIfNeeded( tableCellCopy, cropRow, cropColumn, cropEndColumn, cropEndRow, writer ); + trimTableCellIfNeeded( tableCellCopy, sourceRow, sourceColumn, endRow, endColumn, writer ); } } @@ -138,19 +131,26 @@ export function cropTableToSelection( selectedTableCellsIterator, writer, tableU return cropTableToDimensions( sourceTable, cropDimensions, writer, tableUtils ); } -// Adjusts table cell dimensions to not exceed last row and last column. -function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, lastColumn, lastRow, writer ) { +// Adjusts table cell dimensions to not exceed limit row and column. +// +// If table cell span to a column (or row) that is after a limit column (or row) trim colspan (or rowspan) +// so the table cell will fit in a cropped area. +function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, limitRow, limitColumn, writer ) { const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - if ( cellColumn + colspan > lastColumn ) { - const trimmedSpan = lastColumn - cellColumn; + const endColumn = cellColumn + colspan - 1; + + if ( endColumn > limitColumn ) { + const trimmedSpan = limitColumn - cellColumn + 1; updateNumericAttribute( 'colspan', trimmedSpan, tableCell, writer, 1 ); } - if ( cellRow + rowspan > lastRow ) { - const trimmedSpan = lastRow - cellRow; + const endRow = cellRow + rowspan - 1; + + if ( endRow > limitRow ) { + const trimmedSpan = limitRow - cellRow + 1; updateNumericAttribute( 'rowspan', trimmedSpan, tableCell, writer, 1 ); } From 27330d9c330fa955fa4df3206edbe39c23066185 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 13 May 2020 16:06:57 +0200 Subject: [PATCH 65/71] Fixed complex copy-table fragment test (ascii-art should match test, renumbering cells). --- .../tests/tableclipboard-copy-cut.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js index a7b5b53881f..a52fb406f12 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js +++ b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js @@ -193,7 +193,7 @@ describe( 'table clipboard', () => { it( 'should fix selected table to a selection rectangle (hardcore case)', () => { // This test check how previous simple rules run together (mixed prepending and trimming). - // In the example below a selection is set from cell "32" to "88" + // In the example below a selection is set from cell "21" to "77" // // Input table: Copied table: // @@ -201,29 +201,29 @@ describe( 'table clipboard', () => { // | 00 | 01 | 02 | 03 | 04 | 06 | 07 | 08 | // +----+----+ +----+ +----+----+----+ // | 10 | 11 | | 13 | | 16 | 17 | 18 | - // +----+----+ +----+ +----+----+----+ +----+----+----+---------+----+----+ - // | 20 | 21 | | 23 | | 26 | | 21 | | 23 | | | 26 | | + // +----+----+ +----+ +----+----+----+ +----+----+----+----+----+----+----+ + // | 20 | 21 | | 23 | | 26 | | 21 | | 23 | | | 26 | // +----+----+ +----+ +----+----+----+ +----+----+----+----+----+----+----+ // | 30 | 31 | | 33 | | 36 | 37 | | 31 | | 33 | | | 36 | 37 | // +----+----+----+----+ +----+----+----+ +----+----+----+----+----+----+----+ // | 40 | | 46 | 47 | 48 | | | | | | | 46 | 47 | // +----+----+----+----+ +----+----+----+ ==> +----+----+----+----+----+----+----+ // | 50 | 51 | 52 | 53 | | 56 | 57 | 58 | | 51 | 52 | 53 | | | 56 | 57 | - // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ - // | 60 | 61 | 64 | 65 | | 67 | 68 | | 61 | | | 64 | 65 | | 67 | - // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ + // +----+----+----+----+ + +----+----+ +----+----+----+----+----+ +----+ + // | 60 | 61 | | | 67 | 68 | | 61 | | | | 67 | + // +----+----+----+----+----+----+ +----+----+ +----+----+----+----+----+ +----+ // | 70 | 71 | 72 | 73 | 74 | 75 | | 77 | 78 | | 71 | 72 | 73 | 74 | 75 | | 77 | // +----+ +----+----+----+----+ +----+----+ +----+----+----+----+----+----+----+ // | 80 | | 82 | 83 | 84 | 85 | | 87 | 88 | // +----+----+----+----+----+----+----+----+----+ // setModelData( model, modelTable( [ - [ '00', '01', { contents: '02', rowspan: 4 }, '03', { contents: '04', colspan: 2, rowspan: 7 }, '07', '07', '08' ], - [ '10', '11', '13', '17', '17', '18' ], - [ '20', '21', '23', { contents: '27', colspan: 3 } ], - [ '30', '31', '33', '37', { contents: '37', colspan: 2 } ], - [ { contents: '40', colspan: 4 }, '47', '47', '48' ], - [ '50', '51', '52', '53', { contents: '57', rowspan: 4 }, '57', '58' ], + [ '00', '01', { contents: '02', rowspan: 4 }, '03', { contents: '04', colspan: 2, rowspan: 7 }, '06', '07', '08' ], + [ '10', '11', '13', '16', '17', '18' ], + [ '20', '21', '23', { contents: '26', colspan: 3 } ], + [ '30', '31', '33', '36', { contents: '37', colspan: 2 } ], + [ { contents: '40', colspan: 4 }, '46', '47', '48' ], + [ '50', '51', '52', '53', { contents: '56', rowspan: 4 }, '57', '58' ], [ '60', { contents: '61', colspan: 3 }, '67', '68' ], [ '70', { contents: '71', rowspan: 2 }, '72', '73', '74', '75', '77', '78' ], [ '80', '82', '83', '84', '85', '87', '88' ] @@ -235,10 +235,10 @@ describe( 'table clipboard', () => { ); assertClipboardContentOnMethod( 'copy', viewTable( [ - [ '21', ' ', '23', ' ', ' ', { contents: '27', colspan: 2 } ], - [ '31', ' ', '33', ' ', ' ', '37', '37' ], - [ ' ', ' ', ' ', ' ', ' ', '47', '47' ], - [ '51', '52', '53', ' ', ' ', { contents: '57', rowspan: 3 }, '57' ], + [ '21', ' ', '23', ' ', ' ', { contents: '26', colspan: 2 } ], + [ '31', ' ', '33', ' ', ' ', '36', '37' ], + [ ' ', ' ', ' ', ' ', ' ', '46', '47' ], + [ '51', '52', '53', ' ', ' ', { contents: '56', rowspan: 3 }, '57' ], [ { contents: '61', colspan: 3 }, ' ', ' ', '67' ], [ '71', '72', '73', '74', '75', '77' ] ] ) ); From ede28f4c1a70fd15221238cac0e7d011d13764a7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 13 May 2020 19:34:38 +0200 Subject: [PATCH 66/71] Added missing spaces in tests. --- .../tests/tableclipboard-paste.js | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 2d992233a59..e522dbcf3f2 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -386,7 +386,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -415,10 +415,10 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], + [ 1, 1, 0 ], + [ 1, 0 ], [ 1, 1, 1, 0 ], - [ 1, 1, 0 ] + [ 1, 1, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -445,7 +445,7 @@ describe( 'table clipboard', () => { assertSelectedCells( model, [ [ 0, 0, 0, 0 ], [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ @@ -473,9 +473,9 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0 ] + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -531,10 +531,10 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ @@ -573,7 +573,7 @@ describe( 'table clipboard', () => { [ 1, 1, 1, 0 ], [ 1, 1, 1, 0 ], [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] + [ 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -639,7 +639,7 @@ describe( 'table clipboard', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) // Cell 34. ); pasteTable( [ @@ -688,7 +688,7 @@ describe( 'table clipboard', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 1, 2 ] ), - modelRoot.getNodeByPath( [ 0, 3, 2 ] ) + modelRoot.getNodeByPath( [ 0, 3, 2 ] ) // Cell 33. ); pasteTable( [ @@ -745,8 +745,8 @@ describe( 'table clipboard', () => { ] ) ); tableSelection.setCellSelection( - modelRoot.getNodeByPath( [ 0, 1, 2 ] ), - modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + modelRoot.getNodeByPath( [ 0, 1, 2 ] ), // Cell 13. + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) // Cell 24. ); pasteTable( [ @@ -854,10 +854,10 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], + [ 1, 1, 0 ], + [ 1, 0 ], [ 1, 1, 1, 0 ], - [ 0, 0, 0 ] + [ 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -891,8 +891,8 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], + [ 1, 1 ], + [ 1, 1 ], [ 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ @@ -965,10 +965,10 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 1, 1, 1, 0 ], + [ 1, 1, 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ @@ -1020,7 +1020,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -1218,7 +1218,7 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 1, 1, 0 ], [ 0, 0, 0, 0 ] ] ); @@ -1247,10 +1247,10 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ - [ 1, 1, 0 ], - [ 1, 0 ], + [ 1, 1, 0 ], + [ 1, 0 ], [ 1, 1, 1, 0 ], - [ 1, 1, 0 ] + [ 1, 1, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -1278,7 +1278,7 @@ describe( 'table clipboard', () => { assertSelectedCells( model, [ [ 0, 0, 0, 0 ], [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 0 ], [ 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ @@ -1307,9 +1307,9 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 1, 1, 1, 1 ], - [ 1, 1 ], - [ 1, 1 ], - [ 0, 0, 0 ] + [ 1, 1 ], + [ 1, 1 ], + [ 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ } ); @@ -1366,9 +1366,9 @@ describe( 'table clipboard', () => { /* eslint-disable no-multi-spaces */ assertSelectedCells( model, [ [ 0, 0, 0, 0, 0, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 1, 0 ], - [ 0, 1, 0 ], + [ 0, 1, 1, 0, 0 ], + [ 0, 1, 1, 0, 0 ], + [ 0, 1, 0, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] ); /* eslint-enable no-multi-spaces */ From 507f0de19ab1a52b0c9726245c1ddfe009302903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 14 May 2020 13:45:32 +0200 Subject: [PATCH 67/71] Remove cropTableToSelection() util method. --- .../ckeditor5-table/src/tableselection.js | 22 ++++++++--- .../src/tableselection/croptable.js | 37 +------------------ 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/packages/ckeditor5-table/src/tableselection.js b/packages/ckeditor5-table/src/tableselection.js index 84e32f6bba0..8cd9e92ffd8 100644 --- a/packages/ckeditor5-table/src/tableselection.js +++ b/packages/ckeditor5-table/src/tableselection.js @@ -13,12 +13,9 @@ import first from '@ckeditor/ckeditor5-utils/src/first'; import TableWalker from './tablewalker'; import TableUtils from './tableutils'; import MouseEventsObserver from './tableselection/mouseeventsobserver'; -import { - getSelectedTableCells, - getTableCellsContainingSelection -} from './utils'; +import { getColumnIndexes, getRowIndexes, getSelectedTableCells, getTableCellsContainingSelection } from './utils'; import { findAncestor } from './commands/utils'; -import { cropTableToSelection } from './tableselection/croptable'; +import { cropTableToDimensions } from './tableselection/croptable'; import '../theme/tableselection.css'; @@ -99,7 +96,20 @@ export default class TableSelection extends Plugin { return this.editor.model.change( writer => { const documentFragment = writer.createDocumentFragment(); - const table = cropTableToSelection( selectedCells, writer, this.editor.plugins.get( 'TableUtils' ) ); + + const { first: startColumn, last: endColumn } = getColumnIndexes( selectedCells ); + const { first: startRow, last: endRow } = getRowIndexes( selectedCells ); + + const sourceTable = findAncestor( 'table', selectedCells[ 0 ] ); + + const cropDimensions = { + startRow, + startColumn, + endRow, + endColumn + }; + + const table = cropTableToDimensions( sourceTable, cropDimensions, writer, this.editor.plugins.get( 'TableUtils' ) ); writer.insert( table, documentFragment, 0 ); diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index 915b4cd4f12..de2a432b57a 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -7,9 +7,8 @@ * @module table/tableselection/croptable */ -import { createEmptyTableCell, findAncestor, updateNumericAttribute } from '../commands/utils'; +import { createEmptyTableCell, updateNumericAttribute } from '../commands/utils'; import TableWalker from '../tablewalker'; -import { getColumnIndexes, getRowIndexes } from '../utils'; /** * Returns a cropped table according to given dimensions. @@ -97,40 +96,6 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl return croppedTable; } -/** - * Returns a cropped table from the selected table cells. - * - * This function is to be used with the table selection. - * - * tableSelection.setCellSelection( startCell, endCell ); - * - * const croppedTable = cropTable( tableSelection.getSelectedTableCells(), tableUtils, writer ); - * - * **Note**: This function is also used by {@link module:table/tableselection~TableSelection#getSelectionAsFragment}. - * - * @param {Iterable.} selectedTableCellsIterator - * @param {module:engine/model/writer~Writer} writer - * @param {module:table/tableutils~TableUtils} tableUtils - * @returns {module:engine/model/element~Element} - */ -export function cropTableToSelection( selectedTableCellsIterator, writer, tableUtils ) { - const selectedTableCells = Array.from( selectedTableCellsIterator ); - - const { first: startColumn, last: endColumn } = getColumnIndexes( selectedTableCells ); - const { first: startRow, last: endRow } = getRowIndexes( selectedTableCells ); - - const sourceTable = findAncestor( 'table', selectedTableCells[ 0 ] ); - - const cropDimensions = { - startRow, - startColumn, - endRow, - endColumn - }; - - return cropTableToDimensions( sourceTable, cropDimensions, writer, tableUtils ); -} - // Adjusts table cell dimensions to not exceed limit row and column. // // If table cell span to a column (or row) that is after a limit column (or row) trim colspan (or rowspan) From cc68497242bf3fdbc0c4d900e55972bda5e05314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 14 May 2020 13:47:56 +0200 Subject: [PATCH 68/71] Fix cropTableToDimensions jsdoc. --- packages/ckeditor5-table/src/tableselection/croptable.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableselection/croptable.js b/packages/ckeditor5-table/src/tableselection/croptable.js index de2a432b57a..241fc1d40b1 100644 --- a/packages/ckeditor5-table/src/tableselection/croptable.js +++ b/packages/ckeditor5-table/src/tableselection/croptable.js @@ -15,7 +15,12 @@ import TableWalker from '../tablewalker'; * To return a cropped table that starts at first row and first column and end in third row and column: * - * const croppedTable = cropTable( table, 1, 1, 3, 3, tableUtils, writer ); + * const croppedTable = cropTableToDimensions( table, { + * startRow: 1, + * endRow: 1, + * startColumn: 3, + * endColumn: 3 + * }, tableUtils, writer ); * * Calling the code above for the table below: * From d002dc9aa77f489e4f627e35b76d5fd220b7fcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 14 May 2020 14:02:10 +0200 Subject: [PATCH 69/71] Handle paste only when alone table is pasted. --- .../ckeditor5-table/src/tableclipboard.js | 15 ++-- .../tests/tableclipboard-paste.js | 82 ++++++++++++++++++- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index c301785a9ac..398d7030ee2 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -109,7 +109,7 @@ export default class TableClipboard extends Plugin { } // We might need to crop table before inserting so reference might change. - let pastedTable = getTableFromContent( content ); + let pastedTable = getTableIfOnlyTableInContent( content ); if ( !pastedTable ) { return; @@ -244,18 +244,19 @@ export default class TableClipboard extends Plugin { } } -function getTableFromContent( content ) { +function getTableIfOnlyTableInContent( content ) { + // Table passed directly. if ( content.is( 'table' ) ) { return content; } - for ( const child of content.getChildren() ) { - if ( child.is( 'table' ) ) { - return child; - } + // 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( 'table' ) ) { + return null; } - return null; + return content.getChild( 0 ); } // Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location. diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index e522dbcf3f2..61afe3fe9d6 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -124,7 +124,85 @@ describe( 'table clipboard', () => { ] ) ); } ); - it( 'should alter model.insertContent if selectable is document selection', () => { + it( 'should not alter model.insertContent if mixed content is pasted (table + paragraph)', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const table = viewTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] ] ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', `${ table }

foo

` ); + viewDocument.fire( 'paste', data ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'foo', '', '02', '03' ], + [ '', '', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should not alter model.insertContent if mixed content is pasted (paragraph + table)', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const table = viewTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] ] ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', `

foo

${ table }` ); + viewDocument.fire( 'paste', data ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'foo', '', '02', '03' ], + [ '', '', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should not alter model.insertContent if mixed content is pasted (table + table)', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const table = viewTable( [ + [ 'aa', 'ab' ], + [ 'ba', 'bb' ] ] ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', `${ table }${ table }` ); + viewDocument.fire( 'paste', data ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ '', '', '02', '03' ], + [ '', '', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should alter model.insertContent if selectable is a document selection', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), modelRoot.getNodeByPath( [ 0, 1, 1 ] ) @@ -1409,7 +1487,7 @@ describe( 'table clipboard', () => { } ); describe( 'Clipboard integration - paste (content scenarios)', () => { - it( 'handles multiple paragraphs', async () => { + it( 'handles multiple paragraphs in table cell', async () => { await createEditor(); setModelData( model, modelTable( [ From 27a49036feca68405bff14feaf62d1d76cf4be49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 14 May 2020 15:19:33 +0200 Subject: [PATCH 70/71] Add test for content without table is pasted. --- .../tests/tableclipboard-paste.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/ckeditor5-table/tests/tableclipboard-paste.js b/packages/ckeditor5-table/tests/tableclipboard-paste.js index 61afe3fe9d6..8772ffa4976 100644 --- a/packages/ckeditor5-table/tests/tableclipboard-paste.js +++ b/packages/ckeditor5-table/tests/tableclipboard-paste.js @@ -124,6 +124,28 @@ describe( 'table clipboard', () => { ] ) ); } ); + it( 'should not alter model.insertContent if no table pasted', () => { + tableSelection.setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + data.dataTransfer.setData( 'text/html', '

foo

' ); + viewDocument.fire( 'paste', data ); + + assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [ + [ 'foo', '', '02', '03' ], + [ '', '', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + it( 'should not alter model.insertContent if mixed content is pasted (table + paragraph)', () => { tableSelection.setCellSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), From 3633a36510d8f4a721c7546a3ad377413d5dffa3 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 14 May 2020 18:49:42 +0200 Subject: [PATCH 71/71] Fixed getTableIfOnlyTableInContent if content has no children. --- packages/ckeditor5-table/src/tableclipboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-table/src/tableclipboard.js b/packages/ckeditor5-table/src/tableclipboard.js index 398d7030ee2..9ae9e7af8fd 100644 --- a/packages/ckeditor5-table/src/tableclipboard.js +++ b/packages/ckeditor5-table/src/tableclipboard.js @@ -252,7 +252,7 @@ function getTableIfOnlyTableInContent( 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( 'table' ) ) { + if ( content.childCount != 1 || !content.getChild( 0 ).is( 'table' ) ) { return null; }