From cac6afa2ffba2fb52ff009c1f11a163569027281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 24 Jul 2018 16:32:31 +0200 Subject: [PATCH 01/44] Fix: Downcast converter for table attributes should work with not converted child elements. --- src/tableediting.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tableediting.js b/src/tableediting.js index 8cf073fd..dfa8de99 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -68,6 +68,9 @@ export default class TableEditing extends Plugin { isLimit: true } ); + // Allow all $block content inside table cell. + schema.extend( '$block', { allowIn: 'tableCell' } ); + // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); From 8818f4d111c64dbb356d98da12b61e0eb5e17898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 24 Jul 2018 16:40:12 +0200 Subject: [PATCH 02/44] Test: Add proper tests for table schema in table editing tests. --- tests/tableediting.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/tableediting.js b/tests/tableediting.js index 62ea1f2b..21da2696 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -40,6 +40,37 @@ describe( 'TableEditing', () => { } ); it( 'should set proper schema rules', () => { + // table: + expect( model.schema.isRegistered( 'table' ) ).to.be.true; + expect( model.schema.isObject( 'table' ) ).to.be.true; + expect( model.schema.isLimit( 'table' ) ).to.be.true; + + expect( model.schema.checkChild( [ '$root' ], 'table' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingRows' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingColumns' ) ).to.be.true; + + // table row: + expect( model.schema.isRegistered( 'tableRow' ) ).to.be.true; + expect( model.schema.isLimit( 'tableRow' ) ).to.be.true; + + expect( model.schema.checkChild( [ '$root' ], 'tableRow' ) ).to.be.false; + expect( model.schema.checkChild( [ 'table' ], 'tableRow' ) ).to.be.true; + + // table cell: + expect( model.schema.isRegistered( 'tableCell' ) ).to.be.true; + expect( model.schema.isLimit( 'tableCell' ) ).to.be.true; + + expect( model.schema.checkChild( [ '$root' ], 'tableCell' ) ).to.be.false; + expect( model.schema.checkChild( [ 'table' ], 'tableCell' ) ).to.be.false; + expect( model.schema.checkChild( [ 'tableRow' ], 'tableCell' ) ).to.be.true; + expect( model.schema.checkChild( [ 'tableCell' ], 'tableCell' ) ).to.be.false; + + expect( model.schema.checkAttribute( [ 'tableCell' ], 'colspan' ) ).to.be.true; + expect( model.schema.checkAttribute( [ 'tableCell' ], 'rowspan' ) ).to.be.true; + + // table cell contents: + expect( model.schema.checkChild( [ 'tableCell' ], '$text' ) ).to.be.true; + expect( model.schema.checkChild( [ 'tableCell' ], '$block' ) ).to.be.true; } ); it( 'adds insertTable command', () => { From bdf8f45ba58002b718b2abf99fdf7cfce6b3b540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 24 Jul 2018 16:50:13 +0200 Subject: [PATCH 03/44] Changed: Disallow table in table. --- src/tableediting.js | 7 +++++++ tests/tableediting.js | 13 +++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index dfa8de99..def4c9a5 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -71,6 +71,13 @@ export default class TableEditing extends Plugin { // Allow all $block content inside table cell. schema.extend( '$block', { allowIn: 'tableCell' } ); + // Disallow table in table. + schema.addChildCheck( ( context, childDefinition ) => { + if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) { + return false; + } + } ); + // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); diff --git a/tests/tableediting.js b/tests/tableediting.js index 21da2696..b4a75ccb 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -40,7 +40,7 @@ describe( 'TableEditing', () => { } ); it( 'should set proper schema rules', () => { - // table: + // Table: expect( model.schema.isRegistered( 'table' ) ).to.be.true; expect( model.schema.isObject( 'table' ) ).to.be.true; expect( model.schema.isLimit( 'table' ) ).to.be.true; @@ -49,14 +49,14 @@ describe( 'TableEditing', () => { expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingRows' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingColumns' ) ).to.be.true; - // table row: + // Table row: expect( model.schema.isRegistered( 'tableRow' ) ).to.be.true; expect( model.schema.isLimit( 'tableRow' ) ).to.be.true; expect( model.schema.checkChild( [ '$root' ], 'tableRow' ) ).to.be.false; expect( model.schema.checkChild( [ 'table' ], 'tableRow' ) ).to.be.true; - // table cell: + // Table cell: expect( model.schema.isRegistered( 'tableCell' ) ).to.be.true; expect( model.schema.isLimit( 'tableCell' ) ).to.be.true; @@ -68,9 +68,10 @@ describe( 'TableEditing', () => { expect( model.schema.checkAttribute( [ 'tableCell' ], 'colspan' ) ).to.be.true; expect( model.schema.checkAttribute( [ 'tableCell' ], 'rowspan' ) ).to.be.true; - // table cell contents: - expect( model.schema.checkChild( [ 'tableCell' ], '$text' ) ).to.be.true; - expect( model.schema.checkChild( [ 'tableCell' ], '$block' ) ).to.be.true; + // Table cell contents: + expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.true; + expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$block' ) ).to.be.true; + expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'table' ) ).to.be.false; } ); it( 'adds insertTable command', () => { From bc5da5ca1a41cccb999dd3b8e60228335c8d135c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 25 Jul 2018 11:56:13 +0200 Subject: [PATCH 04/44] Added: Wrap table cell in paragraph on enter. --- src/tableediting.js | 44 +++++++++++++++++++++ tests/tableediting.js | 89 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/tableediting.js b/src/tableediting.js index def4c9a5..ac06a08c 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -10,6 +10,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import upcastTable from './converters/upcasttable'; @@ -132,6 +133,8 @@ export default class TableEditing extends Plugin { // Handle tab key navigation. this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) ); this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabInsideTable( ...args ) ); + // Add listener to 'enter' key pressed inside table cell. + this.listenTo( editor.editing.view.document, 'enter', ( ...args ) => this._handleEnterInsideTable( ...args ) ); } /** @@ -249,4 +252,45 @@ export default class TableEditing extends Plugin { writer.setSelection( Range.createIn( cellToFocus ) ); } ); } + + /** + * Handles {@link module:engine/view/document~Document#event:enter keydown} events for the Enter key executed inside table + * cell. + * + * @private + * @param {module:utils/eventinfo~EventInfo} evt + * @param {module:engine/view/observer/domeventdata~DomEventData} data + */ + _handleEnterInsideTable( evt, data ) { + // Do not act on SHIFT-ENTER. + if ( data.isSoft ) { + return; + } + + const editor = this.editor; + + const position = editor.model.document.selection.getFirstPosition(); + const parent = position.parent; + + // Either the selection is not inside a table or it is in a table cell that has already a block content. + if ( parent.name != 'tableCell' || ( !parent.getChild( 0 ) || !parent.getChild( 0 ).is( 'text' ) ) ) { + return; + } + + data.preventDefault(); + // Stop the event to prevent default enter event listener. + evt.stop(); + + editor.model.change( writer => { + // Wrap $text from table cell in paragraph. + writer.wrap( Range.createIn( parent ), 'paragraph' ); + + // Enforce selection to be inside newly created paragraph as consequent 'enter' keys would create nested paragraphs otherwise. + const positionInParagraph = Position.createFromParentAndOffset( parent.getChild( 0 ), position.offset ); + writer.setSelection( positionInParagraph ); + + // Execute 'enter' command. + editor.execute( 'enter' ); + } ); + } } diff --git a/tests/tableediting.js b/tests/tableediting.js index b4a75ccb..f7cd5ac2 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -390,4 +390,93 @@ describe( 'TableEditing', () => { } ); } ); } ); + + describe( 'enter key', () => { + let evtDataStub, viewDocument; + + beforeEach( () => { + evtDataStub = { + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + isSoft: false + }; + + return VirtualTestEditor + .create( { + plugins: [ TableEditing, Paragraph ] + } ) + .then( newEditor => { + editor = newEditor; + + sinon.stub( editor, 'execute' ); + + viewDocument = editor.editing.view.document; + model = editor.model; + } ); + } ); + + it( 'should do nothing if not in table cell', () => { + setModelData( model, '[]foo' ); + + viewDocument.fire( 'enter', evtDataStub ); + + sinon.assert.notCalled( editor.execute ); + expect( formatTable( getModelData( model ) ) ).to.equal( '[]foo' ); + } ); + + it( 'should do nothing if table cell has already a block content', () => { + setModelData( model, modelTable( [ + [ '[]11' ] + ] ) ); + + viewDocument.fire( 'enter', evtDataStub ); + + sinon.assert.notCalled( editor.execute ); + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]11' ] + ] ) ); + } ); + + it( 'should do nothing if table cell with a block content is selected as a whole', () => { + setModelData( model, modelTable( [ + [ '[11]' ] + ] ) ); + + viewDocument.fire( 'enter', evtDataStub ); + + sinon.assert.notCalled( editor.execute ); + setModelData( model, modelTable( [ + [ '[11]' ] + ] ) ); + } ); + + it( 'should allow default behavior of Shift+Enter pressed', () => { + setModelData( model, modelTable( [ + [ '[]11' ] + ] ) ); + + evtDataStub.isSoft = true; + viewDocument.fire( 'enter', evtDataStub ); + + sinon.assert.notCalled( editor.execute ); + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]11' ] + ] ) ); + } ); + + it( 'should wrap table cell in paragraph and set selection', () => { + setModelData( model, modelTable( [ + [ '[]11' ] + ] ) ); + + viewDocument.fire( 'enter', evtDataStub ); + + sinon.assert.calledOnce( editor.execute ); + sinon.assert.calledWithExactly( editor.execute, 'enter' ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]11' ] + ] ) ); + } ); + } ); } ); From 978b2f6f7a50492df57bffb2e87bfde952d605f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 25 Jul 2018 13:13:49 +0200 Subject: [PATCH 05/44] Added: Tab/Shift+Tab should properly move selection when part of block content is selected inside table cell. --- src/commands/utils.js | 15 ++++++++++-- src/tableediting.js | 8 ++++--- tests/manual/table.html | 51 ++++++++++++++++++++++++++++++++++++++++- tests/manual/table.js | 7 ++++-- tests/tableediting.js | 24 +++++++++++++++++++ 5 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/commands/utils.js b/src/commands/utils.js index bf956b1a..f22f4553 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -8,16 +8,27 @@ */ /** - * Returns the parent table. + * Returns the parent table. Returns undefined if position is not inside table. * * @param {module:engine/model/position~Position} position * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} */ export function getParentTable( position ) { + return getParentElement( 'table', position ); +} + +/** + * Returns the parent element of given name. Returns undefined if position is not inside desired parent. + * + * @param {String} parentName Name of parent element to find. + * @param {module:engine/model/position~Position} position + * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} + */ +export function getParentElement( parentName, position ) { let parent = position.parent; while ( parent ) { - if ( parent.name === 'table' ) { + if ( parent.name === parentName ) { return parent; } diff --git a/src/tableediting.js b/src/tableediting.js index ac06a08c..b9529fe2 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -31,7 +31,7 @@ import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; import SetHeaderColumnCommand from './commands/setheadercolumncommand'; -import { getParentTable } from './commands/utils'; +import { getParentElement, getParentTable } from './commands/utils'; import TableUtils from './tableutils'; import '../theme/tableediting.css'; @@ -199,7 +199,9 @@ export default class TableEditing extends Plugin { const editor = this.editor; const selection = editor.model.document.selection; - const table = getParentTable( selection.getFirstPosition() ); + const firstPosition = selection.getFirstPosition(); + + const table = getParentTable( firstPosition ); if ( !table ) { return; @@ -208,7 +210,7 @@ export default class TableEditing extends Plugin { domEventData.preventDefault(); domEventData.stopPropagation(); - const tableCell = selection.focus.parent; + const tableCell = getParentElement( 'tableCell', firstPosition ); const tableRow = tableCell.parent; const currentRowIndex = table.getChildIndex( tableRow ); diff --git a/tests/manual/table.html b/tests/manual/table.html index 99710e05..0b2cf558 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -6,6 +6,53 @@
+ +

Table with block contents:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
whatexample
textFoo
paragraph

Foo

list +
    +
  • foo
  • +
  • bar
  • +
+
heading +

a h1

+

a h2

+

a h3

+

a h4

+
a h5
+
block quote +
a quote
+
+

Table with everything:

