diff --git a/src/tableclipboard.js b/src/tableclipboard.js index 59b43cfb..4fc7a34a 100644 --- a/src/tableclipboard.js +++ b/src/tableclipboard.js @@ -48,18 +48,18 @@ export default class TableClipboard extends Plugin { */ this._tableSelection = editor.plugins.get( 'TableSelection' ); - this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopy( evt, data ), { priority: 'normal' } ); - this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCut( evt, data ), { priority: 'high' } ); + this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) ); + this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) ); } /** - * A clipboard "copy" event handler. + * Copies table content to a clipboard on "copy" & "cut" events. * * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event. * @param {Object} data Clipboard event data. * @private */ - _onCopy( evt, data ) { + _onCopyCut( evt, data ) { const tableSelection = this._tableSelection; if ( !tableSelection.hasMultiCellSelection ) { @@ -72,7 +72,7 @@ export default class TableClipboard extends Plugin { const dataController = this.editor.data; const viewDocument = this.editor.editing.view.document; - const content = dataController.toView( tableSelection.getSelectionAsFragment() ); + const content = dataController.toView( this._tableSelection.getSelectionAsFragment() ); viewDocument.fire( 'clipboardOutput', { dataTransfer: data.dataTransfer, @@ -80,18 +80,4 @@ export default class TableClipboard extends Plugin { method: evt.name } ); } - - /** - * A clipboard "cut" event handler. - * - * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event. - * @param {Object} data Clipboard event data. - * @private - */ - _onCut( evt, data ) { - if ( this._tableSelection.hasMultiCellSelection ) { - data.preventDefault(); - evt.stop(); - } - } } diff --git a/src/tableselection.js b/src/tableselection.js index a1ce388e..73d9f60d 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -14,6 +14,7 @@ import TableUtils from './tableutils'; import { setupTableSelectionHighlighting } from './tableselection/converters'; import MouseSelectionHandler from './tableselection/mouseselectionhandler'; import { findAncestor } from './commands/utils'; +import { clearTableCellsContents } from './tableselection/utils'; import cropTable from './tableselection/croptable'; import '../theme/tableselection.css'; @@ -95,13 +96,38 @@ export default class TableSelection extends Plugin { */ init() { const editor = this.editor; - const selection = editor.model.document.selection; + const model = editor.model; + const selection = model.document.selection; this._tableUtils = editor.plugins.get( 'TableUtils' ); setupTableSelectionHighlighting( editor, this ); - selection.on( 'change:range', () => this._clearSelectionOnExternalChange( selection ) ); + this.listenTo( selection, 'change:range', () => this._clearSelectionOnExternalChange( selection ) ); + this.listenTo( model, 'deleteContent', ( evt, args ) => this._handleDeleteContent( evt, args ), { priority: 'high' } ); + } + + /** + * @inheritDoc + */ + afterInit() { + const editor = this.editor; + + const deleteCommand = editor.commands.get( 'delete' ); + + if ( deleteCommand ) { + this.listenTo( deleteCommand, 'execute', event => { + this._handleDeleteCommand( event, { isForward: false } ); + }, { priority: 'high' } ); + } + + const forwardDeleteCommand = editor.commands.get( 'forwardDelete' ); + + if ( forwardDeleteCommand ) { + this.listenTo( forwardDeleteCommand, 'execute', event => { + this._handleDeleteCommand( event, { isForward: true } ); + }, { priority: 'high' } ); + } } /** @@ -279,4 +305,50 @@ export default class TableSelection extends Plugin { this.clearSelection(); } } + + /** + * It overrides default `model.deleteContent()` behavior over a selected table fragment. + * + * @private + * @param {module:utils/eventinfo~EventInfo} event + * @param {Array.<*>} args Delete content method arguments. + */ + _handleDeleteContent( event, args ) { + const [ selection ] = args; + const model = this.editor.model; + + if ( this.hasMultiCellSelection && selection.is( 'documentSelection' ) ) { + event.stop(); + + clearTableCellsContents( model, this.getSelectedTableCells() ); + + model.change( writer => { + writer.setSelection( Array.from( this.getSelectedTableCells() ).pop(), 0 ); + } ); + } + } + + /** + * It overrides default `DeleteCommand` behavior over a selected table fragment. + * + * @private + * @param {module:utils/eventinfo~EventInfo} event + * @param {Object} options + * @param {Boolean} options.isForward Whether it handles forward or backward delete. + */ + _handleDeleteCommand( event, options ) { + const model = this.editor.model; + + if ( this.hasMultiCellSelection ) { + event.stop(); + + clearTableCellsContents( model, this.getSelectedTableCells() ); + + const tableCell = options.isForward ? this._startElement : this._endElement; + + model.change( writer => { + writer.setSelection( tableCell, 0 ); + } ); + } + } } diff --git a/src/tableselection/croptable.js b/src/tableselection/croptable.js index 7c9bd745..050304b7 100644 --- a/src/tableselection/croptable.js +++ b/src/tableselection/croptable.js @@ -17,7 +17,7 @@ import { findAncestor } from '../commands/utils'; * tableSelection.startSelectingFrom( startCell ) * tableSelection.setSelectingFrom( endCell ) * - * const croppedTable = cropTable( tableSelection.getSelectedTableCells ); + * const croppedTable = cropTable( tableSelection.getSelectedTableCells() ); * * **Note**: This function is used also by {@link module:table/tableselection~TableSelection#getSelectionAsFragment} * diff --git a/src/tableselection/utils.js b/src/tableselection/utils.js new file mode 100644 index 00000000..52d18c3d --- /dev/null +++ b/src/tableselection/utils.js @@ -0,0 +1,31 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module table/tableselection/utils + */ + +/** + * Clears contents of the passed table cells. + * + * This is to be used with table selection + * + * tableSelection.startSelectingFrom( startCell ) + * tableSelection.setSelectingFrom( endCell ) + * + * clearTableCellsContents( editor.model, tableSelection.getSelectedTableCells() ); + * + * **Note**: This function is used also by {@link module:table/tableselection~TableSelection#getSelectionAsFragment} + * + * @param {module:engine/model/model~Model} model + * @param {Iterable.} tableCells + */ +export function clearTableCellsContents( model, tableCells ) { + model.change( writer => { + for ( const tableCell of tableCells ) { + model.deleteContent( writer.createSelection( tableCell, 'in' ) ); + } + } ); +} diff --git a/tests/tableclipboard.js b/tests/tableclipboard.js index d280d3d0..f0089939 100644 --- a/tests/tableclipboard.js +++ b/tests/tableclipboard.js @@ -5,14 +5,13 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +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 ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; -import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import TableClipboard from '../src/tableclipboard'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; describe( 'table clipboard', () => { let editor, model, modelRoot, tableSelection, viewDocument; @@ -40,7 +39,7 @@ describe( 'table clipboard', () => { describe( 'Clipboard integration', () => { describe( 'copy', () => { - it( 'should to nothing for normal selection in table', () => { + it( 'should do nothing for normal selection in table', () => { const dataTransferMock = createDataTransfer(); const spy = sinon.spy(); @@ -54,35 +53,26 @@ describe( 'table clipboard', () => { sinon.assert.calledOnce( spy ); } ); - it( 'should copy selected table cells as standalone table', done => { - const dataTransferMock = createDataTransfer(); + it( 'should copy selected table cells as a standalone table', () => { const preventDefaultSpy = sinon.spy(); tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); - viewDocument.on( 'clipboardOutput', ( evt, data ) => { - expect( preventDefaultSpy.calledOnce ).to.be.true; - expect( data.method ).to.equal( 'copy' ); - - expect( data.dataTransfer ).to.equal( dataTransferMock ); - - expect( data.content ).is.instanceOf( ViewDocumentFragment ); - expect( stringifyView( data.content ) ).to.equal( viewTable( [ - [ '01', '02' ], - [ '11', '12' ] - ] ) ); - - done(); - } ); - - viewDocument.fire( 'copy', { - dataTransfer: dataTransferMock, + 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)', done => { + 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 } ], @@ -92,14 +82,14 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '00', '01' ], [ '10', '11' ], [ '20', '21' ] - ] ), done ); + ] ) ); } ); - it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', done => { + 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 } ], @@ -109,14 +99,14 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '00', '01' ], [ { contents: '10', colspan: 2 } ], [ '20', '21' ] - ] ), done ); + ] ) ); } ); - it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', done => { + 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' ], @@ -126,13 +116,13 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '00', '01', '02' ], [ '10', '11', '12' ] - ] ), done ); + ] ) ); } ); - it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', done => { + 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' ], @@ -142,13 +132,13 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '00', { contents: '01', rowspan: 2 }, '02' ], [ '10', '12' ] - ] ), done ); + ] ) ); } ); - it( 'should prepend spanned columns with empty cells (outside cell with colspan)', done => { + it( 'should prepend spanned columns with empty cells (outside cell with colspan)', () => { setModelData( model, modelTable( [ [ '00[]', '01', '02' ], [ { contents: '10', colspan: 2 }, '12' ], @@ -158,14 +148,14 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '01', '02' ], - [ '', '12' ], + [ ' ', '12' ], [ '21', '22' ] - ] ), done ); + ] ) ); } ); - it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', done => { + it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', () => { setModelData( model, modelTable( [ [ '00[]', { contents: '01', rowspan: 2 }, '02' ], [ '10', '12' ], @@ -175,13 +165,13 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); - assertClipboardCopy( viewTable( [ - [ '10', '', '12' ], + assertClipboardContentOnMethod( 'copy', viewTable( [ + [ '10', ' ', '12' ], [ '20', '21', '22' ] - ] ), done ); + ] ) ); } ); - it( 'should fix selected table to a selection rectangle (hardcore case)', done => { + 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" // @@ -222,17 +212,17 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 7, 6 ] ) ); - assertClipboardCopy( 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' ], + 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' ] - ] ), done ); + ] ) ); } ); - it( 'should update table heading attributes (selection with headings)', done => { + it( 'should update table heading attributes (selection with headings)', () => { setModelData( model, modelTable( [ [ '00', '01', '02', '03', '04' ], [ '10', '11', '12', '13', '14' ], @@ -244,14 +234,14 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 3, 3 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '11', '12', '13' ], [ '21', '22', '23' ], [ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable - ], { headingRows: 2, headingColumns: 1 } ), done ); + ], { headingRows: 2, headingColumns: 1 } ) ); } ); - it( 'should update table heading attributes (selection without headings)', done => { + it( 'should update table heading attributes (selection without headings)', () => { setModelData( model, modelTable( [ [ '00', '01', '02', '03', '04' ], [ '10', '11', '12', '13', '14' ], @@ -263,60 +253,98 @@ describe( 'table clipboard', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 3, 2 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 4, 4 ] ) ); - assertClipboardCopy( viewTable( [ + assertClipboardContentOnMethod( 'copy', viewTable( [ [ '32', '33', '34' ], [ '42', '43', '44' ] - ] ), done ); + ] ) ); } ); } ); describe( 'cut', () => { - it( 'is disabled for multi-range selection over a table', () => { + 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(); - const preventDefaultSpy = sinon.spy(); - const spy = sinon.spy(); - viewDocument.on( 'clipboardOutput', spy ); + viewDocument.fire( 'cut', { + dataTransfer: dataTransferMock, + preventDefault: sinon.spy() + } ); - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); + expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '00' ); + } ); + + it( 'should be preventable', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + viewDocument.on( 'clipboardOutput', evt => evt.stop(), { priority: 'high' } ); viewDocument.fire( 'cut', { - dataTransfer: dataTransferMock, - preventDefault: preventDefaultSpy + dataTransfer: createDataTransfer(), + preventDefault: sinon.spy() } ); - sinon.assert.notCalled( spy ); - sinon.assert.calledOnce( preventDefaultSpy ); + 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 not disabled normal selection over a table', () => { - const dataTransferMock = createDataTransfer(); + it( 'is clears selected table cells', () => { const spy = sinon.spy(); viewDocument.on( 'clipboardOutput', spy ); + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + viewDocument.fire( 'cut', { - dataTransfer: dataTransferMock, + dataTransfer: createDataTransfer(), preventDefault: sinon.spy() } ); - sinon.assert.calledOnce( spy ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '', '', '02' ], + [ '', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); } ); - } ); - } ); - function assertClipboardCopy( expectedViewTable, callback ) { - viewDocument.on( 'clipboardOutput', ( evt, data ) => { - expect( stringifyView( data.content ) ).to.equal( expectedViewTable ); + it( 'should copy selected table cells as a standalone table', () => { + const preventDefaultSpy = sinon.spy(); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) ); + + const data = { + dataTransfer: createDataTransfer(), + preventDefault: preventDefaultSpy + }; + viewDocument.fire( 'cut', data ); - callback(); + sinon.assert.calledOnce( preventDefaultSpy ); + expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [ + [ '01', '02' ], + [ '11', '12' ] + ] ) ); + } ); } ); + } ); - viewDocument.fire( 'copy', { + 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() { diff --git a/tests/tableselection-integration.js b/tests/tableselection-integration.js new file mode 100644 index 00000000..ee6d03c0 --- /dev/null +++ b/tests/tableselection-integration.js @@ -0,0 +1,173 @@ +/** + * @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 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 TableSelection from '../src/tableselection'; +import { modelTable } from './_utils/utils'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; +import Delete from '@ckeditor/ckeditor5-typing/src/delete'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import Input from '@ckeditor/ckeditor5-typing/src/input'; +import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; + +describe( 'table selection', () => { + let editor, model, tableSelection, modelRoot, element, viewDocument; + + describe( 'TableSelection - input integration', () => { + afterEach( async () => { + element.remove(); + await editor.destroy(); + } ); + + describe( 'on delete', () => { + beforeEach( async () => { + await setupEditor( [ Delete ] ); + } ); + + it( 'should clear contents of the selected table cells and put selection in last cell on backward delete', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + const domEventData = new DomEventData( viewDocument, { + preventDefault: sinon.spy() + }, { + direction: 'backward', + unit: 'character', + sequence: 1 + } ); + viewDocument.fire( 'delete', domEventData ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '', '', '13' ], + [ '', '[]', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + + it( 'should clear contents of the selected table cells and put selection in last cell on forward delete', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + const domEventData = new DomEventData( viewDocument, { + preventDefault: sinon.spy() + }, { + direction: 'forward', + unit: 'character', + sequence: 1 + } ); + viewDocument.fire( 'delete', domEventData ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]', '', '13' ], + [ '', '', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + + it( 'should not interfere with default key handler if no table selection', () => { + setModelData( model, modelTable( [ + [ '11[]', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + + const domEventData = new DomEventData( viewDocument, { + preventDefault: sinon.spy() + }, { + direction: 'backward', + unit: 'character', + sequence: 1 + } ); + viewDocument.fire( 'delete', domEventData ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '1[]', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + } ); + + describe( 'on user input', () => { + beforeEach( async () => { + await setupEditor( [ Input ] ); + } ); + + it( 'should clear contents of the selected table cells and put selection in last cell on user input', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } ); + + // figure table tbody tr td span + const viewSpan = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 1 ).getChild( 1 ).getChild( 0 ); + + viewDocument.fire( 'mutations', [ + { + type: 'children', + oldChildren: [], + newChildren: [ new ViewText( 'x' ) ], + node: viewSpan + } + ] ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '', '', '13' ], + [ '', 'x[]', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + + it( 'should not interfere with default key handler if no table selection', () => { + viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } ); + + // figure table tbody tr td span + const viewSpan = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ); + + viewDocument.fire( 'mutations', [ + { + type: 'children', + oldChildren: [], + newChildren: [ new ViewText( 'x' ) ], + node: viewSpan + } + ] ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ 'x[]11', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + } ); + } ); + + async function setupEditor( plugins ) { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ TableEditing, TableSelection, Paragraph, ...plugins ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + viewDocument = editor.editing.view.document; + tableSelection = editor.plugins.get( TableSelection ); + + setModelData( model, modelTable( [ + [ '[]11', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + } +} );