@@ -140,7 +187,9 @@ 5906.4 -225 5 - Declassified as a planet in 2006, but this remains controversial. + Declassified as a planet in 2006, but this remains controversial. + diff --git a/tests/manual/table.js b/tests/manual/table.js index 4daac2ce..c7448daf 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -9,12 +9,15 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor' import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import Table from '../../src/table'; import TableToolbar from '../../src/tabletoolbar'; +import Alignment from '../../../ckeditor5-alignment/src/alignment'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar ], + plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ], toolbar: [ - 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', + 'alignment', 'insertImage', + '|', 'undo', 'redo' ], table: { toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] diff --git a/tests/tableediting.js b/tests/tableediting.js index f7cd5ac2..60e09088 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -274,6 +274,18 @@ describe( 'TableEditing', () => { ] ) ); } ); + it( 'should move to the next table cell if part of block content is selected', () => { + setModelData( model, modelTable( [ + [ '11', '12[foo]bar', '13' ], + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '11', '12foobar', '[13]' ], + ] ) ); + } ); + describe( 'on table widget selected', () => { beforeEach( () => { editor.model.schema.register( 'block', { @@ -388,6 +400,18 @@ describe( 'TableEditing', () => { [ '21', '22' ] ] ) ); } ); + + it( 'should move to the previous table cell if part of block content is selected', () => { + setModelData( model, modelTable( [ + [ '11', '12[foo]bar', '13' ], + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[11]', '12foobar', '13' ], + ] ) ); + } ); } ); } ); From 8a8a0f2833def2064dc0561345a68539af5b57af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 25 Jul 2018 13:23:31 +0200 Subject: [PATCH 06/44] Fix: InsertColumnCommand should work with block content. --- src/commands/insertcolumncommand.js | 8 +++--- tests/commands/insertcolumncommand.js | 36 +++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/commands/insertcolumncommand.js b/src/commands/insertcolumncommand.js index d248dad0..28eb5dae 100644 --- a/src/commands/insertcolumncommand.js +++ b/src/commands/insertcolumncommand.js @@ -8,7 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentTable } from './utils'; +import { getParentElement, getParentTable } from './utils'; import TableUtils from '../tableutils'; /** @@ -72,8 +72,10 @@ export default class InsertColumnCommand extends Command { const selection = editor.model.document.selection; const tableUtils = editor.plugins.get( TableUtils ); - const table = getParentTable( selection.getFirstPosition() ); - const tableCell = selection.getFirstPosition().parent; + const firstPosition = selection.getFirstPosition(); + + const tableCell = getParentElement( 'tableCell', firstPosition ); + const table = tableCell.parent.parent; const { column } = tableUtils.getCellLocation( tableCell ); const insertAt = this.order === 'after' ? column + 1 : column; diff --git a/tests/commands/insertcolumncommand.js b/tests/commands/insertcolumncommand.js index d6129520..fe388034 100644 --- a/tests/commands/insertcolumncommand.js +++ b/tests/commands/insertcolumncommand.js @@ -50,7 +50,9 @@ describe( 'InsertColumnCommand', () => { isLimit: true } ); - model.schema.register( 'p', { inheritAllFrom: '$block' } ); + schema.extend( '$block', { allowIn: 'tableCell' } ); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); @@ -88,7 +90,7 @@ describe( 'InsertColumnCommand', () => { describe( 'isEnabled', () => { it( 'should be false if wrong node', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -99,7 +101,7 @@ describe( 'InsertColumnCommand', () => { } ); describe( 'execute()', () => { - it( 'should insert column in given table at given index', () => { + it( 'should insert column in given table after selection\'s column', () => { setData( model, modelTable( [ [ '11[]', '12' ], [ '21', '22' ] @@ -113,6 +115,18 @@ describe( 'InsertColumnCommand', () => { ] ) ); } ); + it( 'should insert column in given table after selection\'s column (selection in block content)', () => { + setData( model, modelTable( [ + [ '11', '12[]', '13' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '11', '12[]', '', '13' ] + ] ) ); + } ); + it( 'should insert columns at table end', () => { setData( model, modelTable( [ [ '11', '12' ], @@ -200,7 +214,7 @@ describe( 'InsertColumnCommand', () => { describe( 'isEnabled', () => { it( 'should be false if wrong node', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -211,7 +225,7 @@ describe( 'InsertColumnCommand', () => { } ); describe( 'execute()', () => { - it( 'should insert column in given table at given index', () => { + it( 'should insert column in given table before selection\'s column', () => { setData( model, modelTable( [ [ '11', '12[]' ], [ '21', '22' ] @@ -225,6 +239,18 @@ describe( 'InsertColumnCommand', () => { ] ) ); } ); + it( 'should insert column in given table before selection\'s column (selection in block content)', () => { + setData( model, modelTable( [ + [ '11', '12[]', '13' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '11', '', '12[]', '13' ] + ] ) ); + } ); + it( 'should insert columns at the table start', () => { setData( model, modelTable( [ [ '11', '12' ], From f68f902bab1329af8b39193a085bed50cdae1491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 25 Jul 2018 13:29:59 +0200 Subject: [PATCH 07/44] Fix: InsertRowCommand should work with block content. --- src/commands/insertrowcommand.js | 9 ++++--- tests/commands/insertrowcommand.js | 42 +++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/commands/insertrowcommand.js b/src/commands/insertrowcommand.js index a3292e21..0771d895 100644 --- a/src/commands/insertrowcommand.js +++ b/src/commands/insertrowcommand.js @@ -8,7 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentTable } from './utils'; +import { getParentElement, getParentTable } from './utils'; import TableUtils from '../tableutils'; /** @@ -71,10 +71,11 @@ export default class InsertRowCommand extends Command { const selection = editor.model.document.selection; const tableUtils = editor.plugins.get( TableUtils ); - const tableCell = selection.getFirstPosition().parent; - const table = getParentTable( selection.getFirstPosition() ); + const tableCell = getParentElement( 'tableCell', selection.getFirstPosition() ); + const tableRow = tableCell.parent; + const table = tableRow.parent; - const row = table.getChildIndex( tableCell.parent ); + const row = table.getChildIndex( tableRow ); const insertAt = this.order === 'below' ? row + 1 : row; tableUtils.insertRows( table, { rows: 1, at: insertAt } ); diff --git a/tests/commands/insertrowcommand.js b/tests/commands/insertrowcommand.js index c8e20b25..9f3dbe61 100644 --- a/tests/commands/insertrowcommand.js +++ b/tests/commands/insertrowcommand.js @@ -50,7 +50,9 @@ describe( 'InsertRowCommand', () => { isLimit: true } ); - model.schema.register( 'p', { inheritAllFrom: '$block' } ); + schema.extend( '$block', { allowIn: 'tableCell' } ); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); @@ -88,7 +90,7 @@ describe( 'InsertRowCommand', () => { describe( 'isEnabled', () => { it( 'should be false if wrong node', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -114,6 +116,23 @@ describe( 'InsertRowCommand', () => { ] ) ); } ); + it( 'should insert row after current position (selection in block content)', () => { + setData( model, modelTable( [ + [ '00' ], + [ '[]10' ], + [ '20' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '00' ], + [ '[]10' ], + [ '' ], + [ '20' ] + ] ) ); + } ); + it( 'should update table heading rows attribute when inserting row in headings section', () => { setData( model, modelTable( [ [ '00[]', '01' ], @@ -223,7 +242,7 @@ describe( 'InsertRowCommand', () => { describe( 'isEnabled', () => { it( 'should be false if wrong node', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -234,6 +253,23 @@ describe( 'InsertRowCommand', () => { } ); describe( 'execute()', () => { + it( 'should insert row before current position (selection in block content)', () => { + setData( model, modelTable( [ + [ '00' ], + [ '[]10' ], + [ '20' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '00' ], + [ '' ], + [ '[]10' ], + [ '20' ] + ] ) ); + } ); + it( 'should insert row at the beginning of a table', () => { setData( model, modelTable( [ [ '00[]', '01' ], From 86350e72b0df7317c093f94995faa967baae0342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 25 Jul 2018 13:45:42 +0200 Subject: [PATCH 08/44] Fix: InsertRowCommand should work with block content (#value only). --- src/commands/mergecellcommand.js | 14 +++---- tests/commands/mergecellcommand.js | 62 ++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/commands/mergecellcommand.js b/src/commands/mergecellcommand.js index d331c00d..f874b596 100644 --- a/src/commands/mergecellcommand.js +++ b/src/commands/mergecellcommand.js @@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import TableWalker from '../tablewalker'; -import { updateNumericAttribute } from './utils'; +import { getParentElement, updateNumericAttribute } from './utils'; import TableUtils from '../tableutils'; /** @@ -83,7 +83,7 @@ export default class MergeCellCommand extends Command { execute() { const model = this.editor.model; const doc = model.document; - const tableCell = doc.selection.getFirstPosition().parent; + const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); const cellToMerge = this.value; const direction = this.direction; @@ -125,9 +125,9 @@ export default class MergeCellCommand extends Command { _getMergeableCell() { const model = this.editor.model; const doc = model.document; - const element = doc.selection.getFirstPosition().parent; + const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); - if ( !element.is( 'tableCell' ) ) { + if ( !tableCell ) { return; } @@ -135,8 +135,8 @@ export default class MergeCellCommand extends Command { // First get the cell on proper direction. const cellToMerge = this.isHorizontal ? - getHorizontalCell( element, this.direction, tableUtils ) : - getVerticalCell( element, this.direction ); + getHorizontalCell( tableCell, this.direction, tableUtils ) : + getVerticalCell( tableCell, this.direction ); if ( !cellToMerge ) { return; @@ -144,7 +144,7 @@ export default class MergeCellCommand extends Command { // If found check if the span perpendicular to merge direction is equal on both cells. const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan'; - const span = parseInt( element.getAttribute( spanAttribute ) || 1 ); + const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 ); const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 ); diff --git a/tests/commands/mergecellcommand.js b/tests/commands/mergecellcommand.js index cdfe58b2..6662788d 100644 --- a/tests/commands/mergecellcommand.js +++ b/tests/commands/mergecellcommand.js @@ -51,7 +51,9 @@ describe( 'MergeCellCommand', () => { isLimit: true } ); - model.schema.register( 'p', { inheritAllFrom: '$block' } ); + schema.extend( '$block', { allowIn: 'tableCell' } ); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); @@ -139,7 +141,7 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be false if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -154,6 +156,14 @@ describe( 'MergeCellCommand', () => { expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) ); } ); + it( 'should be set to mergeable sibling if in cell that has sibling on the right (selection in block content)', () => { + setData( model, modelTable( [ + [ '00', '[]01', '02' ] + ] ) ); + + expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 2 ] ) ); + } ); + it( 'should be undefined if last cell of a row', () => { setData( model, modelTable( [ [ '00', '01[]' ] @@ -179,14 +189,14 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be undefined if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.value ).to.be.undefined; } ); } ); describe( 'execute()', () => { - it( 'should merge table cells ', () => { + it( 'should merge table cells', () => { setData( model, modelTable( [ [ '[]00', '01' ] ] ) ); @@ -257,7 +267,7 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be false if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -272,6 +282,14 @@ describe( 'MergeCellCommand', () => { expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) ); } ); + it( 'should be set to mergeable sibling if in cell that has sibling on the left (selection in block content)', () => { + setData( model, modelTable( [ + [ '00', '01[]', '02' ] + ] ) ); + + expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + it( 'should be undefined if first cell of a row', () => { setData( model, modelTable( [ [ '00[]', '01' ] @@ -297,14 +315,14 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be undefined if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.value ).to.be.undefined; } ); } ); describe( 'execute()', () => { - it( 'should merge table cells ', () => { + it( 'should merge table cells', () => { setData( model, modelTable( [ [ '00', '[]01' ] ] ) ); @@ -361,7 +379,7 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be false if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -386,6 +404,16 @@ describe( 'MergeCellCommand', () => { expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 1 ] ) ); } ); + it( 'should be set to mergeable cell (selection in block content)', () => { + setData( model, modelTable( [ + [ '00' ], + [ '10[]' ], + [ '20' ] + ] ) ); + + expect( command.value ).to.equal( root.getNodeByPath( [ 0, 2, 0 ] ) ); + } ); + it( 'should be undefined if in last row', () => { setData( model, modelTable( [ [ '00', '01' ], @@ -414,14 +442,14 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be undefined if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.value ).to.be.undefined; } ); } ); describe( 'execute()', () => { - it( 'should merge table cells ', () => { + it( 'should merge table cells', () => { setData( model, modelTable( [ [ '00', '01[]' ], [ '10', '11' ] @@ -514,7 +542,7 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be false if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -539,6 +567,16 @@ describe( 'MergeCellCommand', () => { expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) ); } ); + it( 'should be set to mergeable cell (selection in block content)', () => { + setData( model, modelTable( [ + [ '00' ], + [ '10[]' ], + [ '20' ] + ] ) ); + + expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + it( 'should be undefined if in first row', () => { setData( model, modelTable( [ [ '00[]', '01' ], @@ -578,7 +616,7 @@ describe( 'MergeCellCommand', () => { } ); it( 'should be undefined if not in a cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.value ).to.be.undefined; } ); From 4a6c8d6a88d2decc718135b248d4f2d015b712fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 26 Jul 2018 16:28:48 +0200 Subject: [PATCH 09/44] Changed: Downcast conversion will work with a block content always present in the table. --- src/commands/inserttablecommand.js | 2 +- src/commands/removecolumncommand.js | 8 +- src/commands/removerowcommand.js | 8 +- src/commands/setheadercolumncommand.js | 10 +- src/commands/setheaderrowcommand.js | 15 +- src/commands/splitcellcommand.js | 7 +- src/converters/downcast.js | 25 ++- src/tableutils.js | 6 +- tests/_utils/utils.js | 95 ++++++++- tests/commands/insertcolumncommand.js | 60 +----- tests/commands/insertrowcommand.js | 60 +----- tests/commands/inserttablecommand.js | 70 +------ tests/commands/mergecellcommand.js | 80 ++------ tests/commands/removecolumncommand.js | 68 +------ tests/commands/removerowcommand.js | 68 +------ tests/commands/setheadercolumncommand.js | 67 +------ tests/commands/setheaderrowcommand.js | 61 +----- tests/commands/splitcellcommand.js | 62 +----- tests/commands/utils.js | 43 +--- tests/converters/downcast.js | 243 ++++++----------------- tests/tableediting.js | 39 ++-- tests/tableutils.js | 61 +----- tests/tablewalker.js | 23 +-- 23 files changed, 306 insertions(+), 875 deletions(-) diff --git a/src/commands/inserttablecommand.js b/src/commands/inserttablecommand.js index fce12d35..54e74216 100644 --- a/src/commands/inserttablecommand.js +++ b/src/commands/inserttablecommand.js @@ -62,7 +62,7 @@ export default class InsertTableCommand extends Command { model.change( writer => { const table = tableUtils.createTable( insertPosition, rows, columns ); - writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ) ) ); + writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ); } ); } } diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index 22b7f0ab..918b9d5a 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; import TableUtils from '../tableutils'; -import { updateNumericAttribute } from './utils'; +import { getParentElement, updateNumericAttribute } from './utils'; /** * The remove column command. @@ -33,9 +33,9 @@ export default class RemoveColumnCommand extends Command { const selection = editor.model.document.selection; const tableUtils = editor.plugins.get( TableUtils ); - const selectedElement = selection.getFirstPosition().parent; + const tableCell = getParentElement( 'tableCell', selection.getFirstPosition() ); - this.isEnabled = selectedElement.is( 'tableCell' ) && tableUtils.getColumns( selectedElement.parent.parent ) > 1; + this.isEnabled = !!tableCell && tableUtils.getColumns( tableCell.parent.parent ) > 1; } /** @@ -47,7 +47,7 @@ export default class RemoveColumnCommand extends Command { const firstPosition = selection.getFirstPosition(); - const tableCell = firstPosition.parent; + const tableCell = getParentElement( 'tableCell', firstPosition ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index 5391f0b8..efcba598 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -12,7 +12,7 @@ import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import TableWalker from '../tablewalker'; -import { updateNumericAttribute } from './utils'; +import { getParentElement, updateNumericAttribute } from './utils'; /** * The remove row command. @@ -33,9 +33,9 @@ export default class RemoveRowCommand extends Command { const model = this.editor.model; const doc = model.document; - const element = doc.selection.getFirstPosition().parent; + const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); - this.isEnabled = element.is( 'tableCell' ) && element.parent.parent.childCount > 1; + this.isEnabled = !!tableCell && tableCell.parent.parent.childCount > 1; } /** @@ -46,7 +46,7 @@ export default class RemoveRowCommand extends Command { const selection = model.document.selection; const firstPosition = selection.getFirstPosition(); - const tableCell = firstPosition.parent; + const tableCell = getParentElement( 'tableCell', firstPosition ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/setheadercolumncommand.js b/src/commands/setheadercolumncommand.js index 53bf3352..f37f278b 100644 --- a/src/commands/setheadercolumncommand.js +++ b/src/commands/setheadercolumncommand.js @@ -9,7 +9,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentTable, updateNumericAttribute } from './utils'; +import { getParentElement, updateNumericAttribute } from './utils'; /** * The header column command. @@ -36,9 +36,9 @@ export default class SetHeaderColumnCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableParent = getParentTable( position ); + const tableCell = getParentElement( 'tableCell', position ); - const isInTable = !!tableParent; + const isInTable = !!tableCell; this.isEnabled = isInTable; @@ -50,7 +50,7 @@ export default class SetHeaderColumnCommand extends Command { * @readonly * @member {Boolean} #value */ - this.value = isInTable && this._isInHeading( position.parent, tableParent ); + this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent ); } /** @@ -69,7 +69,7 @@ export default class SetHeaderColumnCommand extends Command { const tableUtils = this.editor.plugins.get( 'TableUtils' ); const position = selection.getFirstPosition(); - const tableCell = position.parent; + const tableCell = getParentElement( 'tableCell', position.parent ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/setheaderrowcommand.js b/src/commands/setheaderrowcommand.js index 8a8b1869..7675c414 100644 --- a/src/commands/setheaderrowcommand.js +++ b/src/commands/setheaderrowcommand.js @@ -10,7 +10,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { getParentTable, updateNumericAttribute } from './utils'; +import { getParentElement, updateNumericAttribute } from './utils'; import TableWalker from '../tablewalker'; /** @@ -37,9 +37,8 @@ export default class SetHeaderRowCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableParent = getParentTable( position ); - - const isInTable = !!tableParent; + const tableCell = getParentElement( 'tableCell', position ); + const isInTable = !!tableCell; this.isEnabled = isInTable; @@ -51,7 +50,7 @@ export default class SetHeaderRowCommand extends Command { * @readonly * @member {Boolean} #value */ - this.value = isInTable && this._isInHeading( position.parent, tableParent ); + this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent ); } /** @@ -69,7 +68,7 @@ export default class SetHeaderRowCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableCell = position.parent; + const tableCell = getParentElement( 'tableCell', position ); const tableRow = tableCell.parent; const table = tableRow.parent; @@ -173,7 +172,9 @@ function splitHorizontally( tableCell, headingRows, writer ) { if ( columnIndex !== undefined && columnIndex === column && row === endRow ) { const tableRow = table.getChild( row ); - writer.insertElement( 'tableCell', attributes, Position.createFromParentAndOffset( tableRow, cellIndex ) ); + const newCell = writer.createElement( 'tableCell', attributes ); + writer.insert( newCell, Position.createFromParentAndOffset( tableRow, cellIndex ) ); + writer.insertElement( 'paragraph', Position.createAt( newCell, 0 ) ); } } diff --git a/src/commands/splitcellcommand.js b/src/commands/splitcellcommand.js index 50684285..fb929040 100644 --- a/src/commands/splitcellcommand.js +++ b/src/commands/splitcellcommand.js @@ -9,6 +9,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableUtils from '../tableutils'; +import { getParentElement } from './utils'; /** * The split cell command. @@ -49,9 +50,9 @@ export default class SplitCellCommand extends Command { const model = this.editor.model; const doc = model.document; - const element = doc.selection.getFirstPosition().parent; + const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); - this.isEnabled = element.is( 'tableCell' ); + this.isEnabled = !!tableCell; } /** @@ -63,7 +64,7 @@ export default class SplitCellCommand extends Command { const selection = document.selection; const firstPosition = selection.getFirstPosition(); - const tableCell = firstPosition.parent; + const tableCell = getParentElement( 'tableCell', firstPosition ); const isHorizontally = this.direction === 'horizontally'; diff --git a/src/converters/downcast.js b/src/converters/downcast.js index 372350b4..10538b20 100644 --- a/src/converters/downcast.js +++ b/src/converters/downcast.js @@ -368,8 +368,31 @@ function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPo const tableCell = tableWalkerValue.cell; - conversionApi.mapper.bindElements( tableCell, cellElement ); + const isSingleParagraph = tableCell.childCount === 1 && tableCell.getChild( 0 ).name === 'paragraph'; + conversionApi.writer.insert( insertPosition, cellElement ); + + if ( isSingleParagraph ) { + const innerParagraph = tableCell.getChild( 0 ); + const paragraphInsertPosition = ViewPosition.createAt( cellElement, 'end' ); + + conversionApi.consumable.consume( innerParagraph, 'insert' ); + + if ( options.asWidget ) { + const fakeParagraph = conversionApi.writer.createContainerElement( 'span' ); + + conversionApi.mapper.bindElements( innerParagraph, fakeParagraph ); + conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph ); + + conversionApi.mapper.bindElements( tableCell, cellElement ); + } else { + // TODO: binding two to one seems supspicious... + conversionApi.mapper.bindElements( tableCell, cellElement ); + conversionApi.mapper.bindElements( innerParagraph, cellElement ); + } + } else { + conversionApi.mapper.bindElements( tableCell, cellElement ); + } } // Creates or returns an existing `` element from the view. diff --git a/src/tableutils.js b/src/tableutils.js index 707df5ae..4101a41a 100644 --- a/src/tableutils.js +++ b/src/tableutils.js @@ -480,7 +480,7 @@ export default class TableUtils extends Plugin { if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) { const position = Position.createFromParentAndOffset( table.getChild( row ), cellIndex ); - writer.insertElement( 'tableCell', newCellsAttributes, position ); + createCells( 1, writer, position, newCellsAttributes ); } } } @@ -569,7 +569,9 @@ function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attr // @param {module:engine/model/position~Position} insertPosition function createCells( cells, writer, insertPosition, attributes = {} ) { for ( let i = 0; i < cells; i++ ) { - writer.insertElement( 'tableCell', attributes, insertPosition ); + const tableCell = writer.createElement( 'tableCell', attributes ); + writer.insert( tableCell, insertPosition ); + writer.insertElement( 'paragraph', Position.createAt( tableCell ) ); } } diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 858109ef..b98213ab 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -3,6 +3,16 @@ * For licensing, see LICENSE.md. */ +import { + downcastInsertCell, + downcastInsertRow, + downcastInsertTable, + downcastRemoveRow, + downcastTableHeadingColumnsChange, + downcastTableHeadingRowsChange +} from '../../src/converters/downcast'; +import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; + /** * Returns a model representation of a table shorthand notation: * @@ -31,7 +41,13 @@ * @returns {String} */ export function modelTable( tableData, attributes ) { - const tableRows = makeRows( tableData, 'tableCell', 'tableRow', 'tableCell' ); + const tableRows = makeRows( tableData, { + cellElement: 'tableCell', + rowElement: 'tableRow', + headingElement: 'tableCell', + wrappingElement: 'paragraph', + enforceWrapping: true + } ); return `${ tableRows }`; } @@ -68,14 +84,25 @@ export function modelTable( tableData, attributes ) { export function viewTable( tableData, attributes = {} ) { const headingRows = attributes.headingRows || 0; - const thead = headingRows > 0 ? `${ makeRows( tableData.slice( 0, headingRows ), 'th', 'tr' ) }` : ''; - const tbody = tableData.length > headingRows ? `${ makeRows( tableData.slice( headingRows ), 'td', 'tr' ) }` : ''; + const thead = headingRows > 0 ? `${ makeRows( tableData.slice( 0, headingRows ), { + cellElement: 'th', + rowElement: 'tr', + headingElement: 'th', + wrappingElement: 'p' + } ) }` : ''; + const tbody = tableData.length > headingRows ? + `${ makeRows( tableData.slice( headingRows ), { + cellElement: 'td', + rowElement: 'tr', + headingElement: 'th', + wrappingElement: 'p' + } ) }` : ''; return `
${ thead }${ tbody }
`; } /** - * Formats model or view table - useful for chai assertions debuging. + * Formats model or view table - useful for chai assertions debugging. * * @param {String} tableString * @returns {String} @@ -118,6 +145,51 @@ export function formattedViewTable( tableData, attributes ) { return formatTable( viewTable( tableData, attributes ) ); } +export function defaultSchema( schema ) { + schema.register( 'table', { + allowWhere: '$block', + allowAttributes: [ 'headingRows', 'headingColumns' ], + isObject: true + } ); + + schema.register( 'tableRow', { allowIn: 'table' } ); + + schema.register( 'tableCell', { + allowIn: 'tableRow', + allowContentOf: '$block', + allowAttributes: [ 'colspan', 'rowspan' ], + isLimit: true + } ); + + schema.extend( '$block', { allowIn: 'tableCell' } ); + schema.register( 'paragraph', { inheritAllFrom: '$block' } ); +} + +export function defaultConversion( conversion, asWidget = false ) { + conversion.elementToElement( { model: 'paragraph', view: 'p' } ); + + // Table conversion. + conversion.for( 'upcast' ).add( upcastTable() ); + conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget } ) ); + + // Table row conversion. + conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget } ) ); + conversion.for( 'downcast' ).add( downcastRemoveRow( { asWidget } ) ); + + // Table cell conversion. + conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) ); + conversion.for( 'upcast' ).add( upcastTableCell() ); + // conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); + // conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); + + // Table attributes conversion. + conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); + conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + + conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget } ) ); + conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget } ) ); +} + // Formats table cell attributes // // @param {Object} attributes Attributes of a cell. @@ -135,19 +207,22 @@ function formatAttributes( attributes ) { } // Formats passed table data to a set of table rows. -function makeRows( tableData, cellElement, rowElement, headingElement = 'th' ) { +function makeRows( tableData, options ) { + const { cellElement, rowElement, headingElement, wrappingElement, enforceWrapping } = options; + return tableData .reduce( ( previousRowsString, tableRow ) => { const tableRowString = tableRow.reduce( ( tableRowString, tableCellData ) => { - let tableCell = tableCellData; + let contents = tableCellData; const isObject = typeof tableCellData === 'object'; let resultingCellElement = cellElement; if ( isObject ) { - tableCell = tableCellData.contents; + contents = tableCellData.contents; + // TODO: check... if ( tableCellData.isHeading ) { resultingCellElement = headingElement; } @@ -156,8 +231,12 @@ function makeRows( tableData, cellElement, rowElement, headingElement = 'th' ) { delete tableCellData.isHeading; } + if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) { + contents = `<${ wrappingElement }>${ contents }`; + } + const formattedAttributes = formatAttributes( isObject ? tableCellData : '' ); - tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ tableCell }`; + tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }`; return tableRowString; }, '' ); diff --git a/tests/commands/insertcolumncommand.js b/tests/commands/insertcolumncommand.js index fe388034..740ff052 100644 --- a/tests/commands/insertcolumncommand.js +++ b/tests/commands/insertcolumncommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import InsertColumnCommand from '../../src/commands/insertcolumncommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'InsertColumnCommand', () => { @@ -32,50 +22,8 @@ describe( 'InsertColumnCommand', () => { editor = newEditor; model = editor.model; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - schema.extend( '$block', { allowIn: 'tableCell' } ); - - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); diff --git a/tests/commands/insertrowcommand.js b/tests/commands/insertrowcommand.js index 9f3dbe61..a43bee43 100644 --- a/tests/commands/insertrowcommand.js +++ b/tests/commands/insertrowcommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import InsertRowCommand from '../../src/commands/insertrowcommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'InsertRowCommand', () => { @@ -32,50 +22,8 @@ describe( 'InsertRowCommand', () => { editor = newEditor; model = editor.model; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - schema.extend( '$block', { allowIn: 'tableCell' } ); - - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); diff --git a/tests/commands/inserttablecommand.js b/tests/commands/inserttablecommand.js index 9d60ee2b..9d362dc5 100644 --- a/tests/commands/inserttablecommand.js +++ b/tests/commands/inserttablecommand.js @@ -4,22 +4,12 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import InsertTableCommand from '../../src/commands/inserttablecommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; import TableUtils from '../../src/tableutils'; -import { formatTable, formattedModelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable } from '../_utils/utils'; describe( 'InsertTableCommand', () => { let editor, model, command; @@ -34,48 +24,8 @@ describe( 'InsertTableCommand', () => { model = editor.model; command = new InsertTableCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -86,7 +36,7 @@ describe( 'InsertTableCommand', () => { describe( 'isEnabled', () => { describe( 'when selection is collapsed', () => { it( 'should be true if in paragraph', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.true; } ); @@ -99,7 +49,7 @@ describe( 'InsertTableCommand', () => { describe( 'execute()', () => { it( 'should create a single batch', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); const spy = sinon.spy(); @@ -123,12 +73,12 @@ describe( 'InsertTableCommand', () => { } ); it( 'should insert table with two rows and two columns after non-empty paragraph', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); command.execute(); expect( formatTable( getData( model ) ) ).to.equal( - '

foo

' + + 'foo' + formattedModelTable( [ [ '[]', '' ], [ '', '' ] @@ -137,12 +87,12 @@ describe( 'InsertTableCommand', () => { } ); it( 'should insert table with given rows and columns after non-empty paragraph', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); command.execute( { rows: 3, columns: 4 } ); expect( formatTable( getData( model ) ) ).to.equal( - '

foo

' + + 'foo' + formattedModelTable( [ [ '[]', '', '', '' ], [ '', '', '', '' ], diff --git a/tests/commands/mergecellcommand.js b/tests/commands/mergecellcommand.js index 6662788d..65244b2a 100644 --- a/tests/commands/mergecellcommand.js +++ b/tests/commands/mergecellcommand.js @@ -5,19 +5,9 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import MergeCellCommand from '../../src/commands/mergecellcommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'MergeCellCommand', () => { @@ -33,50 +23,8 @@ describe( 'MergeCellCommand', () => { model = editor.model; root = model.document.getRoot( 'main' ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - schema.extend( '$block', { allowIn: 'tableCell' } ); - - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -204,7 +152,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[0001]' } ] + [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); } ); @@ -330,7 +278,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[0001]' } ] + [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); } ); @@ -458,7 +406,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], + [ '00', { rowspan: 2, contents: '[0111]' } ], [ '10' ] ] ) ); } ); @@ -473,7 +421,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], + [ '00', '[0111]', { rowspan: 2, contents: '02' } ], [ '20', '21' ] ] ) ); } ); @@ -492,7 +440,7 @@ describe( 'MergeCellCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 2, contents: '00' }, '01', '02' ], [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], + [ '20', '[2131]', { rowspan: 2, contents: '22' } ], [ '40', '41' ] ] ) ); } ); @@ -632,7 +580,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], + [ '00', { rowspan: 2, contents: '[0111]' } ], [ '10' ] ] ) ); } ); @@ -649,7 +597,11 @@ describe( 'MergeCellCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 3, contents: '00' }, '11', '12', '13' ], - [ { rowspan: 2, contents: '21' }, '22', { rowspan: 3, contents: '[2333]' } ], + [ + { rowspan: 2, contents: '21' }, + '22', + { rowspan: 3, contents: '[2333]' } + ], [ '32' ], [ { colspan: 2, contents: '40' }, '42' ] ] ) ); @@ -665,7 +617,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], + [ '00', '[0111]', { rowspan: 2, contents: '02' } ], [ '20', '21' ] ] ) ); } ); @@ -684,7 +636,7 @@ describe( 'MergeCellCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 2, contents: '00' }, '01', '02' ], [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], + [ '20', '[2131]', { rowspan: 2, contents: '22' } ], [ '40', '41' ] ] ) ); } ); diff --git a/tests/commands/removecolumncommand.js b/tests/commands/removecolumncommand.js index c9064d17..f1ba21b4 100644 --- a/tests/commands/removecolumncommand.js +++ b/tests/commands/removecolumncommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import RemoveColumnCommand from '../../src/commands/removecolumncommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'RemoveColumnCommand', () => { @@ -33,48 +23,8 @@ describe( 'RemoveColumnCommand', () => { model = editor.model; command = new RemoveColumnCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -103,7 +53,7 @@ describe( 'RemoveColumnCommand', () => { } ); it( 'should be false if selection is outside a table', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -121,7 +71,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ '00', '02' ], - [ '10[]', '12' ], + [ '10[]', '12' ], [ '20', '22' ] ] ) ); } ); @@ -136,7 +86,7 @@ describe( 'RemoveColumnCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '[]01' ], + [ '[]01' ], [ '11' ], [ '21' ] ] ) ); @@ -153,7 +103,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ '01' ], - [ '[]11' ], + [ '[]11' ], [ '21' ] ], { headingColumns: 1 } ) ); } ); @@ -172,7 +122,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { colspan: 3, contents: '00' }, '03' ], [ { colspan: 2, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20[]' }, '23' ], + [ { colspan: 2, contents: '20[]' }, '23' ], [ '30', '31', '33' ], [ '40', '41', '43' ] diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js index 937f2cac..0ea9557a 100644 --- a/tests/commands/removerowcommand.js +++ b/tests/commands/removerowcommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import RemoveRowCommand from '../../src/commands/removerowcommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; describe( 'RemoveRowCommand', () => { let editor, model, command; @@ -29,48 +19,8 @@ describe( 'RemoveRowCommand', () => { model = editor.model; command = new RemoveRowCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -97,7 +47,7 @@ describe( 'RemoveRowCommand', () => { } ); it( 'should be false if selection is outside a table', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -114,7 +64,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], + [ '00', '01[]' ], [ '20', '21' ] ] ) ); } ); @@ -129,7 +79,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '[]10', '11' ], + [ '[]10', '11' ], [ '20', '21' ] ] ) ); } ); @@ -144,7 +94,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], + [ '00', '01[]' ], [ '20', '21' ] ], { headingRows: 1 } ) ); } ); @@ -161,7 +111,7 @@ describe( 'RemoveRowCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], - [ '13', '14[]' ], + [ '13', '14[]' ], [ '30', '31', '32', '33', '34' ] ] ) ); } ); diff --git a/tests/commands/setheadercolumncommand.js b/tests/commands/setheadercolumncommand.js index dbbe16b9..a1b38ea5 100644 --- a/tests/commands/setheadercolumncommand.js +++ b/tests/commands/setheadercolumncommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'HeaderColumnCommand', () => { @@ -33,55 +23,8 @@ describe( 'HeaderColumnCommand', () => { model = editor.model; command = new SetHeaderColumnCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isBlock: true, - isObject: true - } ); - - schema.register( 'tableRow', { - allowIn: 'table', - allowAttributes: [], - isBlock: true, - isLimit: true - } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isBlock: true, - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -91,7 +34,7 @@ describe( 'HeaderColumnCommand', () => { describe( 'isEnabled', () => { it( 'should be false if selection is not in a table', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); diff --git a/tests/commands/setheaderrowcommand.js b/tests/commands/setheaderrowcommand.js index 0c3e9a58..db1526a5 100644 --- a/tests/commands/setheaderrowcommand.js +++ b/tests/commands/setheaderrowcommand.js @@ -4,20 +4,9 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand'; - -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'HeaderRowCommand', () => { @@ -33,48 +22,8 @@ describe( 'HeaderRowCommand', () => { model = editor.model; command = new SetHeaderRowCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -84,7 +33,7 @@ describe( 'HeaderRowCommand', () => { describe( 'isEnabled', () => { it( 'should be false if selection is not in a table', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( command.isEnabled ).to.be.false; } ); diff --git a/tests/commands/splitcellcommand.js b/tests/commands/splitcellcommand.js index 0159853a..317ce0d6 100644 --- a/tests/commands/splitcellcommand.js +++ b/tests/commands/splitcellcommand.js @@ -4,20 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import SplitCellCommand from '../../src/commands/splitcellcommand'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; describe( 'SplitCellCommand', () => { @@ -33,48 +23,8 @@ describe( 'SplitCellCommand', () => { model = editor.model; command = new SplitCellCommand( editor ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -97,7 +47,7 @@ describe( 'SplitCellCommand', () => { } ); it( 'should be false if not in cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); @@ -201,7 +151,7 @@ describe( 'SplitCellCommand', () => { } ); it( 'should be false if not in cell', () => { - setData( model, '

11[]

' ); + setData( model, '11[]' ); expect( command.isEnabled ).to.be.false; } ); diff --git a/tests/commands/utils.js b/tests/commands/utils.js index 4fa16536..0eb193a0 100644 --- a/tests/commands/utils.js +++ b/tests/commands/utils.js @@ -5,11 +5,9 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; -import { downcastInsertTable } from '../../src/converters/downcast'; -import upcastTable from '../../src/converters/upcasttable'; -import { modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils'; + import { getParentTable } from '../../src/commands/utils'; describe( 'commands utils', () => { @@ -21,39 +19,8 @@ describe( 'commands utils', () => { editor = newEditor; model = editor.model; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Table row upcast only since downcast conversion is done in `downcastTable()`. - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); - - // Table cell conversion. - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -63,7 +30,7 @@ describe( 'commands utils', () => { describe( 'getParentTable()', () => { it( 'should return undefined if not in table', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); expect( getParentTable( model.document.selection.focus ) ).to.be.undefined; } ); diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index 48a4e0e4..9df47be7 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -7,15 +7,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../../src/converters/downcast'; -import { formatTable, formattedViewTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils'; import env from '@ckeditor/ckeditor5-utils/src/env'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; @@ -36,36 +28,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable() ); - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - // Insert conversion - conversion.for( 'downcast' ).add( downcastInsertRow() ); - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -142,6 +106,25 @@ describe( 'downcast converters', () => { ) ); } ); + it( 'should create table with block content', () => { + setModelData( model, modelTable( [ + [ '00foo', '01' ] + ] ) ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '

00

foo

01
' + + '
' + ) ); + } ); + it( 'should be possible to overwrite', () => { editor.conversion.elementToElement( { model: 'tableRow', view: 'tr', converterPriority: 'high' } ); editor.conversion.elementToElement( { model: 'tableCell', view: 'td', converterPriority: 'high' } ); @@ -161,7 +144,7 @@ describe( 'downcast converters', () => { expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( '' + - '' + + '' + '

' ) ); } ); @@ -240,7 +223,7 @@ describe( 'downcast converters', () => { } ); } ); - describe( 'asWidget', () => { + describe( 'options.asWidget=true', () => { beforeEach( () => { return VirtualTestEditor.create() .then( newEditor => { @@ -250,30 +233,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); } ); } ); @@ -285,7 +246,9 @@ describe( 'downcast converters', () => { '
' + '' + '' + - '' + + '' + + '' + + '' + '' + '
' + '
' @@ -483,7 +446,7 @@ describe( 'downcast converters', () => { ] ) ); } ); - describe( 'asWidget', () => { + describe( 'options.asWidget=true', () => { beforeEach( () => { return VirtualTestEditor.create() .then( newEditor => { @@ -493,30 +456,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); } ); } ); @@ -532,13 +473,21 @@ describe( 'downcast converters', () => { writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + // TODO check span always? + expect( formatTable( + getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + '' + '
00
' + + '00' + + '
' + '
' @@ -646,7 +595,7 @@ describe( 'downcast converters', () => { ] ) ); } ); - describe( 'asWidget', () => { + describe( 'options.asWidget=true', () => { beforeEach( () => { return VirtualTestEditor.create() .then( newEditor => { @@ -656,30 +605,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); } ); } ); @@ -700,7 +627,9 @@ describe( 'downcast converters', () => { '' + '' + '' + - '' + + '' + '' + '' + '' + @@ -845,7 +774,7 @@ describe( 'downcast converters', () => { ] ) ); } ); - describe( 'asWidget', () => { + describe( 'options.asWidget=true', () => { beforeEach( () => { return VirtualTestEditor.create() .then( newEditor => { @@ -855,33 +784,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); } ); } ); @@ -899,7 +803,11 @@ describe( 'downcast converters', () => { '
' + '
00' + + '00' + + '
' + '' + - '' + + '' + + '' + + '' + '' + '
00
' + + '00' + + '
' + '' @@ -1053,7 +961,7 @@ describe( 'downcast converters', () => { ], { headingRows: 2 } ) ); } ); - describe( 'asWidget', () => { + describe( 'options.asWidget=true', () => { beforeEach( () => { return VirtualTestEditor.create() .then( newEditor => { @@ -1063,33 +971,8 @@ describe( 'downcast converters', () => { root = doc.getRoot( 'main' ); viewDocument = editor.editing.view; - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) ); - - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); } ); } ); @@ -1107,7 +990,11 @@ describe( 'downcast converters', () => { '
' + '' + '' + - '' + + '' + + '' + + '' + '' + '
00
' + + '00' + + '
' + '' diff --git a/tests/tableediting.js b/tests/tableediting.js index 60e09088..1d2c4b7f 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -243,7 +243,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ] + [ '11', '[12]' ] ] ) ); } ); @@ -256,7 +256,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[]', '' ] + [ '[]', '' ] ] ) ); } ); @@ -270,7 +270,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[21]', '22' ] + [ '[21]', '22' ] ] ) ); } ); @@ -282,7 +282,11 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '12foobar', '[13]' ], + [ + '11', + '12foobar', + '[13]' + ], ] ) ); } ); @@ -312,7 +316,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); // Should cancel event - so no other tab handler is called. @@ -371,7 +375,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); } ); @@ -396,7 +400,7 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ], + [ '11', '[12]' ], [ '21', '22' ] ] ) ); } ); @@ -409,7 +413,11 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12foobar', '13' ], + [ + '[11]', + '12foobar', + '13' + ], ] ) ); } ); } ); @@ -487,20 +495,5 @@ describe( 'TableEditing', () => { [ '[]11' ] ] ) ); } ); - - it( 'should wrap table cell in paragraph and set selection', () => { - setModelData( model, modelTable( [ - [ '[]11' ] - ] ) ); - - viewDocument.fire( 'enter', evtDataStub ); - - sinon.assert.calledOnce( editor.execute ); - sinon.assert.calledWithExactly( editor.execute, 'enter' ); - - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[]11' ] - ] ) ); - } ); } ); } ); diff --git a/tests/tableutils.js b/tests/tableutils.js index 22e5fc1b..7af80133 100644 --- a/tests/tableutils.js +++ b/tests/tableutils.js @@ -4,19 +4,10 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; - -import { - downcastInsertCell, - downcastInsertRow, - downcastInsertTable, - downcastRemoveRow, - downcastTableHeadingColumnsChange, - downcastTableHeadingRowsChange -} from '../src/converters/downcast'; -import upcastTable from '../src/converters/upcasttable'; -import { formatTable, formattedModelTable, modelTable } from './_utils/utils'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from './_utils/utils'; + import TableUtils from '../src/tableutils'; describe( 'TableUtils', () => { @@ -31,48 +22,8 @@ describe( 'TableUtils', () => { root = model.document.getRoot( 'main' ); tableUtils = editor.plugins.get( TableUtils ); - const conversion = editor.conversion; - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - conversion.for( 'downcast' ).add( downcastInsertTable() ); - - // Insert row conversion. - conversion.for( 'downcast' ).add( downcastInsertRow() ); - - // Remove row conversion. - conversion.for( 'downcast' ).add( downcastRemoveRow() ); - - // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell() ); - - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); - - conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() ); - conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); diff --git a/tests/tablewalker.js b/tests/tablewalker.js index 2d5f8222..c0c4d23a 100644 --- a/tests/tablewalker.js +++ b/tests/tablewalker.js @@ -5,7 +5,8 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { modelTable } from './_utils/utils'; + +import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; import TableWalker from '../src/tablewalker'; @@ -20,22 +21,8 @@ describe( 'TableWalker', () => { doc = model.document; root = doc.getRoot( 'main' ); - const schema = model.schema; - - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isObject: true - } ); - - schema.register( 'tableRow', { allowIn: 'table' } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowContentOf: '$block', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); } ); } ); @@ -53,7 +40,7 @@ describe( 'TableWalker', () => { const formattedResult = result.map( ( { row, column, cell, cellIndex } ) => ( { row, column, - data: cell && cell.getChild( 0 ).data, + data: cell && cell.getChild( 0 ).getChild( 0 ).data, index: cellIndex } ) ); From 5320cd4ece51f7ed4cec10be9f4c9cb2f56eae7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 26 Jul 2018 17:13:21 +0200 Subject: [PATCH 10/44] Changed: Properly upcast table cell contents. --- src/converters/upcasttable.js | 55 ++++++++- src/tableediting.js | 51 +-------- tests/_utils/utils.js | 5 +- tests/converters/upcasttable.js | 118 +++++++++++++------ tests/integration.js | 15 ++- tests/manual/table.html | 196 -------------------------------- tests/tableediting.js | 27 +++-- tests/tabletoolbar.js | 12 +- 8 files changed, 175 insertions(+), 304 deletions(-) diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index 847fc31f..9324d116 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -53,9 +53,12 @@ export default function upcastTable() { } else { // Create one row and one table cell for empty table. const row = conversionApi.writer.createElement( 'tableRow' ); - conversionApi.writer.insert( row, ModelPosition.createAt( table, 'end' ) ); - conversionApi.writer.insertElement( 'tableCell', ModelPosition.createAt( row, 'end' ) ); + + const tableCell = conversionApi.writer.createElement( 'tableCell' ); + conversionApi.writer.insert( tableCell, ModelPosition.createAt( row, 'end' ) ); + + conversionApi.writer.insertElement( 'paragraph', ModelPosition.createAt( tableCell, 'end' ) ); } // Set conversion result range. @@ -85,6 +88,54 @@ export default function upcastTable() { }; } +export function upcastTableCell( elementName ) { + return dispatcher => { + dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => { + const viewTableCell = data.viewItem; + + // When element was already consumed then skip it. + if ( !conversionApi.consumable.test( viewTableCell, { name: true } ) ) { + return; + } + + const tableCell = conversionApi.writer.createElement( 'tableCell' ); + + // Insert element on allowed position. + const splitResult = conversionApi.splitToAllowedParent( tableCell, data.modelCursor ); + conversionApi.writer.insert( tableCell, splitResult.position ); + conversionApi.consumable.consume( viewTableCell, { name: true } ); + + for ( const child of viewTableCell.getChildren() ) { + conversionApi.convertItem( child, ModelPosition.createAt( tableCell, 'end' ) ); + } + + // Set conversion result range. + data.modelRange = new ModelRange( + // Range should start before inserted element + ModelPosition.createBefore( tableCell ), + // Should end after but we need to take into consideration that children could split our + // element, so we need to move range after parent of the last converted child. + // before: [] + // after: [] + ModelPosition.createAfter( tableCell ) + ); + + // Now we need to check where the modelCursor should be. + // If we had to split parent to insert our element then we want to continue conversion inside split parent. + // + // before: [] + // after: [] + if ( splitResult.cursorParent ) { + data.modelCursor = ModelPosition.createAt( splitResult.cursorParent ); + + // Otherwise just continue after inserted element. + } else { + data.modelCursor = data.modelRange.end; + } + }, { priority: 'normal' } ); + }; +} + // Scans table rows and extracts required metadata from the table: // // headingRows - the number of rows that goes as table header. diff --git a/src/tableediting.js b/src/tableediting.js index b9529fe2..8c3dc410 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -10,10 +10,9 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; -import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; -import upcastTable from './converters/upcasttable'; +import upcastTable, { upcastTableCell } from './converters/upcasttable'; import { downcastInsertCell, downcastInsertRow, @@ -64,7 +63,6 @@ export default class TableEditing extends Plugin { schema.register( 'tableCell', { allowIn: 'tableRow', - allowContentOf: '$block', allowAttributes: [ 'colspan', 'rowspan' ], isLimit: true } ); @@ -93,8 +91,8 @@ export default class TableEditing extends Plugin { conversion.for( 'downcast' ).add( downcastRemoveRow() ); // Table cell conversion. - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); conversion.for( 'editingDowncast' ).add( downcastInsertCell( { asWidget: true } ) ); conversion.for( 'dataDowncast' ).add( downcastInsertCell() ); @@ -133,8 +131,6 @@ export default class TableEditing extends Plugin { // Handle tab key navigation. this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) ); this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabInsideTable( ...args ) ); - // Add listener to 'enter' key pressed inside table cell. - this.listenTo( editor.editing.view.document, 'enter', ( ...args ) => this._handleEnterInsideTable( ...args ) ); } /** @@ -254,45 +250,4 @@ export default class TableEditing extends Plugin { writer.setSelection( Range.createIn( cellToFocus ) ); } ); } - - /** - * Handles {@link module:engine/view/document~Document#event:enter keydown} events for the Enter key executed inside table - * cell. - * - * @private - * @param {module:utils/eventinfo~EventInfo} evt - * @param {module:engine/view/observer/domeventdata~DomEventData} data - */ - _handleEnterInsideTable( evt, data ) { - // Do not act on SHIFT-ENTER. - if ( data.isSoft ) { - return; - } - - const editor = this.editor; - - const position = editor.model.document.selection.getFirstPosition(); - const parent = position.parent; - - // Either the selection is not inside a table or it is in a table cell that has already a block content. - if ( parent.name != 'tableCell' || ( !parent.getChild( 0 ) || !parent.getChild( 0 ).is( 'text' ) ) ) { - return; - } - - data.preventDefault(); - // Stop the event to prevent default enter event listener. - evt.stop(); - - editor.model.change( writer => { - // Wrap $text from table cell in paragraph. - writer.wrap( Range.createIn( parent ), 'paragraph' ); - - // Enforce selection to be inside newly created paragraph as consequent 'enter' keys would create nested paragraphs otherwise. - const positionInParagraph = Position.createFromParentAndOffset( parent.getChild( 0 ), position.offset ); - writer.setSelection( positionInParagraph ); - - // Execute 'enter' command. - editor.execute( 'enter' ); - } ); - } } diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index b98213ab..f6dd2715 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -178,9 +178,8 @@ export function defaultConversion( conversion, asWidget = false ) { // Table cell conversion. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) ); - conversion.for( 'upcast' ).add( upcastTableCell() ); - // conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - // conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); // Table attributes conversion. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); diff --git a/tests/converters/upcasttable.js b/tests/converters/upcasttable.js index 3d2f784d..df6fd059 100644 --- a/tests/converters/upcasttable.js +++ b/tests/converters/upcasttable.js @@ -7,13 +7,18 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import upcastTable from '../../src/converters/upcasttable'; +import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; +import { formatTable } from '../_utils/utils'; +import Paragraph from '../../../ckeditor5-paragraph/src/paragraph'; describe( 'upcastTable()', () => { let editor, model; beforeEach( () => { - return VirtualTestEditor.create() + return VirtualTestEditor + .create( { + plugins: [ Paragraph ] + } ) .then( newEditor => { editor = newEditor; model = editor.model; @@ -24,26 +29,31 @@ describe( 'upcastTable()', () => { schema.register( 'table', { allowWhere: '$block', allowAttributes: [ 'headingRows', 'headingColumns' ], + isLimit: true, isObject: true } ); - schema.register( 'tableRow', { allowIn: 'table' } ); + schema.register( 'tableRow', { + allowIn: 'table', + isLimit: true + } ); schema.register( 'tableCell', { allowIn: 'tableRow', - allowContentOf: '$block', allowAttributes: [ 'colspan', 'rowspan' ], isLimit: true } ); + schema.extend( '$block', { allowIn: 'tableCell' } ); + conversion.for( 'upcast' ).add( upcastTable() ); // Table row upcast only since downcast conversion is done in `downcastTable()`. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); // Table cell conversion. - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) ); - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); + conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); @@ -55,7 +65,7 @@ describe( 'upcastTable()', () => { } ); function expectModel( data ) { - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( data ); + expect( formatTable( getModelData( model, { withoutSelection: true } ) ) ).to.equal( formatTable( data ) ); } it( 'should convert table figure', () => { @@ -69,7 +79,7 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + + '1' + '
' ); } ); @@ -83,7 +93,7 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + + '1' + '
' ); } ); @@ -91,7 +101,7 @@ describe( 'upcastTable()', () => { it( 'should not convert empty figure', () => { '
'; - expectModel( '' ); + expectModel( '' ); } ); it( 'should convert if figure do not have class="table" attribute', () => { @@ -105,7 +115,7 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + + '1' + '
' ); } ); @@ -119,12 +129,12 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + + '1' + '
' ); } ); - it( 'should create table model from table with one thead with more then on row', () => { + it( 'should create table model from table with one thead with more then one row', () => { editor.setData( '' + '' + @@ -137,9 +147,9 @@ describe( 'upcastTable()', () => { expectModel( '
' + - '1' + - '2' + - '3' + + '1' + + '2' + + '3' + '
' ); } ); @@ -155,9 +165,9 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + - '2' + - '3' + + '1' + + '2' + + '3' + '
' ); } ); @@ -172,8 +182,8 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + - '2' + + '1' + + '2' + '
' ); } ); @@ -187,7 +197,7 @@ describe( 'upcastTable()', () => { expectModel( '' + - '1' + + '1' + '
' ); } ); @@ -199,7 +209,7 @@ describe( 'upcastTable()', () => { ); expectModel( - '
' + '
' ); } ); @@ -212,17 +222,17 @@ describe( 'upcastTable()', () => { ); expectModel( - 'bar
' + 'bar
' ); } ); it( 'should create table model from some broken table', () => { editor.setData( - '

foo

' + '
foo
' ); expectModel( - 'foo
' + 'foo
' ); } ); @@ -245,8 +255,8 @@ describe( 'upcastTable()', () => { expectModel( '
foo
' + '' + - '1' + - '2' + + '1' + + '2' + '
' + '
bar
' ); @@ -258,8 +268,19 @@ describe( 'upcastTable()', () => { allowAttributes: [ 'headingRows' ], isObject: true } ); + editor.model.schema.register( 'fooCell', { + allowIn: 'fooRow', + isObject: true + } ); + editor.model.schema.register( 'fooRow', { + allowIn: 'fooTable', + isObject: true + } ); editor.conversion.elementToElement( { model: 'fooTable', view: 'table', converterPriority: 'high' } ); + editor.conversion.elementToElement( { model: 'fooRow', view: 'tr', converterPriority: 'high' } ); + editor.conversion.elementToElement( { model: 'fooCell', view: 'td', converterPriority: 'high' } ); + editor.conversion.elementToElement( { model: 'fooCell', view: 'th', converterPriority: 'high' } ); editor.setData( '' + @@ -268,7 +289,7 @@ describe( 'upcastTable()', () => { ); expectModel( - '' + '' ); } ); @@ -296,19 +317,34 @@ describe( 'upcastTable()', () => { expectModel( '
' + '' + - '11121314' + + '11' + + '12' + + '13' + + '14' + '' + '' + - '21222324' + + '21' + + '22' + + '23' + + '24' + '' + '' + - '31323334' + + '31' + + '32' + + '33' + + '34' + '' + '' + - '41424344' + + '41' + + '42' + + '43' + + '44' + '' + '' + - '51525354' + + '51' + + '52' + + '53' + + '54' + '' + '
' ); @@ -332,13 +368,21 @@ describe( 'upcastTable()', () => { expectModel( '' + '' + - '11121314' + + '11' + + '12' + + '13' + + '14' + '' + '' + - '21222324' + + '21' + + '22' + + '23' + + '24' + '' + '' + - '313334' + + '31' + + '33' + + '34' + '' + '
' ); diff --git a/tests/integration.js b/tests/integration.js index c298eb5d..75b65efe 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -55,7 +55,10 @@ describe( 'TableToolbar integration', () => { } ); it( 'should allow the BalloonToolbar to be displayed when a table content is selected', () => { - setModelData( newEditor.model, 'foox[y]z
' ); + setModelData( + newEditor.model, + 'foox[y]z
' + ); balloonToolbar.show(); @@ -63,7 +66,10 @@ describe( 'TableToolbar integration', () => { } ); it( 'should prevent the BalloonToolbar from being displayed when a table is selected as whole', () => { - setModelData( newEditor.model, 'foo[foo
]' ); + setModelData( + newEditor.model, + 'foo[foo
]' + ); balloonToolbar.show(); @@ -76,7 +82,10 @@ describe( 'TableToolbar integration', () => { const normalPrioritySpy = sinon.spy(); // Select an table - setModelData( newEditor.model, 'foo[x
]' ); + setModelData( + newEditor.model, + 'foo[x
]' + ); newEditor.listenTo( balloonToolbar, 'show', highestPrioritySpy, { priority: 'highest' } ); newEditor.listenTo( balloonToolbar, 'show', highPrioritySpy, { priority: 'high' } ); diff --git a/tests/manual/table.html b/tests/manual/table.html index 0b2cf558..a9c167f6 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -52,200 +52,4 @@
a h5
- -

Table with everything:

- -
-
Data about the planets of our solar system (Planetary facts taken from Nasa's Planetary Fact Sheet - Metric. -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 NameMass (1024kg)Diameter (km)Density (kg/m3)Gravity (m/s2)Length of day (hours)Distance from Sun (106km)Mean temperature (°C)Number of moonsNotes
Terrestrial planetsMercury0.3304,87954273.74222.657.91670Closest to the Sun
Venus4.8712,10452438.92802.0108.24640 
Earth5.9712,75655149.824.0149.6151Our world
Mars0.6426,79239333.724.7227.9-652The red planet
Jovian planetsGas giantsJupiter1898142,984132623.19.9778.6-11067The largest planet
Saturn568120,5366879.010.71433.5-14062 
Ice giantsUranus86.851,11812718.717.22872.5-19527 
Neptune10249,528163811.016.14495.1-20014 
Dwarf planetsPluto0.01462,37020950.7153.35906.4-2255Declassified as a planet in 2006, but this remains controversial. -
-
- -

Table with 2 tbody:

- - - - - - - - - - - - - - - - -
abc
abc
- -

Table with no tbody:

- - - - - - - - - - - - -
abc
abc
- -

Table with thead section between two tbody sections

- - - - - - - - - - - - - - - - - -
2
1
3
diff --git a/tests/tableediting.js b/tests/tableediting.js index 1d2c4b7f..157dea2b 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -69,7 +69,7 @@ describe( 'TableEditing', () => { expect( model.schema.checkAttribute( [ 'tableCell' ], 'rowspan' ) ).to.be.true; // Table cell contents: - expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.true; + expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.false; expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$block' ) ).to.be.true; expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'table' ) ).to.be.false; } ); @@ -137,7 +137,7 @@ describe( 'TableEditing', () => { describe( 'conversion in data pipeline', () => { describe( 'model to view', () => { it( 'should create tbody section', () => { - setModelData( model, 'foo[]
' ); + setModelData( model, 'foo[]
' ); expect( editor.getData() ).to.equal( '
' + @@ -151,7 +151,10 @@ describe( 'TableEditing', () => { } ); it( 'should create thead section', () => { - setModelData( model, 'foo[]
' ); + setModelData( + model, + 'foo[]
' + ); expect( editor.getData() ).to.equal( '
' + @@ -170,7 +173,7 @@ describe( 'TableEditing', () => { editor.setData( '
foo
' ); expect( getModelData( model, { withoutSelection: true } ) ) - .to.equal( 'foo
' ); + .to.equal( 'foo
' ); } ); } ); } ); @@ -243,7 +246,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ] + [ '11', '[12]' ] ] ) ); } ); @@ -256,7 +259,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[]', '' ] + [ '[]', '' ] ] ) ); } ); @@ -270,7 +273,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[21]', '22' ] + [ '[21]', '22' ] ] ) ); } ); @@ -285,7 +288,7 @@ describe( 'TableEditing', () => { [ '11', '12foobar', - '[13]' + '[13]' ], ] ) ); } ); @@ -316,7 +319,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); // Should cancel event - so no other tab handler is called. @@ -375,7 +378,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); } ); @@ -400,7 +403,7 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ], + [ '11', '[12]' ], [ '21', '22' ] ] ) ); } ); @@ -414,7 +417,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ - '[11]', + '[11]', '12foobar', '13' ], diff --git a/tests/tabletoolbar.js b/tests/tabletoolbar.js index 01423980..382a0894 100644 --- a/tests/tabletoolbar.js +++ b/tests/tabletoolbar.js @@ -123,7 +123,10 @@ describe( 'TableToolbar', () => { } ); it( 'should show the toolbar on ui#update when the table content is selected', () => { - setData( model, '[foo]bar
' ); + setData( + model, + '[foo]bar
' + ); expect( balloon.visibleView ).to.be.null; @@ -147,7 +150,7 @@ describe( 'TableToolbar', () => { } ); it( 'should not engage when the toolbar is in the balloon yet invisible', () => { - setData( model, 'x[y]z
' ); + setData( model, 'x[y]z
' ); expect( balloon.visibleView ).to.equal( toolbar ); @@ -170,7 +173,10 @@ describe( 'TableToolbar', () => { } ); it( 'should hide the toolbar on render if the table is de–selected', () => { - setData( model, 'foo[]
' ); + setData( + model, + 'foo[]
' + ); expect( balloon.visibleView ).to.equal( toolbar ); From 50249ff6378a42114df867ba6752c4df2dac69a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 12:44:49 +0200 Subject: [PATCH 11/44] Added: A view post-fixer for spans in editing view. --- src/tableediting.js | 51 +++++++++++++++++++++++++++++ tests/_utils/utils.js | 31 ++++++++++++------ tests/converters/downcast.js | 62 ++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index 8c3dc410..42467066 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -107,6 +107,8 @@ export default class TableEditing extends Plugin { conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) ); conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() ); + injectParagraphInTableCellPostFixer( editor.model, editor.editing ); + // Define all the commands. editor.commands.add( 'insertTable', new InsertTableCommand( editor ) ); editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) ); @@ -251,3 +253,52 @@ export default class TableEditing extends Plugin { } ); } } + +function injectParagraphInTableCellPostFixer( model, editing ) { + editing.view.document.registerPostFixer( writer => paragraphInTableCellPostFixer( writer, model, editing.mapper ) ); +} + +function paragraphInTableCellPostFixer( writer, model, mapper ) { + const changes = model.document.differ.getChanges(); + + for ( const entry of changes ) { + const tableCell = entry.position.parent; + + if ( tableCell.is( 'tableCell' ) ) { + if ( tableCell.childCount > 1 ) { + for ( const child of tableCell.getChildren() ) { + if ( child.name != 'paragraph' ) { + continue; + } + + const viewElement = mapper.toViewElement( child ); + + if ( viewElement && viewElement.name === 'span' ) { + // Unbind table cell as will be renamed to

. + // mapper.unbindModelElement( tableCell ); + + const renamedViewElement = writer.rename( viewElement, 'p' ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( child, renamedViewElement ); + } + } + } else { + const singleChild = tableCell.getChild( 0 ); + if ( !singleChild || !singleChild.is( 'paragraph' ) ) { + return; + } + + const viewElement = mapper.toViewElement( singleChild ); + + // Unbind table cell as will be renamed to

. + // mapper.unbindModelElement( tableCell ); + + const renamedViewElement = writer.rename( viewElement, 'span' ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( singleChild, renamedViewElement ); + } + } + } +} diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index f6dd2715..f438032e 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -83,22 +83,29 @@ export function modelTable( tableData, attributes ) { */ export function viewTable( tableData, attributes = {} ) { const headingRows = attributes.headingRows || 0; + const asWidget = !!attributes.asWidget; const thead = headingRows > 0 ? `${ makeRows( tableData.slice( 0, headingRows ), { cellElement: 'th', rowElement: 'tr', headingElement: 'th', - wrappingElement: 'p' + wrappingElement: 'p', + asWidget } ) }` : ''; + const tbody = tableData.length > headingRows ? `${ makeRows( tableData.slice( headingRows ), { cellElement: 'td', rowElement: 'tr', headingElement: 'th', - wrappingElement: 'p' + wrappingElement: 'p', + asWidget } ) }` : ''; - return `

${ thead }${ tbody }
`; + const figureAttributes = asWidget ? 'class="ck-widget ck-widget_selectable table" contenteditable="false"' : 'class="table"'; + const widgetHandler = '
'; + + return `
${ asWidget ? widgetHandler : '' }${ thead }${ tbody }
`; } /** @@ -202,25 +209,24 @@ function formatAttributes( attributes ) { attributesString = ' ' + entries.map( entry => `${ entry[ 0 ] }="${ entry[ 1 ] }"` ).join( ' ' ); } } + return attributesString; } // Formats passed table data to a set of table rows. function makeRows( tableData, options ) { - const { cellElement, rowElement, headingElement, wrappingElement, enforceWrapping } = options; + const { cellElement, rowElement, headingElement, wrappingElement, enforceWrapping, asWidget } = options; return tableData .reduce( ( previousRowsString, tableRow ) => { const tableRowString = tableRow.reduce( ( tableRowString, tableCellData ) => { - let contents = tableCellData; - const isObject = typeof tableCellData === 'object'; + let contents = isObject ? tableCellData.contents : tableCellData; + let resultingCellElement = cellElement; if ( isObject ) { - contents = tableCellData.contents; - // TODO: check... if ( tableCellData.isHeading ) { resultingCellElement = headingElement; @@ -230,11 +236,18 @@ function makeRows( tableData, options ) { delete tableCellData.isHeading; } + const attributes = isObject ? tableCellData : {}; + + if ( asWidget ) { + attributes.class = 'ck-editor__editable ck-editor__nested-editable'; + attributes.contenteditable = 'true'; + } + if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) { contents = `<${ wrappingElement }>${ contents }`; } - const formattedAttributes = formatAttributes( isObject ? tableCellData : '' ); + const formattedAttributes = formatAttributes( attributes ); tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }`; return tableRowString; diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index 9df47be7..f41f9159 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -11,6 +11,28 @@ import { defaultConversion, defaultSchema, formatTable, formattedViewTable, mode import env from '@ckeditor/ckeditor5-utils/src/env'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +function paragraphInTableCell() { + return dispatcher => dispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => { + const tableCell = data.item.parent; + + if ( tableCell.is( 'tableCell' ) && tableCell.childCount > 1 ) { + for ( const child of tableCell.getChildren() ) { + if ( child.name != 'paragraph' ) { + continue; + } + + const viewElement = conversionApi.mapper.toViewElement( child ); + + if ( viewElement && viewElement.name === 'span' ) { + conversionApi.mapper.unbindModelElement( tableCell ); + conversionApi.writer.rename( viewElement, 'p' ); + conversionApi.mapper.bindElements( child, viewElement ); + } + } + } + }, { converterPriority: 'highest' } ); +} + describe( 'downcast converters', () => { let editor, model, doc, root, viewDocument; @@ -1107,4 +1129,44 @@ describe( 'downcast converters', () => { ], { headingRows: 1 } ) ); } ); } ); + + describe( 'options.asWidget=true', () => { + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + viewDocument = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); + + editor.conversion.for( 'downcast' ).add( paragraphInTableCell() ); + } ); + } ); + + it( 'should rename to

when more then one block content inside table cell', () => { + setModelData( model, modelTable( [ + [ '00[]' ] + ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); + + const paragraph = writer.createElement( 'paragraph' ); + + writer.insert( paragraph, nodeByPath, 'after' ); + + writer.setSelection( nodeByPath.nextSibling, 0 ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); + } ); } ); From 8479dac1a4e96d2825a2fc6c3106449d9ca2ad46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 12:47:40 +0200 Subject: [PATCH 12/44] Test: Update table cells post fixer tests. --- tests/_utils/utils.js | 6 ++++-- tests/converters/downcast.js | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index f438032e..0abf9cd9 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -89,7 +89,8 @@ export function viewTable( tableData, attributes = {} ) { cellElement: 'th', rowElement: 'tr', headingElement: 'th', - wrappingElement: 'p', + wrappingElement: asWidget ? 'span' : 'p', + enforceWrapping: asWidget, asWidget } ) }` : ''; @@ -98,7 +99,8 @@ export function viewTable( tableData, attributes = {} ) { cellElement: 'td', rowElement: 'tr', headingElement: 'th', - wrappingElement: 'p', + wrappingElement: asWidget ? 'span' : 'p', + enforceWrapping: asWidget, asWidget } ) }` : ''; diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index f41f9159..6c10ddaf 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -1152,6 +1152,10 @@ describe( 'downcast converters', () => { [ '00[]' ] ] ) ); + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '00' ] + ], { asWidget: true } ) ); + const table = root.getChild( 0 ); model.change( writer => { From 228d5039a59f1f1e431f0111369c3801e576eaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 16:01:23 +0200 Subject: [PATCH 13/44] Added: Table post fixer should react on paragraph attribute change. --- src/tableediting.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/tableediting.js b/src/tableediting.js index 42467066..5e6fcfc0 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -262,7 +262,40 @@ function paragraphInTableCellPostFixer( writer, model, mapper ) { const changes = model.document.differ.getChanges(); for ( const entry of changes ) { - const tableCell = entry.position.parent; + const tableCell = entry.position && entry.position.parent; + + if ( !tableCell && entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { + const tableCell = entry.range.start.parent; + + if ( tableCell.childCount === 1 ) { + const singleChild = tableCell.getChild( 0 ); + + if ( !singleChild || !singleChild.is( 'paragraph' ) ) { + return; + } + + const viewElement = mapper.toViewElement( singleChild ); + + let renameTo = 'p'; + + if ( viewElement.name === 'p' ) { + if ( [ ...singleChild.getAttributes() ].length ) { + return; + } else { + renameTo = 'span'; + } + } + + const renamedViewElement = writer.rename( viewElement, renameTo ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( singleChild, renamedViewElement ); + } + } + + if ( !tableCell ) { + continue; + } if ( tableCell.is( 'tableCell' ) ) { if ( tableCell.childCount > 1 ) { From fa55ebe994873c8d8c30dbb47119fe2fd633e170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 16:09:23 +0200 Subject: [PATCH 14/44] Changed: Extract tablecell-post-fixer. --- src/converters/tablecell-post-fixer.js | 101 +++++++++++++++++++++++++ src/tableediting.js | 85 +-------------------- 2 files changed, 103 insertions(+), 83 deletions(-) create mode 100644 src/converters/tablecell-post-fixer.js diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js new file mode 100644 index 00000000..0d399149 --- /dev/null +++ b/src/converters/tablecell-post-fixer.js @@ -0,0 +1,101 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module table/converters/tablecell-post-fixer + */ + +/** + * Injects a table cell post-fixer into the editing controller. + * + * @param {module:engine/model/model~Model} model + * @param {module:engine/controller/editingcontroller~EditingController} editing + */ +export default function injectTableCellPostFixer( model, editing ) { + editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) ); +} + +// The table cell post-fixer. +// +// @param {module:engine/view/writer~Writer} writer +// @param {module:engine/model/model~Model} model +// @param {module:engine/conversion/mapper~Mapper} mapper +function tableCellPostFixer( writer, model, mapper ) { + const changes = model.document.differ.getChanges(); + + for ( const entry of changes ) { + const tableCell = entry.position && entry.position.parent; + + if ( !tableCell && entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { + const tableCell = entry.range.start.parent; + + if ( tableCell.childCount === 1 ) { + const singleChild = tableCell.getChild( 0 ); + + if ( !singleChild || !singleChild.is( 'paragraph' ) ) { + return; + } + + const viewElement = mapper.toViewElement( singleChild ); + + let renameTo = 'p'; + + if ( viewElement.name === 'p' ) { + if ( [ ...singleChild.getAttributes() ].length ) { + return; + } else { + renameTo = 'span'; + } + } + + const renamedViewElement = writer.rename( viewElement, renameTo ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( singleChild, renamedViewElement ); + } + } + + if ( !tableCell ) { + continue; + } + + if ( tableCell.is( 'tableCell' ) ) { + if ( tableCell.childCount > 1 ) { + for ( const child of tableCell.getChildren() ) { + if ( child.name != 'paragraph' ) { + continue; + } + + const viewElement = mapper.toViewElement( child ); + + if ( viewElement && viewElement.name === 'span' ) { + // Unbind table cell as will be renamed to

. + // mapper.unbindModelElement( tableCell ); + + const renamedViewElement = writer.rename( viewElement, 'p' ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( child, renamedViewElement ); + } + } + } else { + const singleChild = tableCell.getChild( 0 ); + if ( !singleChild || !singleChild.is( 'paragraph' ) ) { + return; + } + + const viewElement = mapper.toViewElement( singleChild ); + + // Unbind table cell as will be renamed to

. + // mapper.unbindModelElement( tableCell ); + + const renamedViewElement = writer.rename( viewElement, 'span' ); + + // Re-bind table cell to renamed view element. + mapper.bindElements( singleChild, renamedViewElement ); + } + } + } +} diff --git a/src/tableediting.js b/src/tableediting.js index 5e6fcfc0..5776b3dc 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -34,6 +34,7 @@ import { getParentElement, getParentTable } from './commands/utils'; import TableUtils from './tableutils'; import '../theme/tableediting.css'; +import injectTableCellPostFixer from './converters/tablecell-post-fixer'; /** * The table editing feature. @@ -107,7 +108,7 @@ export default class TableEditing extends Plugin { conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) ); conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() ); - injectParagraphInTableCellPostFixer( editor.model, editor.editing ); + injectTableCellPostFixer( editor.model, editor.editing ); // Define all the commands. editor.commands.add( 'insertTable', new InsertTableCommand( editor ) ); @@ -253,85 +254,3 @@ export default class TableEditing extends Plugin { } ); } } - -function injectParagraphInTableCellPostFixer( model, editing ) { - editing.view.document.registerPostFixer( writer => paragraphInTableCellPostFixer( writer, model, editing.mapper ) ); -} - -function paragraphInTableCellPostFixer( writer, model, mapper ) { - const changes = model.document.differ.getChanges(); - - for ( const entry of changes ) { - const tableCell = entry.position && entry.position.parent; - - if ( !tableCell && entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { - const tableCell = entry.range.start.parent; - - if ( tableCell.childCount === 1 ) { - const singleChild = tableCell.getChild( 0 ); - - if ( !singleChild || !singleChild.is( 'paragraph' ) ) { - return; - } - - const viewElement = mapper.toViewElement( singleChild ); - - let renameTo = 'p'; - - if ( viewElement.name === 'p' ) { - if ( [ ...singleChild.getAttributes() ].length ) { - return; - } else { - renameTo = 'span'; - } - } - - const renamedViewElement = writer.rename( viewElement, renameTo ); - - // Re-bind table cell to renamed view element. - mapper.bindElements( singleChild, renamedViewElement ); - } - } - - if ( !tableCell ) { - continue; - } - - if ( tableCell.is( 'tableCell' ) ) { - if ( tableCell.childCount > 1 ) { - for ( const child of tableCell.getChildren() ) { - if ( child.name != 'paragraph' ) { - continue; - } - - const viewElement = mapper.toViewElement( child ); - - if ( viewElement && viewElement.name === 'span' ) { - // Unbind table cell as will be renamed to

. - // mapper.unbindModelElement( tableCell ); - - const renamedViewElement = writer.rename( viewElement, 'p' ); - - // Re-bind table cell to renamed view element. - mapper.bindElements( child, renamedViewElement ); - } - } - } else { - const singleChild = tableCell.getChild( 0 ); - if ( !singleChild || !singleChild.is( 'paragraph' ) ) { - return; - } - - const viewElement = mapper.toViewElement( singleChild ); - - // Unbind table cell as will be renamed to

. - // mapper.unbindModelElement( tableCell ); - - const renamedViewElement = writer.rename( viewElement, 'span' ); - - // Re-bind table cell to renamed view element. - mapper.bindElements( singleChild, renamedViewElement ); - } - } - } -} From 5b293aed6cfd0ab7c09517e8b880c8d4a6134674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 16:28:11 +0200 Subject: [PATCH 15/44] Tests: Add basic tests for the tablecell-post-fixer. --- tests/converters/tablecell-post-fixer.js | 103 +++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/converters/tablecell-post-fixer.js diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js new file mode 100644 index 00000000..b0f262bb --- /dev/null +++ b/tests/converters/tablecell-post-fixer.js @@ -0,0 +1,103 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer'; + +describe( 'TableCell post-fixer', () => { + let editor, model, doc, root, viewDocument; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + viewDocument = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion, true ); + + injectTableCellPostFixer( model, editor.editing ); + } ); + } ); + + it( 'should create element for single paragraph inside table cell', () => { + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + '

' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '00' + + '
' + + '
' + ) ); + } ); + + it( 'should rename to

when more then one block content inside table cell', () => { + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); + + const paragraph = writer.createElement( 'paragraph' ); + + writer.insert( paragraph, nodeByPath, 'after' ); + + writer.setSelection( nodeByPath.nextSibling, 0 ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); + + it( 'should rename

to when removing all but one paragraph inside table cell', () => { + setModelData( model, modelTable( [ [ '00[]foo' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '00' ] + ], { asWidget: true } ) ); + } ); + + it( 'should do nothing on rename to ', () => { + setModelData( model, modelTable( [ [ '00' ] ] ) ); + + const table = root.getChild( 0 ); + + editor.conversion.elementToElement( { model: 'heading1', view: 'h1' } ); + + model.change( writer => { + writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'heading1' ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); +} ); From 71388efe707ef228d2d53157656ec07ec3652f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 27 Jul 2018 16:30:36 +0200 Subject: [PATCH 16/44] Fix: Wrong import path fo the paragraph plugin. --- tests/converters/upcasttable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/converters/upcasttable.js b/tests/converters/upcasttable.js index df6fd059..d0738e1a 100644 --- a/tests/converters/upcasttable.js +++ b/tests/converters/upcasttable.js @@ -9,7 +9,7 @@ import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; import { formatTable } from '../_utils/utils'; -import Paragraph from '../../../ckeditor5-paragraph/src/paragraph'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'upcastTable()', () => { let editor, model; From 9e72b2ab7fa0cdba0f6639c6b9509276bf315ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 10:56:16 +0200 Subject: [PATCH 17/44] Tests: Table tests for widgets should stub env.isEdge variable. --- tests/converters/tablecell-post-fixer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js index b0f262bb..752848e0 100644 --- a/tests/converters/tablecell-post-fixer.js +++ b/tests/converters/tablecell-post-fixer.js @@ -8,15 +8,20 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils'; -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer'; +import env from '@ckeditor/ckeditor5-utils/src/env'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + describe( 'TableCell post-fixer', () => { let editor, model, doc, root, viewDocument; testUtils.createSinonSandbox(); beforeEach( () => { + // Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`. + testUtils.sinon.stub( env, 'isEdge' ).get( () => false ); + return VirtualTestEditor.create() .then( newEditor => { editor = newEditor; From 52755059b98f415b329bceee701b5d25c18eb606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 11:17:04 +0200 Subject: [PATCH 18/44] Tests: Revert previous manual table test. --- tests/manual/table.html | 196 ++++++++++++++++++++++++++++++++++++++++ tests/manual/table.js | 7 +- 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/tests/manual/table.html b/tests/manual/table.html index a9c167f6..0b2cf558 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -52,4 +52,200 @@
a h5
+ +

Table with everything:

+ +
+
Data about the planets of our solar system (Planetary facts taken from Nasa's Planetary Fact Sheet - Metric. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 NameMass (1024kg)Diameter (km)Density (kg/m3)Gravity (m/s2)Length of day (hours)Distance from Sun (106km)Mean temperature (°C)Number of moonsNotes
Terrestrial planetsMercury0.3304,87954273.74222.657.91670Closest to the Sun
Venus4.8712,10452438.92802.0108.24640 
Earth5.9712,75655149.824.0149.6151Our world
Mars0.6426,79239333.724.7227.9-652The red planet
Jovian planetsGas giantsJupiter1898142,984132623.19.9778.6-11067The largest planet
Saturn568120,5366879.010.71433.5-14062 
Ice giantsUranus86.851,11812718.717.22872.5-19527 
Neptune10249,528163811.016.14495.1-20014 
Dwarf planetsPluto0.01462,37020950.7153.35906.4-2255Declassified as a planet in 2006, but this remains controversial. +
+
+ +

Table with 2 tbody:

+ + + + + + + + + + + + + + + + +
abc
abc
+ +

Table with no tbody:

+ + + + + + + + + + + + +
abc
abc
+ +

Table with thead section between two tbody sections

+ + + + + + + + + + + + + + + + + +
2
1
3
diff --git a/tests/manual/table.js b/tests/manual/table.js index c7448daf..4daac2ce 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -9,15 +9,12 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor' import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import Table from '../../src/table'; import TableToolbar from '../../src/tabletoolbar'; -import Alignment from '../../../ckeditor5-alignment/src/alignment'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ], + plugins: [ ArticlePluginSet, Table, TableToolbar ], toolbar: [ - 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', - 'alignment', 'insertImage', - '|', 'undo', 'redo' + 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], table: { toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] From 4edb2f86e3cff1f19891dc9559e62ec64fb9bdf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 11:33:14 +0200 Subject: [PATCH 19/44] Tests: Create manual tests for block content inside table cell. --- package.json | 2 + tests/manual/sample.jpg | Bin 0 -> 114298 bytes tests/manual/tableblockcontent.html | 74 ++++++++++++++++++++++++++++ tests/manual/tableblockcontent.js | 37 ++++++++++++++ tests/manual/tableblockcontent.md | 7 +++ 5 files changed, 120 insertions(+) create mode 100644 tests/manual/sample.jpg create mode 100644 tests/manual/tableblockcontent.html create mode 100644 tests/manual/tableblockcontent.js create mode 100644 tests/manual/tableblockcontent.md diff --git a/package.json b/package.json index cdd66cbc..230db611 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "@ckeditor/ckeditor5-widget": "^10.2.0" }, "devDependencies": { + "@ckeditor/ckeditor5-alignment": "^10.0.2", "@ckeditor/ckeditor5-editor-classic": "^11.0.0", + "@ckeditor/ckeditor5-image": "^10.2.0", "@ckeditor/ckeditor5-paragraph": "^10.0.2", "@ckeditor/ckeditor5-utils": "^10.2.0", "eslint": "^4.15.0", diff --git a/tests/manual/sample.jpg b/tests/manual/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b77d07e7bfff7fed1563fe2cd190c46ade02905f GIT binary patch literal 114298 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 literal 0 HcmV?d00001 diff --git a/tests/manual/tableblockcontent.html b/tests/manual/tableblockcontent.html new file mode 100644 index 00000000..906ff04d --- /dev/null +++ b/tests/manual/tableblockcontent.html @@ -0,0 +1,74 @@ + + +
+ +

Table with block contents:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
$textFooBrownie carrot cake soufflé jelly-o lemon drops cupcake tart.
paragraph

Foo

+

A paragraph with default alignment

+

A paragraph with right alignment

+

A paragraph with center alignment

+

A paragraph with justify alignment

+
list +
    +
  • Foo
  • +
  • Bar
  • +
+
+
    +
  • A list item with default alignment
  • +
  • A list item with right alignment
  • +
  • A list item with center alignment
  • +
  • A list item with justify alignment
  • +
+
heading +

An h2 title.

+

An h3 title.

+

An h4 title.

+
+

An h2 with default alignment.

+

An h2 with right alignment.

+

An h2 with center alignment.

+

An h2 with justify alignment.

+
block quote +

A quote

+

A quote with paragraph with right alignment

imageSample imageSample image
+
diff --git a/tests/manual/tableblockcontent.js b/tests/manual/tableblockcontent.js new file mode 100644 index 00000000..1e2f27e1 --- /dev/null +++ b/tests/manual/tableblockcontent.js @@ -0,0 +1,37 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Table from '../../src/table'; +import TableToolbar from '../../src/tabletoolbar'; +import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; +import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; +import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle'; +import Image from '@ckeditor/ckeditor5-image/src/image'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment, Image, ImageCaption, ImageStyle ], + toolbar: [ + 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', + 'alignment', 'insertImage', + '|', 'undo', 'redo' + ], + image: { + toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ] + }, + table: { + toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/tableblockcontent.md b/tests/manual/tableblockcontent.md new file mode 100644 index 00000000..3f169feb --- /dev/null +++ b/tests/manual/tableblockcontent.md @@ -0,0 +1,7 @@ +### Loading + +1. The data should be loaded with: + * a complex table with: + +### Testing + From 6e7ccb5ee2321d66f91a23fa44aec78d348d3916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 14:17:04 +0200 Subject: [PATCH 20/44] Tests: Update manual tests description. --- tests/manual/table.html | 49 +------------------------------ tests/manual/tableblockcontent.md | 16 ++++++++-- 2 files changed, 15 insertions(+), 50 deletions(-) diff --git a/tests/manual/table.html b/tests/manual/table.html index 0b2cf558..feae0e74 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -6,54 +6,7 @@
- -

Table with block contents:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
whatexample
textFoo
paragraph

Foo

list -
    -
  • foo
  • -
  • bar
  • -
-
heading -

a h1

-

a h2

-

a h3

-

a h4

-
a h5
-
block quote -
a quote
-
- -

Table with everything:

+

Complex table:

Data about the planets of our solar system (Planetary facts taken from ` if single paragraph is inside table cell. + * List + * Heading + * Block Quote (with inner paragraph) + * Image + +2. The third column consist blocks with text alignment. + * Paragraph - should be rendered was `

` when alignment is set (apart from default) for single paragraph. ### Testing +1. Use Enter in cells with single ``. When two ``'s are in one table cell they should be rendered as `

`. +2. Undo previous step - the `

` element should be changed to `` for single paragraph. +3. Change `` to paragraph - it should be rendered as `

` element if there are other headings or other block content. +4. Change one `` to paragraph and remove other headings. The `` should be rendered as ``. From 5f0c4c28a35b3fce46d0e4a5f51084b208cd3f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 16:25:13 +0200 Subject: [PATCH 21/44] Tests: Dissalow table in table test for upcast converter. --- src/converters/upcasttable.js | 12 ++++++++ tests/_utils/utils.js | 26 ++++++++++++---- tests/converters/upcasttable.js | 53 +++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index 9324d116..55b6a932 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -44,6 +44,12 @@ export default function upcastTable() { // Insert element on allowed position. const splitResult = conversionApi.splitToAllowedParent( table, data.modelCursor ); + + // When there is no split result it means that we can't insert element to model tree, so let's skip it. + if ( !splitResult ) { + return; + } + conversionApi.writer.insert( table, splitResult.position ); conversionApi.consumable.consume( viewTable, { name: true } ); @@ -102,6 +108,12 @@ export function upcastTableCell( elementName ) { // Insert element on allowed position. const splitResult = conversionApi.splitToAllowedParent( tableCell, data.modelCursor ); + + // When there is no split result it means that we can't insert element to model tree, so let's skip it. + if ( !splitResult ) { + return; + } + conversionApi.writer.insert( tableCell, splitResult.position ); conversionApi.consumable.consume( viewTableCell, { name: true } ); diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 0abf9cd9..5d357b5a 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -12,6 +12,7 @@ import { downcastTableHeadingRowsChange } from '../../src/converters/downcast'; import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; +import { upcastElementToElement } from '../../../ckeditor5-engine/src/conversion/upcast-converters'; /** * Returns a model representation of a table shorthand notation: @@ -154,24 +155,38 @@ export function formattedViewTable( tableData, attributes ) { return formatTable( viewTable( tableData, attributes ) ); } -export function defaultSchema( schema ) { +export function defaultSchema( schema, registerParagraph = true ) { schema.register( 'table', { allowWhere: '$block', allowAttributes: [ 'headingRows', 'headingColumns' ], + isLimit: true, isObject: true } ); - schema.register( 'tableRow', { allowIn: 'table' } ); + schema.register( 'tableRow', { + allowIn: 'table', + isLimit: true + } ); schema.register( 'tableCell', { allowIn: 'tableRow', - allowContentOf: '$block', allowAttributes: [ 'colspan', 'rowspan' ], isLimit: true } ); + // Allow all $block content inside table cell. schema.extend( '$block', { allowIn: 'tableCell' } ); - schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + + // Disallow table in table. + schema.addChildCheck( ( context, childDefinition ) => { + if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) { + return false; + } + } ); + + if ( registerParagraph ) { + schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + } } export function defaultConversion( conversion, asWidget = false ) { @@ -182,13 +197,14 @@ export function defaultConversion( conversion, asWidget = false ) { conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget } ) ); // Table row conversion. + conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget } ) ); conversion.for( 'downcast' ).add( downcastRemoveRow( { asWidget } ) ); // Table cell conversion. - conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) ); conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); + conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) ); // Table attributes conversion. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); diff --git a/tests/converters/upcasttable.js b/tests/converters/upcasttable.js index d0738e1a..bba4cc26 100644 --- a/tests/converters/upcasttable.js +++ b/tests/converters/upcasttable.js @@ -8,7 +8,7 @@ import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversio import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; -import { formatTable } from '../_utils/utils'; +import { defaultSchema, formatTable } from '../_utils/utils'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'upcastTable()', () => { @@ -24,37 +24,20 @@ describe( 'upcastTable()', () => { model = editor.model; const conversion = editor.conversion; - const schema = model.schema; - schema.register( 'table', { - allowWhere: '$block', - allowAttributes: [ 'headingRows', 'headingColumns' ], - isLimit: true, - isObject: true - } ); - - schema.register( 'tableRow', { - allowIn: 'table', - isLimit: true - } ); - - schema.register( 'tableCell', { - allowIn: 'tableRow', - allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true - } ); - - schema.extend( '$block', { allowIn: 'tableCell' } ); + defaultSchema( model.schema, false ); + // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); - // Table row upcast only since downcast conversion is done in `downcastTable()`. + // Table row conversion. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); // Table cell conversion. conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); + // Table attributes conversion. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); @@ -293,6 +276,32 @@ describe( 'upcastTable()', () => { ); } ); + it( 'should strip table in table', () => { + editor.setData( + '' + + '' + + '' + + '' + + '
' + + '' + + '' + + '' + + '' + + '
tableception
' + + '
' + ); + + expectModel( + '' + + '' + + '' + + 'tableception' + + '' + + '' + + '
' + ); + } ); + describe( 'headingColumns', () => { it( 'should properly calculate heading columns', () => { editor.setData( From 5695b4545ed0fd4873c178f523b14f13c7fb1477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 16:31:24 +0200 Subject: [PATCH 22/44] Tests: Unify schema and conversion across tests. --- tests/commands/inserttablecommand.js | 2 +- tests/commands/mergecellcommand.js | 18 +++++++++--------- tests/commands/removecolumncommand.js | 8 ++++---- tests/commands/removerowcommand.js | 8 ++++---- tests/commands/setheadercolumncommand.js | 4 ++-- tests/commands/setheaderrowcommand.js | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/commands/inserttablecommand.js b/tests/commands/inserttablecommand.js index 9d362dc5..c60b430c 100644 --- a/tests/commands/inserttablecommand.js +++ b/tests/commands/inserttablecommand.js @@ -41,7 +41,7 @@ describe( 'InsertTableCommand', () => { } ); it( 'should be false if in table', () => { - setData( model, 'foo[]
' ); + setData( model, 'foo[]
' ); expect( command.isEnabled ).to.be.false; } ); } ); diff --git a/tests/commands/mergecellcommand.js b/tests/commands/mergecellcommand.js index 8dc091ee..2cdf63dc 100644 --- a/tests/commands/mergecellcommand.js +++ b/tests/commands/mergecellcommand.js @@ -152,7 +152,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[0001]' } ] + [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); } ); @@ -278,7 +278,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[0001]' } ] + [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); } ); @@ -416,7 +416,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], + [ '00', { rowspan: 2, contents: '[0111]' } ], [ '10' ] ] ) ); } ); @@ -431,7 +431,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], + [ '00', '[0111]', { rowspan: 2, contents: '02' } ], [ '20', '21' ] ] ) ); } ); @@ -450,7 +450,7 @@ describe( 'MergeCellCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 2, contents: '00' }, '01', '02' ], [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], + [ '20', '[2131]', { rowspan: 2, contents: '22' } ], [ '40', '41' ] ] ) ); } ); @@ -590,7 +590,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], + [ '00', { rowspan: 2, contents: '[0111]' } ], [ '10' ] ] ) ); } ); @@ -610,7 +610,7 @@ describe( 'MergeCellCommand', () => { [ { rowspan: 2, contents: '21' }, '22', - { rowspan: 3, contents: '[2333]' } + { rowspan: 3, contents: '[2333]' } ], [ '32' ], [ { colspan: 2, contents: '40' }, '42' ] @@ -627,7 +627,7 @@ describe( 'MergeCellCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], + [ '00', '[0111]', { rowspan: 2, contents: '02' } ], [ '20', '21' ] ] ) ); } ); @@ -646,7 +646,7 @@ describe( 'MergeCellCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 2, contents: '00' }, '01', '02' ], [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], + [ '20', '[2131]', { rowspan: 2, contents: '22' } ], [ '40', '41' ] ] ) ); } ); diff --git a/tests/commands/removecolumncommand.js b/tests/commands/removecolumncommand.js index f1ba21b4..22def6f4 100644 --- a/tests/commands/removecolumncommand.js +++ b/tests/commands/removecolumncommand.js @@ -71,7 +71,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ '00', '02' ], - [ '10[]', '12' ], + [ '10[]', '12' ], [ '20', '22' ] ] ) ); } ); @@ -86,7 +86,7 @@ describe( 'RemoveColumnCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '[]01' ], + [ '[]01' ], [ '11' ], [ '21' ] ] ) ); @@ -103,7 +103,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ '01' ], - [ '[]11' ], + [ '[]11' ], [ '21' ] ], { headingColumns: 1 } ) ); } ); @@ -122,7 +122,7 @@ describe( 'RemoveColumnCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { colspan: 3, contents: '00' }, '03' ], [ { colspan: 2, contents: '10' }, '13' ], - [ { colspan: 2, contents: '20[]' }, '23' ], + [ { colspan: 2, contents: '20[]' }, '23' ], [ '30', '31', '33' ], [ '40', '41', '43' ] diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js index 0ea9557a..0dc4546b 100644 --- a/tests/commands/removerowcommand.js +++ b/tests/commands/removerowcommand.js @@ -64,7 +64,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], + [ '00', '01[]' ], [ '20', '21' ] ] ) ); } ); @@ -79,7 +79,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '[]10', '11' ], + [ '[]10', '11' ], [ '20', '21' ] ] ) ); } ); @@ -94,7 +94,7 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], + [ '00', '01[]' ], [ '20', '21' ] ], { headingRows: 1 } ) ); } ); @@ -111,7 +111,7 @@ describe( 'RemoveRowCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], - [ '13', '14[]' ], + [ '13', '14[]' ], [ '30', '31', '32', '33', '34' ] ] ) ); } ); diff --git a/tests/commands/setheadercolumncommand.js b/tests/commands/setheadercolumncommand.js index 714b1f3a..e1454d82 100644 --- a/tests/commands/setheadercolumncommand.js +++ b/tests/commands/setheadercolumncommand.js @@ -10,7 +10,7 @@ import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand'; import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; -describe( 'HeaderColumnCommand', () => { +describe( 'SetHeaderColumnCommand', () => { let editor, model, command; beforeEach( () => { @@ -39,7 +39,7 @@ describe( 'HeaderColumnCommand', () => { } ); it( 'should be true if selection is in table', () => { - setData( model, 'foo[]
' ); + setData( model, 'foo[]
' ); expect( command.isEnabled ).to.be.true; } ); } ); diff --git a/tests/commands/setheaderrowcommand.js b/tests/commands/setheaderrowcommand.js index 1eab8433..22ddfe62 100644 --- a/tests/commands/setheaderrowcommand.js +++ b/tests/commands/setheaderrowcommand.js @@ -9,7 +9,7 @@ import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand'; import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; -describe( 'HeaderRowCommand', () => { +describe( 'SetHeaderRowCommand', () => { let editor, model, command; beforeEach( () => { @@ -38,7 +38,7 @@ describe( 'HeaderRowCommand', () => { } ); it( 'should be true if selection is in table', () => { - setData( model, 'foo[]
' ); + setData( model, 'foo[]
' ); expect( command.isEnabled ).to.be.true; } ); } ); From 9a428e9eb23b63fc1657151058f6d13f50980d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 16:49:25 +0200 Subject: [PATCH 23/44] Tests: Add table integration tests. --- src/converters/upcasttable.js | 14 +--- tests/table-integration.js | 129 ++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 tests/table-integration.js diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index 55b6a932..1f4706f4 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -132,18 +132,8 @@ export function upcastTableCell( elementName ) { ModelPosition.createAfter( tableCell ) ); - // Now we need to check where the modelCursor should be. - // If we had to split parent to insert our element then we want to continue conversion inside split parent. - // - // before: [] - // after: [] - if ( splitResult.cursorParent ) { - data.modelCursor = ModelPosition.createAt( splitResult.cursorParent ); - - // Otherwise just continue after inserted element. - } else { - data.modelCursor = data.modelRange.end; - } + // Continue after inserted element. + data.modelCursor = data.modelRange.end; }, { priority: 'normal' } ); }; } diff --git a/tests/table-integration.js b/tests/table-integration.js new file mode 100644 index 00000000..2282697b --- /dev/null +++ b/tests/table-integration.js @@ -0,0 +1,129 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { + getData as getModelData, + setData as setModelData +} from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +import TableEditing from '../src/tableediting'; +import { formatTable, formattedModelTable, modelTable } from './_utils/utils'; + +describe( 'Table feature – integration', () => { + describe( 'with clipboard', () => { + it( 'pastes td as p when pasting into the table', () => { + return VirtualTestEditor + .create( { plugins: [ Paragraph, TableEditing, Widget, Clipboard ] } ) + .then( newEditor => { + const editor = newEditor; + const clipboard = editor.plugins.get( 'Clipboard' ); + + setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) ); + + clipboard.fire( 'inputTransformation', { + content: parseView( 'bar' ) + } ); + + expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [ + [ 'foobar[]' ] + ] ) ); + } ); + } ); + + it( 'pastes td as p when pasting into the p', () => { + return VirtualTestEditor + .create( { plugins: [ Paragraph, TableEditing, Widget, Clipboard ] } ) + .then( newEditor => { + const editor = newEditor; + const clipboard = editor.plugins.get( 'Clipboard' ); + + setModelData( editor.model, 'foo[]' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( 'bar' ) + } ); + + expect( formatTable( getModelData( editor.model ) ) ).to.equal( 'foobar[]' ); + } ); + } ); + } ); + + describe( 'with undo', () => { + it( 'fixing empty roots should be transparent to undo', () => { + return VirtualTestEditor + .create( { plugins: [ Paragraph, UndoEditing ] } ) + .then( newEditor => { + const editor = newEditor; + const doc = editor.model.document; + const root = doc.getRoot(); + + expect( editor.getData() ).to.equal( '

 

' ); + expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false; + + editor.setData( '

Foobar.

' ); + + editor.model.change( writer => { + writer.remove( root.getChild( 0 ) ); + } ); + + expect( editor.getData() ).to.equal( '

 

' ); + + editor.execute( 'undo' ); + + expect( editor.getData() ).to.equal( '

Foobar.

' ); + + editor.execute( 'redo' ); + + expect( editor.getData() ).to.equal( '

 

' ); + + editor.execute( 'undo' ); + + expect( editor.getData() ).to.equal( '

Foobar.

' ); + } ); + } ); + + it( 'fixing empty roots should be transparent to undo - multiple roots', () => { + return VirtualTestEditor + .create( { plugins: [ Paragraph, UndoEditing ] } ) + .then( newEditor => { + const editor = newEditor; + const doc = editor.model.document; + const root = doc.getRoot(); + const otherRoot = doc.createRoot( '$root', 'otherRoot' ); + + editor.data.set( '

Foobar.

', 'main' ); + editor.data.set( '

Foobar.

', 'otherRoot' ); + + editor.model.change( writer => { + writer.remove( root.getChild( 0 ) ); + } ); + + editor.model.change( writer => { + writer.remove( otherRoot.getChild( 0 ) ); + } ); + + expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( '

 

' ); + + editor.execute( 'undo' ); + + expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( '

Foobar.

' ); + + editor.execute( 'undo' ); + + expect( editor.data.get( 'main' ) ).to.equal( '

Foobar.

' ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( '

Foobar.

' ); + } ); + } ); + } ); +} ); From 10cf333eb00b0853e6b8ba6adec111074bd92a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 17:26:07 +0200 Subject: [PATCH 24/44] Tests: Add tests for converting single paragraph with attributes. --- src/converters/downcast.js | 4 ++- src/converters/tablecell-post-fixer.js | 1 + tests/converters/downcast.js | 13 +++++++++ tests/converters/tablecell-post-fixer.js | 36 +++++++++++++++++++++++- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/converters/downcast.js b/src/converters/downcast.js index a96661e3..8fba7165 100644 --- a/src/converters/downcast.js +++ b/src/converters/downcast.js @@ -386,7 +386,9 @@ function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPo conversionApi.consumable.consume( innerParagraph, 'insert' ); if ( options.asWidget ) { - const fakeParagraph = conversionApi.writer.createContainerElement( 'span' ); + const containerName = [ ...innerParagraph.getAttributeKeys() ].length ? 'p' : 'span'; + + const fakeParagraph = conversionApi.writer.createContainerElement( containerName ); conversionApi.mapper.bindElements( innerParagraph, fakeParagraph ); conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph ); diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js index 0d399149..4d4a8864 100644 --- a/src/converters/tablecell-post-fixer.js +++ b/src/converters/tablecell-post-fixer.js @@ -82,6 +82,7 @@ function tableCellPostFixer( writer, model, mapper ) { } } else { const singleChild = tableCell.getChild( 0 ); + if ( !singleChild || !singleChild.is( 'paragraph' ) ) { return; } diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index 54baa434..93c79202 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -1199,5 +1199,18 @@ describe( 'downcast converters', () => { [ '

00

' ] ], { asWidget: true } ) ); } ); + + it( 'should rename to

for single paragraph with attribute', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + setModelData( model, modelTable( [ + [ '00[]' ] + ] ) ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); } ); } ); diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js index 752848e0..d4dc7842 100644 --- a/tests/converters/tablecell-post-fixer.js +++ b/tests/converters/tablecell-post-fixer.js @@ -90,7 +90,41 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should do nothing on rename to ', () => { + it( 'should rename to

when setting attribute on paragraph', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); + + it( 'should rename

to when removing attribute from paragraph', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '00' ] + ], { asWidget: true } ) ); + } ); + + it( 'should do nothing on rename to ', () => { setModelData( model, modelTable( [ [ '00' ] ] ) ); const table = root.getChild( 0 ); From a450bd38ad8ee7a314c9145f0d353a89bf1dab34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 17:28:40 +0200 Subject: [PATCH 25/44] Other: Update dev-dependencies. --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 230db611..cde63cd4 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ }, "devDependencies": { "@ckeditor/ckeditor5-alignment": "^10.0.2", + "@ckeditor/ckeditor5-clipboard": "^10.0.2", "@ckeditor/ckeditor5-editor-classic": "^11.0.0", "@ckeditor/ckeditor5-image": "^10.2.0", + "@ckeditor/ckeditor5-undo": "^10.0.2", "@ckeditor/ckeditor5-paragraph": "^10.0.2", "@ckeditor/ckeditor5-utils": "^10.2.0", "eslint": "^4.15.0", From 137dc0c811d8290f6a363acb4dbebbf99b9bed14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 30 Jul 2018 17:33:47 +0200 Subject: [PATCH 26/44] Tests: Fix package links in tests. --- tests/_utils/utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 5d357b5a..88e74f6f 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md. */ +import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; + import { downcastInsertCell, downcastInsertRow, @@ -12,7 +14,6 @@ import { downcastTableHeadingRowsChange } from '../../src/converters/downcast'; import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; -import { upcastElementToElement } from '../../../ckeditor5-engine/src/conversion/upcast-converters'; /** * Returns a model representation of a table shorthand notation: From aef2cf4753407be1888588d67381bd478ca7ddac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Jul 2018 11:26:44 +0200 Subject: [PATCH 27/44] Tests: Update code coverage for tablecell post fixer. --- tests/converters/tablecell-post-fixer.js | 85 +++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js index d4dc7842..cf98fff3 100644 --- a/tests/converters/tablecell-post-fixer.js +++ b/tests/converters/tablecell-post-fixer.js @@ -90,6 +90,31 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); + it( 'should rename to

on adding other block element to the same table cell', () => { + editor.model.schema.register( 'block', { + inheritAllFrom: '$block' + } ); + editor.conversion.elementToElement( { model: 'block', view: 'div' } ); + + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); + + const paragraph = writer.createElement( 'block' ); + + writer.insert( paragraph, nodeByPath, 'after' ); + + writer.setSelection( nodeByPath.nextSibling, 0 ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); + it( 'should rename to

when setting attribute on paragraph', () => { model.schema.extend( '$block', { allowAttributes: 'foo' } ); editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); @@ -125,12 +150,12 @@ describe( 'TableCell post-fixer', () => { } ); it( 'should do nothing on rename to ', () => { + editor.conversion.elementToElement( { model: 'heading1', view: 'h1' } ); + setModelData( model, modelTable( [ [ '00' ] ] ) ); const table = root.getChild( 0 ); - editor.conversion.elementToElement( { model: 'heading1', view: 'h1' } ); - model.change( writer => { writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'heading1' ); } ); @@ -139,4 +164,60 @@ describe( 'TableCell post-fixer', () => { [ '

00

' ] ], { asWidget: true } ) ); } ); + + it( 'should do nothing

when attribute value is changed', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + } ); + + it( 'should do nothing

when attribute value is changed on multiple nodes', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + setModelData( model, modelTable( [ [ '00[]00' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

00

' ] + ], { asWidget: true } ) ); + } ); + + it( 'should do nothing when setting attribute on other block item', () => { + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + + editor.model.schema.register( 'block', { + inheritAllFrom: '$block' + } ); + editor.conversion.elementToElement( { model: 'block', view: 'div' } ); + + setModelData( model, modelTable( [ [ 'foo' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '
foo
' ] + ], { asWidget: true } ) ); + } ); } ); From 3542841153f7198c9409ef77b47161608d62f5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Jul 2018 11:49:58 +0200 Subject: [PATCH 28/44] Tests: Fix table post fixer tests for changes in tests utils. --- src/converters/table-post-fixer.js | 4 +++- src/converters/upcasttable.js | 1 - tests/converters/table-post-fixer.js | 7 +++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/converters/table-post-fixer.js b/src/converters/table-post-fixer.js index 0cba6207..42cb3465 100644 --- a/src/converters/table-post-fixer.js +++ b/src/converters/table-post-fixer.js @@ -304,7 +304,9 @@ function fixTableRowsSizes( table, writer ) { if ( columnsToInsert ) { for ( let i = 0; i < columnsToInsert; i++ ) { - writer.insertElement( 'tableCell', Position.createAt( table.getChild( rowIndex ), 'end' ) ); + const tableCell = writer.createElement( 'tableCell' ); + writer.insert( tableCell, Position.createAt( table.getChild( rowIndex ), 'end' ) ); + writer.insertElement( 'paragraph', tableCell ); } wasFixed = true; diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index 1f4706f4..ee4d0eca 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -63,7 +63,6 @@ export default function upcastTable() { const tableCell = conversionApi.writer.createElement( 'tableCell' ); conversionApi.writer.insert( tableCell, ModelPosition.createAt( row, 'end' ) ); - conversionApi.writer.insertElement( 'paragraph', ModelPosition.createAt( tableCell, 'end' ) ); } diff --git a/tests/converters/table-post-fixer.js b/tests/converters/table-post-fixer.js index 7de0b546..9626a482 100644 --- a/tests/converters/table-post-fixer.js +++ b/tests/converters/table-post-fixer.js @@ -421,7 +421,9 @@ describe( 'Table post-fixer', () => { const table = root.getChild( 0 ); const tableRow = table.getChild( rowIndex ); - writer.insertElement( 'tableCell', tableRow, index ); + const tableCell = writer.createElement( 'tableCell' ); + writer.insert( tableCell, tableRow, index ); + writer.insertElement( 'paragraph', tableCell ); } function _setAttribute( writer, attributeKey, attributeValue, path ) { @@ -441,9 +443,10 @@ describe( 'Table post-fixer', () => { for ( const index of rows ) { const tableRow = table.getChild( index ); - const tableCell = writer.createElement( 'tableCell' ); + const tableCell = writer.createElement( 'tableCell' ); writer.insert( tableCell, tableRow, columnIndex ); + writer.insertElement( 'paragraph', tableCell ); } } } ); From ebeb53fae1d80bb80e78e0689d1825d355abd9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Jul 2018 12:45:37 +0200 Subject: [PATCH 29/44] Tests: Refactor tablecell-post-fixer tests. --- tests/converters/tablecell-post-fixer.js | 100 +++++++---------------- 1 file changed, 31 insertions(+), 69 deletions(-) diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js index cf98fff3..d1dc9ce0 100644 --- a/tests/converters/tablecell-post-fixer.js +++ b/tests/converters/tablecell-post-fixer.js @@ -33,27 +33,16 @@ describe( 'TableCell post-fixer', () => { defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); - injectTableCellPostFixer( model, editor.editing ); - } ); - } ); + editor.model.schema.register( 'block', { + inheritAllFrom: '$block' + } ); + editor.conversion.elementToElement( { model: 'block', view: 'div' } ); - it( 'should create element for single paragraph inside table cell', () => { - setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + model.schema.extend( '$block', { allowAttributes: 'foo' } ); + editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( - '
' + - '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '
' + - '00' + - '
' + - '
' - ) ); + injectTableCellPostFixer( model, editor.editing ); + } ); } ); it( 'should rename to

when more then one block content inside table cell', () => { @@ -76,26 +65,7 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should rename

to when removing all but one paragraph inside table cell', () => { - setModelData( model, modelTable( [ [ '00[]foo' ] ] ) ); - - const table = root.getChild( 0 ); - - model.change( writer => { - writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); - } ); - - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ - [ '00' ] - ], { asWidget: true } ) ); - } ); - it( 'should rename to

on adding other block element to the same table cell', () => { - editor.model.schema.register( 'block', { - inheritAllFrom: '$block' - } ); - editor.conversion.elementToElement( { model: 'block', view: 'div' } ); - setModelData( model, modelTable( [ [ '00[]' ] ] ) ); const table = root.getChild( 0 ); @@ -115,10 +85,7 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should rename to

when setting attribute on paragraph', () => { - model.schema.extend( '$block', { allowAttributes: 'foo' } ); - editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); - + it( 'should rename to

when setting attribute on ', () => { setModelData( model, modelTable( [ [ '00[]' ] ] ) ); const table = root.getChild( 0 ); @@ -132,43 +99,35 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should rename

to when removing attribute from paragraph', () => { - model.schema.extend( '$block', { allowAttributes: 'foo' } ); - editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); - - setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + it( 'should rename

to when removing all but one paragraph inside table cell', () => { + setModelData( model, modelTable( [ [ '00[]foo' ] ] ) ); const table = root.getChild( 0 ); model.change( writer => { - writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) ); + writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); } ); expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ - [ '00' ] + [ '00' ] ], { asWidget: true } ) ); } ); - it( 'should do nothing on rename to ', () => { - editor.conversion.elementToElement( { model: 'heading1', view: 'h1' } ); - - setModelData( model, modelTable( [ [ '00' ] ] ) ); + it( 'should rename

to when removing attribute from ', () => { + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); const table = root.getChild( 0 ); model.change( writer => { - writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'heading1' ); + writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) ); } ); expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ - [ '

00

' ] + [ '00' ] ], { asWidget: true } ) ); } ); - it( 'should do nothing

when attribute value is changed', () => { - model.schema.extend( '$block', { allowAttributes: 'foo' } ); - editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); - + it( 'should keep

in the view when attribute value is changed', () => { setModelData( model, modelTable( [ [ '00[]' ] ] ) ); const table = root.getChild( 0 ); @@ -182,10 +141,7 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should do nothing

when attribute value is changed on multiple nodes', () => { - model.schema.extend( '$block', { allowAttributes: 'foo' } ); - editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); - + it( 'should keep

in the view when attribute value is changed (table cell with multiple blocks)', () => { setModelData( model, modelTable( [ [ '00[]00' ] ] ) ); const table = root.getChild( 0 ); @@ -199,15 +155,21 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); - it( 'should do nothing when setting attribute on other block item', () => { - model.schema.extend( '$block', { allowAttributes: 'foo' } ); - editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } ); + it( 'should do nothing on rename to other block', () => { + setModelData( model, modelTable( [ [ '00' ] ] ) ); + + const table = root.getChild( 0 ); - editor.model.schema.register( 'block', { - inheritAllFrom: '$block' + model.change( writer => { + writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'block' ); } ); - editor.conversion.elementToElement( { model: 'block', view: 'div' } ); + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00
' ] + ], { asWidget: true } ) ); + } ); + + it( 'should do nothing when setting attribute on block item other then ', () => { setModelData( model, modelTable( [ [ 'foo' ] ] ) ); const table = root.getChild( 0 ); From f6934a141a6b11427fe88d4890efb587c72315e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Jul 2018 14:19:40 +0200 Subject: [PATCH 30/44] Other: Refactor table cell post fixer and add docs. --- src/converters/tablecell-post-fixer.js | 152 ++++++++++++++--------- tests/converters/tablecell-post-fixer.js | 28 ++++- 2 files changed, 121 insertions(+), 59 deletions(-) diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js index 4d4a8864..8579ed89 100644 --- a/src/converters/tablecell-post-fixer.js +++ b/src/converters/tablecell-post-fixer.js @@ -10,6 +10,63 @@ /** * Injects a table cell post-fixer into the editing controller. * + * The role of the table cell post-fixer is to ensure that the table cell contents in the editing view are properly converted. + * + * This post-fixer will ensure that after model changes in the editing view: + * * single paragraphs are rendered as ` + * * single paragraphs with one or more attributes are rendered as `

` + * * single paragraphs in table cell with other block elements are rendered as `

` + * * paragraphs in table cells with other block elements (including other paragraphs) are rendered as `

`. + * + * In the model each table cell has always at least one block element inside. If no other block was defined (empty table cell) the table + * feature will insert empty ``. Similarly text nodes will be wrapped in paragraphs. Rendering in the data pipeline differs + * from rendering in the editing pipeline - text nodes in single `` are rendered in the data pipeline as direct children + * of the `` or `` elements. In other cases `` elements are rendered as `

` blocks. + * + * To ensure proper mappings between model and view elements and positions in the editing pipeline the table feature will always render + * an element in the view: `` for single or empty `` and `

` otherwise. + * + * Example: + * + * + * + * + * foo + * foo + * barbaz + * + *
+ * + * The editor will render in the data pipeline: + * + *

+ * + * + * + * + * + * + * + * + * + *
foo

foo

bar

baz

+ *
+ * + * and in the editing view (without widget markup): + * + *
+ * + * + * + * + * + * + * + * + * + *
foo

foo

bar

baz

+ *
+ * * @param {module:engine/model/model~Model} model * @param {module:engine/controller/editingcontroller~EditingController} editing */ @@ -25,78 +82,57 @@ export default function injectTableCellPostFixer( model, editing ) { function tableCellPostFixer( writer, model, mapper ) { const changes = model.document.differ.getChanges(); + // While this is view post fixer only nodes that changed are worth investigating. for ( const entry of changes ) { - const tableCell = entry.position && entry.position.parent; - - if ( !tableCell && entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { + // Attribute change - check if it is single paragraph inside table cell that has attributes changed. + if ( entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) { const tableCell = entry.range.start.parent; if ( tableCell.childCount === 1 ) { const singleChild = tableCell.getChild( 0 ); + const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span'; - if ( !singleChild || !singleChild.is( 'paragraph' ) ) { - return; - } - - const viewElement = mapper.toViewElement( singleChild ); - - let renameTo = 'p'; - - if ( viewElement.name === 'p' ) { - if ( [ ...singleChild.getAttributes() ].length ) { - return; - } else { - renameTo = 'span'; - } - } - - const renamedViewElement = writer.rename( viewElement, renameTo ); - - // Re-bind table cell to renamed view element. - mapper.bindElements( singleChild, renamedViewElement ); + renameParagraphIfDifferent( singleChild, renameTo, writer, mapper ); } - } + } else { + // Check all nodes inside table cell on insert/remove operations (also other blocks). + const tableCell = entry.position && entry.position.parent; - if ( !tableCell ) { - continue; - } + if ( tableCell && tableCell.is( 'tableCell' ) ) { + const renameTo = tableCell.childCount > 1 ? 'p' : 'span'; - if ( tableCell.is( 'tableCell' ) ) { - if ( tableCell.childCount > 1 ) { for ( const child of tableCell.getChildren() ) { - if ( child.name != 'paragraph' ) { - continue; - } - - const viewElement = mapper.toViewElement( child ); - - if ( viewElement && viewElement.name === 'span' ) { - // Unbind table cell as will be renamed to

. - // mapper.unbindModelElement( tableCell ); - - const renamedViewElement = writer.rename( viewElement, 'p' ); - - // Re-bind table cell to renamed view element. - mapper.bindElements( child, renamedViewElement ); - } + renameParagraphIfDifferent( child, renameTo, writer, mapper ); } - } else { - const singleChild = tableCell.getChild( 0 ); + } + } + } +} - if ( !singleChild || !singleChild.is( 'paragraph' ) ) { - return; - } +// Renames associated view element to a desired one. It will only rename if: +// - model elemenet is a paragraph +// - view element is converted (mapped) +// - view element has different name then requested. +// +// @param modelElement +// @param desiredElementName +// @param {module:engine/view/writer~Writer} writer +// @param {module:engine/conversion/mapper~Mapper} mapper +function renameParagraphIfDifferent( modelElement, desiredElementName, writer, mapper ) { + // Only rename paragraph elements. + if ( !modelElement.is( 'paragraph' ) ) { + return; + } - const viewElement = mapper.toViewElement( singleChild ); + const viewElement = mapper.toViewElement( modelElement ); - // Unbind table cell as will be renamed to

. - // mapper.unbindModelElement( tableCell ); + // Only rename converted elements which aren't desired ones. + if ( !viewElement || viewElement.name === desiredElementName ) { + return; + } - const renamedViewElement = writer.rename( viewElement, 'span' ); + const renamedViewElement = writer.rename( viewElement, desiredElementName ); - // Re-bind table cell to renamed view element. - mapper.bindElements( singleChild, renamedViewElement ); - } - } - } + // Bind table cell to renamed view element. + mapper.bindElements( modelElement, renamedViewElement ); } diff --git a/tests/converters/tablecell-post-fixer.js b/tests/converters/tablecell-post-fixer.js index d1dc9ce0..3405a066 100644 --- a/tests/converters/tablecell-post-fixer.js +++ b/tests/converters/tablecell-post-fixer.js @@ -45,7 +45,7 @@ describe( 'TableCell post-fixer', () => { } ); } ); - it( 'should rename to

when more then one block content inside table cell', () => { + it( 'should rename to

when adding more elements to the same table cell', () => { setModelData( model, modelTable( [ [ '00[]' ] ] ) ); const table = root.getChild( 0 ); @@ -85,6 +85,32 @@ describe( 'TableCell post-fixer', () => { ], { asWidget: true } ) ); } ); + it( 'should properly rename the same element on consecutive changes', () => { + setModelData( model, modelTable( [ [ '00[]' ] ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] ); + + writer.insertElement( 'paragraph', nodeByPath, 'after' ); + + writer.setSelection( nodeByPath.nextSibling, 0 ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '

00

' ] + ], { asWidget: true } ) ); + + model.change( writer => { + writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) ); + } ); + + expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + [ '00' ] + ], { asWidget: true } ) ); + } ); + it( 'should rename to

when setting attribute on ', () => { setModelData( model, modelTable( [ [ '00[]' ] ] ) ); From e8f33d50257b2ce7519bc4b0f7d8814ae320e55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 31 Jul 2018 14:52:31 +0200 Subject: [PATCH 31/44] Other: Remove todos. --- src/converters/downcast.js | 1 - tests/_utils/utils.js | 1 - tests/converters/downcast.js | 1 - tests/manual/tableblockcontent.js | 3 +-- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/converters/downcast.js b/src/converters/downcast.js index 8fba7165..910b7b8f 100644 --- a/src/converters/downcast.js +++ b/src/converters/downcast.js @@ -395,7 +395,6 @@ function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPo conversionApi.mapper.bindElements( tableCell, cellElement ); } else { - // TODO: binding two to one seems supspicious... conversionApi.mapper.bindElements( tableCell, cellElement ); conversionApi.mapper.bindElements( innerParagraph, cellElement ); } diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index f778ede4..ba4b49e8 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -246,7 +246,6 @@ function makeRows( tableData, options ) { let resultingCellElement = cellElement; if ( isObject ) { - // TODO: check... if ( tableCellData.isHeading ) { resultingCellElement = headingElement; } diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index 93c79202..f4c3ad6c 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -495,7 +495,6 @@ describe( 'downcast converters', () => { writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' ); } ); - // TODO check span always? expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( '

' + diff --git a/tests/manual/tableblockcontent.js b/tests/manual/tableblockcontent.js index 1e2f27e1..596276f6 100644 --- a/tests/manual/tableblockcontent.js +++ b/tests/manual/tableblockcontent.js @@ -19,8 +19,7 @@ ClassicEditor plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment, Image, ImageCaption, ImageStyle ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', - 'alignment', 'insertImage', - '|', 'undo', 'redo' + 'alignment', '|', 'undo', 'redo' ], image: { toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ] From db63f1716259557352fb79438a499f538ae884a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Aug 2018 10:48:17 +0200 Subject: [PATCH 32/44] Changed: Make block content tab key event listener higher priority then table's one. --- src/tableediting.js | 122 ++++++++++++++++++------------------------ tests/tableediting.js | 21 +++++++- 2 files changed, 73 insertions(+), 70 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index e0344d1c..c87ea18f 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -10,7 +10,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import upcastTable, { upcastTableCell } from './converters/upcasttable'; import { @@ -31,7 +30,7 @@ import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; import SetHeaderColumnCommand from './commands/setheadercolumncommand'; -import { getParentElement, getParentTable } from './commands/utils'; +import { getParentElement } from './commands/utils'; import TableUtils from '../src/tableutils'; import injectTablePostFixer from './converters/table-post-fixer'; @@ -137,8 +136,9 @@ export default class TableEditing extends Plugin { injectTablePostFixer( model ); // Handle tab key navigation. - this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) ); - this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabInsideTable( ...args ) ); + this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } ); + this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } ); + this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } ); } /** @@ -156,27 +156,18 @@ export default class TableEditing extends Plugin { * @param {module:utils/eventinfo~EventInfo} eventInfo * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData */ - _handleTabOnSelectedTable( eventInfo, domEventData ) { - const tabPressed = domEventData.keyCode == keyCodes.tab; - - // Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler. - if ( !tabPressed || domEventData.ctrlKey ) { - return; - } - + _handleTabOnSelectedTable( domEventData, cancel ) { const editor = this.editor; const selection = editor.model.document.selection; if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) { const selectedElement = selection.getSelectedElement(); - if ( !selectedElement || selectedElement.name != 'table' ) { + if ( !selectedElement || !selectedElement.is( 'table' ) ) { return; } - eventInfo.stop(); - domEventData.preventDefault(); - domEventData.stopPropagation(); + cancel(); editor.model.change( writer => { writer.setSelection( Range.createIn( selectedElement.getChild( 0 ).getChild( 0 ) ) ); @@ -185,77 +176,70 @@ export default class TableEditing extends Plugin { } /** - * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed inside table - * cell. + * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed + * inside table cell. * * @private - * @param {module:utils/eventinfo~EventInfo} eventInfo - * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + * @param {Boolean} isForward Whether this handler will move selection to the next cell or previous. */ - _handleTabInsideTable( eventInfo, domEventData ) { - const tabPressed = domEventData.keyCode == keyCodes.tab; - - // Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler. - if ( !tabPressed || domEventData.ctrlKey ) { - return; - } - + _getTabHandler( isForward ) { const editor = this.editor; - const selection = editor.model.document.selection; - const firstPosition = selection.getFirstPosition(); + return ( domEventData, cancel ) => { + const selection = editor.model.document.selection; - const table = getParentTable( firstPosition ); + const firstPosition = selection.getFirstPosition(); - if ( !table ) { - return; - } + const tableCell = getParentElement( 'tableCell', firstPosition ); - domEventData.preventDefault(); - domEventData.stopPropagation(); + if ( !tableCell ) { + return; + } - const tableCell = getParentElement( 'tableCell', firstPosition ); - const tableRow = tableCell.parent; + cancel(); - const currentRowIndex = table.getChildIndex( tableRow ); - const currentCellIndex = tableRow.getChildIndex( tableCell ); + const tableRow = tableCell.parent; + const table = tableRow.parent; - const isForward = !domEventData.shiftKey; - const isFirstCellInRow = currentCellIndex === 0; + const currentRowIndex = table.getChildIndex( tableRow ); + const currentCellIndex = tableRow.getChildIndex( tableCell ); - if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) { - // It's the first cell of a table - don't do anything (stay in current position). - return; - } + const isFirstCellInRow = currentCellIndex === 0; - const isLastCellInRow = currentCellIndex === tableRow.childCount - 1; - const isLastRow = currentRowIndex === table.childCount - 1; + if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) { + // It's the first cell of a table - don't do anything (stay in current position). + return; + } - if ( isForward && isLastRow && isLastCellInRow ) { - editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } ); - } + const isLastCellInRow = currentCellIndex === tableRow.childCount - 1; + const isLastRow = currentRowIndex === table.childCount - 1; - let cellToFocus; + if ( isForward && isLastRow && isLastCellInRow ) { + editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } ); + } - // Move to first cell in next row. - if ( isForward && isLastCellInRow ) { - const nextRow = table.getChild( currentRowIndex + 1 ); + let cellToFocus; - cellToFocus = nextRow.getChild( 0 ); - } - // Move to last cell in a previous row. - else if ( !isForward && isFirstCellInRow ) { - const previousRow = table.getChild( currentRowIndex - 1 ); + // Move to first cell in next row. + if ( isForward && isLastCellInRow ) { + const nextRow = table.getChild( currentRowIndex + 1 ); - cellToFocus = previousRow.getChild( previousRow.childCount - 1 ); - } - // Move to next/previous cell. - else { - cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) ); - } + cellToFocus = nextRow.getChild( 0 ); + } + // Move to last cell in a previous row. + else if ( !isForward && isFirstCellInRow ) { + const previousRow = table.getChild( currentRowIndex - 1 ); - editor.model.change( writer => { - writer.setSelection( Range.createIn( cellToFocus ) ); - } ); + cellToFocus = previousRow.getChild( previousRow.childCount - 1 ); + } + // Move to next/previous cell. + else { + cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) ); + } + + editor.model.change( writer => { + writer.setSelection( Range.createIn( cellToFocus ) ); + } ); + }; } } diff --git a/tests/tableediting.js b/tests/tableediting.js index 157dea2b..da82a69c 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -293,6 +293,24 @@ describe( 'TableEditing', () => { ] ) ); } ); + it( 'should listen with lower priority then its children', () => { + // Cancel TAB event. + editor.keystrokes.set( 'Tab', ( data, cancel ) => cancel() ); + + setModelData( model, modelTable( [ + [ '11[]', '12' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '11[]', '12' ] + ] ) ); + } ); + describe( 'on table widget selected', () => { beforeEach( () => { editor.model.schema.register( 'block', { @@ -307,7 +325,7 @@ describe( 'TableEditing', () => { it( 'should move caret to the first table cell on TAB', () => { const spy = sinon.spy(); - editor.editing.view.document.on( 'keydown', spy ); + editor.keystrokes.set( 'Tab', spy, { priority: 'lowest' } ); setModelData( model, '[' + modelTable( [ [ '11', '12' ] @@ -377,6 +395,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '[11]', '12' ] ] ) ); From 16ed73b599658b7db0536b68b89e2b8796148cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 1 Aug 2018 11:31:06 +0200 Subject: [PATCH 33/44] Changed: Merging empty paragraphs should result in one paragraph. --- src/commands/mergecellcommand.js | 32 +++- tests/commands/mergecellcommand.js | 232 +++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 3 deletions(-) diff --git a/src/commands/mergecellcommand.js b/src/commands/mergecellcommand.js index 8e023b37..a3a653eb 100644 --- a/src/commands/mergecellcommand.js +++ b/src/commands/mergecellcommand.js @@ -97,9 +97,7 @@ export default class MergeCellCommand extends Command { // Cache the parent of cell to remove for later check. const removedTableCellRow = cellToRemove.parent; - // Remove table cell and merge it contents with merged cell. - writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) ); - writer.remove( cellToRemove ); + mergeTableCells( cellToRemove, cellToExpand, writer ); const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan'; const cellSpan = parseInt( tableCell.getAttribute( spanAttribute ) || 1 ); @@ -254,3 +252,31 @@ function removeEmptyRow( removedTableCellRow, writer ) { writer.remove( removedTableCellRow ); } + +// Merges two table cells - will ensure that after merging cells with empty paragraph the result table cell will only have one paragraph. +// If one of the merged table cell is empty the merged table cell will have contents of the non-empty table cell. +// If both are empty the merged table cell will have only one empty paragraph. +// +// @param {module:engine/model/element~Element} cellToRemove +// @param {module:engine/model/element~Element} cellToExpand +// @param {module:engine/model/writer~Writer} writer +function mergeTableCells( cellToRemove, cellToExpand, writer ) { + if ( !isEmpty( cellToRemove ) ) { + if ( isEmpty( cellToExpand ) ) { + writer.remove( Range.createIn( cellToExpand ) ); + } + + writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) ); + } + + // Remove merged table cell. + writer.remove( cellToRemove ); +} + +// Checks if passed table cell contains empty paragraph. +// +// @param {module:engine/model/element~Element} tableCell +// @returns {Boolean} +function isEmpty( tableCell ) { + return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty; +} diff --git a/tests/commands/mergecellcommand.js b/tests/commands/mergecellcommand.js index 2cdf63dc..bd856466 100644 --- a/tests/commands/mergecellcommand.js +++ b/tests/commands/mergecellcommand.js @@ -155,6 +155,60 @@ describe( 'MergeCellCommand', () => { [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); + + it( 'should result in single empty paragraph if both cells are empty', () => { + setData( model, modelTable( [ + [ '[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); + + it( 'should result in single paragraph (other cell is empty)', () => { + setData( model, modelTable( [ + [ 'foo[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should result in single paragraph (selection cell is empty)', () => { + setData( model, modelTable( [ + [ '[]', 'foo' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should not merge other empty blocks to single block', () => { + model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isBlock: true + } ); + + setData( model, modelTable( [ + [ '[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); } ); } ); @@ -281,6 +335,60 @@ describe( 'MergeCellCommand', () => { [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); + + it( 'should result in single empty paragraph if both cells are empty', () => { + setData( model, modelTable( [ + [ '', '[]' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); + + it( 'should result in single paragraph (other cell is empty)', () => { + setData( model, modelTable( [ + [ '', 'foo[]' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should result in single paragraph (selection cell is empty)', () => { + setData( model, modelTable( [ + [ 'foo', '[]' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should not merge other empty blocks to single block', () => { + model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isBlock: true + } ); + + setData( model, modelTable( [ + [ '', '[]' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); } ); } ); @@ -421,6 +529,68 @@ describe( 'MergeCellCommand', () => { ] ) ); } ); + it( 'should result in single empty paragraph if both cells are empty', () => { + setData( model, modelTable( [ + [ '[]', '' ], + [ '', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should result in single paragraph (other cell is empty)', () => { + setData( model, modelTable( [ + [ 'foo[]', '' ], + [ '', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[foo]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should result in single paragraph (selection cell is empty)', () => { + setData( model, modelTable( [ + [ '[]', '' ], + [ 'foo', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[foo]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should not merge other empty blocks to single block', () => { + model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isBlock: true + } ); + + setData( model, modelTable( [ + [ '[]', '' ], + [ '', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[]' }, '' ], + [ '' ] + ] ) ); + } ); + it( 'should remove empty row if merging table cells ', () => { setData( model, modelTable( [ [ { rowspan: 2, contents: '00' }, '01[]', { rowspan: 3, contents: '02' } ], @@ -595,6 +765,68 @@ describe( 'MergeCellCommand', () => { ] ) ); } ); + it( 'should result in single empty paragraph if both cells are empty', () => { + setData( model, modelTable( [ + [ '', '' ], + [ '[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should result in single paragraph (other cell is empty)', () => { + setData( model, modelTable( [ + [ '', '' ], + [ 'foo[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[foo]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should result in single paragraph (selection cell is empty)', () => { + setData( model, modelTable( [ + [ 'foo', '' ], + [ '[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[foo]' }, '' ], + [ '' ] + ] ) ); + } ); + + it( 'should not merge other empty blocks to single block', () => { + model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isBlock: true + } ); + + setData( model, modelTable( [ + [ '', '' ], + [ '[]', '' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { rowspan: 2, contents: '[]' }, '' ], + [ '' ] + ] ) ); + } ); + it( 'should properly merge cells in rows with spaned cells', () => { setData( model, modelTable( [ [ { rowspan: 3, contents: '00' }, '11', '12', '13' ], From 2fea49bf7a64e0200ff35b54578dba41a95338dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 10:28:05 +0200 Subject: [PATCH 34/44] Remove default priority from converters. --- src/converters/downcast.js | 10 +++++----- src/converters/upcasttable.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/converters/downcast.js b/src/converters/downcast.js index 910b7b8f..e7047734 100644 --- a/src/converters/downcast.js +++ b/src/converters/downcast.js @@ -74,7 +74,7 @@ export function downcastInsertTable( options = {} ) { conversionApi.mapper.bindElements( table, asWidget ? tableWidget : figureElement ); conversionApi.writer.insert( viewPosition, asWidget ? tableWidget : figureElement ); - }, { priority: 'normal' } ); + } ); } /** @@ -117,7 +117,7 @@ export function downcastInsertRow( options = {} ) { createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options ); } - }, { priority: 'normal' } ); + } ); } /** @@ -159,7 +159,7 @@ export function downcastInsertCell( options = {} ) { return; } } - }, { priority: 'normal' } ); + } ); } /** @@ -236,7 +236,7 @@ export function downcastTableHeadingRowsChange( options = {} ) { function isBetween( index, lower, upper ) { return index > lower && index < upper; } - }, { priority: 'normal' } ); + } ); } /** @@ -274,7 +274,7 @@ export function downcastTableHeadingColumnsChange( options = {} ) { renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget ); } - }, { priority: 'normal' } ); + } ); } /** diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index ee4d0eca..a2184045 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -89,7 +89,7 @@ export default function upcastTable() { } else { data.modelCursor = data.modelRange.end; } - }, { priority: 'normal' } ); + } ); }; } @@ -133,7 +133,7 @@ export function upcastTableCell( elementName ) { // Continue after inserted element. data.modelCursor = data.modelRange.end; - }, { priority: 'normal' } ); + } ); }; } From d9cbfe15292a48c11243e6dedff3cedd912ab6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 10:48:30 +0200 Subject: [PATCH 35/44] Unify creation of empty table cells with block content. --- src/commands/setheaderrowcommand.js | 7 +++---- src/commands/utils.js | 13 +++++++++++++ src/converters/table-post-fixer.js | 6 ++---- src/converters/upcasttable.js | 5 ++--- src/tableutils.js | 6 ++---- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/commands/setheaderrowcommand.js b/src/commands/setheaderrowcommand.js index 8e1a28c5..3d4e6374 100644 --- a/src/commands/setheaderrowcommand.js +++ b/src/commands/setheaderrowcommand.js @@ -10,7 +10,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { getParentElement, updateNumericAttribute } from './utils'; +import { createEmptyTableCell, getParentElement, updateNumericAttribute } from './utils'; import TableWalker from '../tablewalker'; /** @@ -169,10 +169,9 @@ function splitHorizontally( tableCell, headingRows, writer ) { if ( columnIndex !== undefined && columnIndex === column && row === endRow ) { const tableRow = table.getChild( row ); + const tableCellPosition = Position.createFromParentAndOffset( tableRow, cellIndex ); - const newCell = writer.createElement( 'tableCell', attributes ); - writer.insert( newCell, Position.createFromParentAndOffset( tableRow, cellIndex ) ); - writer.insertElement( 'paragraph', Position.createAt( newCell, 0 ) ); + createEmptyTableCell( writer, tableCellPosition, attributes ); } } diff --git a/src/commands/utils.js b/src/commands/utils.js index f22f4553..0dbb6e9e 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -52,3 +52,16 @@ export function updateNumericAttribute( key, value, item, writer, defaultValue = writer.removeAttribute( key, item ); } } + +/** + * Common method to create empty table cell - it will create proper model structure as table cell must have at least one block inside. + * + * @param {module:engine/model/writer~Writer} writer Model writer. + * @param {module:engine/model/position~Position} insertPosition Position at which table cell should be inserted. + * @param {Object} attributes Element's attributes. + */ +export function createEmptyTableCell( writer, insertPosition, attributes = {} ) { + const tableCell = writer.createElement( 'tableCell', attributes ); + writer.insertElement( 'paragraph', tableCell ); + writer.insert( tableCell, insertPosition ); +} diff --git a/src/converters/table-post-fixer.js b/src/converters/table-post-fixer.js index 42cb3465..6f4b0c3c 100644 --- a/src/converters/table-post-fixer.js +++ b/src/converters/table-post-fixer.js @@ -8,7 +8,7 @@ */ import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { getParentTable, updateNumericAttribute } from './../commands/utils'; +import { createEmptyTableCell, getParentTable, updateNumericAttribute } from './../commands/utils'; import TableWalker from './../tablewalker'; /** @@ -304,9 +304,7 @@ function fixTableRowsSizes( table, writer ) { if ( columnsToInsert ) { for ( let i = 0; i < columnsToInsert; i++ ) { - const tableCell = writer.createElement( 'tableCell' ); - writer.insert( tableCell, Position.createAt( table.getChild( rowIndex ), 'end' ) ); - writer.insertElement( 'paragraph', tableCell ); + createEmptyTableCell( writer, Position.createAt( table.getChild( rowIndex ), 'end' ) ); } wasFixed = true; diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index a2184045..a51e7cd5 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -9,6 +9,7 @@ import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range'; import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position'; +import { createEmptyTableCell } from '../commands/utils'; /** * View table element to model table element conversion helper. @@ -61,9 +62,7 @@ export default function upcastTable() { const row = conversionApi.writer.createElement( 'tableRow' ); conversionApi.writer.insert( row, ModelPosition.createAt( table, 'end' ) ); - const tableCell = conversionApi.writer.createElement( 'tableCell' ); - conversionApi.writer.insert( tableCell, ModelPosition.createAt( row, 'end' ) ); - conversionApi.writer.insertElement( 'paragraph', ModelPosition.createAt( tableCell, 'end' ) ); + createEmptyTableCell( conversionApi.writer, ModelPosition.createAt( row, 'end' ) ); } // Set conversion result range. diff --git a/src/tableutils.js b/src/tableutils.js index 4101a41a..4d69c3a0 100644 --- a/src/tableutils.js +++ b/src/tableutils.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import TableWalker from './tablewalker'; -import { getParentTable, updateNumericAttribute } from './commands/utils'; +import { createEmptyTableCell, getParentTable, updateNumericAttribute } from './commands/utils'; /** * The table utilities plugin. @@ -569,9 +569,7 @@ function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attr // @param {module:engine/model/position~Position} insertPosition function createCells( cells, writer, insertPosition, attributes = {} ) { for ( let i = 0; i < cells; i++ ) { - const tableCell = writer.createElement( 'tableCell', attributes ); - writer.insert( tableCell, insertPosition ); - writer.insertElement( 'paragraph', Position.createAt( tableCell ) ); + createEmptyTableCell( writer, insertPosition, attributes ); } } From d9f2790eec5d6b0aba6049216fb3694a3417c69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 11:06:24 +0200 Subject: [PATCH 36/44] Changed: Removed commands/utils~getParentTable() method and renamed commands/utils~getParentElement() to findAncestor(). --- src/commands/insertcolumncommand.js | 6 +++--- src/commands/insertrowcommand.js | 6 +++--- src/commands/mergecellcommand.js | 6 +++--- src/commands/removecolumncommand.js | 6 +++--- src/commands/removerowcommand.js | 6 +++--- src/commands/setheadercolumncommand.js | 6 +++--- src/commands/setheaderrowcommand.js | 6 +++--- src/commands/splitcellcommand.js | 6 +++--- src/commands/utils.js | 14 ++------------ src/converters/table-post-fixer.js | 6 +++--- src/tableediting.js | 7 ++++--- src/tableutils.js | 10 ++++++---- src/ui/utils.js | 4 ++-- src/utils.js | 6 +++--- tests/commands/utils.js | 6 +++--- 15 files changed, 47 insertions(+), 54 deletions(-) diff --git a/src/commands/insertcolumncommand.js b/src/commands/insertcolumncommand.js index 28eb5dae..854926a5 100644 --- a/src/commands/insertcolumncommand.js +++ b/src/commands/insertcolumncommand.js @@ -8,7 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentElement, getParentTable } from './utils'; +import { findAncestor } from './utils'; import TableUtils from '../tableutils'; /** @@ -54,7 +54,7 @@ export default class InsertColumnCommand extends Command { refresh() { const selection = this.editor.model.document.selection; - const tableParent = getParentTable( selection.getFirstPosition() ); + const tableParent = findAncestor( 'table', selection.getFirstPosition() ); this.isEnabled = !!tableParent; } @@ -74,7 +74,7 @@ export default class InsertColumnCommand extends Command { const firstPosition = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', firstPosition ); + const tableCell = findAncestor( 'tableCell', firstPosition ); const table = tableCell.parent.parent; const { column } = tableUtils.getCellLocation( tableCell ); diff --git a/src/commands/insertrowcommand.js b/src/commands/insertrowcommand.js index 0771d895..2e65d12c 100644 --- a/src/commands/insertrowcommand.js +++ b/src/commands/insertrowcommand.js @@ -8,7 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentElement, getParentTable } from './utils'; +import { findAncestor } from './utils'; import TableUtils from '../tableutils'; /** @@ -54,7 +54,7 @@ export default class InsertRowCommand extends Command { refresh() { const selection = this.editor.model.document.selection; - const tableParent = getParentTable( selection.getFirstPosition() ); + const tableParent = findAncestor( 'table', selection.getFirstPosition() ); this.isEnabled = !!tableParent; } @@ -71,7 +71,7 @@ export default class InsertRowCommand extends Command { const selection = editor.model.document.selection; const tableUtils = editor.plugins.get( TableUtils ); - const tableCell = getParentElement( 'tableCell', selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/mergecellcommand.js b/src/commands/mergecellcommand.js index a3a653eb..29bfdcf5 100644 --- a/src/commands/mergecellcommand.js +++ b/src/commands/mergecellcommand.js @@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import TableWalker from '../tablewalker'; -import { getParentElement, updateNumericAttribute } from './utils'; +import { findAncestor, updateNumericAttribute } from './utils'; import TableUtils from '../tableutils'; /** @@ -83,7 +83,7 @@ export default class MergeCellCommand extends Command { execute() { const model = this.editor.model; const doc = model.document; - const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); const cellToMerge = this.value; const direction = this.direction; @@ -123,7 +123,7 @@ export default class MergeCellCommand extends Command { _getMergeableCell() { const model = this.editor.model; const doc = model.document; - const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); if ( !tableCell ) { return; diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index 918b9d5a..84417e8d 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; import TableUtils from '../tableutils'; -import { getParentElement, updateNumericAttribute } from './utils'; +import { findAncestor, updateNumericAttribute } from './utils'; /** * The remove column command. @@ -33,7 +33,7 @@ export default class RemoveColumnCommand extends Command { const selection = editor.model.document.selection; const tableUtils = editor.plugins.get( TableUtils ); - const tableCell = getParentElement( 'tableCell', selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() ); this.isEnabled = !!tableCell && tableUtils.getColumns( tableCell.parent.parent ) > 1; } @@ -47,7 +47,7 @@ export default class RemoveColumnCommand extends Command { const firstPosition = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', firstPosition ); + const tableCell = findAncestor( 'tableCell', firstPosition ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index efcba598..42c8a524 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -12,7 +12,7 @@ import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import TableWalker from '../tablewalker'; -import { getParentElement, updateNumericAttribute } from './utils'; +import { findAncestor, updateNumericAttribute } from './utils'; /** * The remove row command. @@ -33,7 +33,7 @@ export default class RemoveRowCommand extends Command { const model = this.editor.model; const doc = model.document; - const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); this.isEnabled = !!tableCell && tableCell.parent.parent.childCount > 1; } @@ -46,7 +46,7 @@ export default class RemoveRowCommand extends Command { const selection = model.document.selection; const firstPosition = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', firstPosition ); + const tableCell = findAncestor( 'tableCell', firstPosition ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/setheadercolumncommand.js b/src/commands/setheadercolumncommand.js index 3ced4283..cceac69f 100644 --- a/src/commands/setheadercolumncommand.js +++ b/src/commands/setheadercolumncommand.js @@ -9,7 +9,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { getParentElement, updateNumericAttribute } from './utils'; +import { findAncestor, updateNumericAttribute } from './utils'; /** * The header column command. @@ -36,7 +36,7 @@ export default class SetHeaderColumnCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', position ); + const tableCell = findAncestor( 'tableCell', position ); const isInTable = !!tableCell; @@ -69,7 +69,7 @@ export default class SetHeaderColumnCommand extends Command { const tableUtils = this.editor.plugins.get( 'TableUtils' ); const position = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', position.parent ); + const tableCell = findAncestor( 'tableCell', position.parent ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/setheaderrowcommand.js b/src/commands/setheaderrowcommand.js index 3d4e6374..e0c2710c 100644 --- a/src/commands/setheaderrowcommand.js +++ b/src/commands/setheaderrowcommand.js @@ -10,7 +10,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { createEmptyTableCell, getParentElement, updateNumericAttribute } from './utils'; +import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils'; import TableWalker from '../tablewalker'; /** @@ -37,7 +37,7 @@ export default class SetHeaderRowCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', position ); + const tableCell = findAncestor( 'tableCell', position ); const isInTable = !!tableCell; this.isEnabled = isInTable; @@ -68,7 +68,7 @@ export default class SetHeaderRowCommand extends Command { const selection = doc.selection; const position = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', position ); + const tableCell = findAncestor( 'tableCell', position ); const tableRow = tableCell.parent; const table = tableRow.parent; diff --git a/src/commands/splitcellcommand.js b/src/commands/splitcellcommand.js index fb929040..8bd5c6b3 100644 --- a/src/commands/splitcellcommand.js +++ b/src/commands/splitcellcommand.js @@ -9,7 +9,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableUtils from '../tableutils'; -import { getParentElement } from './utils'; +import { findAncestor } from './utils'; /** * The split cell command. @@ -50,7 +50,7 @@ export default class SplitCellCommand extends Command { const model = this.editor.model; const doc = model.document; - const tableCell = getParentElement( 'tableCell', doc.selection.getFirstPosition() ); + const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); this.isEnabled = !!tableCell; } @@ -64,7 +64,7 @@ export default class SplitCellCommand extends Command { const selection = document.selection; const firstPosition = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', firstPosition ); + const tableCell = findAncestor( 'tableCell', firstPosition ); const isHorizontally = this.direction === 'horizontally'; diff --git a/src/commands/utils.js b/src/commands/utils.js index 0dbb6e9e..782af070 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -7,24 +7,14 @@ * @module table/commands/utils */ -/** - * Returns the parent table. Returns undefined if position is not inside table. - * - * @param {module:engine/model/position~Position} position - * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} - */ -export function getParentTable( position ) { - return getParentElement( 'table', position ); -} - /** * Returns the parent element of given name. Returns undefined if position is not inside desired parent. * * @param {String} parentName Name of parent element to find. - * @param {module:engine/model/position~Position} position + * @param {module:engine/model/position~Position|module:engine/model/position~Position} position Position to start searching. * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} */ -export function getParentElement( parentName, position ) { +export function findAncestor( parentName, position ) { let parent = position.parent; while ( parent ) { diff --git a/src/converters/table-post-fixer.js b/src/converters/table-post-fixer.js index 6f4b0c3c..16023a9f 100644 --- a/src/converters/table-post-fixer.js +++ b/src/converters/table-post-fixer.js @@ -8,7 +8,7 @@ */ import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { createEmptyTableCell, getParentTable, updateNumericAttribute } from './../commands/utils'; +import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './../commands/utils'; import TableWalker from './../tablewalker'; /** @@ -240,12 +240,12 @@ function tablePostFixer( writer, model ) { // Fix table on adding/removing table cells and rows. if ( entry.name == 'tableRow' || entry.name == 'tableCell' ) { - table = getParentTable( entry.position ); + table = findAncestor( 'table', entry.position ); } // Fix table on any table's attribute change - including attributes of table cells. if ( isTableAttributeEntry( entry ) ) { - table = getParentTable( entry.range.start ); + table = findAncestor( 'table', entry.range.start ); } if ( table && !analyzedTables.has( table ) ) { diff --git a/src/tableediting.js b/src/tableediting.js index c87ea18f..7e0da835 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -30,12 +30,13 @@ import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; import SetHeaderColumnCommand from './commands/setheadercolumncommand'; -import { getParentElement } from './commands/utils'; +import { findAncestor } from './commands/utils'; import TableUtils from '../src/tableutils'; + import injectTablePostFixer from './converters/table-post-fixer'; +import injectTableCellPostFixer from './converters/tablecell-post-fixer'; import '../theme/tableediting.css'; -import injectTableCellPostFixer from './converters/tablecell-post-fixer'; /** * The table editing feature. @@ -190,7 +191,7 @@ export default class TableEditing extends Plugin { const firstPosition = selection.getFirstPosition(); - const tableCell = getParentElement( 'tableCell', firstPosition ); + const tableCell = findAncestor( 'tableCell', firstPosition ); if ( !tableCell ) { return; diff --git a/src/tableutils.js b/src/tableutils.js index 4d69c3a0..16a6e60f 100644 --- a/src/tableutils.js +++ b/src/tableutils.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import TableWalker from './tablewalker'; -import { createEmptyTableCell, getParentTable, updateNumericAttribute } from './commands/utils'; +import { createEmptyTableCell, updateNumericAttribute } from './commands/utils'; /** * The table utilities plugin. @@ -294,7 +294,8 @@ export default class TableUtils extends Plugin { */ splitCellVertically( tableCell, numberOfCells = 2 ) { const model = this.editor.model; - const table = getParentTable( tableCell ); + const tableRow = tableCell.parent; + const table = tableRow.parent; const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); @@ -430,8 +431,9 @@ export default class TableUtils extends Plugin { splitCellHorizontally( tableCell, numberOfCells = 2 ) { const model = this.editor.model; - const table = getParentTable( tableCell ); - const splitCellRow = table.getChildIndex( tableCell.parent ); + const tableRow = tableCell.parent; + const table = tableRow.parent; + const splitCellRow = table.getChildIndex( tableRow ); const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); diff --git a/src/ui/utils.js b/src/ui/utils.js index 0fbd984b..ef42b1a7 100644 --- a/src/ui/utils.js +++ b/src/ui/utils.js @@ -8,7 +8,7 @@ */ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview'; -import { getParentTable } from '../commands/utils'; +import { findAncestor } from '../commands/utils'; /** * A helper utility that positions the @@ -36,7 +36,7 @@ export function getBalloonPositionData( editor ) { const defaultPositions = BalloonPanelView.defaultPositions; const viewSelection = editingView.document.selection; - const parentTable = getParentTable( viewSelection.getFirstPosition() ); + const parentTable = findAncestor( 'table', viewSelection.getFirstPosition() ); return { target: editingView.domConverter.viewToDom( parentTable ), diff --git a/src/utils.js b/src/utils.js index 1152af95..47ff6921 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,8 +7,8 @@ * @module table/utils */ -import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils'; -import { getParentTable } from './commands/utils'; +import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; +import { findAncestor } from './commands/utils'; const tableSymbol = Symbol( 'isTable' ); @@ -57,7 +57,7 @@ export function isTableWidgetSelected( selection ) { * @returns {Boolean} */ export function isTableContentSelected( selection ) { - const parentTable = getParentTable( selection.getFirstPosition() ); + const parentTable = findAncestor( 'table', selection.getFirstPosition() ); return !!( parentTable && isTableWidget( parentTable.parent ) ); } diff --git a/tests/commands/utils.js b/tests/commands/utils.js index 0eb193a0..ef6f3d5c 100644 --- a/tests/commands/utils.js +++ b/tests/commands/utils.js @@ -8,7 +8,7 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils'; -import { getParentTable } from '../../src/commands/utils'; +import { findAncestor } from '../../src/commands/utils'; describe( 'commands utils', () => { let editor, model; @@ -32,13 +32,13 @@ describe( 'commands utils', () => { it( 'should return undefined if not in table', () => { setData( model, 'foo[]' ); - expect( getParentTable( model.document.selection.focus ) ).to.be.undefined; + expect( findAncestor( 'table', model.document.selection.focus ) ).to.be.undefined; } ); it( 'should return table if position is in tableCell', () => { setData( model, modelTable( [ [ '[]' ] ] ) ); - const parentTable = getParentTable( model.document.selection.focus ); + const parentTable = findAncestor( 'table', model.document.selection.focus ); expect( parentTable ).to.not.be.undefined; expect( parentTable.is( 'table' ) ).to.be.true; From 9b2fe294489164680b980fb9a4af8446dc687966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 11:08:25 +0200 Subject: [PATCH 37/44] Rename tableCell to parent as tableCell might be confusing. --- src/converters/tablecell-post-fixer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js index 8579ed89..1f942b10 100644 --- a/src/converters/tablecell-post-fixer.js +++ b/src/converters/tablecell-post-fixer.js @@ -96,12 +96,12 @@ function tableCellPostFixer( writer, model, mapper ) { } } else { // Check all nodes inside table cell on insert/remove operations (also other blocks). - const tableCell = entry.position && entry.position.parent; + const parent = entry.position && entry.position.parent; - if ( tableCell && tableCell.is( 'tableCell' ) ) { - const renameTo = tableCell.childCount > 1 ? 'p' : 'span'; + if ( parent && parent.is( 'tableCell' ) ) { + const renameTo = parent.childCount > 1 ? 'p' : 'span'; - for ( const child of tableCell.getChildren() ) { + for ( const child of parent.getChildren() ) { renameParagraphIfDifferent( child, renameTo, writer, mapper ); } } From d304c56f0ebfdcc366df009d817551bdf6bd48a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 7 Aug 2018 16:55:51 +0200 Subject: [PATCH 38/44] Fix view selection on renamed elements by the table cell post-fixer. --- src/converters/tablecell-post-fixer.js | 60 ++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js index 1f942b10..2a803864 100644 --- a/src/converters/tablecell-post-fixer.js +++ b/src/converters/tablecell-post-fixer.js @@ -7,6 +7,8 @@ * @module table/converters/tablecell-post-fixer */ +import Range from '@ckeditor/ckeditor5-engine/src/view/range'; + /** * Injects a table cell post-fixer into the editing controller. * @@ -71,7 +73,7 @@ * @param {module:engine/controller/editingcontroller~EditingController} editing */ export default function injectTableCellPostFixer( model, editing ) { - editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) ); + editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing ) ); } // The table cell post-fixer. @@ -79,8 +81,9 @@ export default function injectTableCellPostFixer( model, editing ) { // @param {module:engine/view/writer~Writer} writer // @param {module:engine/model/model~Model} model // @param {module:engine/conversion/mapper~Mapper} mapper -function tableCellPostFixer( writer, model, mapper ) { +function tableCellPostFixer( writer, model, mapper, editing ) { const changes = model.document.differ.getChanges(); + let wasFixed = false; // While this is view post fixer only nodes that changed are worth investigating. for ( const entry of changes ) { @@ -92,7 +95,7 @@ function tableCellPostFixer( writer, model, mapper ) { const singleChild = tableCell.getChild( 0 ); const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span'; - renameParagraphIfDifferent( singleChild, renameTo, writer, mapper ); + wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, mapper, editing, model ) || wasFixed; } } else { // Check all nodes inside table cell on insert/remove operations (also other blocks). @@ -102,15 +105,17 @@ function tableCellPostFixer( writer, model, mapper ) { const renameTo = parent.childCount > 1 ? 'p' : 'span'; for ( const child of parent.getChildren() ) { - renameParagraphIfDifferent( child, renameTo, writer, mapper ); + wasFixed = renameParagraphIfDifferent( child, renameTo, writer, mapper, editing, model ) || wasFixed; } } } } + + return wasFixed; } // Renames associated view element to a desired one. It will only rename if: -// - model elemenet is a paragraph +// - model element is a paragraph // - view element is converted (mapped) // - view element has different name then requested. // @@ -118,21 +123,60 @@ function tableCellPostFixer( writer, model, mapper ) { // @param desiredElementName // @param {module:engine/view/writer~Writer} writer // @param {module:engine/conversion/mapper~Mapper} mapper -function renameParagraphIfDifferent( modelElement, desiredElementName, writer, mapper ) { +function renameParagraphIfDifferent( modelElement, desiredElementName, writer, mapper, editing, model ) { // Only rename paragraph elements. if ( !modelElement.is( 'paragraph' ) ) { - return; + return false; } const viewElement = mapper.toViewElement( modelElement ); // Only rename converted elements which aren't desired ones. if ( !viewElement || viewElement.name === desiredElementName ) { - return; + return false; } + const rangesToFix = checkRangesToFix( model, modelElement, editing, mapper ); + + mapper.unbindViewElement( viewElement ); + const renamedViewElement = writer.rename( viewElement, desiredElementName ); // Bind table cell to renamed view element. mapper.bindElements( modelElement, renamedViewElement ); + + if ( rangesToFix.length ) { + fixViewSelection( rangesToFix, mapper, writer ); + } + + return true; +} + +function checkRangesToFix( model, modelElement, editing, mapper ) { + const needsFix = !![ ...model.document.selection.getSelectedBlocks() ].find( el => el === modelElement ); + + if ( !needsFix ) { + return []; + } + + const rangesToFix = []; + + const selection = editing.view.document.selection; + + const ranges = selection.getRanges(); + + for ( const range of ranges ) { + rangesToFix.push( { + start: mapper.toModelPosition( range.start ), + end: mapper.toModelPosition( range.end ) + } ); + } + + return rangesToFix; +} + +function fixViewSelection( rangesToFix, mapper, writer ) { + const fixedRanges = rangesToFix.map( range => new Range( mapper.toViewPosition( range.start ), mapper.toViewPosition( range.end ) ) ); + + writer.setSelection( fixedRanges ); } From 3f1b8115dbf5e6dacc0ef83e132c61c760be13ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 8 Aug 2018 15:23:10 +0200 Subject: [PATCH 39/44] Disallow image in table. --- src/converters/upcasttable.js | 7 ++- src/tableediting.js | 7 +++ tests/_utils/utils.js | 7 +++ tests/converters/upcasttable.js | 95 ++++++++++++++++++++++++++------- tests/table-integration.js | 87 +++++++++++++++--------------- tests/tableediting.js | 11 +++- 6 files changed, 149 insertions(+), 65 deletions(-) diff --git a/src/converters/upcasttable.js b/src/converters/upcasttable.js index a51e7cd5..9c329471 100644 --- a/src/converters/upcasttable.js +++ b/src/converters/upcasttable.js @@ -116,7 +116,12 @@ export function upcastTableCell( elementName ) { conversionApi.consumable.consume( viewTableCell, { name: true } ); for ( const child of viewTableCell.getChildren() ) { - conversionApi.convertItem( child, ModelPosition.createAt( tableCell, 'end' ) ); + const { modelCursor } = conversionApi.convertItem( child, ModelPosition.createAt( tableCell, 'end' ) ); + + // Ensure empty paragraph in table cell. + if ( modelCursor.parent.name == 'tableCell' && !modelCursor.parent.childCount ) { + conversionApi.writer.insertElement( 'paragraph', modelCursor ); + } } // Set conversion result range. diff --git a/src/tableediting.js b/src/tableediting.js index 7e0da835..3c1c03b8 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -81,6 +81,13 @@ export default class TableEditing extends Plugin { } } ); + // Disallow image in table cell. + schema.addChildCheck( ( context, childDefinition ) => { + if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) { + return false; + } + } ); + // Table conversion. conversion.for( 'upcast' ).add( upcastTable() ); diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index ba4b49e8..a4d419c6 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -185,6 +185,13 @@ export function defaultSchema( schema, registerParagraph = true ) { } } ); + // Disallow image in table. + schema.addChildCheck( ( context, childDefinition ) => { + if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) { + return false; + } + } ); + if ( registerParagraph ) { schema.register( 'paragraph', { inheritAllFrom: '$block' } ); } diff --git a/tests/converters/upcasttable.js b/tests/converters/upcasttable.js index bba4cc26..9cf0ff45 100644 --- a/tests/converters/upcasttable.js +++ b/tests/converters/upcasttable.js @@ -6,10 +6,11 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable'; -import { defaultSchema, formatTable } from '../_utils/utils'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; + +import { defaultConversion, defaultSchema, formatTable, modelTable } from '../_utils/utils'; describe( 'upcastTable()', () => { let editor, model; @@ -17,29 +18,15 @@ describe( 'upcastTable()', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph ] + plugins: [ Paragraph, ImageEditing, Widget ] } ) .then( newEditor => { editor = newEditor; model = editor.model; - const conversion = editor.conversion; - defaultSchema( model.schema, false ); - // Table conversion. - conversion.for( 'upcast' ).add( upcastTable() ); - - // Table row conversion. - conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) ); - - // Table cell conversion. - conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) ); - conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) ); - - // Table attributes conversion. - conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); - conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } ); + defaultConversion( editor.conversion, true ); // Since this part of test tests only view->model conversion editing pipeline is not necessary // so defining model->view converters won't be necessary. @@ -397,4 +384,74 @@ describe( 'upcastTable()', () => { ); } ); } ); + + describe( 'block contents', () => { + it( 'should upcast table with empty table cell to paragraph', () => { + editor.setData( + '' + + '' + + '' + + '' + + '' + + '' + + '
foo
' + ); + + expectModel( modelTable( [ + [ 'foo' ] + ] ) ); + } ); + + it( 'should upcast table with

in table cell', () => { + editor.setData( + '' + + '' + + '' + + '' + + '' + + '' + + '

foo

' + ); + + expectModel( modelTable( [ + [ 'foo' ] + ] ) ); + } ); + + it( 'should upcast table with multiple

in table cell', () => { + editor.setData( + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '

foo

' + + '

bar

' + + '

baz

' + + '
' + ); + + expectModel( modelTable( [ + [ 'foobarbaz' ] + ] ) ); + } ); + + it( 'should upcast table with in table cell to empty table cell', () => { + editor.setData( + '' + + '' + + '' + + '' + + '' + + '' + + '
' + ); + + expectModel( modelTable( [ + [ '' ] + ] ) ); + } ); + } ); } ); diff --git a/tests/table-integration.js b/tests/table-integration.js index 2282697b..e9ff5372 100644 --- a/tests/table-integration.js +++ b/tests/table-integration.js @@ -16,7 +16,7 @@ import { import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import TableEditing from '../src/tableediting'; -import { formatTable, formattedModelTable, modelTable } from './_utils/utils'; +import { formatTable, formattedModelTable, modelTable, viewTable } from './_utils/utils'; describe( 'Table feature – integration', () => { describe( 'with clipboard', () => { @@ -58,72 +58,71 @@ describe( 'Table feature – integration', () => { } ); describe( 'with undo', () => { - it( 'fixing empty roots should be transparent to undo', () => { + let editor, doc, root; + + beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, UndoEditing ] } ) + .create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } ) .then( newEditor => { - const editor = newEditor; - const doc = editor.model.document; - const root = doc.getRoot(); + editor = newEditor; + doc = editor.model.document; + root = doc.getRoot(); + } ); + } ); - expect( editor.getData() ).to.equal( '

 

' ); - expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false; + it( 'fixing empty roots should be transparent to undo', () => { + expect( editor.getData() ).to.equal( '

 

' ); + expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false; - editor.setData( '

Foobar.

' ); + editor.data.set( viewTable( [ [ 'foo' ] ] ) ); - editor.model.change( writer => { - writer.remove( root.getChild( 0 ) ); - } ); + expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) ); - expect( editor.getData() ).to.equal( '

 

' ); + editor.model.change( writer => { + writer.remove( root.getChild( 0 ) ); + } ); - editor.execute( 'undo' ); + expect( editor.getData() ).to.equal( '

 

' ); - expect( editor.getData() ).to.equal( '

Foobar.

' ); + editor.execute( 'undo' ); - editor.execute( 'redo' ); + expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) ); - expect( editor.getData() ).to.equal( '

 

' ); + editor.execute( 'redo' ); - editor.execute( 'undo' ); + expect( editor.getData() ).to.equal( '

 

' ); - expect( editor.getData() ).to.equal( '

Foobar.

' ); - } ); + editor.execute( 'undo' ); + + expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) ); } ); it( 'fixing empty roots should be transparent to undo - multiple roots', () => { - return VirtualTestEditor - .create( { plugins: [ Paragraph, UndoEditing ] } ) - .then( newEditor => { - const editor = newEditor; - const doc = editor.model.document; - const root = doc.getRoot(); - const otherRoot = doc.createRoot( '$root', 'otherRoot' ); + const otherRoot = doc.createRoot( '$root', 'otherRoot' ); - editor.data.set( '

Foobar.

', 'main' ); - editor.data.set( '

Foobar.

', 'otherRoot' ); + editor.data.set( viewTable( [ [ 'foo' ] ] ), 'main' ); + editor.data.set( viewTable( [ [ 'foo' ] ] ), 'otherRoot' ); - editor.model.change( writer => { - writer.remove( root.getChild( 0 ) ); - } ); + editor.model.change( writer => { + writer.remove( root.getChild( 0 ) ); + } ); - editor.model.change( writer => { - writer.remove( otherRoot.getChild( 0 ) ); - } ); + editor.model.change( writer => { + writer.remove( otherRoot.getChild( 0 ) ); + } ); - expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); - expect( editor.data.get( 'otherRoot' ) ).to.equal( '

 

' ); + expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( '

 

' ); - editor.execute( 'undo' ); + editor.execute( 'undo' ); - expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); - expect( editor.data.get( 'otherRoot' ) ).to.equal( '

Foobar.

' ); + expect( editor.data.get( 'main' ) ).to.equal( '

 

' ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) ); - editor.execute( 'undo' ); + editor.execute( 'undo' ); - expect( editor.data.get( 'main' ) ).to.equal( '

Foobar.

' ); - expect( editor.data.get( 'otherRoot' ) ).to.equal( '

Foobar.

' ); - } ); + expect( editor.data.get( 'main' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) ); + expect( editor.data.get( 'otherRoot' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) ); } ); } ); } ); diff --git a/tests/tableediting.js b/tests/tableediting.js index da82a69c..736ea4db 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -7,6 +7,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; import TableEditing from '../src/tableediting'; import { formatTable, formattedModelTable, modelTable } from './_utils/utils'; @@ -26,7 +27,7 @@ describe( 'TableEditing', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, Paragraph ] + plugins: [ TableEditing, Paragraph, ImageEditing ] } ) .then( newEditor => { editor = newEditor; @@ -72,6 +73,7 @@ describe( 'TableEditing', () => { expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.false; expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$block' ) ).to.be.true; expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'table' ) ).to.be.false; + expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'image' ) ).to.be.false; } ); it( 'adds insertTable command', () => { @@ -175,6 +177,13 @@ describe( 'TableEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( 'foo
' ); } ); + + it( 'should convert table with image', () => { + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '
' ); + } ); } ); } ); From dd878ca10d0578d19e1047a2d1ebd3b33844beac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 8 Aug 2018 15:51:29 +0200 Subject: [PATCH 40/44] Add more tests for table integration. --- tests/table-integration.js | 69 +++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/tests/table-integration.js b/tests/table-integration.js index e9ff5372..78d048ec 100644 --- a/tests/table-integration.js +++ b/tests/table-integration.js @@ -8,6 +8,8 @@ import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; +import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { getData as getModelData, @@ -20,40 +22,61 @@ import { formatTable, formattedModelTable, modelTable, viewTable } from './_util describe( 'Table feature – integration', () => { describe( 'with clipboard', () => { - it( 'pastes td as p when pasting into the table', () => { + let editor, clipboard; + + beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, Widget, Clipboard ] } ) + .create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) .then( newEditor => { - const editor = newEditor; - const clipboard = editor.plugins.get( 'Clipboard' ); + editor = newEditor; + clipboard = editor.plugins.get( 'Clipboard' ); + } ); + } ); - setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) ); + it( 'pastes td as p when pasting into the table', () => { + setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) ); - clipboard.fire( 'inputTransformation', { - content: parseView( 'bar' ) - } ); + clipboard.fire( 'inputTransformation', { + content: parseView( 'bar' ) + } ); - expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [ - [ 'foobar[]' ] - ] ) ); - } ); + expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [ + [ 'foobar[]' ] + ] ) ); } ); it( 'pastes td as p when pasting into the p', () => { - return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, Widget, Clipboard ] } ) - .then( newEditor => { - const editor = newEditor; - const clipboard = editor.plugins.get( 'Clipboard' ); + setModelData( editor.model, 'foo[]' ); - setModelData( editor.model, 'foo[]' ); + clipboard.fire( 'inputTransformation', { + content: parseView( 'bar' ) + } ); - clipboard.fire( 'inputTransformation', { - content: parseView( 'bar' ) - } ); + expect( formatTable( getModelData( editor.model ) ) ).to.equal( 'foobar[]' ); + } ); - expect( formatTable( getModelData( editor.model ) ) ).to.equal( 'foobar[]' ); - } ); + it( 'pastes list into the td', () => { + setModelData( editor.model, modelTable( [ [ '[]' ] ] ) ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • bar
  • ' ) + } ); + + expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [ + [ 'bar[]' ] + ] ) ); + } ); + + it( 'pastes blockquote into the td', () => { + setModelData( editor.model, modelTable( [ [ '[]' ] ] ) ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
    bar
    ' ) + } ); + + expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [ + [ '
    bar[]
    ' ] + ] ) ); } ); } ); From 4c2d0b5d4f038de2151144422f66dffba249c24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 8 Aug 2018 16:00:02 +0200 Subject: [PATCH 41/44] Fix package.json missing dev dependencies. --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6b17095e..35910fe1 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ }, "devDependencies": { "@ckeditor/ckeditor5-alignment": "^10.0.2", + "@ckeditor/ckeditor5-block-quote": "^10.0.2", "@ckeditor/ckeditor5-clipboard": "^10.0.2", "@ckeditor/ckeditor5-editor-classic": "^11.0.0", "@ckeditor/ckeditor5-image": "^10.2.0", - "@ckeditor/ckeditor5-undo": "^10.0.2", + "@ckeditor/ckeditor5-list": "^11.0.1", "@ckeditor/ckeditor5-paragraph": "^10.0.2", - "@ckeditor/ckeditor5-undo": "^10.0.1", + "@ckeditor/ckeditor5-undo": "^10.0.2", "@ckeditor/ckeditor5-utils": "^10.2.0", "eslint": "^4.15.0", "eslint-config-ckeditor5": "^1.0.7", From c55942b8f587e0c54f46b096a0c17dfa5f8198a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 8 Aug 2018 16:34:35 +0200 Subject: [PATCH 42/44] Refactor table cell post fixer. --- src/converters/tablecell-post-fixer.js | 71 ++++++++++++-------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/src/converters/tablecell-post-fixer.js b/src/converters/tablecell-post-fixer.js index 2a803864..17272570 100644 --- a/src/converters/tablecell-post-fixer.js +++ b/src/converters/tablecell-post-fixer.js @@ -7,8 +7,6 @@ * @module table/converters/tablecell-post-fixer */ -import Range from '@ckeditor/ckeditor5-engine/src/view/range'; - /** * Injects a table cell post-fixer into the editing controller. * @@ -73,7 +71,7 @@ import Range from '@ckeditor/ckeditor5-engine/src/view/range'; * @param {module:engine/controller/editingcontroller~EditingController} editing */ export default function injectTableCellPostFixer( model, editing ) { - editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper, editing ) ); + editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) ); } // The table cell post-fixer. @@ -81,7 +79,7 @@ export default function injectTableCellPostFixer( model, editing ) { // @param {module:engine/view/writer~Writer} writer // @param {module:engine/model/model~Model} model // @param {module:engine/conversion/mapper~Mapper} mapper -function tableCellPostFixer( writer, model, mapper, editing ) { +function tableCellPostFixer( writer, model, mapper ) { const changes = model.document.differ.getChanges(); let wasFixed = false; @@ -95,7 +93,7 @@ function tableCellPostFixer( writer, model, mapper, editing ) { const singleChild = tableCell.getChild( 0 ); const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span'; - wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, mapper, editing, model ) || wasFixed; + wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, model, mapper ) || wasFixed; } } else { // Check all nodes inside table cell on insert/remove operations (also other blocks). @@ -105,7 +103,7 @@ function tableCellPostFixer( writer, model, mapper, editing ) { const renameTo = parent.childCount > 1 ? 'p' : 'span'; for ( const child of parent.getChildren() ) { - wasFixed = renameParagraphIfDifferent( child, renameTo, writer, mapper, editing, model ) || wasFixed; + wasFixed = renameParagraphIfDifferent( child, renameTo, writer, model, mapper ) || wasFixed; } } } @@ -119,11 +117,12 @@ function tableCellPostFixer( writer, model, mapper, editing ) { // - view element is converted (mapped) // - view element has different name then requested. // -// @param modelElement -// @param desiredElementName +// @param {module:engine/model/element~Element} modelElement +// @param {String} desiredElementName // @param {module:engine/view/writer~Writer} writer +// @param {module:engine/model/model~Model} model // @param {module:engine/conversion/mapper~Mapper} mapper -function renameParagraphIfDifferent( modelElement, desiredElementName, writer, mapper, editing, model ) { +function renameParagraphIfDifferent( modelElement, desiredElementName, writer, model, mapper ) { // Only rename paragraph elements. if ( !modelElement.is( 'paragraph' ) ) { return false; @@ -136,47 +135,41 @@ function renameParagraphIfDifferent( modelElement, desiredElementName, writer, m return false; } - const rangesToFix = checkRangesToFix( model, modelElement, editing, mapper ); + // After renaming element in the view by a post-fixer the selection would have references to the previous element. + const selection = model.document.selection; + const shouldFixSelection = checkSelectionForRenamedElement( selection, modelElement ); + // Unbind current view element as it should be cleared from mapper. mapper.unbindViewElement( viewElement ); - const renamedViewElement = writer.rename( viewElement, desiredElementName ); - - // Bind table cell to renamed view element. + // Bind paragraph inside table cell to the renamed view element. mapper.bindElements( modelElement, renamedViewElement ); - if ( rangesToFix.length ) { - fixViewSelection( rangesToFix, mapper, writer ); + if ( shouldFixSelection ) { + // Re-create view selection based on model selection. + updateRangesInViewSelection( selection, mapper, writer ); } return true; } -function checkRangesToFix( model, modelElement, editing, mapper ) { - const needsFix = !![ ...model.document.selection.getSelectedBlocks() ].find( el => el === modelElement ); - - if ( !needsFix ) { - return []; - } - - const rangesToFix = []; - - const selection = editing.view.document.selection; - - const ranges = selection.getRanges(); - - for ( const range of ranges ) { - rangesToFix.push( { - start: mapper.toModelPosition( range.start ), - end: mapper.toModelPosition( range.end ) - } ); - } - - return rangesToFix; +// Checks if model selection contains renamed element. +// +// @param {module:engine/model/selection~Selection} selection +// @param {module:engine/model/element~Element} modelElement +// @returns {boolean} +function checkSelectionForRenamedElement( selection, modelElement ) { + return !![ ...selection.getSelectedBlocks() ].find( block => block === modelElement ); } -function fixViewSelection( rangesToFix, mapper, writer ) { - const fixedRanges = rangesToFix.map( range => new Range( mapper.toViewPosition( range.start ), mapper.toViewPosition( range.end ) ) ); +// Re-create view selection from model selection. +// +// @param {module:engine/model/selection~Selection} selection +// @param {module:engine/view/writer~Writer} writer +// @param {module:engine/conversion/mapper~Mapper} mapper +function updateRangesInViewSelection( selection, mapper, writer ) { + const fixedRanges = Array.from( selection.getRanges() ) + .map( range => mapper.toViewRange( range ) ); - writer.setSelection( fixedRanges ); + writer.setSelection( fixedRanges, { backward: selection.isBackward } ); } From 82845d90f9911167584fa4ec8b37bb3c082acd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 3 Sep 2018 13:17:11 +0200 Subject: [PATCH 43/44] Remove image from block content manual test. --- tests/manual/tableblockcontent.html | 5 ----- tests/manual/tableblockcontent.js | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/manual/tableblockcontent.html b/tests/manual/tableblockcontent.html index 906ff04d..3eb4e628 100644 --- a/tests/manual/tableblockcontent.html +++ b/tests/manual/tableblockcontent.html @@ -64,11 +64,6 @@

    An h2 with justify alignment.

    A quote with paragraph with right alignment

    - - image - Sample image - Sample image -
    diff --git a/tests/manual/tableblockcontent.js b/tests/manual/tableblockcontent.js index 596276f6..410100f2 100644 --- a/tests/manual/tableblockcontent.js +++ b/tests/manual/tableblockcontent.js @@ -10,20 +10,14 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi import Table from '../../src/table'; import TableToolbar from '../../src/tabletoolbar'; import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment'; -import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; -import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle'; -import Image from '@ckeditor/ckeditor5-image/src/image'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment, Image, ImageCaption, ImageStyle ], + plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'alignment', '|', 'undo', 'redo' ], - image: { - toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ] - }, table: { toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] } From bd5b81d0062cc984d08f6ea456108ede632b7b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 3 Sep 2018 13:21:08 +0200 Subject: [PATCH 44/44] Revert table cell contents alignment to default one (left). --- theme/table.css | 1 - 1 file changed, 1 deletion(-) diff --git a/theme/table.css b/theme/table.css index 70bd3e57..02943a1e 100644 --- a/theme/table.css +++ b/theme/table.css @@ -21,7 +21,6 @@ & th { min-width: 2em; padding: .4em; - text-align: center; border-color: hsl(0, 0%, 85%); }