From 6f5947a9d36179d8353c948929dbe1ee3262946c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 16:04:31 +0200 Subject: [PATCH 001/107] Added: Fake table selection POC. --- src/tableediting.js | 228 +++++++++++++++++++++++++++++++++++++++++ theme/tableediting.css | 7 ++ 2 files changed, 235 insertions(+) diff --git a/src/tableediting.js b/src/tableediting.js index 3c1c03b8..8271404a 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -34,7 +34,10 @@ import { findAncestor } from './commands/utils'; import TableUtils from '../src/tableutils'; import injectTablePostFixer from './converters/table-post-fixer'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import injectTableCellPostFixer from './converters/tablecell-post-fixer'; +import ViewRange from '../../ckeditor5-engine/src/view/range'; +import TableWalker from './tablewalker'; import '../theme/tableediting.css'; @@ -52,6 +55,7 @@ export default class TableEditing extends Plugin { const model = editor.model; const schema = model.schema; const conversion = editor.conversion; + const viewDocument = editor.editing.view.document; schema.register( 'table', { allowWhere: '$block', @@ -147,6 +151,70 @@ export default class TableEditing extends Plugin { 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' } ); + + const selected = new Set(); + + const tableUtils = editor.plugins.get( TableUtils ); + const tableSelection = new TableSelection( editor, tableUtils ); + + this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { + const tableCell = getTableCell( domEventData, this.editor ); + + if ( !tableCell ) { + return; + } + + const { column, row } = tableUtils.getCellLocation( tableCell ); + + const mode = getSelectionMode( domEventData, column, row ); + + tableSelection.startSelection( tableCell, mode ); + + domEventData.preventDefault(); + } ); + + this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { + if ( !tableSelection.isSelecting ) { + return; + } + + const tableCell = getTableCell( domEventData, this.editor ); + + if ( !tableCell ) { + return; + } + + tableSelection.updateSelection( tableCell ); + } ); + + this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { + if ( !tableSelection.isSelecting ) { + return; + } + + const tableCell = getTableCell( domEventData, this.editor ); + + if ( !tableCell ) { + return; + } + + tableSelection.stopSelection( tableCell ); + } ); + + this.listenTo( viewDocument, 'blur', () => { + tableSelection.clearSelection(); + } ); + + viewDocument.selection.on( 'change', () => { + for ( const range of viewDocument.selection.getRanges() ) { + const node = range.start.nodeAfter; + + if ( node && ( node.is( 'td' ) || node.is( 'th' ) ) ) { + editor.editing.view.change( writer => writer.addClass( 'selected', node ) ); + selected.add( node ); + } + } + } ); } /** @@ -251,3 +319,163 @@ export default class TableEditing extends Plugin { }; } } + +class TableSelection { + constructor( editor, tableUtils ) { + // block | column | row + this._mode = 'block'; + this._isSelecting = false; + this._highlighted = new Set(); + + this.editor = editor; + this.tableUtils = tableUtils; + } + + get isSelecting() { + return this._isSelecting; + } + + startSelection( tableCell, mode = 'block' ) { + this.clearSelection(); + this._isSelecting = true; + this._startElement = tableCell; + this._endElement = tableCell; + this._mode = mode; + this._redrawSelection(); + } + + updateSelection( tableCell ) { + this._endElement = tableCell; + this._redrawSelection(); + } + + _redrawSelection() { + const viewRanges = []; + + const selected = [ ...this.getSelection() ]; + const previous = [ ...this._highlighted.values() ]; + + this._highlighted.clear(); + + for ( const tableCell of selected ) { + const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); + viewRanges.push( ViewRange.createOn( viewElement ) ); + + this._highlighted.add( viewElement ); + } + + this.editor.editing.view.change( writer => { + for ( const previouslyHighlighted of previous ) { + if ( !selected.includes( previouslyHighlighted ) ) { + writer.removeClass( 'selected', previouslyHighlighted ); + } + } + + writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); + } ); + } + + stopSelection( tableCell ) { + this._isSelecting = false; + this._endElement = tableCell; + this._redrawSelection(); + } + + clearSelection() { + this._startElement = undefined; + this._endElement = undefined; + this._isSelecting = false; + this.updateSelection(); + this._highlighted.clear(); + } + + * getSelection() { + if ( !this._startElement || !this._endElement ) { + return []; + } + + // return selection according to the mode + if ( this._mode == 'block' ) { + yield* this._getBlockSelection(); + } + + if ( this._mode == 'row' ) { + yield* this._getRowSelection(); + } + + if ( this._mode == 'column' ) { + yield* this._getColumnSelection(); + } + + return []; + } + + * _getBlockSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; + const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; + + const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; + const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { + if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { + yield cellInfo.cell; + } + } + } + + * _getRowSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; + const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { + yield cellInfo.cell; + } + } + + * _getColumnSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; + const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent ) ) { + if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { + yield cellInfo.cell; + } + } + } +} + +function getTableCell( domEventData, editor ) { + const element = domEventData.target; + const modelElement = editor.editing.mapper.toModelElement( element ); + + if ( !modelElement ) { + return; + } + + return getParentElement( 'tableCell', Position.createAt( modelElement ) ); +} + +function getSelectionMode( domEventData, column, row ) { + let mode = 'block'; + + const domEvent = domEventData.domEvent; + const target = domEvent.target; + + if ( column == 0 && domEvent.offsetX < target.clientWidth / 2 ) { + mode = 'row'; + } else if ( row == 0 && ( domEvent.offsetY < target.clientHeight / 2 ) ) { + mode = 'column'; + } + + return mode; +} diff --git a/theme/tableediting.css b/theme/tableediting.css index 66c26cca..609d61b0 100644 --- a/theme/tableediting.css +++ b/theme/tableediting.css @@ -8,3 +8,10 @@ * it acts as a message to the builder telling that it should look for the corresponding styles * **in the theme** when compiling the editor. */ + +.ck-content .table table { + & td.selected, + & th.selected { + background: hsl(216, 100%, 67%); + } +} From 9f56ac683438e4ba23e06dff7a019f10cba34724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 16:10:14 +0200 Subject: [PATCH 002/107] Changed: Make stopSelection work when mouse move outside the table. --- src/tableediting.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index 8271404a..70075dae 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -194,10 +194,6 @@ export default class TableEditing extends Plugin { const tableCell = getTableCell( domEventData, this.editor ); - if ( !tableCell ) { - return; - } - tableSelection.stopSelection( tableCell ); } ); @@ -377,7 +373,11 @@ class TableSelection { stopSelection( tableCell ) { this._isSelecting = false; - this._endElement = tableCell; + + if ( tableCell ) { + this._endElement = tableCell; + } + this._redrawSelection(); } From 4a5e8f7618490c4ff0b897cc8299afde3109e697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 16:12:52 +0200 Subject: [PATCH 003/107] Changed: Do not change tables in table selection mode. --- src/tableediting.js | 58 +++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index 70075dae..36837dea 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -341,40 +341,16 @@ class TableSelection { } updateSelection( tableCell ) { - this._endElement = tableCell; - this._redrawSelection(); - } - - _redrawSelection() { - const viewRanges = []; - - const selected = [ ...this.getSelection() ]; - const previous = [ ...this._highlighted.values() ]; - - this._highlighted.clear(); - - for ( const tableCell of selected ) { - const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); - viewRanges.push( ViewRange.createOn( viewElement ) ); - - this._highlighted.add( viewElement ); + if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + this._endElement = tableCell; } - - this.editor.editing.view.change( writer => { - for ( const previouslyHighlighted of previous ) { - if ( !selected.includes( previouslyHighlighted ) ) { - writer.removeClass( 'selected', previouslyHighlighted ); - } - } - - writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); - } ); + this._redrawSelection(); } stopSelection( tableCell ) { this._isSelecting = false; - if ( tableCell ) { + if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } @@ -452,6 +428,32 @@ class TableSelection { } } } + + _redrawSelection() { + const viewRanges = []; + + const selected = [ ...this.getSelection() ]; + const previous = [ ...this._highlighted.values() ]; + + this._highlighted.clear(); + + for ( const tableCell of selected ) { + const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); + viewRanges.push( ViewRange.createOn( viewElement ) ); + + this._highlighted.add( viewElement ); + } + + this.editor.editing.view.change( writer => { + for ( const previouslyHighlighted of previous ) { + if ( !selected.includes( previouslyHighlighted ) ) { + writer.removeClass( 'selected', previouslyHighlighted ); + } + } + + writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); + } ); + } } function getTableCell( domEventData, editor ) { From 631c6bafbfad0fa90064d98b54405dcf96007c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 17:00:26 +0200 Subject: [PATCH 004/107] Changed: Extract TableSelection class. --- src/tableediting.js | 143 +-------------------------------------- src/tableselection.js | 151 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 142 deletions(-) create mode 100644 src/tableselection.js diff --git a/src/tableediting.js b/src/tableediting.js index 36837dea..10988ec6 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -36,8 +36,7 @@ import TableUtils from '../src/tableutils'; import injectTablePostFixer from './converters/table-post-fixer'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import injectTableCellPostFixer from './converters/tablecell-post-fixer'; -import ViewRange from '../../ckeditor5-engine/src/view/range'; -import TableWalker from './tablewalker'; +import TableSelection from './tableselection'; import '../theme/tableediting.css'; @@ -316,146 +315,6 @@ export default class TableEditing extends Plugin { } } -class TableSelection { - constructor( editor, tableUtils ) { - // block | column | row - this._mode = 'block'; - this._isSelecting = false; - this._highlighted = new Set(); - - this.editor = editor; - this.tableUtils = tableUtils; - } - - get isSelecting() { - return this._isSelecting; - } - - startSelection( tableCell, mode = 'block' ) { - this.clearSelection(); - this._isSelecting = true; - this._startElement = tableCell; - this._endElement = tableCell; - this._mode = mode; - this._redrawSelection(); - } - - updateSelection( tableCell ) { - if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { - this._endElement = tableCell; - } - this._redrawSelection(); - } - - stopSelection( tableCell ) { - this._isSelecting = false; - - if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { - this._endElement = tableCell; - } - - this._redrawSelection(); - } - - clearSelection() { - this._startElement = undefined; - this._endElement = undefined; - this._isSelecting = false; - this.updateSelection(); - this._highlighted.clear(); - } - - * getSelection() { - if ( !this._startElement || !this._endElement ) { - return []; - } - - // return selection according to the mode - if ( this._mode == 'block' ) { - yield* this._getBlockSelection(); - } - - if ( this._mode == 'row' ) { - yield* this._getRowSelection(); - } - - if ( this._mode == 'column' ) { - yield* this._getColumnSelection(); - } - - return []; - } - - * _getBlockSelection() { - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); - - const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; - const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; - - const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; - const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; - - for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { - if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { - yield cellInfo.cell; - } - } - } - - * _getRowSelection() { - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); - - const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; - const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; - - for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { - yield cellInfo.cell; - } - } - - * _getColumnSelection() { - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); - - const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; - const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; - - for ( const cellInfo of new TableWalker( this._startElement.parent.parent ) ) { - if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { - yield cellInfo.cell; - } - } - } - - _redrawSelection() { - const viewRanges = []; - - const selected = [ ...this.getSelection() ]; - const previous = [ ...this._highlighted.values() ]; - - this._highlighted.clear(); - - for ( const tableCell of selected ) { - const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); - viewRanges.push( ViewRange.createOn( viewElement ) ); - - this._highlighted.add( viewElement ); - } - - this.editor.editing.view.change( writer => { - for ( const previouslyHighlighted of previous ) { - if ( !selected.includes( previouslyHighlighted ) ) { - writer.removeClass( 'selected', previouslyHighlighted ); - } - } - - writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); - } ); - } -} - function getTableCell( domEventData, editor ) { const element = domEventData.target; const modelElement = editor.editing.mapper.toModelElement( element ); diff --git a/src/tableselection.js b/src/tableselection.js new file mode 100644 index 00000000..41818786 --- /dev/null +++ b/src/tableselection.js @@ -0,0 +1,151 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module table/tableediting + */ + +import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; +import TableWalker from './tablewalker'; + +export default class TableSelection { + constructor( editor, tableUtils ) { + // block | column | row + this._mode = 'block'; + this._isSelecting = false; + this._highlighted = new Set(); + + this.editor = editor; + this.tableUtils = tableUtils; + } + + get isSelecting() { + return this._isSelecting; + } + + startSelection( tableCell, mode = 'block' ) { + this.clearSelection(); + this._isSelecting = true; + this._startElement = tableCell; + this._endElement = tableCell; + this._mode = mode; + this._redrawSelection(); + } + + updateSelection( tableCell ) { + if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + this._endElement = tableCell; + } + this._redrawSelection(); + } + + stopSelection( tableCell ) { + this._isSelecting = false; + + if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + this._endElement = tableCell; + } + + this._redrawSelection(); + } + + clearSelection() { + this._startElement = undefined; + this._endElement = undefined; + this._isSelecting = false; + this.updateSelection(); + this._highlighted.clear(); + } + + * getSelection() { + if ( !this._startElement || !this._endElement ) { + return []; + } + + // return selection according to the mode + if ( this._mode == 'block' ) { + yield* this._getBlockSelection(); + } + + if ( this._mode == 'row' ) { + yield* this._getRowSelection(); + } + + if ( this._mode == 'column' ) { + yield* this._getColumnSelection(); + } + + return []; + } + + * _getBlockSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; + const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; + + const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; + const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { + if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { + yield cellInfo.cell; + } + } + } + + * _getRowSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; + const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { + yield cellInfo.cell; + } + } + + * _getColumnSelection() { + const startLocation = this.tableUtils.getCellLocation( this._startElement ); + const endLocation = this.tableUtils.getCellLocation( this._endElement ); + + const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; + const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; + + for ( const cellInfo of new TableWalker( this._startElement.parent.parent ) ) { + if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { + yield cellInfo.cell; + } + } + } + + _redrawSelection() { + const viewRanges = []; + + const selected = [ ...this.getSelection() ]; + const previous = [ ...this._highlighted.values() ]; + + this._highlighted.clear(); + + for ( const tableCell of selected ) { + const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); + viewRanges.push( ViewRange.createOn( viewElement ) ); + + this._highlighted.add( viewElement ); + } + + this.editor.editing.view.change( writer => { + for ( const previouslyHighlighted of previous ) { + if ( !selected.includes( previouslyHighlighted ) ) { + writer.removeClass( 'selected', previouslyHighlighted ); + } + } + + writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); + } ); + } +} From b46b9c7109961088f3e150a4114aecb6d7b2be3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 17:07:20 +0200 Subject: [PATCH 005/107] Changed: Remove superfluous selection modes from TableSelection. --- src/tableselection.js | 48 +++---------------------------------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 41818786..21f430a6 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -12,8 +12,6 @@ import TableWalker from './tablewalker'; export default class TableSelection { constructor( editor, tableUtils ) { - // block | column | row - this._mode = 'block'; this._isSelecting = false; this._highlighted = new Set(); @@ -25,12 +23,11 @@ export default class TableSelection { return this._isSelecting; } - startSelection( tableCell, mode = 'block' ) { + startSelection( tableCell ) { this.clearSelection(); this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; - this._mode = mode; this._redrawSelection(); } @@ -61,23 +58,10 @@ export default class TableSelection { * getSelection() { if ( !this._startElement || !this._endElement ) { - return []; + return; } - // return selection according to the mode - if ( this._mode == 'block' ) { - yield* this._getBlockSelection(); - } - - if ( this._mode == 'row' ) { - yield* this._getRowSelection(); - } - - if ( this._mode == 'column' ) { - yield* this._getColumnSelection(); - } - - return []; + yield* this._getBlockSelection(); } * _getBlockSelection() { @@ -97,32 +81,6 @@ export default class TableSelection { } } - * _getRowSelection() { - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); - - const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; - const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; - - for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { - yield cellInfo.cell; - } - } - - * _getColumnSelection() { - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); - - const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; - const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; - - for ( const cellInfo of new TableWalker( this._startElement.parent.parent ) ) { - if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { - yield cellInfo.cell; - } - } - } - _redrawSelection() { const viewRanges = []; From fc6401e0ef441255b6aaf441c805cc14af802bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 2 Aug 2018 17:11:40 +0200 Subject: [PATCH 006/107] Changed. Make TableSelection a plugin. --- src/tableediting.js | 10 +++------- src/tableselection.js | 25 ++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index 10988ec6..fc24a6ab 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -151,10 +151,7 @@ export default class TableEditing extends Plugin { this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } ); this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } ); - const selected = new Set(); - - const tableUtils = editor.plugins.get( TableUtils ); - const tableSelection = new TableSelection( editor, tableUtils ); + const tableSelection = editor.plugins.get( TableSelection ); this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { const tableCell = getTableCell( domEventData, this.editor ); @@ -163,7 +160,7 @@ export default class TableEditing extends Plugin { return; } - const { column, row } = tableUtils.getCellLocation( tableCell ); + const { column, row } = editor.plugins.get( TableUtils ).getCellLocation( tableCell ); const mode = getSelectionMode( domEventData, column, row ); @@ -206,7 +203,6 @@ export default class TableEditing extends Plugin { if ( node && ( node.is( 'td' ) || node.is( 'th' ) ) ) { editor.editing.view.change( writer => writer.addClass( 'selected', node ) ); - selected.add( node ); } } } ); @@ -216,7 +212,7 @@ export default class TableEditing extends Plugin { * @inheritDoc */ static get requires() { - return [ TableUtils ]; + return [ TableUtils, TableSelection ]; } /** diff --git a/src/tableselection.js b/src/tableselection.js index 21f430a6..3abf9e3c 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -8,15 +8,34 @@ */ import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + import TableWalker from './tablewalker'; +import TableUtils from './tableutils'; + +export default class TableSelection extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'TableSelection'; + } + + /** + * @inheritDoc + */ + static get requires() { + return [ TableUtils ]; + } + + constructor( editor ) { + super( editor ); -export default class TableSelection { - constructor( editor, tableUtils ) { this._isSelecting = false; this._highlighted = new Set(); this.editor = editor; - this.tableUtils = tableUtils; + this.tableUtils = editor.plugins.get( TableUtils ); } get isSelecting() { From 7d1aaa9d729094c9f23c001a695c98009f833548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 09:48:31 +0200 Subject: [PATCH 007/107] Tests: Stub TableSelection tests. --- tests/tableselection.js | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/tableselection.js diff --git a/tests/tableselection.js b/tests/tableselection.js new file mode 100644 index 00000000..3df18f4e --- /dev/null +++ b/tests/tableselection.js @@ -0,0 +1,52 @@ +/** + * @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 { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; + +import TableSelection from '../src/tableselection'; + +describe( 'TableSelection', () => { + let editor, model, root, tableSelection; + + beforeEach( () => { + return VirtualTestEditor.create( { + plugins: [ TableSelection ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableSelection = editor.plugins.get( TableSelection ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( '#pluginName', () => { + it( 'should provide plugin name', () => { + expect( TableSelection.pluginName ).to.equal( 'TableSelection' ); + } ); + } ); + + describe.only( 'start()', () => { + it( 'should...', () => { + setData( model, modelTable( [ + [ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ], + [ '12' ] + ] ) ); + + const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); + + tableSelection.startSelection( nodeByPath ); + } ); + } ); +} ); From cb97f1aad6bd4aff07ff34102022ef14677c6813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 15:30:11 +0200 Subject: [PATCH 008/107] Fix missing method reference. --- src/tableediting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tableediting.js b/src/tableediting.js index fc24a6ab..ca67c241 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -319,7 +319,7 @@ function getTableCell( domEventData, editor ) { return; } - return getParentElement( 'tableCell', Position.createAt( modelElement ) ); + return findAncestor( 'tableCell', Position.createAt( modelElement ) ); } function getSelectionMode( domEventData, column, row ) { From ead7f98499ff8a5d99515ebdc7432b30585b454c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 15:36:08 +0200 Subject: [PATCH 009/107] Add some tests for TableSelection. --- tests/tableselection.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index 3df18f4e..c445d247 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -37,16 +37,31 @@ describe( 'TableSelection', () => { } ); } ); - describe.only( 'start()', () => { - it( 'should...', () => { + describe( 'start()', () => { + it( 'should start selection', () => { setData( model, modelTable( [ - [ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ], - [ '12' ] + [ '00[]', '01' ], + [ '10', '11' ] ] ) ); const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); tableSelection.startSelection( nodeByPath ); + + expect( tableSelection.isSelecting ).to.be.true; + } ); + + it( 'update selection to single table cell', () => { + setData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ] ) ); + + const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); + + tableSelection.startSelection( nodeByPath ); + + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] ); } ); } ); } ); From 8cc27ce7032343c1ad77400b000a52296cc8afde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 16:04:16 +0200 Subject: [PATCH 010/107] Add tests for TableSelection class methods. --- src/tableselection.js | 8 +- tests/tableselection.js | 180 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 4 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 3abf9e3c..54107a33 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -51,19 +51,19 @@ export default class TableSelection extends Plugin { } updateSelection( tableCell ) { - if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } + this._redrawSelection(); } stopSelection( tableCell ) { - this._isSelecting = false; - - if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } + this._isSelecting = false; this._redrawSelection(); } diff --git a/tests/tableselection.js b/tests/tableselection.js index c445d247..c323c155 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -64,4 +64,184 @@ describe( 'TableSelection', () => { expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] ); } ); } ); + describe( 'stop()', () => { + it( 'should stop selection', () => { + setData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ] ) ); + + const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); + + tableSelection.startSelection( nodeByPath ); + expect( tableSelection.isSelecting ).to.be.true; + + tableSelection.stopSelection( nodeByPath ); + + expect( tableSelection.isSelecting ).to.be.false; + } ); + + it( 'update selection to passed table cell', () => { + setData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const endNode = root.getNodeByPath( [ 0, 1, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.stopSelection( endNode ); + + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ + startNode, + root.getNodeByPath( [ 0, 0, 1 ] ), + root.getNodeByPath( [ 0, 1, 0 ] ), + endNode + ] ); + } ); + + it( 'should not update selection if alredy stopped', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.stopSelection( firstEndNode ); + + expect( tableSelection.isSelecting ).to.be.false; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); + + const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); + tableSelection.stopSelection( secondEndNode ); + + expect( tableSelection.isSelecting ).to.be.false; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); + } ); + + it( 'should not update selection if table cell is from another parent', () => { + setData( model, modelTable( [ + [ '00[]', '01' ] + ] ) + modelTable( [ + [ 'aa', 'bb' ] + ] ) ); + + tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.stopSelection( root.getNodeByPath( [ 1, 0, 1 ] ) ); + + expect( tableSelection.isSelecting ).to.be.false; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); + } ); + } ); + + describe( 'update()', () => { + it( 'should update selection', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.updateSelection( firstEndNode ); + + expect( tableSelection.isSelecting ).to.be.true; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); + + const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); + tableSelection.updateSelection( secondEndNode ); + + expect( tableSelection.isSelecting ).to.be.true; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode, secondEndNode ] ); + } ); + + it( 'should not update selection if stopped', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.updateSelection( firstEndNode ); + + expect( tableSelection.isSelecting ).to.be.true; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); + + tableSelection.stopSelection( firstEndNode ); + expect( tableSelection.isSelecting ).to.be.false; + + const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); + tableSelection.updateSelection( secondEndNode ); + + expect( tableSelection.isSelecting ).to.be.false; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); + } ); + + it( 'should not update selection if table cell is from another parent', () => { + setData( model, modelTable( [ + [ '00[]', '01' ] + ] ) + modelTable( [ + [ 'aa', 'bb' ] + ] ) ); + + tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.updateSelection( root.getNodeByPath( [ 1, 0, 1 ] ) ); + + expect( tableSelection.isSelecting ).to.be.true; + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); + } ); + } ); + + describe( 'getSelection()', () => { + it( 'should return empty array if not started', () => { + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [] ); + } ); + + it( 'should return block of selected nodes', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.startSelection( root.getNodeByPath( [ 0, 1, 1 ] ) ); + tableSelection.updateSelection( root.getNodeByPath( [ 0, 2, 2 ] ) ); + + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ + root.getNodeByPath( [ 0, 1, 1 ] ), + root.getNodeByPath( [ 0, 1, 2 ] ), + root.getNodeByPath( [ 0, 2, 1 ] ), + root.getNodeByPath( [ 0, 2, 2 ] ) + ] ); + } ); + + it( 'should return block of selected nodes (inverted selection)', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection.startSelection( root.getNodeByPath( [ 0, 2, 2 ] ) ); + tableSelection.updateSelection( root.getNodeByPath( [ 0, 1, 1 ] ) ); + + expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ + root.getNodeByPath( [ 0, 1, 1 ] ), + root.getNodeByPath( [ 0, 1, 2 ] ), + root.getNodeByPath( [ 0, 2, 1 ] ), + root.getNodeByPath( [ 0, 2, 2 ] ) + ] ); + } ); + } ); } ); From 17086a1e8625576e300d1f2ae9e839a8e705ebff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 3 Aug 2018 16:47:57 +0200 Subject: [PATCH 011/107] Add tests for TableSelection view selection. --- src/tableselection.js | 1 + tests/tableselection.js | 82 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/tableselection.js b/src/tableselection.js index 54107a33..6389af74 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -118,6 +118,7 @@ export default class TableSelection extends Plugin { this.editor.editing.view.change( writer => { for ( const previouslyHighlighted of previous ) { if ( !selected.includes( previouslyHighlighted ) ) { + // TODO: unify somewhere... writer.removeClass( 'selected', previouslyHighlighted ); } } diff --git a/tests/tableselection.js b/tests/tableselection.js index c323c155..5612f390 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -9,6 +9,7 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; import TableSelection from '../src/tableselection'; +import { getData as getViewData } from '../../ckeditor5-engine/src/dev-utils/view'; describe( 'TableSelection', () => { let editor, model, root, tableSelection; @@ -63,7 +64,32 @@ describe( 'TableSelection', () => { expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] ); } ); + + it( 'should set view selection', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
' + + '' + + '' + + '' + + '[]' + + '' + + '' + + '' + + '' + + '' + + '
000102
101112
' + + '
' + ); + } ); } ); + describe( 'stop()', () => { it( 'should stop selection', () => { setData( model, modelTable( [ @@ -136,6 +162,34 @@ describe( 'TableSelection', () => { expect( tableSelection.isSelecting ).to.be.false; expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); } ); + + it( 'should update view selection', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.stopSelection( firstEndNode ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
' + + '' + + '' + + '' + + '[][]' + + '' + + '' + + '' + + '' + + '' + + '
000102
101112
' + + '
' + ); + } ); } ); describe( 'update()', () => { @@ -199,6 +253,34 @@ describe( 'TableSelection', () => { expect( tableSelection.isSelecting ).to.be.true; expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); } ); + + it( 'should update view selection', () => { + setData( model, modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ] + ] ) ); + + const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); + const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelection( startNode ); + tableSelection.updateSelection( firstEndNode ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
' + + '' + + '' + + '' + + '[][]' + + '' + + '' + + '' + + '' + + '' + + '
000102
101112
' + + '
' + ); + } ); } ); describe( 'getSelection()', () => { From 922bfa239aa9f8f4d589d344b32e5388e5ef305b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 13 Aug 2018 11:30:05 +0200 Subject: [PATCH 012/107] Add MergeCellsCommand. --- src/commands/mergecellscommand.js | 161 +++++++++++++++++++++++++++ src/tableediting.js | 90 +-------------- src/tableselection.js | 119 ++++++++++++++++++-- src/tableui.js | 8 ++ tests/converters/table-post-fixer.js | 3 +- tests/integration.js | 3 +- tests/manual/tableblockcontent.js | 3 +- tests/table-integration.js | 5 +- tests/tableediting.js | 5 +- tests/tabletoolbar.js | 3 +- tests/tableui.js | 7 +- 11 files changed, 303 insertions(+), 104 deletions(-) create mode 100644 src/commands/mergecellscommand.js diff --git a/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js new file mode 100644 index 00000000..586d91ba --- /dev/null +++ b/src/commands/mergecellscommand.js @@ -0,0 +1,161 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module table/commands/mergecellscommand + */ + +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 TableUtils from '../tableutils'; +import TableSelection from '../tableselection'; + +/** + * The merge cells command. + * + * The command is registered by {@link module:table/tableediting~TableEditing} as `'mergeTableCellRight'`, `'mergeTableCellLeft'`, + * `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands. + * + * To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction. + * + * For example, to merge with a cell to the right: + * + * editor.execute( 'mergeTableCellRight' ); + * + * **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan) + * (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan) + * (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled. + * + * @extends module:core/command~Command + */ +export default class MergeCellsCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const tableSelection = this.editor.plugins.get( TableSelection ); + + this.isEnabled = !!tableSelection.size && canMerge( Array.from( tableSelection.getSelection() ) ); + } + + /** + * Executes the command. + * + * Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`. + * + * @fires execute + */ + execute() { + const model = this.editor.model; + + const tableSelection = this.editor.plugins.get( TableSelection ); + const tableUtils = this.editor.plugins.get( TableUtils ); + + model.change( writer => { + const selectedTableCells = [ ... tableSelection.getSelection() ]; + + tableSelection.clearSelection(); + + const firstTableCell = selectedTableCells.shift(); + const { row, column } = tableUtils.getCellLocation( firstTableCell ); + + const colspan = parseInt( firstTableCell.getAttribute( 'colspan' ) || 1 ); + const rowspan = parseInt( firstTableCell.getAttribute( 'rowspan' ) || 1 ); + + let rightMax = column + colspan; + let bottomMax = row + rowspan; + + const rowsToCheck = new Set(); + + for ( const tableCell of selectedTableCells ) { + const { row, column } = tableUtils.getCellLocation( tableCell ); + + const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); + + if ( column + colspan > rightMax ) { + rightMax = column + colspan; + } + + if ( row + rowspan > bottomMax ) { + bottomMax = row + rowspan; + } + } + + for ( const tableCell of selectedTableCells ) { + rowsToCheck.add( tableCell.parent ); + mergeTableCells( tableCell, firstTableCell, writer ); + } + + // Update table cell span attribute and merge set selection on merged contents. + updateNumericAttribute( 'colspan', rightMax - column, firstTableCell, writer ); + updateNumericAttribute( 'rowspan', bottomMax - row, firstTableCell, writer ); + + writer.setSelection( Range.createIn( firstTableCell ) ); + + // Remove empty rows after merging table cells. + for ( const row of rowsToCheck ) { + if ( !row.childCount ) { + removeEmptyRow( row, writer ); + } + } + } ); + } +} + +// Properly removes empty row from a table. Will update `rowspan` attribute of cells that overlaps removed row. +// +// @param {module:engine/model/element~Element} removedTableCellRow +// @param {module:engine/model/writer~Writer} writer +function removeEmptyRow( removedTableCellRow, writer ) { + const table = removedTableCellRow.parent; + + const removedRowIndex = table.getChildIndex( removedTableCellRow ); + + for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) { + const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex; + + if ( overlapsRemovedRow ) { + updateNumericAttribute( 'rowspan', rowspan - 1, cell, 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; +} + +function canMerge() { + return true; +} diff --git a/src/tableediting.js b/src/tableediting.js index ca67c241..d36fa5b6 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -34,11 +34,10 @@ import { findAncestor } from './commands/utils'; import TableUtils from '../src/tableutils'; import injectTablePostFixer from './converters/table-post-fixer'; -import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import injectTableCellPostFixer from './converters/tablecell-post-fixer'; -import TableSelection from './tableselection'; import '../theme/tableediting.css'; +import MergeCellsCommand from './commands/mergecellscommand'; /** * The table editing feature. @@ -54,7 +53,6 @@ export default class TableEditing extends Plugin { const model = editor.model; const schema = model.schema; const conversion = editor.conversion; - const viewDocument = editor.editing.view.document; schema.register( 'table', { allowWhere: '$block', @@ -141,6 +139,8 @@ export default class TableEditing extends Plugin { editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) ); editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) ); + editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) ); + editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) ); editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) ); @@ -150,69 +150,13 @@ export default class TableEditing extends Plugin { 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' } ); - - const tableSelection = editor.plugins.get( TableSelection ); - - this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { - const tableCell = getTableCell( domEventData, this.editor ); - - if ( !tableCell ) { - return; - } - - const { column, row } = editor.plugins.get( TableUtils ).getCellLocation( tableCell ); - - const mode = getSelectionMode( domEventData, column, row ); - - tableSelection.startSelection( tableCell, mode ); - - domEventData.preventDefault(); - } ); - - this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { - if ( !tableSelection.isSelecting ) { - return; - } - - const tableCell = getTableCell( domEventData, this.editor ); - - if ( !tableCell ) { - return; - } - - tableSelection.updateSelection( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { - if ( !tableSelection.isSelecting ) { - return; - } - - const tableCell = getTableCell( domEventData, this.editor ); - - tableSelection.stopSelection( tableCell ); - } ); - - this.listenTo( viewDocument, 'blur', () => { - tableSelection.clearSelection(); - } ); - - viewDocument.selection.on( 'change', () => { - for ( const range of viewDocument.selection.getRanges() ) { - const node = range.start.nodeAfter; - - if ( node && ( node.is( 'td' ) || node.is( 'th' ) ) ) { - editor.editing.view.change( writer => writer.addClass( 'selected', node ) ); - } - } - } ); } /** * @inheritDoc */ static get requires() { - return [ TableUtils, TableSelection ]; + return [ TableUtils ]; } /** @@ -310,29 +254,3 @@ export default class TableEditing extends Plugin { }; } } - -function getTableCell( domEventData, editor ) { - const element = domEventData.target; - const modelElement = editor.editing.mapper.toModelElement( element ); - - if ( !modelElement ) { - return; - } - - return findAncestor( 'tableCell', Position.createAt( modelElement ) ); -} - -function getSelectionMode( domEventData, column, row ) { - let mode = 'block'; - - const domEvent = domEventData.domEvent; - const target = domEvent.target; - - if ( column == 0 && domEvent.offsetX < target.clientWidth / 2 ) { - mode = 'row'; - } else if ( row == 0 && ( domEvent.offsetY < target.clientHeight / 2 ) ) { - mode = 'column'; - } - - return mode; -} diff --git a/src/tableselection.js b/src/tableselection.js index 6389af74..d14a7a1f 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -9,9 +9,11 @@ import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import TableWalker from './tablewalker'; import TableUtils from './tableutils'; +import { findAncestor } from './commands/utils'; export default class TableSelection extends Plugin { /** @@ -38,24 +40,101 @@ export default class TableSelection extends Plugin { this.tableUtils = editor.plugins.get( TableUtils ); } + init() { + const editor = this.editor; + const viewDocument = editor.editing.view.document; + + this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { + const tableCell = getTableCell( domEventData, this.editor ); + + if ( !tableCell ) { + return; + } + + this.startSelection( tableCell ); + } ); + + this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { + if ( !this.isSelecting ) { + return; + } + + const tableCell = getTableCell( domEventData, this.editor ); + + if ( !tableCell ) { + return; + } + + const wasOne = this.size === 1; + + this.updateSelection( tableCell ); + + if ( this.size > 1 ) { + domEventData.preventDefault(); + + if ( wasOne ) { + editor.editing.view.change( writer => { + const viewElement = editor.editing.mapper.toViewElement( this._startElement ); + + writer.setSelection( ViewRange.createIn( viewElement ), { + fake: true, + label: 'fake selection over table cell' + } ); + } ); + } + + this.redrawSelection(); + } + } ); + + this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { + if ( !this.isSelecting ) { + return; + } + + const tableCell = getTableCell( domEventData, this.editor ); + + this.stopSelection( tableCell ); + } ); + } + get isSelecting() { return this._isSelecting; } + get size() { + return [ ...this.getSelection() ].length; + } + startSelection( tableCell ) { this.clearSelection(); this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; - this._redrawSelection(); } updateSelection( tableCell ) { - if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { - this._endElement = tableCell; + // Do not update if not in selection mode or no table cell passed. + if ( !this.isSelecting || !tableCell ) { + return; } - this._redrawSelection(); + const table = this._startElement.parent.parent; + + // Do not add tableCell to selection if it is from other table or is already set as end element. + if ( table !== tableCell.parent.parent || this._endElement === tableCell ) { + return; + } + + const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 ); + const startInHeading = this._startElement.parent.index < headingRows; + const updateCellInHeading = tableCell.parent.index < headingRows; + + // Only add cell to selection if they are in the same table section. + if ( startInHeading === updateCellInHeading ) { + this._endElement = tableCell; + this.redrawSelection(); + } } stopSelection( tableCell ) { @@ -64,14 +143,13 @@ export default class TableSelection extends Plugin { } this._isSelecting = false; - this._redrawSelection(); } clearSelection() { this._startElement = undefined; this._endElement = undefined; this._isSelecting = false; - this.updateSelection(); + this.clearPreviousSelection(); this._highlighted.clear(); } @@ -100,7 +178,7 @@ export default class TableSelection extends Plugin { } } - _redrawSelection() { + redrawSelection() { const viewRanges = []; const selected = [ ...this.getSelection() ]; @@ -118,12 +196,37 @@ export default class TableSelection extends Plugin { this.editor.editing.view.change( writer => { for ( const previouslyHighlighted of previous ) { if ( !selected.includes( previouslyHighlighted ) ) { - // TODO: unify somewhere... writer.removeClass( 'selected', previouslyHighlighted ); } } + for ( const currently of this._highlighted ) { + writer.addClass( 'selected', currently ); + } + + // TODO works on FF ony... :| writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); } ); } + + clearPreviousSelection() { + const previous = [ ...this._highlighted.values() ]; + + this.editor.editing.view.change( writer => { + for ( const previouslyHighlighted of previous ) { + writer.removeClass( 'selected', previouslyHighlighted ); + } + } ); + } +} + +function getTableCell( domEventData, editor ) { + const element = domEventData.target; + const modelElement = editor.editing.mapper.toModelElement( element ); + + if ( !modelElement ) { + return; + } + + return findAncestor( 'tableCell', Position.createAt( modelElement ) ); } diff --git a/src/tableui.js b/src/tableui.js index f1aedb24..cfb2ca40 100644 --- a/src/tableui.js +++ b/src/tableui.js @@ -148,6 +148,14 @@ export default class TableUI extends Plugin { editor.ui.componentFactory.add( 'mergeTableCells', locale => { const options = [ + { + type: 'button', + model: { + commandName: 'mergeTableCells', + label: t( 'Merge cells' ) + } + }, + { type: 'separator' }, { type: 'button', model: { diff --git a/tests/converters/table-post-fixer.js b/tests/converters/table-post-fixer.js index 9626a482..b3cb4b83 100644 --- a/tests/converters/table-post-fixer.js +++ b/tests/converters/table-post-fixer.js @@ -11,6 +11,7 @@ import { getData as getModelData, parse, setData as setModelData } from '@ckedit import TableEditing from '../../src/tableediting'; import { formatTable, formattedModelTable, modelTable } from './../_utils/utils'; import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; +import TableSelection from '../../src/tableselection'; describe( 'Table post-fixer', () => { let editor, model, root; @@ -18,7 +19,7 @@ describe( 'Table post-fixer', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, Paragraph, UndoEditing ] + plugins: [ TableEditing, TableSelection, Paragraph, UndoEditing ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/integration.js b/tests/integration.js index 75b65efe..2fbc8ee1 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -11,6 +11,7 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import Table from '../src/table'; import TableToolbar from '../src/tabletoolbar'; import View from '@ckeditor/ckeditor5-ui/src/view'; +import TableSelection from '../src/tableselection'; describe( 'TableToolbar integration', () => { describe( 'with the BalloonToolbar', () => { @@ -22,7 +23,7 @@ describe( 'TableToolbar integration', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ Table, TableToolbar, BalloonToolbar, Paragraph ] + plugins: [ Table, TableSelection, TableToolbar, BalloonToolbar, Paragraph ] } ) .then( editor => { newEditor = editor; diff --git a/tests/manual/tableblockcontent.js b/tests/manual/tableblockcontent.js index 410100f2..af9b9aaa 100644 --- a/tests/manual/tableblockcontent.js +++ b/tests/manual/tableblockcontent.js @@ -10,10 +10,11 @@ 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 TableSelection from '../../src/tableselection'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ], + plugins: [ ArticlePluginSet, Table, TableToolbar, TableSelection, Alignment ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'alignment', '|', 'undo', 'redo' diff --git a/tests/table-integration.js b/tests/table-integration.js index 78d048ec..1c6368b1 100644 --- a/tests/table-integration.js +++ b/tests/table-integration.js @@ -19,6 +19,7 @@ import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/vie import TableEditing from '../src/tableediting'; import { formatTable, formattedModelTable, modelTable, viewTable } from './_utils/utils'; +import TableSelection from '../src/tableselection'; describe( 'Table feature – integration', () => { describe( 'with clipboard', () => { @@ -26,7 +27,7 @@ describe( 'Table feature – integration', () => { beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) + .create( { plugins: [ Paragraph, TableEditing, TableSelection, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) .then( newEditor => { editor = newEditor; clipboard = editor.plugins.get( 'Clipboard' ); @@ -85,7 +86,7 @@ describe( 'Table feature – integration', () => { beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } ) + .create( { plugins: [ Paragraph, TableEditing, TableSelection, Widget, UndoEditing ] } ) .then( newEditor => { editor = newEditor; doc = editor.model.document; diff --git a/tests/tableediting.js b/tests/tableediting.js index 736ea4db..86fc3a74 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -20,6 +20,7 @@ import SplitCellCommand from '../src/commands/splitcellcommand'; import MergeCellCommand from '../src/commands/mergecellcommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; +import TableSelection from '../src/tableselection'; describe( 'TableEditing', () => { let editor, model; @@ -27,7 +28,7 @@ describe( 'TableEditing', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, Paragraph, ImageEditing ] + plugins: [ TableEditing, TableSelection, Paragraph, ImageEditing ] } ) .then( newEditor => { editor = newEditor; @@ -466,7 +467,7 @@ describe( 'TableEditing', () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, Paragraph ] + plugins: [ TableEditing, TableSelection, Paragraph ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/tabletoolbar.js b/tests/tabletoolbar.js index 382a0894..a243a578 100644 --- a/tests/tabletoolbar.js +++ b/tests/tabletoolbar.js @@ -15,6 +15,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import View from '@ckeditor/ckeditor5-ui/src/view'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import TableSelection from '../src/tableselection'; describe( 'TableToolbar', () => { let editor, model, doc, plugin, toolbar, balloon, editorElement; @@ -25,7 +26,7 @@ describe( 'TableToolbar', () => { return ClassicEditor .create( editorElement, { - plugins: [ Paragraph, Table, TableToolbar, FakeButton ], + plugins: [ Paragraph, Table, TableSelection, TableToolbar, FakeButton ], table: { toolbar: [ 'fake_button' ] } diff --git a/tests/tableui.js b/tests/tableui.js index 8461c401..3ee32048 100644 --- a/tests/tableui.js +++ b/tests/tableui.js @@ -14,8 +14,9 @@ import TableUI from '../src/tableui'; import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview'; import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview'; import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview'; +import TableSelection from '../src/tableselection'; -describe( 'TableUI', () => { +describe.only( 'TableUI', () => { let editor, element; testUtils.createSinonSandbox(); @@ -35,7 +36,7 @@ describe( 'TableUI', () => { return ClassicTestEditor .create( element, { - plugins: [ TableEditing, TableUI ] + plugins: [ TableEditing, TableSelection, TableUI ] } ) .then( newEditor => { editor = newEditor; @@ -326,6 +327,8 @@ describe( 'TableUI', () => { const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label ); expect( labels ).to.deep.equal( [ + 'Merge cells', + '|', 'Merge cell up', 'Merge cell right', 'Merge cell down', From 843eb9246a428f2dcaea96a57a4483bee28970ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 13 Aug 2018 11:55:41 +0200 Subject: [PATCH 013/107] Tests: Fix table test dependencies. --- tests/manual/table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/manual/table.js b/tests/manual/table.js index 4daac2ce..cf1c8ff1 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -9,10 +9,11 @@ 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 TableSelection from '../../src/tableselection'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar ], + plugins: [ ArticlePluginSet, Table, TableSelection, TableToolbar ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], From aa398a90e6ae17680920875ad377cbbf51c249a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 13 Aug 2018 17:02:58 +0200 Subject: [PATCH 014/107] Tests: Simplify table manual test data and add model output. --- tests/manual/table.html | 185 +--------------------------------------- tests/manual/table.js | 28 +++++- 2 files changed, 29 insertions(+), 184 deletions(-) diff --git a/tests/manual/table.html b/tests/manual/table.html index feae0e74..60a63c81 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -6,199 +6,18 @@
-

Complex table:

- -
-
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
a b c
- -

Table with no tbody:

- - - - - - - -
a b c
abc
- -

Table with thead section between two tbody sections

- - - - - - - - - - - - - - - - -
2
1
3
+

Model contents:

+
diff --git a/tests/manual/table.js b/tests/manual/table.js index cf1c8ff1..53a9dd96 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -3,13 +3,14 @@ * For licensing, see LICENSE.md. */ -/* globals console, window, document */ +/* globals console, window, document, global */ 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 TableSelection from '../../src/tableselection'; +import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -23,7 +24,32 @@ ClassicEditor } ) .then( editor => { window.editor = editor; + editor.model.document.on( 'change', () => { + printModelContents( editor ); + } ); + + printModelContents( editor ); } ) .catch( err => { console.error( err.stack ); } ); + +const modelDiv = global.document.querySelector( '#model' ); + +function printModelContents( editor ) { + modelDiv.innerText = formatTable( getData( editor.model ) ); +} + +function formatTable( tableString ) { + return tableString + .replace( //g, '\n
' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( /<\/tableRow>/g, '\n' ) + .replace( /<\/thead>/g, '\n' ) + .replace( /<\/tbody>/g, '\n' ) + .replace( /<\/tr>/g, '\n' ) + .replace( /<\/table>/g, '\n
' ); +} From c4572a9998705afef780239f9a5788445b97f5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 13 Aug 2018 17:03:31 +0200 Subject: [PATCH 015/107] Make tableCell an object. --- src/tableediting.js | 2 +- tests/tableselection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index d36fa5b6..56ed16c7 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -69,7 +69,7 @@ export default class TableEditing extends Plugin { schema.register( 'tableCell', { allowIn: 'tableRow', allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true + isObject: true } ); // Allow all $block content inside table cell. diff --git a/tests/tableselection.js b/tests/tableselection.js index 5612f390..5c7780fa 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -9,7 +9,7 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; import TableSelection from '../src/tableselection'; -import { getData as getViewData } from '../../ckeditor5-engine/src/dev-utils/view'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; describe( 'TableSelection', () => { let editor, model, root, tableSelection; From 33ef2968e98fa426cc3606fd3a8ab3a0dd5b23aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 13 Aug 2018 17:04:05 +0200 Subject: [PATCH 016/107] MergeCellCommand should update selection before removing table cells. --- src/commands/mergecellscommand.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js index 586d91ba..af2fbddd 100644 --- a/src/commands/mergecellscommand.js +++ b/src/commands/mergecellscommand.js @@ -62,6 +62,10 @@ export default class MergeCellsCommand extends Command { tableSelection.clearSelection(); const firstTableCell = selectedTableCells.shift(); + + // TODO: this shouldn't be necessary (right now the selection could overlap existing. + writer.setSelection( Range.createIn( firstTableCell ) ); + const { row, column } = tableUtils.getCellLocation( firstTableCell ); const colspan = parseInt( firstTableCell.getAttribute( 'colspan' ) || 1 ); From 967c136397588dcf9bd995238ef366aa07b19008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 14 Aug 2018 15:00:31 +0200 Subject: [PATCH 017/107] Remove directional merge cell command. --- src/tableui.js | 29 ----------------------------- tests/manual/table.js | 7 ++++++- tests/tableui.js | 43 +++++++++---------------------------------- 3 files changed, 15 insertions(+), 64 deletions(-) diff --git a/src/tableui.js b/src/tableui.js index cfb2ca40..1254d7b9 100644 --- a/src/tableui.js +++ b/src/tableui.js @@ -156,35 +156,6 @@ export default class TableUI extends Plugin { } }, { type: 'separator' }, - { - type: 'button', - model: { - commandName: 'mergeTableCellUp', - label: t( 'Merge cell up' ) - } - }, - { - type: 'button', - model: { - commandName: 'mergeTableCellRight', - label: t( 'Merge cell right' ) - } - }, - { - type: 'button', - model: { - commandName: 'mergeTableCellDown', - label: t( 'Merge cell down' ) - } - }, - { - type: 'button', - model: { - commandName: 'mergeTableCellLeft', - label: t( 'Merge cell left' ) - } - }, - { type: 'separator' }, { type: 'button', model: { diff --git a/tests/manual/table.js b/tests/manual/table.js index 53a9dd96..7da2cae6 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -37,7 +37,12 @@ ClassicEditor const modelDiv = global.document.querySelector( '#model' ); function printModelContents( editor ) { - modelDiv.innerText = formatTable( getData( editor.model ) ); + modelDiv.innerHTML = formatTable( getData( editor.model ) ) + .replace( //g, '>' ) + .replace( /\n/g, '
' ) + .replace( /\[/g, '[' ) + .replace( /]/g, ']' ); } function formatTable( tableString ) { diff --git a/tests/tableui.js b/tests/tableui.js index 3ee32048..a3e48d97 100644 --- a/tests/tableui.js +++ b/tests/tableui.js @@ -16,7 +16,7 @@ import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview'; import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview'; import TableSelection from '../src/tableselection'; -describe.only( 'TableUI', () => { +describe( 'TableUI', () => { let editor, element; testUtils.createSinonSandbox(); @@ -329,11 +329,6 @@ describe.only( 'TableUI', () => { expect( labels ).to.deep.equal( [ 'Merge cells', '|', - 'Merge cell up', - 'Merge cell right', - 'Merge cell down', - 'Merge cell left', - '|', 'Split cell vertically', 'Split cell horizontally' ] ); @@ -342,49 +337,29 @@ describe.only( 'TableUI', () => { it( 'should bind items in panel to proper commands', () => { const items = dropdown.listView.items; - const mergeCellUpCommand = editor.commands.get( 'mergeTableCellUp' ); - const mergeCellRightCommand = editor.commands.get( 'mergeTableCellRight' ); - const mergeCellDownCommand = editor.commands.get( 'mergeTableCellDown' ); - const mergeCellLeftCommand = editor.commands.get( 'mergeTableCellLeft' ); + const mergeCellsCommand = editor.commands.get( 'mergeTableCells' ); const splitCellVerticallyCommand = editor.commands.get( 'splitTableCellVertically' ); const splitCellHorizontallyCommand = editor.commands.get( 'splitTableCellHorizontally' ); - mergeCellUpCommand.isEnabled = true; - mergeCellRightCommand.isEnabled = true; - mergeCellDownCommand.isEnabled = true; - mergeCellLeftCommand.isEnabled = true; + mergeCellsCommand.isEnabled = true; splitCellVerticallyCommand.isEnabled = true; splitCellHorizontallyCommand.isEnabled = true; - expect( items.first.children.first.isEnabled ).to.be.true; - expect( items.get( 1 ).children.first.isEnabled ).to.be.true; + expect( items.get( 0 ).children.first.isEnabled ).to.be.true; expect( items.get( 2 ).children.first.isEnabled ).to.be.true; expect( items.get( 3 ).children.first.isEnabled ).to.be.true; - expect( items.get( 5 ).children.first.isEnabled ).to.be.true; - expect( items.get( 6 ).children.first.isEnabled ).to.be.true; - expect( dropdown.buttonView.isEnabled ).to.be.true; - - mergeCellUpCommand.isEnabled = false; - - expect( items.first.children.first.isEnabled ).to.be.false; expect( dropdown.buttonView.isEnabled ).to.be.true; - mergeCellRightCommand.isEnabled = false; + mergeCellsCommand.isEnabled = false; - expect( items.get( 1 ).children.first.isEnabled ).to.be.false; + expect( items.get( 0 ).children.first.isEnabled ).to.be.false; expect( dropdown.buttonView.isEnabled ).to.be.true; - mergeCellDownCommand.isEnabled = false; - expect( items.get( 2 ).children.first.isEnabled ).to.be.false; - - mergeCellLeftCommand.isEnabled = false; - expect( items.get( 3 ).children.first.isEnabled ).to.be.false; - splitCellVerticallyCommand.isEnabled = false; - expect( items.get( 5 ).children.first.isEnabled ).to.be.false; + expect( items.get( 2 ).children.first.isEnabled ).to.be.false; splitCellHorizontallyCommand.isEnabled = false; - expect( items.get( 6 ).children.first.isEnabled ).to.be.false; + expect( items.get( 3 ).children.first.isEnabled ).to.be.false; expect( dropdown.buttonView.isEnabled ).to.be.false; } ); @@ -403,7 +378,7 @@ describe.only( 'TableUI', () => { dropdown.listView.items.first.children.first.fire( 'execute' ); expect( spy.calledOnce ).to.be.true; - expect( spy.args[ 0 ][ 0 ] ).to.equal( 'mergeTableCellUp' ); + expect( spy.args[ 0 ][ 0 ] ).to.equal( 'mergeTableCells' ); } ); } ); } ); From 20423175ffbf7245d3076b834f16dd3cb8526c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 6 Sep 2018 14:41:12 +0200 Subject: [PATCH 018/107] Update merge cell command to support ranges not table selection plugin. --- src/commands/mergecellcommand.js | 282 --------- src/commands/mergecellscommand.js | 119 +++- src/tableediting.js | 8 +- src/tableselection.js | 47 +- tests/commands/mergecellcommand.js | 887 ---------------------------- tests/commands/mergecellscommand.js | 421 +++++++++++++ tests/tableediting.js | 34 +- tests/tableselection.js | 60 +- 8 files changed, 576 insertions(+), 1282 deletions(-) delete mode 100644 src/commands/mergecellcommand.js delete mode 100644 tests/commands/mergecellcommand.js create mode 100644 tests/commands/mergecellscommand.js diff --git a/src/commands/mergecellcommand.js b/src/commands/mergecellcommand.js deleted file mode 100644 index 29bfdcf5..00000000 --- a/src/commands/mergecellcommand.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module table/commands/mergecellcommand - */ - -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 { findAncestor, updateNumericAttribute } from './utils'; -import TableUtils from '../tableutils'; - -/** - * The merge cell command. - * - * The command is registered by {@link module:table/tableediting~TableEditing} as `'mergeTableCellRight'`, `'mergeTableCellLeft'`, - * `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands. - * - * To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction. - * - * For example, to merge with a cell to the right: - * - * editor.execute( 'mergeTableCellRight' ); - * - * **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan) - * (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan) - * (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled. - * - * @extends module:core/command~Command - */ -export default class MergeCellCommand extends Command { - /** - * Creates a new `MergeCellCommand` instance. - * - * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used. - * @param {Object} options - * @param {String} options.direction Indicates which cell to merge with the currently selected one. - * Possible values are: `'left'`, `'right'`, `'up'` and `'down'`. - */ - constructor( editor, options ) { - super( editor ); - - /** - * The direction that indicates which cell will be merged with the currently selected one. - * - * @readonly - * @member {String} #direction - */ - this.direction = options.direction; - - /** - * Whether the merge is horizontal (left/right) or vertical (up/down). - * - * @readonly - * @member {Boolean} #isHorizontal - */ - this.isHorizontal = this.direction == 'right' || this.direction == 'left'; - } - - /** - * @inheritDoc - */ - refresh() { - const cellToMerge = this._getMergeableCell(); - - this.isEnabled = !!cellToMerge; - // In order to check if currently selected cell can be merged with one defined by #direction some computation are done beforehand. - // As such we can cache it as a command's value. - this.value = cellToMerge; - } - - /** - * Executes the command. - * - * Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`. - * - * @fires execute - */ - execute() { - const model = this.editor.model; - const doc = model.document; - const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); - const cellToMerge = this.value; - const direction = this.direction; - - model.change( writer => { - const isMergeNext = direction == 'right' || direction == 'down'; - - // The merge mechanism is always the same so sort cells to be merged. - const cellToExpand = isMergeNext ? tableCell : cellToMerge; - const cellToRemove = isMergeNext ? cellToMerge : tableCell; - - // Cache the parent of cell to remove for later check. - const removedTableCellRow = cellToRemove.parent; - - mergeTableCells( cellToRemove, cellToExpand, writer ); - - const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan'; - const cellSpan = parseInt( tableCell.getAttribute( spanAttribute ) || 1 ); - const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 ); - - // Update table cell span attribute and merge set selection on merged contents. - writer.setAttribute( spanAttribute, cellSpan + cellToMergeSpan, cellToExpand ); - writer.setSelection( Range.createIn( cellToExpand ) ); - - // Remove empty row after merging. - if ( !removedTableCellRow.childCount ) { - removeEmptyRow( removedTableCellRow, writer ); - } - } ); - } - - /** - * Returns a cell that can be merged with the current cell depending on the command's direction. - * - * @returns {module:engine/model/element|undefined} - * @private - */ - _getMergeableCell() { - const model = this.editor.model; - const doc = model.document; - const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() ); - - if ( !tableCell ) { - return; - } - - const tableUtils = this.editor.plugins.get( TableUtils ); - - // First get the cell on proper direction. - const cellToMerge = this.isHorizontal ? - getHorizontalCell( tableCell, this.direction, tableUtils ) : - getVerticalCell( tableCell, this.direction ); - - if ( !cellToMerge ) { - return; - } - - // If found check if the span perpendicular to merge direction is equal on both cells. - const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan'; - const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 ); - - const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 ); - - if ( cellToMergeSpan === span ) { - return cellToMerge; - } - } -} - -// Returns the cell that can be merged horizontally. -// -// @param {module:engine/model/element~Element} tableCell -// @param {String} direction -// @returns {module:engine/model/node~Node|null} -function getHorizontalCell( tableCell, direction, tableUtils ) { - const horizontalCell = direction == 'right' ? tableCell.nextSibling : tableCell.previousSibling; - - if ( !horizontalCell ) { - return; - } - - // Sort cells: - const cellOnLeft = direction == 'right' ? tableCell : horizontalCell; - const cellOnRight = direction == 'right' ? horizontalCell : tableCell; - - // Get their column indexes: - const { column: leftCellColumn } = tableUtils.getCellLocation( cellOnLeft ); - const { column: rightCellColumn } = tableUtils.getCellLocation( cellOnRight ); - - const leftCellSpan = parseInt( cellOnLeft.getAttribute( 'colspan' ) || 1 ); - - // The cell on the right must have index that is distant to the cell on the left by the left cell's width (colspan). - const cellsAreTouching = leftCellColumn + leftCellSpan === rightCellColumn; - - // If the right cell's column index is different it means that there are rowspanned cells between them. - return cellsAreTouching ? horizontalCell : undefined; -} - -// Returns the cell that can be merged vertically. -// -// @param {module:engine/model/element~Element} tableCell -// @param {String} direction -// @returns {module:engine/model/node~Node|null} -function getVerticalCell( tableCell, direction ) { - const tableRow = tableCell.parent; - const table = tableRow.parent; - - const rowIndex = table.getChildIndex( tableRow ); - - // Don't search for mergeable cell if direction points out of the table. - if ( ( direction == 'down' && rowIndex === table.childCount - 1 ) || ( direction == 'up' && rowIndex === 0 ) ) { - return; - } - - const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - const headingRows = table.getAttribute( 'headingRows' ) || 0; - - const isMergeWithBodyCell = direction == 'down' && ( rowIndex + rowspan ) === headingRows; - const isMergeWithHeadCell = direction == 'up' && rowIndex === headingRows; - - // Don't search for mergeable cell if direction points out of the current table section. - if ( headingRows && ( isMergeWithBodyCell || isMergeWithHeadCell ) ) { - return; - } - - const currentCellRowSpan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - const rowOfCellToMerge = direction == 'down' ? rowIndex + currentCellRowSpan : rowIndex; - - const tableMap = [ ...new TableWalker( table, { endRow: rowOfCellToMerge } ) ]; - - const currentCellData = tableMap.find( value => value.cell === tableCell ); - const mergeColumn = currentCellData.column; - - const cellToMergeData = tableMap.find( ( { row, rowspan, column } ) => { - if ( column !== mergeColumn ) { - return false; - } - - if ( direction == 'down' ) { - // If merging a cell below the mergeRow is already calculated. - return row === rowOfCellToMerge; - } else { - // If merging a cell above calculate if it spans to mergeRow. - return rowOfCellToMerge === row + rowspan; - } - } ); - - return cellToMergeData && cellToMergeData.cell; -} - -// Properly removes empty row from a table. Will update `rowspan` attribute of cells that overlaps removed row. -// -// @param {module:engine/model/element~Element} removedTableCellRow -// @param {module:engine/model/writer~Writer} writer -function removeEmptyRow( removedTableCellRow, writer ) { - const table = removedTableCellRow.parent; - - const removedRowIndex = table.getChildIndex( removedTableCellRow ); - - for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) { - const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex; - - if ( overlapsRemovedRow ) { - updateNumericAttribute( 'rowspan', rowspan - 1, cell, 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/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js index af2fbddd..a33a91b3 100644 --- a/src/commands/mergecellscommand.js +++ b/src/commands/mergecellscommand.js @@ -11,9 +11,8 @@ 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 { findAncestor, updateNumericAttribute } from './utils'; import TableUtils from '../tableutils'; -import TableSelection from '../tableselection'; /** * The merge cells command. @@ -38,9 +37,7 @@ export default class MergeCellsCommand extends Command { * @inheritDoc */ refresh() { - const tableSelection = this.editor.plugins.get( TableSelection ); - - this.isEnabled = !!tableSelection.size && canMerge( Array.from( tableSelection.getSelection() ) ); + this.isEnabled = canMergeCells( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) ); } /** @@ -53,13 +50,10 @@ export default class MergeCellsCommand extends Command { execute() { const model = this.editor.model; - const tableSelection = this.editor.plugins.get( TableSelection ); const tableUtils = this.editor.plugins.get( TableUtils ); model.change( writer => { - const selectedTableCells = [ ... tableSelection.getSelection() ]; - - tableSelection.clearSelection(); + const selectedTableCells = [ ... this.editor.model.document.selection.getRanges() ].map( range => range.start.nodeAfter ); const firstTableCell = selectedTableCells.shift(); @@ -160,6 +154,109 @@ function isEmpty( tableCell ) { return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty; } -function canMerge() { - return true; +// Check if selection contains mergeable cells. +// +// In a table below: +// +// +---+---+---+---+ +// | a | b | c | d | +// +---+---+---+ + +// | e | f | | +// + +---+---+ +// | | g | h | +// +---+---+---+---+ +// +// Valid selections are those which creates a solid rectangle (without gaps), such as: +// - a, b (two horizontal cells) +// - c, f (two vertical cells) +// - a, b, e (cell "e" spans over four cells) +// - c, d, f (cell d spans over cell in row below) +// +// While invalid selection would be: +// - a, c (cell "b" not selected creates a gap) +// - f, g, h (cell "d" spans over a cell from row of "f" cell - thus creates a gap) +// +// @param {module:engine/model/selection~Selection} selection +// @param {module:table/tableUtils~TableUtils} tableUtils +// @returns {boolean} +function canMergeCells( selection, tableUtils ) { + // Collapsed selection or selection only one range can't contain mergeable table cells. + if ( selection.isCollapsed || selection.rangeCount < 2 ) { + return false; + } + + // All cells must be inside the same table. + let firstRangeTable; + + const tableCells = []; + + for ( const range of selection.getRanges() ) { + // Selection ranges must be set on whole element. + if ( range.isCollapsed || !range.isFlat || !range.start.nodeAfter.is( 'tableCell' ) ) { + return false; + } + + const parentTable = findAncestor( 'table', range.start ); + + if ( !firstRangeTable ) { + firstRangeTable = parentTable; + } else if ( firstRangeTable !== parentTable ) { + return false; + } + + tableCells.push( range.start.nodeAfter ); + } + + // At this point selection contains ranges over table cells in the same table. + // The valid selection is a fully occupied rectangle composed of table cells. + // Below we calculate area of selected cells and the area of valid selection. + // The area of valid selection is defined by top-left and bottom-right cells. + const rows = new Set(); + const columns = new Set(); + + let areaOfSelectedCells = 0; + + for ( const tableCell of tableCells ) { + const { row, column } = tableUtils.getCellLocation( tableCell ); + const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); + const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + + // Record row & column indexes of current cell. + rows.add( row ); + columns.add( column ); + + // For cells that spans over multiple rows add also the last row that this cell spans over. + if ( rowspan > 1 ) { + rows.add( row + rowspan - 1 ); + } + + // For cells that spans over multiple columns add also the last column that this cell spans over. + if ( colspan > 1 ) { + columns.add( column + colspan - 1 ); + } + + areaOfSelectedCells += ( rowspan * colspan ); + } + + // We can only merge table cells that are in adjacent rows... + const areaOfValidSelection = getBiggestRectangleArea( rows, columns ); + + return areaOfValidSelection == areaOfSelectedCells; +} + +// Calculates the area of a maximum rectangle that can span over provided row & column indexes. +// +// @param {Array.} rows +// @param {Array.} columns +// @returns {Number} +function getBiggestRectangleArea( rows, columns ) { + const rowsIndexes = Array.from( rows.values() ); + const columnIndexes = Array.from( columns.values() ); + + const lastRow = Math.max( ...rowsIndexes ); + const firstRow = Math.min( ...rowsIndexes ); + const lastColumn = Math.max( ...columnIndexes ); + const firstColumn = Math.min( ...columnIndexes ); + + return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 ); } diff --git a/src/tableediting.js b/src/tableediting.js index 56ed16c7..157c8a07 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -25,11 +25,11 @@ import InsertTableCommand from './commands/inserttablecommand'; import InsertRowCommand from './commands/insertrowcommand'; import InsertColumnCommand from './commands/insertcolumncommand'; import SplitCellCommand from './commands/splitcellcommand'; -import MergeCellCommand from './commands/mergecellcommand'; import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; import SetHeaderColumnCommand from './commands/setheadercolumncommand'; +import MergeCellsCommand from './commands/mergecellscommand'; import { findAncestor } from './commands/utils'; import TableUtils from '../src/tableutils'; @@ -37,7 +37,6 @@ import injectTablePostFixer from './converters/table-post-fixer'; import injectTableCellPostFixer from './converters/tablecell-post-fixer'; import '../theme/tableediting.css'; -import MergeCellsCommand from './commands/mergecellscommand'; /** * The table editing feature. @@ -134,11 +133,6 @@ export default class TableEditing extends Plugin { editor.commands.add( 'splitTableCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) ); editor.commands.add( 'splitTableCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) ); - editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) ); - editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) ); - editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) ); - editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) ); - editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) ); editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) ); diff --git a/src/tableselection.js b/src/tableselection.js index d14a7a1f..e34251d2 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -7,13 +7,13 @@ * @module table/tableediting */ -import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import TableWalker from './tablewalker'; import TableUtils from './tableutils'; import { findAncestor } from './commands/utils'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; export default class TableSelection extends Plugin { /** @@ -73,14 +73,16 @@ export default class TableSelection extends Plugin { domEventData.preventDefault(); if ( wasOne ) { - editor.editing.view.change( writer => { - const viewElement = editor.editing.mapper.toViewElement( this._startElement ); - - writer.setSelection( ViewRange.createIn( viewElement ), { - fake: true, - label: 'fake selection over table cell' - } ); - } ); + // TODO: + // editor.editing.view.change( writer => { + // // TODO const viewElement = editor.editing.mapper.toViewElement( this._startElement ); + // + // // Set selection to the first selected table cell. + // // writer.setSelection( ViewRange.createIn( viewElement ), { + // // fake: true, + // // label: 'fake selection over table cell' + // // } ); + // } ); } this.redrawSelection(); @@ -111,6 +113,9 @@ export default class TableSelection extends Plugin { this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; + + // todo: stop rendering + this.editor.editing.view._renderer.renderSelection = false; } updateSelection( tableCell ) { @@ -143,6 +148,7 @@ export default class TableSelection extends Plugin { } this._isSelecting = false; + this.editor.editing.view._renderer.renderSelection = true; } clearSelection() { @@ -151,6 +157,8 @@ export default class TableSelection extends Plugin { this._isSelecting = false; this.clearPreviousSelection(); this._highlighted.clear(); + + this.editor.editing.view._renderer.renderSelection = true; } * getSelection() { @@ -179,7 +187,12 @@ export default class TableSelection extends Plugin { } redrawSelection() { - const viewRanges = []; + const editor = this.editor; + const mapper = editor.editing.mapper; + const view = editor.editing.view; + const model = editor.model; + + const modelRanges = []; const selected = [ ...this.getSelection() ]; const previous = [ ...this._highlighted.values() ]; @@ -187,13 +200,18 @@ export default class TableSelection extends Plugin { this._highlighted.clear(); for ( const tableCell of selected ) { - const viewElement = this.editor.editing.mapper.toViewElement( tableCell ); - viewRanges.push( ViewRange.createOn( viewElement ) ); + const viewElement = mapper.toViewElement( tableCell ); + modelRanges.push( Range.createOn( tableCell ) ); this._highlighted.add( viewElement ); } - this.editor.editing.view.change( writer => { + // Update model's selection + model.change( writer => { + writer.setSelection( modelRanges ); + } ); + + view.change( writer => { for ( const previouslyHighlighted of previous ) { if ( !selected.includes( previouslyHighlighted ) ) { writer.removeClass( 'selected', previouslyHighlighted ); @@ -203,9 +221,6 @@ export default class TableSelection extends Plugin { for ( const currently of this._highlighted ) { writer.addClass( 'selected', currently ); } - - // TODO works on FF ony... :| - writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } ); } ); } diff --git a/tests/commands/mergecellcommand.js b/tests/commands/mergecellcommand.js deleted file mode 100644 index bd856466..00000000 --- a/tests/commands/mergecellcommand.js +++ /dev/null @@ -1,887 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import MergeCellCommand from '../../src/commands/mergecellcommand'; -import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; -import TableUtils from '../../src/tableutils'; - -describe( 'MergeCellCommand', () => { - let editor, model, command, root; - - beforeEach( () => { - return ModelTestEditor - .create( { - plugins: [ TableUtils ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - root = model.document.getRoot( 'main' ); - - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); - } ); - - afterEach( () => { - return editor.destroy(); - } ); - - describe( 'direction=right', () => { - beforeEach( () => { - command = new MergeCellCommand( editor, { direction: 'right' } ); - } ); - - describe( 'isEnabled', () => { - it( 'should be true if in cell that has sibling on the right', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if last cell of a row', () => { - setData( model, modelTable( [ - [ '00', '01[]' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if in a cell that has sibling on the right with the same rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00[]' }, { rowspan: 2, contents: '01' } ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in a cell that has sibling but with different rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false when next cell is rowspanned', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 3, contents: '01' }, '02' ], - [ '10[]', '12' ], - [ '20', '22' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true when current cell is colspanned', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00[]' }, '02' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.isEnabled ).to.be.false; - } ); - } ); - - describe( 'value', () => { - it( 'should be set to mergeable sibling if in cell that has sibling on the right', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) ); - - 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[]' ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be set to mergeable sibling if in a cell that has sibling on the right with the same rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00[]' }, { rowspan: 2, contents: '01' } ] - ] ) ); - - expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) ); - } ); - - it( 'should be undefined if in a cell that has sibling but with different rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be undefined if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.value ).to.be.undefined; - } ); - } ); - - describe( 'execute()', () => { - it( 'should merge table cells', () => { - setData( model, modelTable( [ - [ '[]00', '01' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { 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: '[]' } ] - ] ) ); - } ); - } ); - } ); - - describe( 'direction=left', () => { - beforeEach( () => { - command = new MergeCellCommand( editor, { direction: 'left' } ); - } ); - - describe( 'isEnabled', () => { - it( 'should be true if in cell that has sibling on the left', () => { - setData( model, modelTable( [ - [ '00', '01[]' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if first cell of a row', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if in a cell that has sibling on the left with the same rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, { rowspan: 2, contents: '01[]' } ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in a cell that has sibling but with different rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false when next cell is rowspanned', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 3, contents: '01' }, '02' ], - [ '10', '12[]' ], - [ '20', '22' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true when mergeable cell is colspanned', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02[]' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.isEnabled ).to.be.false; - } ); - } ); - - describe( 'value', () => { - it( 'should be set to mergeable sibling if in cell that has sibling on the left', () => { - setData( model, modelTable( [ - [ '00', '01[]' ] - ] ) ); - - 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' ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be set to mergeable sibling if in a cell that has sibling on the left with the same rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, { rowspan: 2, contents: '01[]' } ] - ] ) ); - - expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) ); - } ); - - it( 'should be undefined if in a cell that has sibling but with different rowspan', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be undefined if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.value ).to.be.undefined; - } ); - } ); - - describe( 'execute()', () => { - it( 'should merge table cells', () => { - setData( model, modelTable( [ - [ '00', '[]01' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { 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: '[]' } ] - ] ) ); - } ); - } ); - } ); - - describe( 'direction=down', () => { - beforeEach( () => { - command = new MergeCellCommand( editor, { direction: 'down' } ); - } ); - - describe( 'isEnabled', () => { - it( 'should be true if in cell that has mergeable cell in next row', () => { - setData( model, modelTable( [ - [ '00', '01[]' ], - [ '10', '11' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in last row', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10[]', '11' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if in a cell that has mergeable cell with the same colspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00[]' }, '02' ], - [ { colspan: 2, contents: '01' }, '12' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in a cell that potential mergeable cell has different colspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00[]' }, '02' ], - [ { colspan: 3, contents: '01' } ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if mergeable cell is in other table section then current cell', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ], { headingRows: 1 } ) ); - - expect( command.isEnabled ).to.be.false; - } ); - } ); - - describe( 'value', () => { - it( 'should be set to mergeable cell', () => { - setData( model, modelTable( [ - [ '00', '01[]' ], - [ '10', '11' ] - ] ) ); - - 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' ], - [ '10[]', '11' ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be set to mergeable cell with the same rowspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00[]' }, '02' ], - [ { colspan: 2, contents: '01' }, '12' ] - ] ) ); - - expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 0 ] ) ); - } ); - - it( 'should be undefined if in a cell that potential mergeable cell has different rowspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00[]' }, '02' ], - [ { colspan: 3, contents: '01' } ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be undefined if mergable cell is in other table section', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00[]' }, '02' ], - [ '12' ], - [ '21', '22' ] - ], { headingRows: 2 } ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be undefined if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.value ).to.be.undefined; - } ); - } ); - - describe( 'execute()', () => { - it( 'should merge table cells', () => { - setData( model, modelTable( [ - [ '00', '01[]' ], - [ '10', '11' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], - [ '10' ] - ] ) ); - } ); - - 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' } ], - [ '11' ], - [ '20', '21' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], - [ '20', '21' ] - ] ) ); - } ); - - it( 'should not reduce rowspan on cells above removed empty row when merging table cells ', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, '01', '02' ], - [ '11', '12' ], - [ { rowspan: 2, contents: '20' }, '21[]', { rowspan: 3, contents: '22' } ], - [ '31' ], - [ '40', '41' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { rowspan: 2, contents: '00' }, '01', '02' ], - [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], - [ '40', '41' ] - ] ) ); - } ); - } ); - } ); - - describe( 'direction=up', () => { - beforeEach( () => { - command = new MergeCellCommand( editor, { direction: 'up' } ); - } ); - - describe( 'isEnabled', () => { - it( 'should be true if in cell that has mergeable cell in previous row', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11[]' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in first row', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if in a cell that has mergeable cell with the same colspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02' ], - [ { colspan: 2, contents: '01[]' }, '12' ] - ] ) ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if in a cell that potential mergeable cell has different colspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02' ], - [ { colspan: 3, contents: '01[]' } ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if mergeable cell is in other table section then current cell', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10[]', '11' ] - ], { headingRows: 1 } ) ); - - expect( command.isEnabled ).to.be.false; - } ); - } ); - - describe( 'value', () => { - it( 'should be set to mergeable cell', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11[]' ] - ] ) ); - - 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' ], - [ '10', '11' ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be set to mergeable cell with the same rowspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02' ], - [ { colspan: 2, contents: '01[]' }, '12' ] - ] ) ); - - expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) ); - } ); - - it( 'should be set to mergeable cell in rows with spanned cells', () => { - setData( model, modelTable( [ - [ { rowspan: 3, contents: '00' }, '11', '12', '13' ], - [ { rowspan: 2, contents: '21' }, '22', '23' ], - [ '32', { rowspan: 2, contents: '33[]' } ], - [ { colspan: 2, contents: '40' }, '42' ] - ] ) ); - - expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 2 ] ) ); - } ); - - it( 'should be undefined if in a cell that potential mergeable cell has different rowspan', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02' ], - [ { colspan: 3, contents: '01[]' } ] - ] ) ); - - expect( command.value ).to.be.undefined; - } ); - - it( 'should be undefined if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.value ).to.be.undefined; - } ); - } ); - - describe( 'execute()', () => { - it( 'should merge table cells', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10', '[]11' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', { rowspan: 2, contents: '[0111]' } ], - [ '10' ] - ] ) ); - } ); - - 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' ], - [ { rowspan: 2, contents: '21' }, '22', '23' ], - [ '32', { rowspan: 2, contents: '33[]' } ], - [ { colspan: 2, contents: '40' }, '42' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { rowspan: 3, contents: '00' }, '11', '12', '13' ], - [ - { rowspan: 2, contents: '21' }, - '22', - { rowspan: 3, contents: '[2333]' } - ], - [ '32' ], - [ { colspan: 2, contents: '40' }, '42' ] - ] ) ); - } ); - - it( 'should remove empty row if merging table cells ', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' } ], - [ '11[]' ], - [ '20', '21' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '[0111]', { rowspan: 2, contents: '02' } ], - [ '20', '21' ] - ] ) ); - } ); - - it( 'should not reduce rowspan on cells above removed empty row when merging table cells ', () => { - setData( model, modelTable( [ - [ { rowspan: 2, contents: '00' }, '01', '02' ], - [ '11', '12' ], - [ { rowspan: 2, contents: '20' }, '21', { rowspan: 3, contents: '22' } ], - [ '31[]' ], - [ '40', '41' ] - ] ) ); - - command.execute(); - - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { rowspan: 2, contents: '00' }, '01', '02' ], - [ '11', '12' ], - [ '20', '[2131]', { rowspan: 2, contents: '22' } ], - [ '40', '41' ] - ] ) ); - } ); - } ); - } ); -} ); diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js new file mode 100644 index 00000000..6d823595 --- /dev/null +++ b/tests/commands/mergecellscommand.js @@ -0,0 +1,421 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import MergeCellsCommand from '../../src/commands/mergecellscommand'; +import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import TableUtils from '../../src/tableutils'; +import TableSelection from '../../src/tableselection'; +import Range from '../../../ckeditor5-engine/src/model/range'; + +describe( 'MergeCellsCommand', () => { + let editor, model, command, root; + + beforeEach( () => { + return ModelTestEditor + .create( { + plugins: [ TableUtils, TableSelection ] + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + + command = new MergeCellsCommand( editor ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'isEnabled', () => { + it( 'should be false if collapsed selection in table cell', () => { + setData( model, modelTable( [ + [ '00[]', '01' ] + ] ) ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if only one table cell is selected', () => { + setData( model, modelTable( [ + [ '00', '01' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ] ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be true if at least two adjacent table cells are selected', () => { + setData( model, modelTable( [ + [ '00', '01' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if many table cells are selected', () => { + setData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 1 ], [ 0, 0, 2 ], + [ 0, 1, 1 ], [ 0, 1, 2 ], + [ 0, 2, 1 ], [ 0, 2, 2 ], + [ 0, 3, 1 ], [ 0, 3, 2 ] + ] ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if at least one table cell is not selected from an area', () => { + setData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 1 ], [ 0, 0, 2 ], + [ 0, 1, 2 ], // one table cell not selected from this row + [ 0, 2, 1 ], [ 0, 2, 2 ], + [ 0, 3, 1 ], [ 0, 3, 2 ] + ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if table cells are not in adjacent rows', () => { + setData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ] ) ); + + selectNodes( [ + [ 0, 1, 0 ], + [ 0, 0, 1 ] + ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if table cells are not in adjacent columns', () => { + setData( model, modelTable( [ + [ '00', '01', '02' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 2 ] ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if any range is collapsed in selection', () => { + setData( model, modelTable( [ + [ '00', '01', '02' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0, 0, 0 ], // The "00" text node + [ 0, 0, 1 ] + ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if any ranges are on different tables', () => { + setData( model, + modelTable( [ [ '00', '01' ] ] ) + + modelTable( [ [ 'aa', 'ab' ] ] ) + ); + + selectNodes( [ + [ 0, 0, 0 ], // first table + [ 1, 0, 1 ] // second table + ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be false if any table cell with colspan attribute extends over selection area', () => { + setData( model, modelTable( [ + [ '00', { colspan: 2, contents: '01' } ], + [ '10', '11', '12' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ], + [ 0, 1, 0 ], [ 0, 1, 1 ] + ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be true if none table cell with colspan attribute extends over selection area', () => { + setData( model, modelTable( [ + [ '00', { colspan: 2, contents: '01' } ], + [ '10', '11', '12' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ], + [ 0, 1, 0 ], [ 0, 1, 1 ], + [ 0, 1, 2 ] + ] ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true if first table cell is inside selection area', () => { + setData( model, modelTable( [ + [ { colspan: 2, rowspan: 2, contents: '00' }, '02', '03' ], + [ '12', '13' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ], + [ 0, 1, 0 ] + ] ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if any table cell with rowspan attribute extends over selection area', () => { + setData( model, modelTable( [ + [ '00', { rowspan: 2, contents: '01' } ], + [ '10' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + expect( command.isEnabled ).to.be.false; + } ); + + it( 'should be true if none table cell with rowspan attribute extends over selection area', () => { + setData( model, modelTable( [ + [ '00', { rowspan: 2, contents: '01' } ], + [ '10' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ], + [ 0, 1, 0 ] + ] ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false if not in a cell', () => { + setData( model, '11[]' ); + + expect( command.isEnabled ).to.be.false; + } ); + } ); + + describe( 'execute()', () => { + it( 'should merge table cells', () => { + setData( model, modelTable( [ + [ '[]00', '01' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ] + ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[0001]' } ] + ] ) ); + } ); + + it( 'should merge table cells - extend colspan attribute', () => { + setData( model, modelTable( [ + [ { colspan: 2, contents: '00' }, '02', '03' ], + [ '10', '11', '12', '13' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], [ 0, 0, 1 ], + [ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ] + ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { + colspan: 3, + rowspan: 2, + contents: '[00' + + '02' + + '10' + + '11' + + '12]' + }, '03' ], + [ '13' ] + ] ) ); + } ); + + it( 'should merge to a single paragraph - every cell is empty', () => { + setData( model, modelTable( [ + [ '[]', '' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); + + it( 'should merge to a single paragraph - merged cell is empty', () => { + setData( model, modelTable( [ + [ 'foo', '' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should merge to a single paragraph - cell to which others are merged is empty', () => { + setData( model, modelTable( [ + [ '', 'foo' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[foo]' } ] + ] ) ); + } ); + + it( 'should not merge empty blocks other then to a single block', () => { + model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isBlock: true + } ); + + setData( model, modelTable( [ + [ '[]', '' ] + ] ) ); + + selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ { colspan: 2, contents: '[]' } ] + ] ) ); + } ); + + describe( 'removing empty row', () => { + it( 'should remove empty row if merging all table cells from that row', () => { + setData( model, modelTable( [ + [ '00' ], + [ '10' ], + [ '20' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 2, 0 ] + ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ + '[001020]' + ] + ] ) ); + } ); + + it( 'should decrease rowspan if cell overlaps removed row', () => { + setData( model, modelTable( [ + [ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ], + [ '10' ], + [ '20', '21' ] + ] ) ); + + selectNodes( [ + [ 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 2, 0 ] + ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ + { rowspan: 2, contents: '[001020]' }, + '01', + { rowspan: 2, contents: '02' } + ], + [ '21' ] + ] ) ); + } ); + + it( 'should not decrease rowspan if cell from previous row does not overlaps removed row', () => { + setData( model, modelTable( [ + [ '00', { rowspan: 2, contents: '01' } ], + [ '10' ], + [ '20', '21' ], + [ '30', '31' ] + ] ) ); + + selectNodes( [ + [ 0, 2, 0 ], [ 0, 2, 1 ], + [ 0, 3, 0 ], [ 0, 3, 1 ] + ] ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '00', { rowspan: 2, contents: '01' } ], + [ '10' ], + [ + { + colspan: 2, + contents: '[2021' + + '3031]' + } + ] + ] ) ); + } ); + } ); + } ); + + function selectNodes( paths ) { + const ranges = paths.map( path => Range.createOn( root.getNodeByPath( path ) ) ); + + model.change( writer => { + writer.setSelection( ranges ); + } ); + } +} ); diff --git a/tests/tableediting.js b/tests/tableediting.js index 86fc3a74..a19f0782 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -17,7 +17,7 @@ import InsertColumnCommand from '../src/commands/insertcolumncommand'; import RemoveRowCommand from '../src/commands/removerowcommand'; import RemoveColumnCommand from '../src/commands/removecolumncommand'; import SplitCellCommand from '../src/commands/splitcellcommand'; -import MergeCellCommand from '../src/commands/mergecellcommand'; +import MergeCellsCommand from '../src/commands/mergecellscommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; import TableSelection from '../src/tableselection'; @@ -113,20 +113,8 @@ describe( 'TableEditing', () => { expect( editor.commands.get( 'splitTableCellHorizontally' ) ).to.be.instanceOf( SplitCellCommand ); } ); - it( 'adds mergeCellRight command', () => { - expect( editor.commands.get( 'mergeTableCellRight' ) ).to.be.instanceOf( MergeCellCommand ); - } ); - - it( 'adds mergeCellLeft command', () => { - expect( editor.commands.get( 'mergeTableCellLeft' ) ).to.be.instanceOf( MergeCellCommand ); - } ); - - it( 'adds mergeCellDown command', () => { - expect( editor.commands.get( 'mergeTableCellDown' ) ).to.be.instanceOf( MergeCellCommand ); - } ); - - it( 'adds mergeCellUp command', () => { - expect( editor.commands.get( 'mergeTableCellUp' ) ).to.be.instanceOf( MergeCellCommand ); + it( 'adds mergeTableCells command', () => { + expect( editor.commands.get( 'mergeTableCells' ) ).to.be.instanceOf( MergeCellsCommand ); } ); it( 'adds setColumnHeader command', () => { @@ -256,7 +244,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ] + [ '11', '[12]' ] ] ) ); } ); @@ -269,7 +257,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[]', '' ] + [ '[]', '' ] ] ) ); } ); @@ -283,7 +271,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[21]', '22' ] + [ '[21]', '22' ] ] ) ); } ); @@ -298,7 +286,7 @@ describe( 'TableEditing', () => { [ '11', '12foobar', - '[13]' + '[13]' ], ] ) ); } ); @@ -347,7 +335,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. @@ -407,7 +395,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); } ); @@ -432,7 +420,7 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ], + [ '11', '[12]' ], [ '21', '22' ] ] ) ); } ); @@ -446,7 +434,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ - '[11]', + '[11]', '12foobar', '13' ], diff --git a/tests/tableselection.js b/tests/tableselection.js index 5c7780fa..3d655cdd 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -6,7 +6,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; +import { defaultConversion, defaultSchema, formatTable, modelTable } from './_utils/utils'; import TableSelection from '../src/tableselection'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; @@ -64,30 +64,6 @@ describe( 'TableSelection', () => { expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] ); } ); - - it( 'should set view selection', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); - - expect( getViewData( editor.editing.view ) ).to.equal( - '
' + - '' + - '' + - '' + - '[]' + - '' + - '' + - '' + - '' + - '' + - '
000102
101112
' + - '
' - ); - } ); } ); describe( 'stop()', () => { @@ -162,34 +138,6 @@ describe( 'TableSelection', () => { expect( tableSelection.isSelecting ).to.be.false; expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); } ); - - it( 'should update view selection', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.stopSelection( firstEndNode ); - - expect( getViewData( editor.editing.view ) ).to.equal( - '
' + - '' + - '' + - '' + - '[][]' + - '' + - '' + - '' + - '' + - '' + - '
000102
101112
' + - '
' - ); - } ); } ); describe( 'update()', () => { @@ -266,12 +214,12 @@ describe( 'TableSelection', () => { tableSelection.startSelection( startNode ); tableSelection.updateSelection( firstEndNode ); - expect( getViewData( editor.editing.view ) ).to.equal( + expect( formatTable( getViewData( editor.editing.view ) ) ).to.equal( formatTable( '
' + '' + '' + '' + - '[][]' + + '[][]' + '' + '' + '' + @@ -279,7 +227,7 @@ describe( 'TableSelection', () => { '' + '
000102000102
101112
' + '
' - ); + ) ); } ); } ); From 41ae8f5e8a09c6a9d6e151489cb85541322925f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Sep 2018 13:53:56 +0200 Subject: [PATCH 019/107] Fix table commands after changes in selection mode. --- src/commands/removecolumncommand.js | 25 +++++++++++++-- src/commands/removerowcommand.js | 30 +++++++++++++----- src/tableediting.js | 3 +- src/tableselection.js | 45 ++++++++++++++------------- tests/_utils/utils.js | 3 +- tests/commands/mergecellscommand.js | 22 ++++++------- tests/commands/removecolumncommand.js | 24 +++++++++++--- tests/commands/removerowcommand.js | 14 ++++----- tests/tableediting.js | 32 +++++++++---------- 9 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index 84417e8d..e2d1f38e 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -12,6 +12,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; import TableUtils from '../tableutils'; import { findAncestor, updateNumericAttribute } from './utils'; +import Range from '../../../ckeditor5-engine/src/model/range'; /** * The remove column command. @@ -52,7 +53,6 @@ export default class RemoveColumnCommand extends Command { const table = tableRow.parent; const headingColumns = table.getAttribute( 'headingColumns' ) || 0; - const row = table.getChildIndex( tableRow ); // Cache the table before removing or updating colspans. const tableMap = [ ...new TableWalker( table ) ]; @@ -60,14 +60,23 @@ export default class RemoveColumnCommand extends Command { // Get column index of removed column. const cellData = tableMap.find( value => value.cell === tableCell ); const removedColumn = cellData.column; + const removedRow = cellData.row; + + let cellToFocus; + + const tableUtils = this.editor.plugins.get( TableUtils ); + const columns = tableUtils.getColumns( tableCell.parent.parent ); + + const columnToFocus = removedColumn === columns - 1 ? removedColumn - 1 : removedColumn + 1; + const rowToFocus = removedRow; model.change( writer => { // Update heading columns attribute if removing a row from head section. - if ( headingColumns && row <= headingColumns ) { + if ( headingColumns && removedRow <= headingColumns ) { writer.setAttribute( 'headingColumns', headingColumns - 1, table ); } - for ( const { cell, column, colspan } of tableMap ) { + for ( const { cell, row, column, rowspan, colspan } of tableMap ) { // If colspaned cell overlaps removed column decrease it's span. if ( column <= removedColumn && colspan > 1 && column + colspan > removedColumn ) { updateNumericAttribute( 'colspan', colspan - 1, cell, writer ); @@ -75,7 +84,17 @@ export default class RemoveColumnCommand extends Command { // The cell in removed column has colspan of 1. writer.remove( cell ); } + + if ( isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) ) { + cellToFocus = cell; + } } + + writer.setSelection( Range.createCollapsedAt( cellToFocus ) ); } ); } } + +function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { + return ( row <= rowToFocus && row + rowspan >= rowToFocus ) && ( column <= columnToFocus && column + colspan >= columnToFocus ); +} diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index 42c8a524..37f3e3e6 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -50,30 +50,36 @@ export default class RemoveRowCommand extends Command { const tableRow = tableCell.parent; const table = tableRow.parent; - const currentRow = table.getChildIndex( tableRow ); + const removedRow = table.getChildIndex( tableRow ); + + const tableMap = [ ...new TableWalker( table, { endRow: removedRow } ) ]; + + const cellData = tableMap.find( value => value.cell === tableCell ); + const headingRows = table.getAttribute( 'headingRows' ) || 0; + const rowToFocus = removedRow; + const columnToFocus = cellData.column; + model.change( writer => { - if ( headingRows && currentRow <= headingRows ) { + if ( headingRows && removedRow <= headingRows ) { updateNumericAttribute( 'headingRows', headingRows - 1, table, writer, 0 ); } - const tableMap = [ ...new TableWalker( table, { endRow: currentRow } ) ]; - const cellsToMove = new Map(); // Get cells from removed row that are spanned over multiple rows. tableMap - .filter( ( { row, rowspan } ) => row === currentRow && rowspan > 1 ) + .filter( ( { row, rowspan } ) => row === removedRow && rowspan > 1 ) .forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) ); // Reduce rowspan on cells that are above removed row and overlaps removed row. tableMap - .filter( ( { row, rowspan } ) => row <= currentRow - 1 && row + rowspan > currentRow ) + .filter( ( { row, rowspan } ) => row <= removedRow - 1 && row + rowspan > removedRow ) .forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) ); // Move cells to another row. - const targetRow = currentRow + 1; + const targetRow = removedRow + 1; const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } ); let previousCell; @@ -93,6 +99,16 @@ export default class RemoveRowCommand extends Command { } writer.remove( tableRow ); + + const { cell: cellToFocus } = [ ...new TableWalker( table ) ].find( ( { row, column, rowspan, colspan } ) => { + return isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ); + } ); + + writer.setSelection( Range.createCollapsedAt( cellToFocus ) ); } ); } } + +function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { + return ( row <= rowToFocus && row + rowspan >= rowToFocus + 1 ) && ( column <= columnToFocus && column + colspan >= columnToFocus + 1 ); +} diff --git a/src/tableediting.js b/src/tableediting.js index 157c8a07..44745161 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -56,7 +56,6 @@ export default class TableEditing extends Plugin { schema.register( 'table', { allowWhere: '$block', allowAttributes: [ 'headingRows', 'headingColumns' ], - isLimit: true, isObject: true } ); @@ -175,7 +174,7 @@ export default class TableEditing extends Plugin { cancel(); editor.model.change( writer => { - writer.setSelection( Range.createIn( selectedElement.getChild( 0 ).getChild( 0 ) ) ); + writer.setSelection( Range.createIn( selectedElement.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ); } ); } } diff --git a/src/tableselection.js b/src/tableselection.js index e34251d2..861ec152 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -44,10 +44,26 @@ export default class TableSelection extends Plugin { const editor = this.editor; const viewDocument = editor.editing.view.document; + this.listenTo( viewDocument, 'keydown', () => { + if ( this.size > 1 ) { + this.stopSelection(); + const tableCell = this._startElement; + this.clearSelection(); + + editor.model.change( writer => { + // Select the contents of table cell. + writer.setSelection( Range.createIn( tableCell ) ); + } ); + } + } ); + this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { const tableCell = getTableCell( domEventData, this.editor ); if ( !tableCell ) { + this.stopSelection(); + this.clearSelection(); + return; } @@ -65,26 +81,11 @@ export default class TableSelection extends Plugin { return; } - const wasOne = this.size === 1; - this.updateSelection( tableCell ); if ( this.size > 1 ) { domEventData.preventDefault(); - if ( wasOne ) { - // TODO: - // editor.editing.view.change( writer => { - // // TODO const viewElement = editor.editing.mapper.toViewElement( this._startElement ); - // - // // Set selection to the first selected table cell. - // // writer.setSelection( ViewRange.createIn( viewElement ), { - // // fake: true, - // // label: 'fake selection over table cell' - // // } ); - // } ); - } - this.redrawSelection(); } } ); @@ -98,6 +99,14 @@ export default class TableSelection extends Plugin { this.stopSelection( tableCell ); } ); + + this.listenTo( viewDocument, 'mouseleave', () => { + if ( !this.isSelecting ) { + return; + } + + this.stopSelection(); + } ); } get isSelecting() { @@ -113,9 +122,6 @@ export default class TableSelection extends Plugin { this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; - - // todo: stop rendering - this.editor.editing.view._renderer.renderSelection = false; } updateSelection( tableCell ) { @@ -148,7 +154,6 @@ export default class TableSelection extends Plugin { } this._isSelecting = false; - this.editor.editing.view._renderer.renderSelection = true; } clearSelection() { @@ -157,8 +162,6 @@ export default class TableSelection extends Plugin { this._isSelecting = false; this.clearPreviousSelection(); this._highlighted.clear(); - - this.editor.editing.view._renderer.renderSelection = true; } * getSelection() { diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index a4d419c6..d26f44c3 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -160,7 +160,6 @@ export function defaultSchema( schema, registerParagraph = true ) { schema.register( 'table', { allowWhere: '$block', allowAttributes: [ 'headingRows', 'headingColumns' ], - isLimit: true, isObject: true } ); @@ -172,7 +171,7 @@ export function defaultSchema( schema, registerParagraph = true ) { schema.register( 'tableCell', { allowIn: 'tableRow', allowAttributes: [ 'colspan', 'rowspan' ], - isLimit: true + isObject: true } ); // Allow all $block content inside table cell. diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index 6d823595..1756e3fc 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -240,7 +240,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[0001]' } ] + [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); @@ -261,11 +261,11 @@ describe( 'MergeCellsCommand', () => { [ { colspan: 3, rowspan: 2, - contents: '[00' + + contents: '[00' + '02' + '10' + '11' + - '12]' + '12]' }, '03' ], [ '13' ] ] ) ); @@ -281,7 +281,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[]' } ] + [ { colspan: 2, contents: '[]' } ] ] ) ); } ); @@ -295,7 +295,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[foo]' } ] + [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); @@ -309,7 +309,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[foo]' } ] + [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); @@ -329,7 +329,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ { colspan: 2, contents: '[]' } ] + [ { colspan: 2, contents: '[]' } ] ] ) ); } ); @@ -351,7 +351,7 @@ describe( 'MergeCellsCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ - '[001020]' + '[001020]' ] ] ) ); } ); @@ -373,7 +373,7 @@ describe( 'MergeCellsCommand', () => { expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ [ - { rowspan: 2, contents: '[001020]' }, + { rowspan: 2, contents: '[001020]' }, '01', { rowspan: 2, contents: '02' } ], @@ -402,8 +402,8 @@ describe( 'MergeCellsCommand', () => { [ { colspan: 2, - contents: '[2021' + - '3031]' + contents: '[2021' + + '3031]' } ] ] ) ); diff --git a/tests/commands/removecolumncommand.js b/tests/commands/removecolumncommand.js index 22def6f4..f0570a0b 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' ] @@ -144,5 +144,21 @@ describe( 'RemoveColumnCommand', () => { [ '21', '22', '23' ] ] ) ); } ); + + it( 'should move focus to previous column of removed cell if in last column', () => { + setData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12[]' ], + [ '20', '21', '22' ] + ] ) ); + + command.execute(); + + expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + [ '00', '01' ], + [ '10', '[]11' ], + [ '20', '21' ] + ] ) ); + } ); } ); } ); diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js index 0dc4546b..e0a77660 100644 --- a/tests/commands/removerowcommand.js +++ b/tests/commands/removerowcommand.js @@ -64,8 +64,8 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], - [ '20', '21' ] + [ '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,8 +94,8 @@ describe( 'RemoveRowCommand', () => { command.execute(); expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ - [ '00', '01[]' ], - [ '20', '21' ] + [ '00', '01' ], + [ '[]20', '21' ] ], { headingRows: 1 } ) ); } ); @@ -111,8 +111,8 @@ 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[]' ], - [ '30', '31', '32', '33', '34' ] + [ '13', '14' ], + [ '30', '[]31', '32', '33', '34' ] ] ) ); } ); diff --git a/tests/tableediting.js b/tests/tableediting.js index a19f0782..b1e0da91 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -221,17 +221,14 @@ describe( 'TableEditing', () => { describe( 'on TAB', () => { it( 'should do nothing if selection is not in a table', () => { - setModelData( model, '[]' + modelTable( [ - [ '11', '12' ] - ] ) ); + setModelData( model, '[]' + modelTable( [ [ '11', '12' ] ] ) ); editor.editing.view.document.fire( 'keydown', domEvtDataStub ); sinon.assert.notCalled( domEvtDataStub.preventDefault ); sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - expect( formatTable( getModelData( model ) ) ).to.equal( '[]' + formattedModelTable( [ - [ '11', '12' ] - ] ) ); + expect( formatTable( getModelData( model ) ) ) + .to.equal( '[]' + formattedModelTable( [ [ '11', '12' ] ] ) ); } ); it( 'should move to next cell', () => { @@ -244,7 +241,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.preventDefault ); sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ] + [ '11', '[12]' ] ] ) ); } ); @@ -257,7 +254,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[]', '' ] + [ '[]', '' ] ] ) ); } ); @@ -271,7 +268,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ '11', '12' ], - [ '[21]', '22' ] + [ '[21]', '22' ] ] ) ); } ); @@ -286,7 +283,7 @@ describe( 'TableEditing', () => { [ '11', '12foobar', - '[13]' + '[13]' ], ] ) ); } ); @@ -335,7 +332,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. @@ -368,7 +365,7 @@ describe( 'TableEditing', () => { } ); it( 'should do nothing if selection is not in a table', () => { - setModelData( model, '[]' + modelTable( [ + setModelData( model, '[]' + modelTable( [ [ '11', '12' ] ] ) ); @@ -379,9 +376,8 @@ describe( 'TableEditing', () => { sinon.assert.notCalled( domEvtDataStub.preventDefault ); sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - expect( formatTable( getModelData( model ) ) ).to.equal( '[]' + formattedModelTable( [ - [ '11', '12' ] - ] ) ); + expect( formatTable( getModelData( model ) ) ) + .to.equal( '[]' + formattedModelTable( [ [ '11', '12' ] ] ) ); } ); it( 'should move to previous cell', () => { @@ -395,7 +391,7 @@ describe( 'TableEditing', () => { sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '[11]', '12' ] + [ '[11]', '12' ] ] ) ); } ); @@ -420,7 +416,7 @@ describe( 'TableEditing', () => { editor.editing.view.document.fire( 'keydown', domEvtDataStub ); expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ - [ '11', '[12]' ], + [ '11', '[12]' ], [ '21', '22' ] ] ) ); } ); @@ -434,7 +430,7 @@ describe( 'TableEditing', () => { expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ [ - '[11]', + '[11]', '12foobar', '13' ], From 15b3a8e73967203acfaf0457a78f4c8cc245327c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 12 Sep 2018 16:55:41 +0200 Subject: [PATCH 020/107] Test: Add table selection tests in table editing. --- src/commands/removecolumncommand.js | 2 +- tests/_utils/utils.js | 12 +- tests/commands/mergecellscommand.js | 2 +- tests/converters/downcast.js | 116 +++++----- tests/tableediting.js | 340 ++++++++++++++++++++++++++-- tests/tableselection.js | 277 ---------------------- 6 files changed, 395 insertions(+), 354 deletions(-) delete mode 100644 tests/tableselection.js diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index e2d1f38e..477c83dc 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -12,7 +12,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; import TableUtils from '../tableutils'; import { findAncestor, updateNumericAttribute } from './utils'; -import Range from '../../../ckeditor5-engine/src/model/range'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; /** * The remove column command. diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index d26f44c3..9fdd1965 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -250,20 +250,26 @@ function makeRows( tableData, options ) { let contents = isObject ? tableCellData.contents : tableCellData; let resultingCellElement = cellElement; + let isSelected = false; if ( isObject ) { if ( tableCellData.isHeading ) { resultingCellElement = headingElement; } + isSelected = !!tableCellData.isSelected; + delete tableCellData.contents; delete tableCellData.isHeading; + delete tableCellData.isSelected; } const attributes = isObject ? tableCellData : {}; if ( asWidget ) { - attributes.class = 'ck-editor__editable ck-editor__nested-editable'; + attributes.class = attributes.class ? ' ' + attributes.class : ''; + attributes.class = 'ck-editor__editable ck-editor__nested-editable' + attributes.class; + attributes.contenteditable = 'true'; } @@ -272,7 +278,9 @@ function makeRows( tableData, options ) { } const formattedAttributes = formatAttributes( attributes ); - tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }`; + const tableCell = `<${ resultingCellElement }${ formattedAttributes }>${ contents }`; + + tableRowString += isSelected ? `[${ tableCell }]` : tableCell; return tableRowString; }, '' ); diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index 1756e3fc..dce8d4cd 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -10,7 +10,7 @@ import MergeCellsCommand from '../../src/commands/mergecellscommand'; import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; import TableSelection from '../../src/tableselection'; -import Range from '../../../ckeditor5-engine/src/model/range'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'MergeCellsCommand', () => { let editor, model, command, root; diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index f4c3ad6c..6b006a91 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -34,7 +34,7 @@ function paragraphInTableCell() { } describe( 'downcast converters', () => { - let editor, model, doc, root, viewDocument; + let editor, model, doc, root, view; testUtils.createSinonSandbox(); @@ -48,7 +48,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion ); @@ -59,7 +59,7 @@ describe( 'downcast converters', () => { it( 'should create table with tbody', () => { setModelData( model, modelTable( [ [ '' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '' + '' + @@ -76,7 +76,7 @@ describe( 'downcast converters', () => { [ '10' ] ], { headingRows: 1 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -96,7 +96,7 @@ describe( 'downcast converters', () => { [ '10' ] ], { headingRows: 2 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -114,7 +114,7 @@ describe( 'downcast converters', () => { [ '10', '11', '12', '13' ] ], { headingColumns: 3, headingRows: 1 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -133,7 +133,7 @@ describe( 'downcast converters', () => { [ '00foo', '01' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -164,7 +164,7 @@ describe( 'downcast converters', () => { setModelData( model, modelTable( [ [ '' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '' + '

' @@ -178,7 +178,7 @@ describe( 'downcast converters', () => { [ '10', '11', '12' ] ], { headingColumns: 2 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '' + '' + @@ -196,7 +196,7 @@ describe( 'downcast converters', () => { [ { colspan: 2, contents: '10' }, '12', '13' ] ], { headingColumns: 3 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -230,7 +230,7 @@ describe( 'downcast converters', () => { [ '32', '33' ] ], { headingColumns: 3 } ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -253,7 +253,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -263,7 +263,7 @@ describe( 'downcast converters', () => { it( 'should create table as a widget', () => { setModelData( model, modelTable( [ [ '' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '
' + @@ -296,7 +296,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ] ] ) ); @@ -318,7 +318,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ] ] ) ); @@ -332,7 +332,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ], [ '', '' ] @@ -357,7 +357,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ], [ '21', '22' ], @@ -383,7 +383,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ], [ '21', '22' ], @@ -409,7 +409,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ], [ '21', '22' ], @@ -432,7 +432,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { rowspan: 3, contents: '00' }, '01' ], [ '22' ], [ '' ] @@ -460,7 +460,7 @@ describe( 'downcast converters', () => { writer.insert( writer.createElement( 'tableCell' ), secondRow, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { rowspan: 3, contents: '00', isHeading: true }, '01' ], [ '22' ], [ '' ], @@ -476,7 +476,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -496,7 +496,7 @@ describe( 'downcast converters', () => { } ); expect( formatTable( - getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '
' + @@ -531,7 +531,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 1 ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '', '01' ] ] ) ); } ); @@ -549,7 +549,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', row, 1 ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { colspan: 2, contents: '00' }, '', '13' ] ] ) ); } ); @@ -567,7 +567,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', table.getChild( 1 ), 0 ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { rowspan: 2, contents: '00' }, '', '13' ], [ '', '11', '12' ] ] ) ); @@ -589,7 +589,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'colspan', 2, secondRow.getChild( 0 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '', '01' ], [ { colspan: 2, contents: '10' }, '11' ] ] ) ); @@ -610,7 +610,7 @@ describe( 'downcast converters', () => { writer.remove( firstRow.getChild( 1 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { colspan: 2, contents: '00' } ], [ '10', '11' ] ] ) ); @@ -624,7 +624,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -642,7 +642,7 @@ describe( 'downcast converters', () => { writer.insert( writer.createElement( 'tableCell' ), row, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '
' + @@ -674,7 +674,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingColumns', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { isHeading: true, contents: '00' }, '01' ], [ { isHeading: true, contents: '10' }, '11' ] ], { headingColumns: 1 } ) ); @@ -692,7 +692,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingColumns', 3, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { isHeading: true, contents: '00' }, { isHeading: true, contents: '01' }, { isHeading: true, contents: '02' }, '03' ], [ { isHeading: true, contents: '10' }, { isHeading: true, contents: '11' }, { isHeading: true, contents: '12' }, '13' ] ] ) ); @@ -710,7 +710,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingColumns', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { isHeading: true, contents: '00' }, '01', '02', '03' ], [ { isHeading: true, contents: '10' }, '11', '12', '13' ] ], { headingColumns: 3 } ) ); @@ -727,7 +727,7 @@ describe( 'downcast converters', () => { writer.removeAttribute( 'headingColumns', table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ] ] ) ); @@ -743,7 +743,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingColumns', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -772,7 +772,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', table.getChild( 2 ), 1 ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ { isHeading: true, rowspan: 2, contents: '00' }, { isHeading: true, contents: '01' }, @@ -803,7 +803,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -819,7 +819,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '
' + @@ -851,7 +851,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 2, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ], [ '20', '21' ] @@ -871,7 +871,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 2, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ], [ '20', '21' ] @@ -892,7 +892,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 2, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ], [ '20', '21' ], @@ -912,7 +912,7 @@ describe( 'downcast converters', () => { writer.removeAttribute( 'headingRows', table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ] ] ) ); @@ -930,7 +930,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 2, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '10', '11' ] ], { headingRows: 2 } ) ); @@ -946,7 +946,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingRows', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '' + @@ -975,7 +975,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', tableRow, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '', '' ], [ '00', '01' ], [ '10', '11' ] @@ -1001,7 +1001,7 @@ describe( 'downcast converters', () => { writer.insertElement( 'tableCell', tableRow, 'end' ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ], [ '', '' ], [ '10', '11' ], @@ -1017,7 +1017,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -1033,7 +1033,7 @@ describe( 'downcast converters', () => { writer.setAttribute( 'headingColumns', 1, table ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable( + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formatTable( '
' + '
' + '
' + @@ -1064,7 +1064,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 1 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ] ] ) ); } ); @@ -1081,7 +1081,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 0 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '10', '11' ] ] ) ); } ); @@ -1098,7 +1098,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 1 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ] ], { headingRows: 2 } ) ); } ); @@ -1115,7 +1115,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 0 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '10', '11' ] ], { headingRows: 2 } ) ); } ); @@ -1133,7 +1133,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 0 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '10', '11' ] ] ) ); } ); @@ -1150,7 +1150,7 @@ describe( 'downcast converters', () => { writer.remove( table.getChild( 1 ) ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00', '01' ] ], { headingRows: 1 } ) ); } ); @@ -1164,7 +1164,7 @@ describe( 'downcast converters', () => { model = editor.model; doc = model.document; root = doc.getRoot( 'main' ); - viewDocument = editor.editing.view; + view = editor.editing.view; defaultSchema( model.schema ); defaultConversion( editor.conversion, true ); @@ -1178,7 +1178,7 @@ describe( 'downcast converters', () => { [ '00[]' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '00' ] ], { asWidget: true } ) ); @@ -1194,7 +1194,7 @@ describe( 'downcast converters', () => { writer.setSelection( nodeByPath.nextSibling, 0 ); } ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '

00

' ] ], { asWidget: true } ) ); } ); @@ -1207,7 +1207,7 @@ describe( 'downcast converters', () => { [ '00[]' ] ] ) ); - expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ + expect( formatTable( getViewData( view, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [ [ '

00

' ] ], { asWidget: true } ) ); } ); diff --git a/tests/tableediting.js b/tests/tableediting.js index b1e0da91..60307f8b 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -6,11 +6,12 @@ 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; 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'; +import { formatTable, formattedModelTable, formattedViewTable, modelTable } from './_utils/utils'; import InsertRowCommand from '../src/commands/insertrowcommand'; import InsertTableCommand from '../src/commands/inserttablecommand'; import InsertColumnCommand from '../src/commands/insertcolumncommand'; @@ -21,6 +22,7 @@ import MergeCellsCommand from '../src/commands/mergecellscommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; import TableSelection from '../src/tableselection'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'TableEditing', () => { let editor, model; @@ -132,11 +134,11 @@ describe( 'TableEditing', () => { expect( editor.getData() ).to.equal( '
' + - '
' + - '' + - '' + - '' + - '
foo
' + + '' + + '' + + '' + + '' + + '
foo
' + '
' ); } ); @@ -149,11 +151,11 @@ describe( 'TableEditing', () => { expect( editor.getData() ).to.equal( '
' + - '' + - '' + - '' + - '' + - '
foo
' + + '' + + '' + + '' + + '' + + '
foo
' + '
' ); } ); @@ -274,7 +276,7 @@ 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' ], + [ '11', '12[foo]bar', '13' ] ] ) ); editor.editing.view.document.fire( 'keydown', domEvtDataStub ); @@ -284,7 +286,7 @@ describe( 'TableEditing', () => { '11', '12foobar', '[13]' - ], + ] ] ) ); } ); @@ -423,7 +425,7 @@ describe( 'TableEditing', () => { it( 'should move to the previous table cell if part of block content is selected', () => { setModelData( model, modelTable( [ - [ '11', '12[foo]bar', '13' ], + [ '11', '12[foo]bar', '13' ] ] ) ); editor.editing.view.document.fire( 'keydown', domEvtDataStub ); @@ -433,7 +435,7 @@ describe( 'TableEditing', () => { '[11]', '12foobar', '13' - ], + ] ] ) ); } ); } ); @@ -512,4 +514,312 @@ describe( 'TableEditing', () => { ] ) ); } ); } ); + + describe.only( 'table selection', () => { + let view, domEvtDataStub; + + beforeEach( () => { + view = editor.editing.view; + + domEvtDataStub = { + domEvent: { + buttons: 1 + }, + target: undefined, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + } ); + + it( 'should not start table selection when mouse move is inside one table cell', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should start table selection when mouse move expands over two cells', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should update view selection after changing selection rect', () => { + setModelData( model, modelTable( [ + [ '[]00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ], + [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + { contents: '02', class: 'selected', isSelected: true } + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + { contents: '12', class: 'selected', isSelected: true } + ], + [ + { contents: '20', class: 'selected', isSelected: true }, + { contents: '21', class: 'selected', isSelected: true }, + { contents: '22', class: 'selected', isSelected: true } + ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], + [ '20', '21', '22' ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + '02' + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + '12' + ], + [ + '20', + '21', + '22' + ] + ], { asWidget: true } ) ); + } ); + + it( 'should stop selecting after "mouseup" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + view.document.fire( 'mouseup', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should stop selection mode on "mouseleve" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + view.document.fire( 'mouseleave', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should clear view table selection after mouse click outside table', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + domEvtDataStub.target = view.document.getRoot().getChild( 1 ); + + view.document.fire( 'mousemove', domEvtDataStub ); + view.document.fire( 'mousedown', domEvtDataStub ); + view.document.fire( 'mouseup', domEvtDataStub ); + + // The click in the DOM would trigger selection change and it will set the selection: + model.change( writer => { + writer.setSelection( Range.createCollapsedAt( model.document.getRoot().getChild( 1 ) ) ); + } ); + + expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { asWidget: true } ) + '

{}foo

' ); + } ); + } ); } ); + +function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { + domEvtDataStub.target = view.document.getRoot() + .getChild( tableIndex ) + .getChild( 1 ) // Table is second in widget + .getChild( sectionIndex ) + .getChild( rowInSectionIndex ) + .getChild( tableCellIndex ); +} diff --git a/tests/tableselection.js b/tests/tableselection.js deleted file mode 100644 index 3d655cdd..00000000 --- a/tests/tableselection.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @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 { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import { defaultConversion, defaultSchema, formatTable, modelTable } from './_utils/utils'; - -import TableSelection from '../src/tableselection'; -import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; - -describe( 'TableSelection', () => { - let editor, model, root, tableSelection; - - beforeEach( () => { - return VirtualTestEditor.create( { - plugins: [ TableSelection ] - } ).then( newEditor => { - editor = newEditor; - model = editor.model; - root = model.document.getRoot( 'main' ); - tableSelection = editor.plugins.get( TableSelection ); - - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); - } ); - - afterEach( () => { - return editor.destroy(); - } ); - - describe( '#pluginName', () => { - it( 'should provide plugin name', () => { - expect( TableSelection.pluginName ).to.equal( 'TableSelection' ); - } ); - } ); - - describe( 'start()', () => { - it( 'should start selection', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); - - const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); - - tableSelection.startSelection( nodeByPath ); - - expect( tableSelection.isSelecting ).to.be.true; - } ); - - it( 'update selection to single table cell', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); - - const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); - - tableSelection.startSelection( nodeByPath ); - - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] ); - } ); - } ); - - describe( 'stop()', () => { - it( 'should stop selection', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); - - const nodeByPath = root.getNodeByPath( [ 0, 0, 0 ] ); - - tableSelection.startSelection( nodeByPath ); - expect( tableSelection.isSelecting ).to.be.true; - - tableSelection.stopSelection( nodeByPath ); - - expect( tableSelection.isSelecting ).to.be.false; - } ); - - it( 'update selection to passed table cell', () => { - setData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const endNode = root.getNodeByPath( [ 0, 1, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.stopSelection( endNode ); - - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ - startNode, - root.getNodeByPath( [ 0, 0, 1 ] ), - root.getNodeByPath( [ 0, 1, 0 ] ), - endNode - ] ); - } ); - - it( 'should not update selection if alredy stopped', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.stopSelection( firstEndNode ); - - expect( tableSelection.isSelecting ).to.be.false; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); - - const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); - tableSelection.stopSelection( secondEndNode ); - - expect( tableSelection.isSelecting ).to.be.false; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); - } ); - - it( 'should not update selection if table cell is from another parent', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) + modelTable( [ - [ 'aa', 'bb' ] - ] ) ); - - tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.stopSelection( root.getNodeByPath( [ 1, 0, 1 ] ) ); - - expect( tableSelection.isSelecting ).to.be.false; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); - } ); - } ); - - describe( 'update()', () => { - it( 'should update selection', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.updateSelection( firstEndNode ); - - expect( tableSelection.isSelecting ).to.be.true; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); - - const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); - tableSelection.updateSelection( secondEndNode ); - - expect( tableSelection.isSelecting ).to.be.true; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode, secondEndNode ] ); - } ); - - it( 'should not update selection if stopped', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.updateSelection( firstEndNode ); - - expect( tableSelection.isSelecting ).to.be.true; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); - - tableSelection.stopSelection( firstEndNode ); - expect( tableSelection.isSelecting ).to.be.false; - - const secondEndNode = root.getNodeByPath( [ 0, 0, 2 ] ); - tableSelection.updateSelection( secondEndNode ); - - expect( tableSelection.isSelecting ).to.be.false; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ startNode, firstEndNode ] ); - } ); - - it( 'should not update selection if table cell is from another parent', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) + modelTable( [ - [ 'aa', 'bb' ] - ] ) ); - - tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.updateSelection( root.getNodeByPath( [ 1, 0, 1 ] ) ); - - expect( tableSelection.isSelecting ).to.be.true; - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] ); - } ); - - it( 'should update view selection', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02' ], - [ '10', '11', '12' ] - ] ) ); - - const startNode = root.getNodeByPath( [ 0, 0, 0 ] ); - const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] ); - - tableSelection.startSelection( startNode ); - tableSelection.updateSelection( firstEndNode ); - - expect( formatTable( getViewData( editor.editing.view ) ) ).to.equal( formatTable( - '
' + - '' + - '' + - '' + - '[][]' + - '' + - '' + - '' + - '' + - '' + - '
000102
101112
' + - '
' - ) ); - } ); - } ); - - describe( 'getSelection()', () => { - it( 'should return empty array if not started', () => { - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [] ); - } ); - - it( 'should return block of selected nodes', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.startSelection( root.getNodeByPath( [ 0, 1, 1 ] ) ); - tableSelection.updateSelection( root.getNodeByPath( [ 0, 2, 2 ] ) ); - - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ - root.getNodeByPath( [ 0, 1, 1 ] ), - root.getNodeByPath( [ 0, 1, 2 ] ), - root.getNodeByPath( [ 0, 2, 1 ] ), - root.getNodeByPath( [ 0, 2, 2 ] ) - ] ); - } ); - - it( 'should return block of selected nodes (inverted selection)', () => { - setData( model, modelTable( [ - [ '00[]', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - tableSelection.startSelection( root.getNodeByPath( [ 0, 2, 2 ] ) ); - tableSelection.updateSelection( root.getNodeByPath( [ 0, 1, 1 ] ) ); - - expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ - root.getNodeByPath( [ 0, 1, 1 ] ), - root.getNodeByPath( [ 0, 1, 2 ] ), - root.getNodeByPath( [ 0, 2, 1 ] ), - root.getNodeByPath( [ 0, 2, 2 ] ) - ] ); - } ); - } ); -} ); From 263a53a63943ba086e8558c60d59fef9347290be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 14 Sep 2018 15:49:00 +0200 Subject: [PATCH 021/107] Revert "Tests: Simplify table manual test data and add model output." This reverts commit aa398a90 --- tests/manual/table.html | 185 +++++++++++++++++++++++++++++++++++++++- tests/manual/table.js | 33 +------ 2 files changed, 184 insertions(+), 34 deletions(-) diff --git a/tests/manual/table.html b/tests/manual/table.html index 60a63c81..feae0e74 100644 --- a/tests/manual/table.html +++ b/tests/manual/table.html @@ -6,18 +6,199 @@
+

Complex table:

+ +
+
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
a b c
+ +

Table with no tbody:

+ + + + + + + +
a b c
abc
+ +

Table with thead section between two tbody sections

+ + + + + + + + + + + + + + + + +
2
1
3
-

Model contents:

-
diff --git a/tests/manual/table.js b/tests/manual/table.js index 7da2cae6..cf1c8ff1 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -3,14 +3,13 @@ * For licensing, see LICENSE.md. */ -/* globals console, window, document, global */ +/* 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 TableSelection from '../../src/tableselection'; -import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -24,37 +23,7 @@ ClassicEditor } ) .then( editor => { window.editor = editor; - editor.model.document.on( 'change', () => { - printModelContents( editor ); - } ); - - printModelContents( editor ); } ) .catch( err => { console.error( err.stack ); } ); - -const modelDiv = global.document.querySelector( '#model' ); - -function printModelContents( editor ) { - modelDiv.innerHTML = formatTable( getData( editor.model ) ) - .replace( //g, '>' ) - .replace( /\n/g, '
' ) - .replace( /\[/g, '[' ) - .replace( /]/g, ']' ); -} - -function formatTable( tableString ) { - return tableString - .replace( //g, '\n
' ) - .replace( //g, '\n\n ' ) - .replace( //g, '\n\n ' ) - .replace( //g, '\n\n ' ) - .replace( //g, '\n\n ' ) - .replace( /<\/tableRow>/g, '\n' ) - .replace( /<\/thead>/g, '\n' ) - .replace( /<\/tbody>/g, '\n' ) - .replace( /<\/tr>/g, '\n' ) - .replace( /<\/table>/g, '\n
' ); -} From 3f2dc7f62abedc0a9b1ebd2aac2895905212ecb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 14 Sep 2018 16:01:07 +0200 Subject: [PATCH 022/107] Tests: Add manual test for table selection. --- tests/manual/tableselection.html | 53 ++++++++++++++++++++++++++++ tests/manual/tableselection.js | 60 ++++++++++++++++++++++++++++++++ tests/manual/tableselection.md | 8 +++++ 3 files changed, 121 insertions(+) create mode 100644 tests/manual/tableselection.html create mode 100644 tests/manual/tableselection.js create mode 100644 tests/manual/tableselection.md diff --git a/tests/manual/tableselection.html b/tests/manual/tableselection.html new file mode 100644 index 00000000..6f7c5aee --- /dev/null +++ b/tests/manual/tableselection.html @@ -0,0 +1,53 @@ + + +
+

A table to test selection:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
01234
abcde
abcde
abcde
abcde
+
+

Model contents:

+
diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js new file mode 100644 index 00000000..7da2cae6 --- /dev/null +++ b/tests/manual/tableselection.js @@ -0,0 +1,60 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, window, document, global */ + +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 TableSelection from '../../src/tableselection'; +import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, Table, TableSelection, TableToolbar ], + toolbar: [ + 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + ], + table: { + toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + } + } ) + .then( editor => { + window.editor = editor; + editor.model.document.on( 'change', () => { + printModelContents( editor ); + } ); + + printModelContents( editor ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + +const modelDiv = global.document.querySelector( '#model' ); + +function printModelContents( editor ) { + modelDiv.innerHTML = formatTable( getData( editor.model ) ) + .replace( //g, '>' ) + .replace( /\n/g, '
' ) + .replace( /\[/g, '[' ) + .replace( /]/g, ']' ); +} + +function formatTable( tableString ) { + return tableString + .replace( //g, '\n
' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( //g, '\n\n ' ) + .replace( /<\/tableRow>/g, '\n' ) + .replace( /<\/thead>/g, '\n' ) + .replace( /<\/tbody>/g, '\n' ) + .replace( /<\/tr>/g, '\n' ) + .replace( /<\/table>/g, '\n
' ); +} diff --git a/tests/manual/tableselection.md b/tests/manual/tableselection.md new file mode 100644 index 00000000..36376ec6 --- /dev/null +++ b/tests/manual/tableselection.md @@ -0,0 +1,8 @@ +### Testing + +Selecting table cells: + +1. It should be possible to select table cells from the same section (ie.: header) . +2. It should not be possible to extend selection beyond a table section (ie.: header and body). + +Observe selection inn the below model representation - for a block selection the table cells should be selected. From 8fe81c23dda978c59f8ed419b1bc739655443ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Sep 2018 10:22:19 +0200 Subject: [PATCH 023/107] WiP: Refactor TableSelection to an Observer. --- src/tableediting.js | 4 ++ src/tableselection.js | 68 +++++++++++----------------- tests/commands/mergecellscommand.js | 3 +- tests/converters/table-post-fixer.js | 3 +- tests/integration.js | 3 +- tests/manual/table.js | 3 +- tests/manual/tableblockcontent.js | 3 +- tests/manual/tableselection.js | 3 +- tests/table-integration.js | 5 +- tests/tableediting.js | 7 ++- tests/tabletoolbar.js | 3 +- tests/tableui.js | 3 +- 12 files changed, 43 insertions(+), 65 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index 44745161..310fb2c8 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -37,6 +37,7 @@ import injectTablePostFixer from './converters/table-post-fixer'; import injectTableCellPostFixer from './converters/tablecell-post-fixer'; import '../theme/tableediting.css'; +import TableSelection from './tableselection'; /** * The table editing feature. @@ -143,6 +144,9 @@ export default class TableEditing extends Plugin { 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' } ); + + this.tableSelection = new TableSelection(); + this.tableSelection.attach( editor, this.editor.plugins.get( TableUtils ) ); } /** diff --git a/src/tableselection.js b/src/tableselection.js index 861ec152..6432774a 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -4,48 +4,34 @@ */ /** - * @module table/tableediting + * @module table/tableselection */ -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import TableWalker from './tablewalker'; -import TableUtils from './tableutils'; import { findAncestor } from './commands/utils'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import ObservableMixin from '../../ckeditor5-utils/src/observablemixin'; -export default class TableSelection extends Plugin { - /** - * @inheritDoc - */ - static get pluginName() { - return 'TableSelection'; - } - - /** - * @inheritDoc - */ - static get requires() { - return [ TableUtils ]; - } - - constructor( editor ) { - super( editor ); - +// TODO: refactor to an Observer +export default class TableSelection { + constructor() { this._isSelecting = false; this._highlighted = new Set(); + } + // OWN mouse events? + attach( editor, tableUtils ) { + // table selection observer...? + this.tableUtils = tableUtils; this.editor = editor; - this.tableUtils = editor.plugins.get( TableUtils ); - } - init() { - const editor = this.editor; const viewDocument = editor.editing.view.document; this.listenTo( viewDocument, 'keydown', () => { - if ( this.size > 1 ) { + if ( this.isSelectingBlaBla() ) { this.stopSelection(); const tableCell = this._startElement; this.clearSelection(); @@ -67,11 +53,11 @@ export default class TableSelection extends Plugin { return; } - this.startSelection( tableCell ); + this._startSelection( tableCell ); } ); this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { - if ( !this.isSelecting ) { + if ( !this._isSelecting ) { return; } @@ -81,9 +67,9 @@ export default class TableSelection extends Plugin { return; } - this.updateSelection( tableCell ); + this._updateModelSelection( tableCell ); - if ( this.size > 1 ) { + if ( this.isSelectingBlaBla() ) { domEventData.preventDefault(); this.redrawSelection(); @@ -91,7 +77,7 @@ export default class TableSelection extends Plugin { } ); this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { - if ( !this.isSelecting ) { + if ( !this._isSelecting ) { return; } @@ -101,7 +87,7 @@ export default class TableSelection extends Plugin { } ); this.listenTo( viewDocument, 'mouseleave', () => { - if ( !this.isSelecting ) { + if ( !this._isSelecting ) { return; } @@ -109,24 +95,20 @@ export default class TableSelection extends Plugin { } ); } - get isSelecting() { - return this._isSelecting; + isSelectingBlaBla() { + return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; } - get size() { - return [ ...this.getSelection() ].length; - } - - startSelection( tableCell ) { + _startSelection( tableCell ) { this.clearSelection(); this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; } - updateSelection( tableCell ) { + _updateModelSelection( tableCell ) { // Do not update if not in selection mode or no table cell passed. - if ( !this.isSelecting || !tableCell ) { + if ( !this._isSelecting || !tableCell ) { return; } @@ -149,7 +131,7 @@ export default class TableSelection extends Plugin { } stopSelection( tableCell ) { - if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + if ( this._isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } @@ -248,3 +230,5 @@ function getTableCell( domEventData, editor ) { return findAncestor( 'tableCell', Position.createAt( modelElement ) ); } + +mix( TableSelection, ObservableMixin ); diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index dce8d4cd..b711e11b 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -9,7 +9,6 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model import MergeCellsCommand from '../../src/commands/mergecellscommand'; import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; -import TableSelection from '../../src/tableselection'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'MergeCellsCommand', () => { @@ -18,7 +17,7 @@ describe( 'MergeCellsCommand', () => { beforeEach( () => { return ModelTestEditor .create( { - plugins: [ TableUtils, TableSelection ] + plugins: [ TableUtils ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/converters/table-post-fixer.js b/tests/converters/table-post-fixer.js index b3cb4b83..9626a482 100644 --- a/tests/converters/table-post-fixer.js +++ b/tests/converters/table-post-fixer.js @@ -11,7 +11,6 @@ import { getData as getModelData, parse, setData as setModelData } from '@ckedit import TableEditing from '../../src/tableediting'; import { formatTable, formattedModelTable, modelTable } from './../_utils/utils'; import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; -import TableSelection from '../../src/tableselection'; describe( 'Table post-fixer', () => { let editor, model, root; @@ -19,7 +18,7 @@ describe( 'Table post-fixer', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, TableSelection, Paragraph, UndoEditing ] + plugins: [ TableEditing, Paragraph, UndoEditing ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/integration.js b/tests/integration.js index 2fbc8ee1..75b65efe 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -11,7 +11,6 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import Table from '../src/table'; import TableToolbar from '../src/tabletoolbar'; import View from '@ckeditor/ckeditor5-ui/src/view'; -import TableSelection from '../src/tableselection'; describe( 'TableToolbar integration', () => { describe( 'with the BalloonToolbar', () => { @@ -23,7 +22,7 @@ describe( 'TableToolbar integration', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ Table, TableSelection, TableToolbar, BalloonToolbar, Paragraph ] + plugins: [ Table, TableToolbar, BalloonToolbar, Paragraph ] } ) .then( editor => { newEditor = editor; diff --git a/tests/manual/table.js b/tests/manual/table.js index cf1c8ff1..4daac2ce 100644 --- a/tests/manual/table.js +++ b/tests/manual/table.js @@ -9,11 +9,10 @@ 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 TableSelection from '../../src/tableselection'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableSelection, TableToolbar ], + plugins: [ ArticlePluginSet, Table, TableToolbar ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], diff --git a/tests/manual/tableblockcontent.js b/tests/manual/tableblockcontent.js index af9b9aaa..410100f2 100644 --- a/tests/manual/tableblockcontent.js +++ b/tests/manual/tableblockcontent.js @@ -10,11 +10,10 @@ 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 TableSelection from '../../src/tableselection'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar, TableSelection, Alignment ], + plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'alignment', '|', 'undo', 'redo' diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js index 7da2cae6..c228985b 100644 --- a/tests/manual/tableselection.js +++ b/tests/manual/tableselection.js @@ -9,12 +9,11 @@ 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 TableSelection from '../../src/tableselection'; import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableSelection, TableToolbar ], + plugins: [ ArticlePluginSet, Table, TableToolbar ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], diff --git a/tests/table-integration.js b/tests/table-integration.js index 1c6368b1..78d048ec 100644 --- a/tests/table-integration.js +++ b/tests/table-integration.js @@ -19,7 +19,6 @@ import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/vie import TableEditing from '../src/tableediting'; import { formatTable, formattedModelTable, modelTable, viewTable } from './_utils/utils'; -import TableSelection from '../src/tableselection'; describe( 'Table feature – integration', () => { describe( 'with clipboard', () => { @@ -27,7 +26,7 @@ describe( 'Table feature – integration', () => { beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, TableSelection, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) + .create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) .then( newEditor => { editor = newEditor; clipboard = editor.plugins.get( 'Clipboard' ); @@ -86,7 +85,7 @@ describe( 'Table feature – integration', () => { beforeEach( () => { return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, TableSelection, Widget, UndoEditing ] } ) + .create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } ) .then( newEditor => { editor = newEditor; doc = editor.model.document; diff --git a/tests/tableediting.js b/tests/tableediting.js index 60307f8b..5e3cdcc2 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -21,7 +21,6 @@ import SplitCellCommand from '../src/commands/splitcellcommand'; import MergeCellsCommand from '../src/commands/mergecellscommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; -import TableSelection from '../src/tableselection'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'TableEditing', () => { @@ -30,7 +29,7 @@ describe( 'TableEditing', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, TableSelection, Paragraph, ImageEditing ] + plugins: [ TableEditing, Paragraph, ImageEditing ] } ) .then( newEditor => { editor = newEditor; @@ -453,7 +452,7 @@ describe( 'TableEditing', () => { return VirtualTestEditor .create( { - plugins: [ TableEditing, TableSelection, Paragraph ] + plugins: [ TableEditing, Paragraph ] } ) .then( newEditor => { editor = newEditor; @@ -515,7 +514,7 @@ describe( 'TableEditing', () => { } ); } ); - describe.only( 'table selection', () => { + describe( 'table selection', () => { let view, domEvtDataStub; beforeEach( () => { diff --git a/tests/tabletoolbar.js b/tests/tabletoolbar.js index a243a578..382a0894 100644 --- a/tests/tabletoolbar.js +++ b/tests/tabletoolbar.js @@ -15,7 +15,6 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import View from '@ckeditor/ckeditor5-ui/src/view'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import TableSelection from '../src/tableselection'; describe( 'TableToolbar', () => { let editor, model, doc, plugin, toolbar, balloon, editorElement; @@ -26,7 +25,7 @@ describe( 'TableToolbar', () => { return ClassicEditor .create( editorElement, { - plugins: [ Paragraph, Table, TableSelection, TableToolbar, FakeButton ], + plugins: [ Paragraph, Table, TableToolbar, FakeButton ], table: { toolbar: [ 'fake_button' ] } diff --git a/tests/tableui.js b/tests/tableui.js index a3e48d97..dabc2766 100644 --- a/tests/tableui.js +++ b/tests/tableui.js @@ -14,7 +14,6 @@ import TableUI from '../src/tableui'; import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview'; import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview'; import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview'; -import TableSelection from '../src/tableselection'; describe( 'TableUI', () => { let editor, element; @@ -36,7 +35,7 @@ describe( 'TableUI', () => { return ClassicTestEditor .create( element, { - plugins: [ TableEditing, TableSelection, TableUI ] + plugins: [ TableEditing, TableUI ] } ) .then( newEditor => { editor = newEditor; From 6a7bbeac96bfc3946b9b34b92d36ae868c97cae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 26 Nov 2018 15:08:43 +0100 Subject: [PATCH 024/107] Update code to the latest engine API. --- src/commands/mergecellscommand.js | 10 ++++------ src/commands/removecolumncommand.js | 3 +-- src/commands/removerowcommand.js | 2 +- src/tableselection.js | 11 ++++------- tests/commands/mergecellscommand.js | 5 ++--- tests/tableediting.js | 2 +- 6 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js index a33a91b3..3c7b80bf 100644 --- a/src/commands/mergecellscommand.js +++ b/src/commands/mergecellscommand.js @@ -8,8 +8,6 @@ */ 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 { findAncestor, updateNumericAttribute } from './utils'; import TableUtils from '../tableutils'; @@ -58,7 +56,7 @@ export default class MergeCellsCommand extends Command { const firstTableCell = selectedTableCells.shift(); // TODO: this shouldn't be necessary (right now the selection could overlap existing. - writer.setSelection( Range.createIn( firstTableCell ) ); + writer.setSelection( firstTableCell, 'in' ); const { row, column } = tableUtils.getCellLocation( firstTableCell ); @@ -94,7 +92,7 @@ export default class MergeCellsCommand extends Command { updateNumericAttribute( 'colspan', rightMax - column, firstTableCell, writer ); updateNumericAttribute( 'rowspan', bottomMax - row, firstTableCell, writer ); - writer.setSelection( Range.createIn( firstTableCell ) ); + writer.setSelection( firstTableCell, 'in' ); // Remove empty rows after merging table cells. for ( const row of rowsToCheck ) { @@ -136,10 +134,10 @@ function removeEmptyRow( removedTableCellRow, writer ) { function mergeTableCells( cellToRemove, cellToExpand, writer ) { if ( !isEmpty( cellToRemove ) ) { if ( isEmpty( cellToExpand ) ) { - writer.remove( Range.createIn( cellToExpand ) ); + writer.remove( writer.createRangeIn( cellToExpand ) ); } - writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) ); + writer.move( writer.createRangeIn( cellToRemove ), writer.createPositionAt( cellToExpand, 'end' ) ); } // Remove merged table cell. diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index 477c83dc..2331d1f8 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -12,7 +12,6 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; import TableUtils from '../tableutils'; import { findAncestor, updateNumericAttribute } from './utils'; -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; /** * The remove column command. @@ -90,7 +89,7 @@ export default class RemoveColumnCommand extends Command { } } - writer.setSelection( Range.createCollapsedAt( cellToFocus ) ); + writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index b828dc07..926c94a1 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -104,7 +104,7 @@ export default class RemoveRowCommand extends Command { return isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ); } ); - writer.setSelection( Range.createCollapsedAt( cellToFocus ) ); + writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } diff --git a/src/tableselection.js b/src/tableselection.js index 6432774a..d631d682 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -7,13 +7,10 @@ * @module table/tableselection */ -import Position from '@ckeditor/ckeditor5-engine/src/model/position'; - import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import ObservableMixin from '../../ckeditor5-utils/src/observablemixin'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; // TODO: refactor to an Observer export default class TableSelection { @@ -38,7 +35,7 @@ export default class TableSelection { editor.model.change( writer => { // Select the contents of table cell. - writer.setSelection( Range.createIn( tableCell ) ); + writer.setSelection( tableCell, 'in' ); } ); } } ); @@ -186,7 +183,7 @@ export default class TableSelection { for ( const tableCell of selected ) { const viewElement = mapper.toViewElement( tableCell ); - modelRanges.push( Range.createOn( tableCell ) ); + modelRanges.push( model.createRangeOn( tableCell ) ); this._highlighted.add( viewElement ); } @@ -228,7 +225,7 @@ function getTableCell( domEventData, editor ) { return; } - return findAncestor( 'tableCell', Position.createAt( modelElement ) ); + return findAncestor( 'tableCell', editor.model.createPositionAt( modelElement, 0 ) ); } mix( TableSelection, ObservableMixin ); diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index b711e11b..e842053b 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -9,7 +9,6 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model import MergeCellsCommand from '../../src/commands/mergecellscommand'; import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'MergeCellsCommand', () => { let editor, model, command, root; @@ -411,9 +410,9 @@ describe( 'MergeCellsCommand', () => { } ); function selectNodes( paths ) { - const ranges = paths.map( path => Range.createOn( root.getNodeByPath( path ) ) ); - model.change( writer => { + const ranges = paths.map( path => writer.createRangeOn( root.getNodeByPath( path ) ) ); + writer.setSelection( ranges ); } ); } diff --git a/tests/tableediting.js b/tests/tableediting.js index 4f65ce58..7ca68233 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -812,7 +812,7 @@ describe( 'TableEditing', () => { // The click in the DOM would trigger selection change and it will set the selection: model.change( writer => { - writer.setSelection( Range.createCollapsedAt( model.document.getRoot().getChild( 1 ) ) ); + writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); } ); expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ From 72adea1f72b5612b0bd415e8e7eefa916d0f48be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 7 Jun 2019 12:34:55 +0200 Subject: [PATCH 025/107] Fix table selection manual test. --- tests/manual/tableselection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js index c228985b..d9317902 100644 --- a/tests/manual/tableselection.js +++ b/tests/manual/tableselection.js @@ -18,7 +18,7 @@ ClassicEditor 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], table: { - toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] } } ) .then( editor => { From b3206753d9378ad0f1c72489cd787e3e9481991e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 5 Nov 2019 16:14:44 +0100 Subject: [PATCH 026/107] Update manual tests data. --- tests/manual/tableselection.html | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/manual/tableselection.html b/tests/manual/tableselection.html index 6f7c5aee..83001f70 100644 --- a/tests/manual/tableselection.html +++ b/tests/manual/tableselection.html @@ -26,25 +26,25 @@ e - a - b - c - d - e + f + g + h + i + j - a - b - c - d - e + k + l + m + n + o - a - b - c - d - e + p + q + r + s + t From 844cc27c8ca52f5d2b0dfc3b54c99345492e09c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 23 Jan 2020 19:01:34 +0100 Subject: [PATCH 027/107] Make TableSelection an editor plugin. --- src/tableediting.js | 4 ---- src/tableselection.js | 15 ++++++++------- tests/manual/tableselection.js | 3 ++- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/tableediting.js b/src/tableediting.js index fccabd79..2b68f4e9 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -36,7 +36,6 @@ import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph import injectTableCellRefreshPostFixer from './converters/table-cell-refresh-post-fixer'; import '../theme/tableediting.css'; -import TableSelection from './tableselection'; /** * The table editing feature. @@ -145,9 +144,6 @@ export default class TableEditing extends Plugin { 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' } ); - - this.tableSelection = new TableSelection(); - this.tableSelection.attach( editor, this.editor.plugins.get( TableUtils ) ); } /** diff --git a/src/tableselection.js b/src/tableselection.js index d631d682..0cad9041 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -11,19 +11,20 @@ import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; // TODO: refactor to an Observer -export default class TableSelection { - constructor() { +export default class TableSelection extends Plugin { + constructor( editor ) { + super( editor ); + this._isSelecting = false; this._highlighted = new Set(); } - // OWN mouse events? - attach( editor, tableUtils ) { - // table selection observer...? - this.tableUtils = tableUtils; - this.editor = editor; + init() { + const editor = this.editor; + this.tableUtils = editor.plugins.get( 'TableUtils' ); const viewDocument = editor.editing.view.document; diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js index d9317902..eb841fdf 100644 --- a/tests/manual/tableselection.js +++ b/tests/manual/tableselection.js @@ -10,10 +10,11 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi import Table from '../../src/table'; import TableToolbar from '../../src/tabletoolbar'; import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import TableSelection from '../../src/tableselection'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Table, TableToolbar ], + plugins: [ ArticlePluginSet, Table, TableToolbar, TableSelection ], toolbar: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], From d7bcb46f0ab641686811c2efabdac21f84ae1bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 10:28:55 +0100 Subject: [PATCH 028/107] Reset RemoveRowCommand to latest master version. --- src/commands/removerowcommand.js | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index f10336e5..fddb127a 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -48,36 +48,30 @@ export default class RemoveRowCommand extends Command { const tableRow = tableCell.parent; const table = tableRow.parent; - const removedRow = table.getChildIndex( tableRow ); - - const tableMap = [ ...new TableWalker( table, { endRow: removedRow } ) ]; - - const cellData = tableMap.find( value => value.cell === tableCell ); - + const currentRow = table.getChildIndex( tableRow ); const headingRows = table.getAttribute( 'headingRows' ) || 0; - const rowToFocus = removedRow; - const columnToFocus = cellData.column; - model.change( writer => { - if ( headingRows && removedRow <= headingRows ) { + if ( headingRows && currentRow <= headingRows ) { updateNumericAttribute( 'headingRows', headingRows - 1, table, writer, 0 ); } + const tableMap = [ ...new TableWalker( table, { endRow: currentRow } ) ]; + const cellsToMove = new Map(); // Get cells from removed row that are spanned over multiple rows. tableMap - .filter( ( { row, rowspan } ) => row === removedRow && rowspan > 1 ) + .filter( ( { row, rowspan } ) => row === currentRow && rowspan > 1 ) .forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) ); // Reduce rowspan on cells that are above removed row and overlaps removed row. tableMap - .filter( ( { row, rowspan } ) => row <= removedRow - 1 && row + rowspan > removedRow ) + .filter( ( { row, rowspan } ) => row <= currentRow - 1 && row + rowspan > currentRow ) .forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) ); // Move cells to another row. - const targetRow = removedRow + 1; + const targetRow = currentRow + 1; const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } ); let previousCell; @@ -99,16 +93,6 @@ export default class RemoveRowCommand extends Command { } writer.remove( tableRow ); - - const { cell: cellToFocus } = [ ...new TableWalker( table ) ].find( ( { row, column, rowspan, colspan } ) => { - return isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ); - } ); - - writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } - -function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { - return ( row <= rowToFocus && row + rowspan >= rowToFocus + 1 ) && ( column <= columnToFocus && column + colspan >= columnToFocus + 1 ); -} From 520ce167d50cc9b9483dd0f4e9d30ffb00a02c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 10:30:39 +0100 Subject: [PATCH 029/107] Reset RemoveColumnCommand to latest master version. --- src/commands/removecolumncommand.js | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index e9fa187b..3687b087 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -51,6 +51,7 @@ export default class RemoveColumnCommand extends Command { const table = tableRow.parent; const headingColumns = table.getAttribute( 'headingColumns' ) || 0; + const row = table.getChildIndex( tableRow ); // Cache the table before removing or updating colspans. const tableMap = [ ...new TableWalker( table ) ]; @@ -58,23 +59,14 @@ export default class RemoveColumnCommand extends Command { // Get column index of removed column. const cellData = tableMap.find( value => value.cell === tableCell ); const removedColumn = cellData.column; - const removedRow = cellData.row; - - let cellToFocus; - - const tableUtils = this.editor.plugins.get( 'TableUtils' ); - const columns = tableUtils.getColumns( tableCell.parent.parent ); - - const columnToFocus = removedColumn === columns - 1 ? removedColumn - 1 : removedColumn + 1; - const rowToFocus = removedRow; model.change( writer => { // Update heading columns attribute if removing a row from head section. - if ( headingColumns && removedRow <= headingColumns ) { + if ( headingColumns && row <= headingColumns ) { writer.setAttribute( 'headingColumns', headingColumns - 1, table ); } - for ( const { cell, row, column, rowspan, colspan } of tableMap ) { + for ( const { cell, column, colspan } of tableMap ) { // If colspaned cell overlaps removed column decrease it's span. if ( column <= removedColumn && colspan > 1 && column + colspan > removedColumn ) { updateNumericAttribute( 'colspan', colspan - 1, cell, writer ); @@ -82,17 +74,7 @@ export default class RemoveColumnCommand extends Command { // The cell in removed column has colspan of 1. writer.remove( cell ); } - - if ( isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) ) { - cellToFocus = cell; - } } - - writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } - -function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { - return ( row <= rowToFocus && row + rowspan >= rowToFocus ) && ( column <= columnToFocus && column + colspan >= columnToFocus ); -} From 8e8fe76e1120e9e5f78e9d039d158723c5193f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 10:33:17 +0100 Subject: [PATCH 030/107] Revert changes in tab handling. --- src/tableediting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tableediting.js b/src/tableediting.js index 2b68f4e9..b052e1a7 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -175,7 +175,7 @@ export default class TableEditing extends Plugin { cancel(); editor.model.change( writer => { - writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) ); + writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) ); } ); } } From d23e7d3f2b843052a617ce53c69f2e81c7503187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 10:48:37 +0100 Subject: [PATCH 031/107] Restore merge cell up/down/right/bottom commands. --- src/tableediting.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tableediting.js b/src/tableediting.js index b052e1a7..ccd0c8c1 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -23,6 +23,7 @@ import InsertTableCommand from './commands/inserttablecommand'; import InsertRowCommand from './commands/insertrowcommand'; import InsertColumnCommand from './commands/insertcolumncommand'; import SplitCellCommand from './commands/splitcellcommand'; +import MergeCellCommand from './commands/mergecellcommand'; import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; @@ -133,6 +134,11 @@ export default class TableEditing extends Plugin { editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) ); + editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) ); + editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) ); + editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) ); + editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) ); + editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) ); editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) ); From 50f07b961b81a8681277dd40c0f7ac711fabf0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 15:46:30 +0100 Subject: [PATCH 032/107] Revert "Reset RemoveRowCommand to latest master version." This reverts commit d7bcb46f --- src/commands/removerowcommand.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index fddb127a..f10336e5 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -48,30 +48,36 @@ export default class RemoveRowCommand extends Command { const tableRow = tableCell.parent; const table = tableRow.parent; - const currentRow = table.getChildIndex( tableRow ); + const removedRow = table.getChildIndex( tableRow ); + + const tableMap = [ ...new TableWalker( table, { endRow: removedRow } ) ]; + + const cellData = tableMap.find( value => value.cell === tableCell ); + const headingRows = table.getAttribute( 'headingRows' ) || 0; + const rowToFocus = removedRow; + const columnToFocus = cellData.column; + model.change( writer => { - if ( headingRows && currentRow <= headingRows ) { + if ( headingRows && removedRow <= headingRows ) { updateNumericAttribute( 'headingRows', headingRows - 1, table, writer, 0 ); } - const tableMap = [ ...new TableWalker( table, { endRow: currentRow } ) ]; - const cellsToMove = new Map(); // Get cells from removed row that are spanned over multiple rows. tableMap - .filter( ( { row, rowspan } ) => row === currentRow && rowspan > 1 ) + .filter( ( { row, rowspan } ) => row === removedRow && rowspan > 1 ) .forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) ); // Reduce rowspan on cells that are above removed row and overlaps removed row. tableMap - .filter( ( { row, rowspan } ) => row <= currentRow - 1 && row + rowspan > currentRow ) + .filter( ( { row, rowspan } ) => row <= removedRow - 1 && row + rowspan > removedRow ) .forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) ); // Move cells to another row. - const targetRow = currentRow + 1; + const targetRow = removedRow + 1; const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } ); let previousCell; @@ -93,6 +99,16 @@ export default class RemoveRowCommand extends Command { } writer.remove( tableRow ); + + const { cell: cellToFocus } = [ ...new TableWalker( table ) ].find( ( { row, column, rowspan, colspan } ) => { + return isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ); + } ); + + writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } + +function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { + return ( row <= rowToFocus && row + rowspan >= rowToFocus + 1 ) && ( column <= columnToFocus && column + colspan >= columnToFocus + 1 ); +} From 59322afd2f2d784884aa47030042b41716e197dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Jan 2020 15:47:22 +0100 Subject: [PATCH 033/107] Revert "Reset RemoveColumnCommand to latest master version." This reverts commit 520ce167 --- src/commands/removecolumncommand.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index 3687b087..e9fa187b 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -51,7 +51,6 @@ export default class RemoveColumnCommand extends Command { const table = tableRow.parent; const headingColumns = table.getAttribute( 'headingColumns' ) || 0; - const row = table.getChildIndex( tableRow ); // Cache the table before removing or updating colspans. const tableMap = [ ...new TableWalker( table ) ]; @@ -59,14 +58,23 @@ export default class RemoveColumnCommand extends Command { // Get column index of removed column. const cellData = tableMap.find( value => value.cell === tableCell ); const removedColumn = cellData.column; + const removedRow = cellData.row; + + let cellToFocus; + + const tableUtils = this.editor.plugins.get( 'TableUtils' ); + const columns = tableUtils.getColumns( tableCell.parent.parent ); + + const columnToFocus = removedColumn === columns - 1 ? removedColumn - 1 : removedColumn + 1; + const rowToFocus = removedRow; model.change( writer => { // Update heading columns attribute if removing a row from head section. - if ( headingColumns && row <= headingColumns ) { + if ( headingColumns && removedRow <= headingColumns ) { writer.setAttribute( 'headingColumns', headingColumns - 1, table ); } - for ( const { cell, column, colspan } of tableMap ) { + for ( const { cell, row, column, rowspan, colspan } of tableMap ) { // If colspaned cell overlaps removed column decrease it's span. if ( column <= removedColumn && colspan > 1 && column + colspan > removedColumn ) { updateNumericAttribute( 'colspan', colspan - 1, cell, writer ); @@ -74,7 +82,17 @@ export default class RemoveColumnCommand extends Command { // The cell in removed column has colspan of 1. writer.remove( cell ); } + + if ( isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) ) { + cellToFocus = cell; + } } + + writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } + +function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { + return ( row <= rowToFocus && row + rowspan >= rowToFocus ) && ( column <= columnToFocus && column + colspan >= columnToFocus ); +} From 4de73c40dc6d7f7f9629bffc1503ab4f13cc0f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 27 Jan 2020 17:23:11 +0100 Subject: [PATCH 034/107] Update license header. --- src/commands/mergecellscommand.js | 4 ++-- src/tableselection.js | 4 ++-- tests/commands/mergecellscommand.js | 4 ++-- tests/manual/tableselection.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js index 3c7b80bf..6bba1863 100644 --- a/src/commands/mergecellscommand.js +++ b/src/commands/mergecellscommand.js @@ -1,6 +1,6 @@ /** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** diff --git a/src/tableselection.js b/src/tableselection.js index 0cad9041..c302893b 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -1,6 +1,6 @@ /** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index e842053b..edc80ded 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -1,6 +1,6 @@ /** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js index eb841fdf..96cef67f 100644 --- a/tests/manual/tableselection.js +++ b/tests/manual/tableselection.js @@ -1,6 +1,6 @@ /** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals console, window, document, global */ From 837391b90abd5d59b0a51ad0722240417cf7aebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 27 Jan 2020 18:01:27 +0100 Subject: [PATCH 035/107] Use fake selection to render table selection. --- src/tableselection.js | 47 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index c302893b..015d7b28 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -91,6 +91,29 @@ export default class TableSelection extends Plugin { this.stopSelection(); } ); + + editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + if ( this._isSelecting ) { + this.clearPreviousSelection(); + + for ( const tableCell of this.getSelection() ) { + const viewElement = conversionApi.mapper.toViewElement( tableCell ); + + viewWriter.addClass( 'selected', viewElement ); + this._highlighted.add( viewElement ); + } + + const ranges = viewSelection.getRanges(); + const from = Array.from( ranges ); + + viewWriter.setSelection( from, { fake: true, label: 'TABLE' } ); + } else { + this.clearPreviousSelection(); + } + }, { priority: 'lowest' } ) ); } isSelectingBlaBla() { @@ -171,40 +194,18 @@ export default class TableSelection extends Plugin { redrawSelection() { const editor = this.editor; - const mapper = editor.editing.mapper; - const view = editor.editing.view; const model = editor.model; const modelRanges = []; - const selected = [ ...this.getSelection() ]; - const previous = [ ...this._highlighted.values() ]; - - this._highlighted.clear(); - - for ( const tableCell of selected ) { - const viewElement = mapper.toViewElement( tableCell ); + for ( const tableCell of this.getSelection() ) { modelRanges.push( model.createRangeOn( tableCell ) ); - - this._highlighted.add( viewElement ); } // Update model's selection model.change( writer => { writer.setSelection( modelRanges ); } ); - - view.change( writer => { - for ( const previouslyHighlighted of previous ) { - if ( !selected.includes( previouslyHighlighted ) ) { - writer.removeClass( 'selected', previouslyHighlighted ); - } - } - - for ( const currently of this._highlighted ) { - writer.addClass( 'selected', currently ); - } - } ); } clearPreviousSelection() { From 8318c30b8b0ad68f9a1dfbe3222e04f357886728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 28 Jan 2020 14:36:00 +0100 Subject: [PATCH 036/107] Add MouseSelectionObserver. --- src/tableselection.js | 4 +- src/tableselection/mouseselectionobserver.js | 84 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/tableselection/mouseselectionobserver.js diff --git a/src/tableselection.js b/src/tableselection.js index 015d7b28..7f0fb719 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -12,8 +12,8 @@ import { findAncestor } from './commands/utils'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import MouseSelectionObserver from './tableselection/mouseselectionobserver'; -// TODO: refactor to an Observer export default class TableSelection extends Plugin { constructor( editor ) { super( editor ); @@ -28,6 +28,8 @@ export default class TableSelection extends Plugin { const viewDocument = editor.editing.view.document; + editor.editing.view.addObserver( MouseSelectionObserver ); + this.listenTo( viewDocument, 'keydown', () => { if ( this.isSelectingBlaBla() ) { this.stopSelection(); diff --git a/src/tableselection/mouseselectionobserver.js b/src/tableselection/mouseselectionobserver.js new file mode 100644 index 00000000..9a65b1db --- /dev/null +++ b/src/tableselection/mouseselectionobserver.js @@ -0,0 +1,84 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module table/tableselection/mouseselectionobserver~MouseSelectionObserver + */ + +import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; + +/** + * Mouse selection events observer. + * + * It register listener for DOM events: + * + * - `'mousemove'` + * - `'mouseup'` + * - `'mouseleave'` + * + * Note that this observer is not available by default. To make it available it needs to be added to + * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. + * + * @extends module:engine/view/observer/domeventobserver~DomEventObserver + */ +export default class MouseSelectionObserver extends DomEventObserver { + /** + * @inheritDoc + */ + constructor( view ) { + super( view ); + + this.domEventType = [ 'mousemove', 'mouseup', 'mouseleave' ]; + } + + /** + * @inheritDoc + */ + onDomEvent( domEvent ) { + this.fire( domEvent.type, domEvent ); + } +} + +/** + * Fired when mouse button is released over one of the editables. + * + * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * + * Note that this event is not available by default. To make it available + * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * + * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @event module:engine/view/document~Document#event:mouseup + * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. + */ + +/** + * Fired when mouse is moved over one of the editables. + * + * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * + * Note that this event is not available by default. To make it available + * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * + * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @event module:engine/view/document~Document#event:mousemove + * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. + */ + +/** + * Fired when mouse is moved away from one of the editables. + * + * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * + * Note that this event is not available by default. To make it available + * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * + * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @event module:engine/view/document~Document#event:mouseleave + * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. + */ From 16a3d3075c9115fc6c28e2286a7162eb78cae960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 28 Jan 2020 15:08:27 +0100 Subject: [PATCH 037/107] Fix tests. --- tests/commands/mergecellscommand.js | 21 ++++++------ tests/tableediting.js | 52 ++++++++++++++--------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index edc80ded..bba07847 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -7,8 +7,9 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltestedit import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import MergeCellsCommand from '../../src/commands/mergecellscommand'; -import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils'; +import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils'; import TableUtils from '../../src/tableutils'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; describe( 'MergeCellsCommand', () => { let editor, model, command, root; @@ -237,7 +238,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[0001]' } ] ] ) ); } ); @@ -255,7 +256,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 3, rowspan: 2, @@ -278,7 +279,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[]' } ] ] ) ); } ); @@ -292,7 +293,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); @@ -306,7 +307,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[foo]' } ] ] ) ); } ); @@ -326,7 +327,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { colspan: 2, contents: '[]' } ] ] ) ); } ); @@ -347,7 +348,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ '[001020]' ] @@ -369,7 +370,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ { rowspan: 2, contents: '[001020]' }, '01', @@ -394,7 +395,7 @@ describe( 'MergeCellsCommand', () => { command.execute(); - expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getData( model ), modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ], [ diff --git a/tests/tableediting.js b/tests/tableediting.js index 31bf296e..492c8618 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -11,7 +11,7 @@ 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, formattedViewTable, modelTable } from './_utils/utils'; +import { modelTable } from './_utils/utils'; import InsertRowCommand from '../src/commands/insertrowcommand'; import InsertTableCommand from '../src/commands/inserttablecommand'; import InsertColumnCommand from '../src/commands/insertcolumncommand'; @@ -632,14 +632,14 @@ describe( 'TableEditing', () => { view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); @@ -654,7 +654,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); @@ -662,12 +662,12 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ '10', '11' ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -682,7 +682,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); @@ -690,12 +690,12 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] ], { asWidget: true } ) ); @@ -710,7 +710,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '00', '01' ], [ '10', '[]11' ] ] ) ); @@ -718,12 +718,12 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] ], { asWidget: true } ) ); @@ -742,13 +742,13 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ], [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ], [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true }, @@ -769,13 +769,13 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], [ '20', '21', '22' ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true }, @@ -803,7 +803,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); @@ -813,12 +813,12 @@ describe( 'TableEditing', () => { view.document.fire( 'mouseup', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ '10', '11' ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -826,7 +826,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ '10', '11' ] ] ) ); @@ -841,7 +841,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) ); @@ -851,12 +851,12 @@ describe( 'TableEditing', () => { view.document.fire( 'mouseleave', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ '10', '11' ] ] ) ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -864,7 +864,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], [ '10', '11' ] ] ) ); @@ -879,7 +879,7 @@ describe( 'TableEditing', () => { selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [ + assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], [ '10', '11' ] ] ) + 'foo' ); @@ -898,7 +898,7 @@ describe( 'TableEditing', () => { writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); } ); - expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [ + expect( getViewData( view ) ).to.equal( modelTable( [ [ '00', '01' ], [ '10', '11' ] ], { asWidget: true } ) + '

{}foo

' ); From 31f9ce3576761e543318d096956af50a88017f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 28 Jan 2020 15:08:54 +0100 Subject: [PATCH 038/107] Fix test name. --- .../tableselection/mouseselectionobserver.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/tableselection/mouseselectionobserver.js diff --git a/tests/tableselection/mouseselectionobserver.js b/tests/tableselection/mouseselectionobserver.js new file mode 100644 index 00000000..7e222c6c --- /dev/null +++ b/tests/tableselection/mouseselectionobserver.js @@ -0,0 +1,48 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import View from '@ckeditor/ckeditor5-engine/src/view/view'; +import MouseSelectionObserver from '../../src/tableselection/mouseselectionobserver'; + +describe( 'MouseSelectionObserver', () => { + let view, viewDocument, observer; + + beforeEach( () => { + view = new View(); + viewDocument = view.document; + observer = view.addObserver( MouseSelectionObserver ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should define domEventTypes', () => { + expect( observer.domEventType ).to.deep.equal( [ + 'mousemove', + 'mouseup', + 'mouseleave' + ] ); + } ); + + describe( 'onDomEvent', () => { + for ( const eventName of [ 'mousemove', 'mouseup', 'mouseleave' ] ) { + it( `should fire ${ eventName } with the right event data`, () => { + const spy = sinon.spy(); + + viewDocument.on( eventName, spy ); + + observer.onDomEvent( { type: eventName, target: document.body } ); + + expect( spy.calledOnce ).to.be.true; + + const data = spy.args[ 0 ][ 1 ]; + expect( data.domTarget ).to.equal( document.body ); + } ); + } + } ); +} ); From 8cf18bdf5c11cda71ef5704497131ec1ee13371c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 28 Jan 2020 15:13:12 +0100 Subject: [PATCH 039/107] Move table selection tests to own file. --- tests/tableediting.js | 309 ------------------------------------ tests/tableselection.js | 342 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 309 deletions(-) create mode 100644 tests/tableselection.js diff --git a/tests/tableediting.js b/tests/tableediting.js index 492c8618..eb37e2d9 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -6,7 +6,6 @@ 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; @@ -605,312 +604,4 @@ describe( 'TableEditing', () => { ] ) ); } ); } ); - - describe( 'table selection', () => { - let view, domEvtDataStub; - - beforeEach( () => { - view = editor.editing.view; - - domEvtDataStub = { - domEvent: { - buttons: 1 - }, - target: undefined, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - } ); - - it( 'should not start table selection when mouse move is inside one table cell', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - } ); - - it( 'should start table selection when mouse move expands over two cells', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - } ); - - it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] - ], { asWidget: true } ) ); - } ); - - it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '[]11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '00', '01' ], - [ '10', '[]11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] - ], { asWidget: true } ) ); - } ); - - it( 'should update view selection after changing selection rect', () => { - setModelData( model, modelTable( [ - [ '[]00', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ], - [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, - { contents: '02', class: 'selected', isSelected: true } - ], - [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, - { contents: '12', class: 'selected', isSelected: true } - ], - [ - { contents: '20', class: 'selected', isSelected: true }, - { contents: '21', class: 'selected', isSelected: true }, - { contents: '22', class: 'selected', isSelected: true } - ] - ], { asWidget: true } ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], - [ '20', '21', '22' ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, - '02' - ], - [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, - '12' - ], - [ - '20', - '21', - '22' - ] - ], { asWidget: true } ) ); - } ); - - it( 'should stop selecting after "mouseup" event', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - view.document.fire( 'mouseup', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); - } ); - - it( 'should stop selection mode on "mouseleve" event', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - view.document.fire( 'mouseleave', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); - } ); - - it( 'should clear view table selection after mouse click outside table', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) + 'foo' ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) + 'foo' ); - - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - domEvtDataStub.target = view.document.getRoot().getChild( 1 ); - - view.document.fire( 'mousemove', domEvtDataStub ); - view.document.fire( 'mousedown', domEvtDataStub ); - view.document.fire( 'mouseup', domEvtDataStub ); - - // The click in the DOM would trigger selection change and it will set the selection: - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); - } ); - - expect( getViewData( view ) ).to.equal( modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ], { asWidget: true } ) + '

{}foo

' ); - } ); - } ); } ); - -function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { - domEvtDataStub.target = view.document.getRoot() - .getChild( tableIndex ) - .getChild( 1 ) // Table is second in widget - .getChild( sectionIndex ) - .getChild( rowInSectionIndex ) - .getChild( tableCellIndex ); -} diff --git a/tests/tableselection.js b/tests/tableselection.js new file mode 100644 index 00000000..61a83b70 --- /dev/null +++ b/tests/tableselection.js @@ -0,0 +1,342 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +import TableEditing from '../src/tableediting'; +import TableSelection from '../src/tableselection'; +import { modelTable } from './_utils/utils'; + +describe( 'table selection', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ TableEditing, TableSelection, Paragraph ] + } ) + .then( newEditor => { + editor = newEditor; + + model = editor.model; + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + describe( 'behavior', () => { + let view, domEvtDataStub; + + beforeEach( () => { + view = editor.editing.view; + + domEvtDataStub = { + domEvent: { + buttons: 1 + }, + target: undefined, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + } ); + + it( 'should not start table selection when mouse move is inside one table cell', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should start table selection when mouse move expands over two cells', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should update view selection after changing selection rect', () => { + setModelData( model, modelTable( [ + [ '[]00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ], + [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + { contents: '02', class: 'selected', isSelected: true } + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + { contents: '12', class: 'selected', isSelected: true } + ], + [ + { contents: '20', class: 'selected', isSelected: true }, + { contents: '21', class: 'selected', isSelected: true }, + { contents: '22', class: 'selected', isSelected: true } + ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], + [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], + [ '20', '21', '22' ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + '02' + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + '12' + ], + [ + '20', + '21', + '22' + ] + ], { asWidget: true } ) ); + } ); + + it( 'should stop selecting after "mouseup" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + view.document.fire( 'mouseup', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should stop selection mode on "mouseleve" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + view.document.fire( 'mouseleave', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should clear view table selection after mouse click outside table', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + view.document.fire( 'mousemove', domEvtDataStub ); + + domEvtDataStub.target = view.document.getRoot().getChild( 1 ); + + view.document.fire( 'mousemove', domEvtDataStub ); + view.document.fire( 'mousedown', domEvtDataStub ); + view.document.fire( 'mouseup', domEvtDataStub ); + + // The click in the DOM would trigger selection change and it will set the selection: + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); + } ); + + expect( getViewData( view ) ).to.equal( modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { asWidget: true } ) + '

{}foo

' ); + } ); + } ); +} ); + +function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { + domEvtDataStub.target = view.document.getRoot() + .getChild( tableIndex ) + .getChild( 1 ) // Table is second in widget + .getChild( sectionIndex ) + .getChild( rowInSectionIndex ) + .getChild( tableCellIndex ); +} From f6d2a83c7775cbc034bd9895a4a14672ec412a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 29 Jan 2020 12:39:36 +0100 Subject: [PATCH 040/107] Remove MergeCellsCommand from base table selection solution. --- src/commands/mergecellscommand.js | 260 ----------------- src/tableediting.js | 3 - src/tableui.js | 25 +- tests/commands/mergecellscommand.js | 420 ---------------------------- tests/tableediting.js | 5 - 5 files changed, 23 insertions(+), 690 deletions(-) delete mode 100644 src/commands/mergecellscommand.js delete mode 100644 tests/commands/mergecellscommand.js diff --git a/src/commands/mergecellscommand.js b/src/commands/mergecellscommand.js deleted file mode 100644 index 6bba1863..00000000 --- a/src/commands/mergecellscommand.js +++ /dev/null @@ -1,260 +0,0 @@ -/** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module table/commands/mergecellscommand - */ - -import Command from '@ckeditor/ckeditor5-core/src/command'; -import TableWalker from '../tablewalker'; -import { findAncestor, updateNumericAttribute } from './utils'; -import TableUtils from '../tableutils'; - -/** - * The merge cells command. - * - * The command is registered by {@link module:table/tableediting~TableEditing} as `'mergeTableCellRight'`, `'mergeTableCellLeft'`, - * `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands. - * - * To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction. - * - * For example, to merge with a cell to the right: - * - * editor.execute( 'mergeTableCellRight' ); - * - * **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan) - * (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan) - * (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled. - * - * @extends module:core/command~Command - */ -export default class MergeCellsCommand extends Command { - /** - * @inheritDoc - */ - refresh() { - this.isEnabled = canMergeCells( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) ); - } - - /** - * Executes the command. - * - * Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`. - * - * @fires execute - */ - execute() { - const model = this.editor.model; - - const tableUtils = this.editor.plugins.get( TableUtils ); - - model.change( writer => { - const selectedTableCells = [ ... this.editor.model.document.selection.getRanges() ].map( range => range.start.nodeAfter ); - - const firstTableCell = selectedTableCells.shift(); - - // TODO: this shouldn't be necessary (right now the selection could overlap existing. - writer.setSelection( firstTableCell, 'in' ); - - const { row, column } = tableUtils.getCellLocation( firstTableCell ); - - const colspan = parseInt( firstTableCell.getAttribute( 'colspan' ) || 1 ); - const rowspan = parseInt( firstTableCell.getAttribute( 'rowspan' ) || 1 ); - - let rightMax = column + colspan; - let bottomMax = row + rowspan; - - const rowsToCheck = new Set(); - - for ( const tableCell of selectedTableCells ) { - const { row, column } = tableUtils.getCellLocation( tableCell ); - - const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); - const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - - if ( column + colspan > rightMax ) { - rightMax = column + colspan; - } - - if ( row + rowspan > bottomMax ) { - bottomMax = row + rowspan; - } - } - - for ( const tableCell of selectedTableCells ) { - rowsToCheck.add( tableCell.parent ); - mergeTableCells( tableCell, firstTableCell, writer ); - } - - // Update table cell span attribute and merge set selection on merged contents. - updateNumericAttribute( 'colspan', rightMax - column, firstTableCell, writer ); - updateNumericAttribute( 'rowspan', bottomMax - row, firstTableCell, writer ); - - writer.setSelection( firstTableCell, 'in' ); - - // Remove empty rows after merging table cells. - for ( const row of rowsToCheck ) { - if ( !row.childCount ) { - removeEmptyRow( row, writer ); - } - } - } ); - } -} - -// Properly removes empty row from a table. Will update `rowspan` attribute of cells that overlaps removed row. -// -// @param {module:engine/model/element~Element} removedTableCellRow -// @param {module:engine/model/writer~Writer} writer -function removeEmptyRow( removedTableCellRow, writer ) { - const table = removedTableCellRow.parent; - - const removedRowIndex = table.getChildIndex( removedTableCellRow ); - - for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) { - const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex; - - if ( overlapsRemovedRow ) { - updateNumericAttribute( 'rowspan', rowspan - 1, cell, 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( writer.createRangeIn( cellToExpand ) ); - } - - writer.move( writer.createRangeIn( cellToRemove ), writer.createPositionAt( 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; -} - -// Check if selection contains mergeable cells. -// -// In a table below: -// -// +---+---+---+---+ -// | a | b | c | d | -// +---+---+---+ + -// | e | f | | -// + +---+---+ -// | | g | h | -// +---+---+---+---+ -// -// Valid selections are those which creates a solid rectangle (without gaps), such as: -// - a, b (two horizontal cells) -// - c, f (two vertical cells) -// - a, b, e (cell "e" spans over four cells) -// - c, d, f (cell d spans over cell in row below) -// -// While invalid selection would be: -// - a, c (cell "b" not selected creates a gap) -// - f, g, h (cell "d" spans over a cell from row of "f" cell - thus creates a gap) -// -// @param {module:engine/model/selection~Selection} selection -// @param {module:table/tableUtils~TableUtils} tableUtils -// @returns {boolean} -function canMergeCells( selection, tableUtils ) { - // Collapsed selection or selection only one range can't contain mergeable table cells. - if ( selection.isCollapsed || selection.rangeCount < 2 ) { - return false; - } - - // All cells must be inside the same table. - let firstRangeTable; - - const tableCells = []; - - for ( const range of selection.getRanges() ) { - // Selection ranges must be set on whole element. - if ( range.isCollapsed || !range.isFlat || !range.start.nodeAfter.is( 'tableCell' ) ) { - return false; - } - - const parentTable = findAncestor( 'table', range.start ); - - if ( !firstRangeTable ) { - firstRangeTable = parentTable; - } else if ( firstRangeTable !== parentTable ) { - return false; - } - - tableCells.push( range.start.nodeAfter ); - } - - // At this point selection contains ranges over table cells in the same table. - // The valid selection is a fully occupied rectangle composed of table cells. - // Below we calculate area of selected cells and the area of valid selection. - // The area of valid selection is defined by top-left and bottom-right cells. - const rows = new Set(); - const columns = new Set(); - - let areaOfSelectedCells = 0; - - for ( const tableCell of tableCells ) { - const { row, column } = tableUtils.getCellLocation( tableCell ); - const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 ); - const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); - - // Record row & column indexes of current cell. - rows.add( row ); - columns.add( column ); - - // For cells that spans over multiple rows add also the last row that this cell spans over. - if ( rowspan > 1 ) { - rows.add( row + rowspan - 1 ); - } - - // For cells that spans over multiple columns add also the last column that this cell spans over. - if ( colspan > 1 ) { - columns.add( column + colspan - 1 ); - } - - areaOfSelectedCells += ( rowspan * colspan ); - } - - // We can only merge table cells that are in adjacent rows... - const areaOfValidSelection = getBiggestRectangleArea( rows, columns ); - - return areaOfValidSelection == areaOfSelectedCells; -} - -// Calculates the area of a maximum rectangle that can span over provided row & column indexes. -// -// @param {Array.} rows -// @param {Array.} columns -// @returns {Number} -function getBiggestRectangleArea( rows, columns ) { - const rowsIndexes = Array.from( rows.values() ); - const columnIndexes = Array.from( columns.values() ); - - const lastRow = Math.max( ...rowsIndexes ); - const firstRow = Math.min( ...rowsIndexes ); - const lastColumn = Math.max( ...columnIndexes ); - const firstColumn = Math.min( ...columnIndexes ); - - return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 ); -} diff --git a/src/tableediting.js b/src/tableediting.js index ccd0c8c1..999a99f2 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -28,7 +28,6 @@ import RemoveRowCommand from './commands/removerowcommand'; import RemoveColumnCommand from './commands/removecolumncommand'; import SetHeaderRowCommand from './commands/setheaderrowcommand'; import SetHeaderColumnCommand from './commands/setheadercolumncommand'; -import MergeCellsCommand from './commands/mergecellscommand'; import { findAncestor } from './commands/utils'; import TableUtils from '../src/tableutils'; @@ -132,8 +131,6 @@ export default class TableEditing extends Plugin { editor.commands.add( 'splitTableCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) ); editor.commands.add( 'splitTableCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) ); - editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) ); - editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) ); editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) ); editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) ); diff --git a/src/tableui.js b/src/tableui.js index 8b5a1873..34646f1a 100644 --- a/src/tableui.js +++ b/src/tableui.js @@ -153,8 +153,29 @@ export default class TableUI extends Plugin { { type: 'button', model: { - commandName: 'mergeTableCells', - label: t( 'Merge cells' ) + commandName: 'mergeTableCellUp', + label: t( 'Merge cell up' ) + } + }, + { + type: 'button', + model: { + commandName: isContentLtr ? 'mergeTableCellRight' : 'mergeTableCellLeft', + label: t( 'Merge cell right' ) + } + }, + { + type: 'button', + model: { + commandName: 'mergeTableCellDown', + label: t( 'Merge cell down' ) + } + }, + { + type: 'button', + model: { + commandName: isContentLtr ? 'mergeTableCellLeft' : 'mergeTableCellRight', + label: t( 'Merge cell left' ) } }, { type: 'separator' }, diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js deleted file mode 100644 index bba07847..00000000 --- a/tests/commands/mergecellscommand.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; -import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -import MergeCellsCommand from '../../src/commands/mergecellscommand'; -import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils'; -import TableUtils from '../../src/tableutils'; -import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; - -describe( 'MergeCellsCommand', () => { - let editor, model, command, root; - - beforeEach( () => { - return ModelTestEditor - .create( { - plugins: [ TableUtils ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - root = model.document.getRoot( 'main' ); - - command = new MergeCellsCommand( editor ); - - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); - } ); - - afterEach( () => { - return editor.destroy(); - } ); - - describe( 'isEnabled', () => { - it( 'should be false if collapsed selection in table cell', () => { - setData( model, modelTable( [ - [ '00[]', '01' ] - ] ) ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if only one table cell is selected', () => { - setData( model, modelTable( [ - [ '00', '01' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ] ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if at least two adjacent table cells are selected', () => { - setData( model, modelTable( [ - [ '00', '01' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be true if many table cells are selected', () => { - setData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 1 ], [ 0, 0, 2 ], - [ 0, 1, 1 ], [ 0, 1, 2 ], - [ 0, 2, 1 ], [ 0, 2, 2 ], - [ 0, 3, 1 ], [ 0, 3, 2 ] - ] ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if at least one table cell is not selected from an area', () => { - setData( model, modelTable( [ - [ '00', '01', '02', '03' ], - [ '10', '11', '12', '13' ], - [ '20', '21', '22', '23' ], - [ '30', '31', '32', '33' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 1 ], [ 0, 0, 2 ], - [ 0, 1, 2 ], // one table cell not selected from this row - [ 0, 2, 1 ], [ 0, 2, 2 ], - [ 0, 3, 1 ], [ 0, 3, 2 ] - ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if table cells are not in adjacent rows', () => { - setData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ] ) ); - - selectNodes( [ - [ 0, 1, 0 ], - [ 0, 0, 1 ] - ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if table cells are not in adjacent columns', () => { - setData( model, modelTable( [ - [ '00', '01', '02' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 2 ] ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if any range is collapsed in selection', () => { - setData( model, modelTable( [ - [ '00', '01', '02' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0, 0, 0 ], // The "00" text node - [ 0, 0, 1 ] - ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if any ranges are on different tables', () => { - setData( model, - modelTable( [ [ '00', '01' ] ] ) + - modelTable( [ [ 'aa', 'ab' ] ] ) - ); - - selectNodes( [ - [ 0, 0, 0 ], // first table - [ 1, 0, 1 ] // second table - ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be false if any table cell with colspan attribute extends over selection area', () => { - setData( model, modelTable( [ - [ '00', { colspan: 2, contents: '01' } ], - [ '10', '11', '12' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ], - [ 0, 1, 0 ], [ 0, 1, 1 ] - ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if none table cell with colspan attribute extends over selection area', () => { - setData( model, modelTable( [ - [ '00', { colspan: 2, contents: '01' } ], - [ '10', '11', '12' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ], - [ 0, 1, 0 ], [ 0, 1, 1 ], - [ 0, 1, 2 ] - ] ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be true if first table cell is inside selection area', () => { - setData( model, modelTable( [ - [ { colspan: 2, rowspan: 2, contents: '00' }, '02', '03' ], - [ '12', '13' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ], - [ 0, 1, 0 ] - ] ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if any table cell with rowspan attribute extends over selection area', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 2, contents: '01' } ], - [ '10' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - expect( command.isEnabled ).to.be.false; - } ); - - it( 'should be true if none table cell with rowspan attribute extends over selection area', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 2, contents: '01' } ], - [ '10' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ], - [ 0, 1, 0 ] - ] ); - - expect( command.isEnabled ).to.be.true; - } ); - - it( 'should be false if not in a cell', () => { - setData( model, '11[]' ); - - expect( command.isEnabled ).to.be.false; - } ); - } ); - - describe( 'execute()', () => { - it( 'should merge table cells', () => { - setData( model, modelTable( [ - [ '[]00', '01' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ] - ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { colspan: 2, contents: '[0001]' } ] - ] ) ); - } ); - - it( 'should merge table cells - extend colspan attribute', () => { - setData( model, modelTable( [ - [ { colspan: 2, contents: '00' }, '02', '03' ], - [ '10', '11', '12', '13' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], [ 0, 0, 1 ], - [ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ] - ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { - colspan: 3, - rowspan: 2, - contents: '[00' + - '02' + - '10' + - '11' + - '12]' - }, '03' ], - [ '13' ] - ] ) ); - } ); - - it( 'should merge to a single paragraph - every cell is empty', () => { - setData( model, modelTable( [ - [ '[]', '' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { colspan: 2, contents: '[]' } ] - ] ) ); - } ); - - it( 'should merge to a single paragraph - merged cell is empty', () => { - setData( model, modelTable( [ - [ 'foo', '' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { colspan: 2, contents: '[foo]' } ] - ] ) ); - } ); - - it( 'should merge to a single paragraph - cell to which others are merged is empty', () => { - setData( model, modelTable( [ - [ '', 'foo' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { colspan: 2, contents: '[foo]' } ] - ] ) ); - } ); - - it( 'should not merge empty blocks other then to a single block', () => { - model.schema.register( 'block', { - allowWhere: '$block', - allowContentOf: '$block', - isBlock: true - } ); - - setData( model, modelTable( [ - [ '[]', '' ] - ] ) ); - - selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ { colspan: 2, contents: '[]' } ] - ] ) ); - } ); - - describe( 'removing empty row', () => { - it( 'should remove empty row if merging all table cells from that row', () => { - setData( model, modelTable( [ - [ '00' ], - [ '10' ], - [ '20' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 2, 0 ] - ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ - '[001020]' - ] - ] ) ); - } ); - - it( 'should decrease rowspan if cell overlaps removed row', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ], - [ '10' ], - [ '20', '21' ] - ] ) ); - - selectNodes( [ - [ 0, 0, 0 ], - [ 0, 1, 0 ], - [ 0, 2, 0 ] - ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ - { rowspan: 2, contents: '[001020]' }, - '01', - { rowspan: 2, contents: '02' } - ], - [ '21' ] - ] ) ); - } ); - - it( 'should not decrease rowspan if cell from previous row does not overlaps removed row', () => { - setData( model, modelTable( [ - [ '00', { rowspan: 2, contents: '01' } ], - [ '10' ], - [ '20', '21' ], - [ '30', '31' ] - ] ) ); - - selectNodes( [ - [ 0, 2, 0 ], [ 0, 2, 1 ], - [ 0, 3, 0 ], [ 0, 3, 1 ] - ] ); - - command.execute(); - - assertEqualMarkup( getData( model ), modelTable( [ - [ '00', { rowspan: 2, contents: '01' } ], - [ '10' ], - [ - { - colspan: 2, - contents: '[2021' + - '3031]' - } - ] - ] ) ); - } ); - } ); - } ); - - function selectNodes( paths ) { - model.change( writer => { - const ranges = paths.map( path => writer.createRangeOn( root.getNodeByPath( path ) ) ); - - writer.setSelection( ranges ); - } ); - } -} ); diff --git a/tests/tableediting.js b/tests/tableediting.js index eb37e2d9..f51ad0c7 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -17,7 +17,6 @@ import InsertColumnCommand from '../src/commands/insertcolumncommand'; import RemoveRowCommand from '../src/commands/removerowcommand'; import RemoveColumnCommand from '../src/commands/removecolumncommand'; import SplitCellCommand from '../src/commands/splitcellcommand'; -import MergeCellsCommand from '../src/commands/mergecellscommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting'; @@ -119,10 +118,6 @@ describe( 'TableEditing', () => { expect( editor.commands.get( 'splitTableCellHorizontally' ) ).to.be.instanceOf( SplitCellCommand ); } ); - it( 'adds mergeTableCells command', () => { - expect( editor.commands.get( 'mergeTableCells' ) ).to.be.instanceOf( MergeCellsCommand ); - } ); - it( 'adds setColumnHeader command', () => { expect( editor.commands.get( 'setTableColumnHeader' ) ).to.be.instanceOf( SetHeaderColumnCommand ); } ); From 26ec563266cb7f34469fde698e0587e21e8a2340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 29 Jan 2020 12:59:32 +0100 Subject: [PATCH 041/107] Restore tests from master. --- tests/tableediting.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/tableediting.js b/tests/tableediting.js index f51ad0c7..419d4790 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -17,6 +17,7 @@ import InsertColumnCommand from '../src/commands/insertcolumncommand'; import RemoveRowCommand from '../src/commands/removerowcommand'; import RemoveColumnCommand from '../src/commands/removecolumncommand'; import SplitCellCommand from '../src/commands/splitcellcommand'; +import MergeCellCommand from '../src/commands/mergecellcommand'; import SetHeaderRowCommand from '../src/commands/setheaderrowcommand'; import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand'; import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting'; @@ -118,6 +119,22 @@ describe( 'TableEditing', () => { expect( editor.commands.get( 'splitTableCellHorizontally' ) ).to.be.instanceOf( SplitCellCommand ); } ); + it( 'adds mergeCellRight command', () => { + expect( editor.commands.get( 'mergeTableCellRight' ) ).to.be.instanceOf( MergeCellCommand ); + } ); + + it( 'adds mergeCellLeft command', () => { + expect( editor.commands.get( 'mergeTableCellLeft' ) ).to.be.instanceOf( MergeCellCommand ); + } ); + + it( 'adds mergeCellDown command', () => { + expect( editor.commands.get( 'mergeTableCellDown' ) ).to.be.instanceOf( MergeCellCommand ); + } ); + + it( 'adds mergeCellUp command', () => { + expect( editor.commands.get( 'mergeTableCellUp' ) ).to.be.instanceOf( MergeCellCommand ); + } ); + it( 'adds setColumnHeader command', () => { expect( editor.commands.get( 'setTableColumnHeader' ) ).to.be.instanceOf( SetHeaderColumnCommand ); } ); @@ -133,11 +150,11 @@ describe( 'TableEditing', () => { expect( editor.getData() ).to.equal( '
' + - '' + - '' + - '' + - '' + - '
foo
' + + '' + + '' + + '' + + '' + + '
foo
' + '
' ); } ); @@ -150,11 +167,11 @@ describe( 'TableEditing', () => { expect( editor.getData() ).to.equal( '
' + - '' + - '' + - '' + - '' + - '
foo
' + + '' + + '' + + '' + + '' + + '
foo
' + '
' ); } ); From d23ab3697012cf897c192b2ab3439ef060450722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 28 Jan 2020 19:55:12 +0100 Subject: [PATCH 042/107] Align test code to the latest changes in selection post-fixer. --- tests/tableediting.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tableediting.js b/tests/tableediting.js index 419d4790..b466e0eb 100644 --- a/tests/tableediting.js +++ b/tests/tableediting.js @@ -265,7 +265,7 @@ describe( 'TableEditing', () => { sinon.assert.notCalled( domEvtDataStub.preventDefault ); sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ + assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ [ '11', '12' ] ] ) ); } ); @@ -465,7 +465,7 @@ describe( 'TableEditing', () => { sinon.assert.notCalled( domEvtDataStub.preventDefault ); sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ + assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ [ '11', '12' ] ] ) ); } ); From 7f7fe2122700b4aeed3153849c90b9cba64db38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 15:21:46 +0100 Subject: [PATCH 043/107] Fix assertions in tests. --- tests/tableselection.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index 61a83b70..8e107ef6 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -11,7 +11,7 @@ import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils' import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; -import { modelTable } from './_utils/utils'; +import { modelTable, viewTable } from './_utils/utils'; describe( 'table selection', () => { let editor, model; @@ -93,7 +93,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -121,7 +121,7 @@ describe( 'table selection', () => { [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] ], { asWidget: true } ) ); @@ -149,7 +149,7 @@ describe( 'table selection', () => { [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] ], { asWidget: true } ) ); @@ -174,7 +174,7 @@ describe( 'table selection', () => { [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true }, @@ -201,7 +201,7 @@ describe( 'table selection', () => { [ '20', '21', '22' ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true }, @@ -244,7 +244,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -282,7 +282,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -324,7 +324,7 @@ describe( 'table selection', () => { writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); } ); - expect( getViewData( view ) ).to.equal( modelTable( [ + assertEqualMarkup( getViewData( view ), viewTable( [ [ '00', '01' ], [ '10', '11' ] ], { asWidget: true } ) + '

{}foo

' ); From 50011b21b7567033c7d41fa4b33eb9be3682910d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 15:24:06 +0100 Subject: [PATCH 044/107] Revert TableUI tests to the master version. --- tests/tableui.js | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/tests/tableui.js b/tests/tableui.js index 70189aa3..15969cf8 100644 --- a/tests/tableui.js +++ b/tests/tableui.js @@ -355,7 +355,10 @@ describe( 'TableUI', () => { const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label ); expect( labels ).to.deep.equal( [ - 'Merge cells', + 'Merge cell up', + 'Merge cell right', + 'Merge cell down', + 'Merge cell left', '|', 'Split cell vertically', 'Split cell horizontally' @@ -365,30 +368,50 @@ describe( 'TableUI', () => { it( 'should bind items in panel to proper commands (LTR content)', () => { const items = dropdown.listView.items; - const mergeCellsCommand = editor.commands.get( 'mergeTableCells' ); + const mergeCellUpCommand = editor.commands.get( 'mergeTableCellUp' ); + const mergeCellRightCommand = editor.commands.get( 'mergeTableCellRight' ); + const mergeCellDownCommand = editor.commands.get( 'mergeTableCellDown' ); + const mergeCellLeftCommand = editor.commands.get( 'mergeTableCellLeft' ); const splitCellVerticallyCommand = editor.commands.get( 'splitTableCellVertically' ); const splitCellHorizontallyCommand = editor.commands.get( 'splitTableCellHorizontally' ); - mergeCellsCommand.isEnabled = true; + mergeCellUpCommand.isEnabled = true; + mergeCellRightCommand.isEnabled = true; + mergeCellDownCommand.isEnabled = true; + mergeCellLeftCommand.isEnabled = true; splitCellVerticallyCommand.isEnabled = true; splitCellHorizontallyCommand.isEnabled = true; - expect( items.get( 0 ).children.first.isEnabled ).to.be.true; + expect( items.first.children.first.isEnabled ).to.be.true; + expect( items.get( 1 ).children.first.isEnabled ).to.be.true; expect( items.get( 2 ).children.first.isEnabled ).to.be.true; expect( items.get( 3 ).children.first.isEnabled ).to.be.true; + expect( items.get( 5 ).children.first.isEnabled ).to.be.true; + expect( items.get( 6 ).children.first.isEnabled ).to.be.true; expect( dropdown.buttonView.isEnabled ).to.be.true; - mergeCellsCommand.isEnabled = false; + mergeCellUpCommand.isEnabled = false; - expect( items.get( 0 ).children.first.isEnabled ).to.be.false; + expect( items.first.children.first.isEnabled ).to.be.false; expect( dropdown.buttonView.isEnabled ).to.be.true; - splitCellVerticallyCommand.isEnabled = false; + mergeCellRightCommand.isEnabled = false; + + expect( items.get( 1 ).children.first.isEnabled ).to.be.false; + expect( dropdown.buttonView.isEnabled ).to.be.true; + + mergeCellDownCommand.isEnabled = false; expect( items.get( 2 ).children.first.isEnabled ).to.be.false; - splitCellHorizontallyCommand.isEnabled = false; + mergeCellLeftCommand.isEnabled = false; expect( items.get( 3 ).children.first.isEnabled ).to.be.false; + splitCellVerticallyCommand.isEnabled = false; + expect( items.get( 5 ).children.first.isEnabled ).to.be.false; + + splitCellHorizontallyCommand.isEnabled = false; + expect( items.get( 6 ).children.first.isEnabled ).to.be.false; + expect( dropdown.buttonView.isEnabled ).to.be.false; } ); @@ -435,7 +458,7 @@ describe( 'TableUI', () => { dropdown.listView.items.first.children.first.fire( 'execute' ); expect( spy.calledOnce ).to.be.true; - expect( spy.args[ 0 ][ 0 ] ).to.equal( 'mergeTableCells' ); + expect( spy.args[ 0 ][ 0 ] ).to.equal( 'mergeTableCellUp' ); } ); } ); } ); From c3775a7f891bfbb5b7ec7a14db905b484435ff97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 15:29:02 +0100 Subject: [PATCH 045/107] Remove redundant code. --- src/tableselection.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 7f0fb719..93787392 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -7,12 +7,11 @@ * @module table/tableselection */ -import TableWalker from './tablewalker'; -import { findAncestor } from './commands/utils'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + import MouseSelectionObserver from './tableselection/mouseselectionobserver'; +import TableWalker from './tablewalker'; +import { findAncestor } from './commands/utils'; export default class TableSelection extends Plugin { constructor( editor ) { @@ -231,5 +230,3 @@ function getTableCell( domEventData, editor ) { return findAncestor( 'tableCell', editor.model.createPositionAt( modelElement, 0 ) ); } - -mix( TableSelection, ObservableMixin ); From 299abcb0ee4b33f0547954e0d82d26a2f22137c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 15:43:02 +0100 Subject: [PATCH 046/107] Add base docs tags and improve internal methods names. --- src/tableselection.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 93787392..18cd92ae 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -13,7 +13,17 @@ import MouseSelectionObserver from './tableselection/mouseselectionobserver'; import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; +/** + * The table selection plugin. + * + * It introduces the ability to select table cells using mouse. + * + * @extends module:core/plugin~Plugin + */ export default class TableSelection extends Plugin { + /** + * @inheritDoc + */ constructor( editor ) { super( editor ); @@ -21,6 +31,9 @@ export default class TableSelection extends Plugin { this._highlighted = new Set(); } + /** + * @inheritDoc + */ init() { const editor = this.editor; this.tableUtils = editor.plugins.get( 'TableUtils' ); @@ -30,7 +43,7 @@ export default class TableSelection extends Plugin { editor.editing.view.addObserver( MouseSelectionObserver ); this.listenTo( viewDocument, 'keydown', () => { - if ( this.isSelectingBlaBla() ) { + if ( this.hasValidSelection ) { this.stopSelection(); const tableCell = this._startElement; this.clearSelection(); @@ -43,7 +56,7 @@ export default class TableSelection extends Plugin { } ); this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { - const tableCell = getTableCell( domEventData, this.editor ); + const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); if ( !tableCell ) { this.stopSelection(); @@ -60,7 +73,7 @@ export default class TableSelection extends Plugin { return; } - const tableCell = getTableCell( domEventData, this.editor ); + const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); if ( !tableCell ) { return; @@ -68,7 +81,7 @@ export default class TableSelection extends Plugin { this._updateModelSelection( tableCell ); - if ( this.isSelectingBlaBla() ) { + if ( this.hasValidSelection ) { domEventData.preventDefault(); this.redrawSelection(); @@ -80,7 +93,7 @@ export default class TableSelection extends Plugin { return; } - const tableCell = getTableCell( domEventData, this.editor ); + const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); this.stopSelection( tableCell ); } ); @@ -117,12 +130,13 @@ export default class TableSelection extends Plugin { }, { priority: 'lowest' } ) ); } - isSelectingBlaBla() { + get hasValidSelection() { return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; } _startSelection( tableCell ) { this.clearSelection(); + this._isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; @@ -220,9 +234,10 @@ export default class TableSelection extends Plugin { } } -function getTableCell( domEventData, editor ) { - const element = domEventData.target; - const modelElement = editor.editing.mapper.toModelElement( element ); +// Finds model table cell for given DOM event - ie. for 'mousedown'. +function getModelTableCellFromViewEvent( domEventData, editor ) { + const viewTargetElement = domEventData.target; + const modelElement = editor.editing.mapper.toModelElement( viewTargetElement ); if ( !modelElement ) { return; From 37489900f04c25d02f619fa0a80bc741527f2446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 15:45:56 +0100 Subject: [PATCH 047/107] Allow table selection to cross heading. --- src/tableselection.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 18cd92ae..ae77c783 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -155,15 +155,8 @@ export default class TableSelection extends Plugin { return; } - const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 ); - const startInHeading = this._startElement.parent.index < headingRows; - const updateCellInHeading = tableCell.parent.index < headingRows; - - // Only add cell to selection if they are in the same table section. - if ( startInHeading === updateCellInHeading ) { - this._endElement = tableCell; - this.redrawSelection(); - } + this._endElement = tableCell; + this.redrawSelection(); } stopSelection( tableCell ) { From 5aa6f4361e9014459210f4ef64f58380f789abc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 16:38:47 +0100 Subject: [PATCH 048/107] Rename methods to make table selection process more clear. --- src/tableselection.js | 55 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index ae77c783..4fc7c1ee 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -31,6 +31,14 @@ export default class TableSelection extends Plugin { this._highlighted = new Set(); } + /** + * a + * @returns {Boolean} + */ + get hasValidSelection() { + return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; + } + /** * @inheritDoc */ @@ -65,7 +73,7 @@ export default class TableSelection extends Plugin { return; } - this._startSelection( tableCell ); + this.startSelectingFrom( tableCell ); } ); this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { @@ -79,7 +87,7 @@ export default class TableSelection extends Plugin { return; } - this._updateModelSelection( tableCell ); + this.setSelectingTo( tableCell ); if ( this.hasValidSelection ) { domEventData.preventDefault(); @@ -130,11 +138,16 @@ export default class TableSelection extends Plugin { }, { priority: 'lowest' } ) ); } - get hasValidSelection() { - return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; - } - - _startSelection( tableCell ) { + /** + * Starts a selection process. + * + * This method enables the table selection process. + * + * editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell ); + * + * @param {module:engine/model/element~Element} tableCell + */ + startSelectingFrom( tableCell ) { this.clearSelection(); this._isSelecting = true; @@ -142,8 +155,18 @@ export default class TableSelection extends Plugin { this._endElement = tableCell; } - _updateModelSelection( tableCell ) { - // Do not update if not in selection mode or no table cell passed. + /** + * Updates current table selection end element. Table selection is defined by #start and #end element. + * This method updates the #end element. Must be preceded by {@link #startSelectingFrom}. + * + * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); + * + * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); + * + * @param {module:engine/model/element~Element} tableCell + */ + setSelectingTo( tableCell ) { + // Do not update if not in selection mode or no table cell is passed. if ( !this._isSelecting || !tableCell ) { return; } @@ -159,6 +182,11 @@ export default class TableSelection extends Plugin { this.redrawSelection(); } + /** + * Stops selection process (but do not clear the current selection). + * + * @param {module:engine/model/element~Element} tableCell + */ stopSelection( tableCell ) { if ( this._isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; @@ -167,6 +195,15 @@ export default class TableSelection extends Plugin { this._isSelecting = false; } + /** + * Stops current selection process and clears table selection. + * + * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); + * + * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); + * + * editor.plugins.get( 'TableSelection' ).clearSelection(); + */ clearSelection() { this._startElement = undefined; this._endElement = undefined; From 04ab2adc3972b16dd01c59c86c3347c3bdc8a565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 16:54:53 +0100 Subject: [PATCH 049/107] Update documentation. --- src/tableselection.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 4fc7c1ee..af100bd5 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -183,7 +183,13 @@ export default class TableSelection extends Plugin { } /** - * Stops selection process (but do not clear the current selection). + * Stops selection process (but do not clear the current selection). The selecting process is ended but the selection in model remains. + * + * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); + * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); + * editor.plugins.get( 'TableSelection' ).stopSelection(); + * + * To clear selection use {@link #clearSelection}. * * @param {module:engine/model/element~Element} tableCell */ @@ -199,8 +205,8 @@ export default class TableSelection extends Plugin { * Stops current selection process and clears table selection. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); - * * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); + * editor.plugins.get( 'TableSelection' ).stopSelection(); * * editor.plugins.get( 'TableSelection' ).clearSelection(); */ @@ -243,7 +249,7 @@ export default class TableSelection extends Plugin { const modelRanges = []; - for ( const tableCell of this.getSelection() ) { + for ( const tableCell of this.getSelectedTableCells() ) { modelRanges.push( model.createRangeOn( tableCell ) ); } From 00ac0fb9f0f172eb53ee114d259fc5150d965dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 16:55:17 +0100 Subject: [PATCH 050/107] Refactor getSelection() to getSelectedTableCells(). --- src/tableselection.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index af100bd5..986fa8c5 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -121,7 +121,7 @@ export default class TableSelection extends Plugin { if ( this._isSelecting ) { this.clearPreviousSelection(); - for ( const tableCell of this.getSelection() ) { + for ( const tableCell of this.getSelectedTableCells() ) { const viewElement = conversionApi.mapper.toViewElement( tableCell ); viewWriter.addClass( 'selected', viewElement ); @@ -218,15 +218,22 @@ export default class TableSelection extends Plugin { this._highlighted.clear(); } - * getSelection() { + /** + * Returns iterator that iterates over all selected table cells. + * + * tableSelection.startSelectingFrom( startTableCell ); + * tableSelection.stopSelection(); + * + * const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() ); + * // The above array will consist one table cell. + * + * @returns {Iterable.} + */ + * getSelectedTableCells() { if ( !this._startElement || !this._endElement ) { return; } - yield* this._getBlockSelection(); - } - - * _getBlockSelection() { const startLocation = this.tableUtils.getCellLocation( this._startElement ); const endLocation = this.tableUtils.getCellLocation( this._endElement ); From a6027001f92b3d8d91cb3a1e6b81964f034dcdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 16:58:47 +0100 Subject: [PATCH 051/107] Give some TableSelection method better names and hide internal methods as private.. --- src/tableselection.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 986fa8c5..a245158f 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -92,7 +92,7 @@ export default class TableSelection extends Plugin { if ( this.hasValidSelection ) { domEventData.preventDefault(); - this.redrawSelection(); + this._updateModelSelection(); } } ); @@ -119,7 +119,7 @@ export default class TableSelection extends Plugin { const viewSelection = viewWriter.document.selection; if ( this._isSelecting ) { - this.clearPreviousSelection(); + this._clearHighlightedTableCells(); for ( const tableCell of this.getSelectedTableCells() ) { const viewElement = conversionApi.mapper.toViewElement( tableCell ); @@ -133,7 +133,7 @@ export default class TableSelection extends Plugin { viewWriter.setSelection( from, { fake: true, label: 'TABLE' } ); } else { - this.clearPreviousSelection(); + this._clearHighlightedTableCells(); } }, { priority: 'lowest' } ) ); } @@ -179,7 +179,7 @@ export default class TableSelection extends Plugin { } this._endElement = tableCell; - this.redrawSelection(); + this._updateModelSelection(); } /** @@ -214,7 +214,7 @@ export default class TableSelection extends Plugin { this._startElement = undefined; this._endElement = undefined; this._isSelecting = false; - this.clearPreviousSelection(); + this._clearHighlightedTableCells(); this._highlighted.clear(); } @@ -250,7 +250,12 @@ export default class TableSelection extends Plugin { } } - redrawSelection() { + /** + * Set proper model selection for currently selected table cells. + * + * @private + */ + _updateModelSelection() { const editor = this.editor; const model = editor.model; @@ -266,7 +271,13 @@ export default class TableSelection extends Plugin { } ); } - clearPreviousSelection() { + /** + * Removes highlight from table cells. + * + * @TODO move to highlight handling. + * @private + */ + _clearHighlightedTableCells() { const previous = [ ...this._highlighted.values() ]; this.editor.editing.view.change( writer => { From 7c2c090b5e73dab54a1694a282f24c7a3b55af48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 17:01:23 +0100 Subject: [PATCH 052/107] Add docs to hasValidSelection. --- src/tableselection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index a245158f..4006537b 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -32,8 +32,10 @@ export default class TableSelection extends Plugin { } /** - * a - * @returns {Boolean} + * Flag indicating that table selection is selecting valid ranges in table cell. + * + * @readonly + * @member {Boolean} #hasValidSelection */ get hasValidSelection() { return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; From fa69568ba9ffffd9a89ba7aa99449203fb5dd8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 17:02:47 +0100 Subject: [PATCH 053/107] Fix MouseSelectionObserver docs. --- src/tableselection/mouseselectionobserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tableselection/mouseselectionobserver.js b/src/tableselection/mouseselectionobserver.js index 9a65b1db..53981009 100644 --- a/src/tableselection/mouseselectionobserver.js +++ b/src/tableselection/mouseselectionobserver.js @@ -4,7 +4,7 @@ */ /** - * @module table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @module table/tableselection/mouseselectionobserver */ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; From 357be2667f7d9972262ce9bd8fa577234e099031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 17:20:54 +0100 Subject: [PATCH 054/107] Add tests for TableSelection#hasValidSelection. --- src/tableselection.js | 4 +-- tests/tableselection.js | 72 ++++++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 4006537b..b236a4e1 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -38,7 +38,7 @@ export default class TableSelection extends Plugin { * @member {Boolean} #hasValidSelection */ get hasValidSelection() { - return this._isSelecting && this._startElement && this._endElement && this._startElement != this._endElement; + return this._isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; } /** @@ -193,7 +193,7 @@ export default class TableSelection extends Plugin { * * To clear selection use {@link #clearSelection}. * - * @param {module:engine/model/element~Element} tableCell + * @param {module:engine/model/element~Element} [tableCell] */ stopSelection( tableCell ) { if ( this._isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { diff --git a/tests/tableselection.js b/tests/tableselection.js index 8e107ef6..2a625e61 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -13,26 +13,66 @@ import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; import { modelTable, viewTable } from './_utils/utils'; -describe( 'table selection', () => { - let editor, model; - - beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ TableEditing, TableSelection, Paragraph ] - } ) - .then( newEditor => { - editor = newEditor; - - model = editor.model; - } ); +describe.only( 'table selection', () => { + let editor, model, tableSelection, modelRoot; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ TableEditing, TableSelection, Paragraph ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + tableSelection = editor.plugins.get( TableSelection ); + + setModelData( model, modelTable( [ + [ '11', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); } ); - afterEach( () => { - editor.destroy(); + afterEach( async () => { + await editor.destroy(); + } ); + + describe( 'TableSelection', () => { + describe( 'hasValidSelection()', () => { + it( 'should be false if selection is not started', () => { + expect( tableSelection.hasValidSelection ).to.be.false; + } ); + + it( 'should be true if selection is selecting two different cells', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + expect( tableSelection.hasValidSelection ).to.be.true; + } ); + + it( 'should be false if selection start/end is selecting the same table cell', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + expect( tableSelection.hasValidSelection ).to.be.false; + } ); + + it( 'should be false if selection has no end cell', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + expect( tableSelection.hasValidSelection ).to.be.false; + } ); + + it( 'should be false if selection has ended', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + tableSelection.stopSelection(); + + expect( tableSelection.hasValidSelection ).to.be.false; + } ); + } ); } ); - describe( 'behavior', () => { + describe( 'mouse selection', () => { let view, domEvtDataStub; beforeEach( () => { From 4766df2dde726d30470373e4ddb27cb0a2a050b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 17:59:12 +0100 Subject: [PATCH 055/107] Add more tests. --- tests/tableselection.js | 186 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index 2a625e61..46f84b5a 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -26,7 +26,7 @@ describe.only( 'table selection', () => { tableSelection = editor.plugins.get( TableSelection ); setModelData( model, modelTable( [ - [ '11', '12', '13' ], + [ '11[]', '12', '13' ], [ '21', '22', '23' ], [ '31', '32', '33' ] ] ) ); @@ -70,6 +70,146 @@ describe.only( 'table selection', () => { expect( tableSelection.hasValidSelection ).to.be.false; } ); } ); + + describe( 'startSelectingFrom()', () => { + it( 'should not change model selection', () => { + const spy = sinon.spy(); + + model.document.selection.on( 'change', spy ); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + sinon.assert.notCalled( spy ); + } ); + } ); + + describe( 'setSelectingTo()', () => { + it( 'should not change model selection if selection is not started', () => { + const spy = sinon.spy(); + + model.document.selection.on( 'change', spy ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + sinon.assert.notCalled( spy ); + } ); + + it( 'should change model selection if valid selection will be set', () => { + const spy = sinon.spy(); + + model.document.selection.on( 'change', spy ); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should not change model selection if passed table cell is from other table then start cell', () => { + setModelData( model, + modelTable( [ + [ '11[]', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) + + modelTable( [ + [ 'a', 'b' ], + [ 'c', 'd' ] + ] ) + ); + + const spy = sinon.spy(); + + model.document.selection.on( 'change', spy ); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 1, 0, 1 ] ) ); + + sinon.assert.notCalled( spy ); + } ); + + it( 'should select two table cells', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + + it( 'should select four table cells for diagonal selection', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + + it( 'should select row table cells', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 2 ] ) ); + + assertSelectedCells( [ + [ 1, 1, 1 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + + it( 'should select column table cells', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); + + assertSelectedCells( [ + [ 0, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 0 ] + ] ); + } ); + + it( 'should create proper selection on consecutive changes', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); + + assertSelectedCells( [ + [ 0, 0, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 0 ] + ] ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + assertSelectedCells( [ + [ 0, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 0, 0 ] + ] ); + + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); + + assertSelectedCells( [ + [ 0, 0, 0 ], + [ 0, 1, 1 ], + [ 0, 1, 1 ] + ] ); + } ); + } ); + + describe( 'stopSelection()', () => {} ); + + describe( 'clearSelection()', () => {} ); + + describe( '* getSelectedTableCells()', () => {} ); } ); describe( 'mouse selection', () => { @@ -370,6 +510,50 @@ describe.only( 'table selection', () => { ], { asWidget: true } ) + '

{}foo

' ); } ); } ); + + // Helper method for asserting selected table cells. + // + // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: + // + // assertSelectedCells( [ + // [ 0, 1 ], + // [ 0, 1 ] + // ] ); + // + // The above call will check if table has second column selected (assuming no spans). + // + // **Note**: This function operates on child indexes - not rows/columns. + function assertSelectedCells( tableMap ) { + const tableIndex = 0; + + for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) { + const row = tableMap[ rowIndex ]; + + for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) { + const expectSelected = row[ cellIndex ]; + + if ( expectSelected ) { + assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] ); + } else { + assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] ); + } + } + } + } + + function assertNodeIsSelected( path ) { + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true; + } + + function assertNodeIsNotSelected( path ) { + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; + } } ); function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { From a56853744d78bdfa7cff0a8f249e220c29119f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 18:19:06 +0100 Subject: [PATCH 056/107] Remove .only from tests. --- tests/tableselection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index 46f84b5a..afd1dc1c 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -13,7 +13,7 @@ import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; import { modelTable, viewTable } from './_utils/utils'; -describe.only( 'table selection', () => { +describe( 'table selection', () => { let editor, model, tableSelection, modelRoot; beforeEach( async () => { From ad2dc8034a761321fb9f80b821b92a196a5dd41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 30 Jan 2020 18:28:40 +0100 Subject: [PATCH 057/107] Add tests to other TableSelection methods. --- src/tableselection.js | 8 ++-- tests/tableselection.js | 89 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index b236a4e1..062b1b25 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -35,9 +35,9 @@ export default class TableSelection extends Plugin { * Flag indicating that table selection is selecting valid ranges in table cell. * * @readonly - * @member {Boolean} #hasValidSelection + * @member {Boolean} #isSelectingAndSomethingElse */ - get hasValidSelection() { + get isSelectingAndSomethingElse() { return this._isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; } @@ -53,7 +53,7 @@ export default class TableSelection extends Plugin { editor.editing.view.addObserver( MouseSelectionObserver ); this.listenTo( viewDocument, 'keydown', () => { - if ( this.hasValidSelection ) { + if ( this.isSelectingAndSomethingElse ) { this.stopSelection(); const tableCell = this._startElement; this.clearSelection(); @@ -91,7 +91,7 @@ export default class TableSelection extends Plugin { this.setSelectingTo( tableCell ); - if ( this.hasValidSelection ) { + if ( this.isSelectingAndSomethingElse ) { domEventData.preventDefault(); this._updateModelSelection(); diff --git a/tests/tableselection.js b/tests/tableselection.js index afd1dc1c..f9b225f0 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -13,7 +13,7 @@ import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; import { modelTable, viewTable } from './_utils/utils'; -describe( 'table selection', () => { +describe.only( 'table selection', () => { let editor, model, tableSelection, modelRoot; beforeEach( async () => { @@ -37,29 +37,29 @@ describe( 'table selection', () => { } ); describe( 'TableSelection', () => { - describe( 'hasValidSelection()', () => { + describe( 'isSelectingAndSomethingElse()', () => { it( 'should be false if selection is not started', () => { - expect( tableSelection.hasValidSelection ).to.be.false; + expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; } ); it( 'should be true if selection is selecting two different cells', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - expect( tableSelection.hasValidSelection ).to.be.true; + expect( tableSelection.isSelectingAndSomethingElse ).to.be.true; } ); it( 'should be false if selection start/end is selecting the same table cell', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - expect( tableSelection.hasValidSelection ).to.be.false; + expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; } ); it( 'should be false if selection has no end cell', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - expect( tableSelection.hasValidSelection ).to.be.false; + expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; } ); it( 'should be false if selection has ended', () => { @@ -67,7 +67,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); tableSelection.stopSelection(); - expect( tableSelection.hasValidSelection ).to.be.false; + expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; } ); } ); @@ -205,9 +205,80 @@ describe( 'table selection', () => { } ); } ); - describe( 'stopSelection()', () => {} ); + describe( 'stopSelection()', () => { + it( 'should not clear currently selected cells if not cell was passed', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + tableSelection.stopSelection(); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + + it( 'should change model selection if cell was passed', () => { + const spy = sinon.spy(); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + model.document.selection.on( 'change', spy ); + tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should extend selection to passed table cell', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + + tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + } ); + + describe( 'clearSelection()', () => { + it( 'should not change model selection', () => { + const spy = sinon.spy(); + + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + model.document.selection.on( 'change', spy ); + + tableSelection.clearSelection(); + + sinon.assert.notCalled( spy ); + } ); + + it( 'should stop selecting mode', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + tableSelection.clearSelection(); + + expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; + } ); + + it( 'should not reset model selections', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + tableSelection.clearSelection(); - describe( 'clearSelection()', () => {} ); + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + } ); + } ); describe( '* getSelectedTableCells()', () => {} ); } ); From c837e0b1505a8684525d161c57c9bc8249f2e12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 10:16:13 +0100 Subject: [PATCH 058/107] Remove dead code. --- src/tableselection.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 062b1b25..6547065a 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -52,19 +52,6 @@ export default class TableSelection extends Plugin { editor.editing.view.addObserver( MouseSelectionObserver ); - this.listenTo( viewDocument, 'keydown', () => { - if ( this.isSelectingAndSomethingElse ) { - this.stopSelection(); - const tableCell = this._startElement; - this.clearSelection(); - - editor.model.change( writer => { - // Select the contents of table cell. - writer.setSelection( tableCell, 'in' ); - } ); - } - } ); - this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); From f18fa5021e98c7d632b88b3d6b5a8c494fff523a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 10:18:58 +0100 Subject: [PATCH 059/107] Remove code from mousemove handler that looks like unnecessary. --- src/tableselection.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 6547065a..83d28085 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -77,12 +77,6 @@ export default class TableSelection extends Plugin { } this.setSelectingTo( tableCell ); - - if ( this.isSelectingAndSomethingElse ) { - domEventData.preventDefault(); - - this._updateModelSelection(); - } } ); this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { From 69fa430f7cfec9a25786eef378720c5c2ef6e2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 10:23:05 +0100 Subject: [PATCH 060/107] Rename MouseSelectionObserver to MouseEventsObserver. --- src/tableselection.js | 4 ++-- ...tionobserver.js => mouseeventsobserver.js} | 22 +++++++++---------- ...tionobserver.js => mouseeventsobserver.js} | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) rename src/tableselection/{mouseselectionobserver.js => mouseeventsobserver.js} (68%) rename tests/tableselection/{mouseselectionobserver.js => mouseeventsobserver.js} (85%) diff --git a/src/tableselection.js b/src/tableselection.js index 83d28085..ba0db6f9 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import MouseSelectionObserver from './tableselection/mouseselectionobserver'; +import MouseEventsObserver from './tableselection/mouseeventsobserver'; import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; @@ -50,7 +50,7 @@ export default class TableSelection extends Plugin { const viewDocument = editor.editing.view.document; - editor.editing.view.addObserver( MouseSelectionObserver ); + editor.editing.view.addObserver( MouseEventsObserver ); this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); diff --git a/src/tableselection/mouseselectionobserver.js b/src/tableselection/mouseeventsobserver.js similarity index 68% rename from src/tableselection/mouseselectionobserver.js rename to src/tableselection/mouseeventsobserver.js index 53981009..aeae1e99 100644 --- a/src/tableselection/mouseselectionobserver.js +++ b/src/tableselection/mouseeventsobserver.js @@ -4,7 +4,7 @@ */ /** - * @module table/tableselection/mouseselectionobserver + * @module table/tableselection/mouseeventsobserver */ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; @@ -23,7 +23,7 @@ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domev * * @extends module:engine/view/observer/domeventobserver~DomEventObserver */ -export default class MouseSelectionObserver extends DomEventObserver { +export default class MouseEventsObserver extends DomEventObserver { /** * @inheritDoc */ @@ -44,13 +44,13 @@ export default class MouseSelectionObserver extends DomEventObserver { /** * Fired when mouse button is released over one of the editables. * - * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. * - * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mouseup * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. */ @@ -58,13 +58,13 @@ export default class MouseSelectionObserver extends DomEventObserver { /** * Fired when mouse is moved over one of the editables. * - * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. * - * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mousemove * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. */ @@ -72,13 +72,13 @@ export default class MouseSelectionObserver extends DomEventObserver { /** * Fired when mouse is moved away from one of the editables. * - * Introduced by {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver}. + * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseselectionobserver~MouseSelectionObserver} needs to be added + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. * - * @see module:table/tableselection/mouseselectionobserver~MouseSelectionObserver + * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mouseleave * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. */ diff --git a/tests/tableselection/mouseselectionobserver.js b/tests/tableselection/mouseeventsobserver.js similarity index 85% rename from tests/tableselection/mouseselectionobserver.js rename to tests/tableselection/mouseeventsobserver.js index 7e222c6c..cd0d537f 100644 --- a/tests/tableselection/mouseselectionobserver.js +++ b/tests/tableselection/mouseeventsobserver.js @@ -6,15 +6,15 @@ /* globals document */ import View from '@ckeditor/ckeditor5-engine/src/view/view'; -import MouseSelectionObserver from '../../src/tableselection/mouseselectionobserver'; +import MouseEventsObserver from '../../src/tableselection/mouseeventsobserver'; -describe( 'MouseSelectionObserver', () => { +describe( 'MouseEventsObserver', () => { let view, viewDocument, observer; beforeEach( () => { view = new View(); viewDocument = view.document; - observer = view.addObserver( MouseSelectionObserver ); + observer = view.addObserver( MouseEventsObserver ); } ); afterEach( () => { From aafe11baa1418f3d2f2985d3a94aba3349041a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 11:21:55 +0100 Subject: [PATCH 061/107] Extract MouseSelectionHandler class. --- src/commands/utils.js | 9 +-- src/tableselection.js | 128 +++++++++++++++++++++++++----------------- 2 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/commands/utils.js b/src/commands/utils.js index dd1d1cc0..4b3ffa28 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -10,14 +10,15 @@ import { isObject } from 'lodash-es'; /** - * Returns the parent element of given name. Returns undefined if position is not inside desired parent. + * Returns the parent element of given name. Returns undefined if positionOrElement is not inside desired parent. * * @param {String} parentName Name of parent element to find. - * @param {module:engine/model/position~Position|module:engine/model/position~Position} position Position to start searching. + * @param {module:engine/model/position~Position|module:engine/model/element~Position} positionOrElement + * Position or parentElement to start searching. * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} */ -export function findAncestor( parentName, position ) { - let parent = position.parent; +export function findAncestor( parentName, positionOrElement ) { + let parent = positionOrElement.parent; while ( parent ) { if ( parent.name === parentName ) { diff --git a/src/tableselection.js b/src/tableselection.js index ba0db6f9..80f08f12 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -12,6 +12,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import MouseEventsObserver from './tableselection/mouseeventsobserver'; import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; /** * The table selection plugin. @@ -46,56 +48,9 @@ export default class TableSelection extends Plugin { */ init() { const editor = this.editor; - this.tableUtils = editor.plugins.get( 'TableUtils' ); - - const viewDocument = editor.editing.view.document; - - editor.editing.view.addObserver( MouseEventsObserver ); - - this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { - const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); - - if ( !tableCell ) { - this.stopSelection(); - this.clearSelection(); - - return; - } - - this.startSelectingFrom( tableCell ); - } ); - - this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { - if ( !this._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); - - if ( !tableCell ) { - return; - } - this.setSelectingTo( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { - if ( !this._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor ); - - this.stopSelection( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseleave', () => { - if ( !this._isSelecting ) { - return; - } - - this.stopSelection(); - } ); + this.tableUtils = editor.plugins.get( 'TableUtils' ); + this._mouseHandler = new MouseSelectionHandler( this, editor.editing ); editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { const viewWriter = conversionApi.writer; @@ -121,6 +76,14 @@ export default class TableSelection extends Plugin { }, { priority: 'lowest' } ) ); } + /** + * @inheritDoc + */ + destroy() { + super.destroy(); + this._mouseHandler.stopListening(); + } + /** * Starts a selection process. * @@ -272,13 +235,74 @@ export default class TableSelection extends Plugin { } // Finds model table cell for given DOM event - ie. for 'mousedown'. -function getModelTableCellFromViewEvent( domEventData, editor ) { +function getModelTableCellFromViewEvent( domEventData, mapper ) { const viewTargetElement = domEventData.target; - const modelElement = editor.editing.mapper.toModelElement( viewTargetElement ); + const modelElement = mapper.toModelElement( viewTargetElement ); if ( !modelElement ) { return; } - return findAncestor( 'tableCell', editor.model.createPositionAt( modelElement, 0 ) ); + if ( modelElement.is( 'tableCell' ) ) { + return modelElement; + } + + return findAncestor( 'tableCell', modelElement ); +} + +class MouseSelectionHandler { + constructor( tableSelection, editing ) { + const view = editing.view; + const viewDocument = view.document; + const mapper = editing.mapper; + + view.addObserver( MouseEventsObserver ); + + this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + if ( !tableCell ) { + tableSelection.stopSelection(); + tableSelection.clearSelection(); + + return; + } + + tableSelection.startSelectingFrom( tableCell ); + } ); + + this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { + if ( !tableSelection._isSelecting ) { + return; + } + + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + if ( !tableCell ) { + return; + } + + tableSelection.setSelectingTo( tableCell ); + } ); + + this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { + if ( !tableSelection._isSelecting ) { + return; + } + + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + tableSelection.stopSelection( tableCell ); + } ); + + this.listenTo( viewDocument, 'mouseleave', () => { + if ( !tableSelection._isSelecting ) { + return; + } + + tableSelection.stopSelection(); + } ); + } } + +mix( MouseSelectionHandler, ObservableMixin ); From eb49a76b68964ab5af6cb0fa6ece8041a9101ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 11:23:44 +0100 Subject: [PATCH 062/107] Refactor internal properties of TableSelection. --- src/tableselection.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 80f08f12..389c4ccd 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -14,6 +14,7 @@ import TableWalker from './tablewalker'; import { findAncestor } from './commands/utils'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import TableUtils from './tableutils'; /** * The table selection plugin. @@ -33,6 +34,13 @@ export default class TableSelection extends Plugin { this._highlighted = new Set(); } + /** + * @inheritDoc + */ + static get requires() { + return [ TableUtils ]; + } + /** * Flag indicating that table selection is selecting valid ranges in table cell. * @@ -49,7 +57,7 @@ export default class TableSelection extends Plugin { init() { const editor = this.editor; - this.tableUtils = editor.plugins.get( 'TableUtils' ); + this._tableUtils = editor.plugins.get( 'TableUtils' ); this._mouseHandler = new MouseSelectionHandler( this, editor.editing ); editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { @@ -180,8 +188,8 @@ export default class TableSelection extends Plugin { return; } - const startLocation = this.tableUtils.getCellLocation( this._startElement ); - const endLocation = this.tableUtils.getCellLocation( this._endElement ); + const startLocation = this._tableUtils.getCellLocation( this._startElement ); + const endLocation = this._tableUtils.getCellLocation( this._endElement ); const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; From ac9a3985c822aa06e580bf5fc2afc77347f41096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 11:25:42 +0100 Subject: [PATCH 063/107] Refactor static fields of TableSelection class. --- src/tableselection.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 389c4ccd..3768d2d1 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -27,11 +27,8 @@ export default class TableSelection extends Plugin { /** * @inheritDoc */ - constructor( editor ) { - super( editor ); - - this._isSelecting = false; - this._highlighted = new Set(); + static get pluginName() { + return 'TableSelection'; } /** @@ -41,6 +38,16 @@ export default class TableSelection extends Plugin { return [ TableUtils ]; } + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + this._isSelecting = false; + this._highlighted = new Set(); + } + /** * Flag indicating that table selection is selecting valid ranges in table cell. * From 2cc400157d8d678451d83043c1778bf35d6a0f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 12:12:35 +0100 Subject: [PATCH 064/107] Extract table selection highlighting. --- src/tableselection.js | 49 +++----------------------------- src/tableselection/converters.js | 44 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 src/tableselection/converters.js diff --git a/src/tableselection.js b/src/tableselection.js index 3768d2d1..244ddd5f 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -15,6 +15,7 @@ import { findAncestor } from './commands/utils'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import TableUtils from './tableutils'; +import { setupTableSelectionHighlighting } from './tableselection/converters'; /** * The table selection plugin. @@ -45,7 +46,6 @@ export default class TableSelection extends Plugin { super( editor ); this._isSelecting = false; - this._highlighted = new Set(); } /** @@ -62,33 +62,10 @@ export default class TableSelection extends Plugin { * @inheritDoc */ init() { - const editor = this.editor; - - this._tableUtils = editor.plugins.get( 'TableUtils' ); - this._mouseHandler = new MouseSelectionHandler( this, editor.editing ); - - editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - if ( this._isSelecting ) { - this._clearHighlightedTableCells(); - - for ( const tableCell of this.getSelectedTableCells() ) { - const viewElement = conversionApi.mapper.toViewElement( tableCell ); + this._tableUtils = this.editor.plugins.get( 'TableUtils' ); + this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); - viewWriter.addClass( 'selected', viewElement ); - this._highlighted.add( viewElement ); - } - - const ranges = viewSelection.getRanges(); - const from = Array.from( ranges ); - - viewWriter.setSelection( from, { fake: true, label: 'TABLE' } ); - } else { - this._clearHighlightedTableCells(); - } - }, { priority: 'lowest' } ) ); + setupTableSelectionHighlighting( this.editor, this ); } /** @@ -175,8 +152,6 @@ export default class TableSelection extends Plugin { this._startElement = undefined; this._endElement = undefined; this._isSelecting = false; - this._clearHighlightedTableCells(); - this._highlighted.clear(); } /** @@ -231,22 +206,6 @@ export default class TableSelection extends Plugin { writer.setSelection( modelRanges ); } ); } - - /** - * Removes highlight from table cells. - * - * @TODO move to highlight handling. - * @private - */ - _clearHighlightedTableCells() { - const previous = [ ...this._highlighted.values() ]; - - this.editor.editing.view.change( writer => { - for ( const previouslyHighlighted of previous ) { - writer.removeClass( 'selected', previouslyHighlighted ); - } - } ); - } } // Finds model table cell for given DOM event - ie. for 'mousedown'. diff --git a/src/tableselection/converters.js b/src/tableselection/converters.js new file mode 100644 index 00000000..f506ec78 --- /dev/null +++ b/src/tableselection/converters.js @@ -0,0 +1,44 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module table/tableselection/converters + */ + +export function setupTableSelectionHighlighting( editor, tableSelection ) { + const highlighted = new Set(); + editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + if ( tableSelection._isSelecting ) { + clearHighlightedTableCells( highlighted, editor.editing.view ); + + for ( const tableCell of tableSelection.getSelectedTableCells() ) { + const viewElement = conversionApi.mapper.toViewElement( tableCell ); + + viewWriter.addClass( 'selected', viewElement ); + highlighted.add( viewElement ); + } + + const ranges = viewSelection.getRanges(); + const from = Array.from( ranges ); + + viewWriter.setSelection( from, { fake: true, label: 'TABLE' } ); + } else { + clearHighlightedTableCells( highlighted, editor.editing.view ); + } + }, { priority: 'lowest' } ) ); +} + +function clearHighlightedTableCells( highlighted, view ) { + const previous = [ ...highlighted.values() ]; + + view.change( writer => { + for ( const previouslyHighlighted of previous ) { + writer.removeClass( 'selected', previouslyHighlighted ); + } + } ); +} From 69ec56180909fd1de1a3e05cfbed5d5298bc2fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 12:13:59 +0100 Subject: [PATCH 065/107] Re-organize imports. --- src/tableselection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 244ddd5f..487c2c26 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -8,13 +8,13 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; - -import MouseEventsObserver from './tableselection/mouseeventsobserver'; -import TableWalker from './tablewalker'; -import { findAncestor } from './commands/utils'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; + +import TableWalker from './tablewalker'; import TableUtils from './tableutils'; +import { findAncestor } from './commands/utils'; +import MouseEventsObserver from './tableselection/mouseeventsobserver'; import { setupTableSelectionHighlighting } from './tableselection/converters'; /** From 9d97fe044fb0b0204df4e5e09cccab33698f8d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 12:32:01 +0100 Subject: [PATCH 066/107] Fix typing inside table cell. --- src/tableselection/converters.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/tableselection/converters.js b/src/tableselection/converters.js index f506ec78..4a89f657 100644 --- a/src/tableselection/converters.js +++ b/src/tableselection/converters.js @@ -7,14 +7,22 @@ * @module table/tableselection/converters */ +/** + * Adds a visual highlight style to a selected table cells. + * + * @param {module:core/editor/editor~Editor} editor + * @param {module:table/tableselection~TableSelection} tableSelection + */ export function setupTableSelectionHighlighting( editor, tableSelection ) { const highlighted = new Set(); + editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const view = editor.editing.view; const viewWriter = conversionApi.writer; const viewSelection = viewWriter.document.selection; - if ( tableSelection._isSelecting ) { - clearHighlightedTableCells( highlighted, editor.editing.view ); + if ( tableSelection.isSelectingAndSomethingElse ) { + clearHighlightedTableCells( highlighted, view ); for ( const tableCell of tableSelection.getSelectedTableCells() ) { const viewElement = conversionApi.mapper.toViewElement( tableCell ); @@ -23,12 +31,9 @@ export function setupTableSelectionHighlighting( editor, tableSelection ) { highlighted.add( viewElement ); } - const ranges = viewSelection.getRanges(); - const from = Array.from( ranges ); - - viewWriter.setSelection( from, { fake: true, label: 'TABLE' } ); + viewWriter.setSelection( viewSelection.getRanges(), { fake: true, label: 'TABLE' } ); } else { - clearHighlightedTableCells( highlighted, editor.editing.view ); + clearHighlightedTableCells( highlighted, view ); } }, { priority: 'lowest' } ) ); } From c2a68828e83f2e52e30ff62176a8d7d6af2d6427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 12:44:13 +0100 Subject: [PATCH 067/107] Extract MouseSelectionHandler to own file. --- src/tableselection.js | 80 +------------------ src/tableselection/mouseselectionhandler.js | 87 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 src/tableselection/mouseselectionhandler.js diff --git a/src/tableselection.js b/src/tableselection.js index 487c2c26..66025e1b 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -8,14 +8,11 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import TableWalker from './tablewalker'; import TableUtils from './tableutils'; -import { findAncestor } from './commands/utils'; -import MouseEventsObserver from './tableselection/mouseeventsobserver'; import { setupTableSelectionHighlighting } from './tableselection/converters'; +import MouseSelectionHandler from './tableselection/mouseselectionhandler'; /** * The table selection plugin. @@ -46,6 +43,7 @@ export default class TableSelection extends Plugin { super( editor ); this._isSelecting = false; + this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); } /** @@ -63,7 +61,6 @@ export default class TableSelection extends Plugin { */ init() { this._tableUtils = this.editor.plugins.get( 'TableUtils' ); - this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); setupTableSelectionHighlighting( this.editor, this ); } @@ -207,76 +204,3 @@ export default class TableSelection extends Plugin { } ); } } - -// Finds model table cell for given DOM event - ie. for 'mousedown'. -function getModelTableCellFromViewEvent( domEventData, mapper ) { - const viewTargetElement = domEventData.target; - const modelElement = mapper.toModelElement( viewTargetElement ); - - if ( !modelElement ) { - return; - } - - if ( modelElement.is( 'tableCell' ) ) { - return modelElement; - } - - return findAncestor( 'tableCell', modelElement ); -} - -class MouseSelectionHandler { - constructor( tableSelection, editing ) { - const view = editing.view; - const viewDocument = view.document; - const mapper = editing.mapper; - - view.addObserver( MouseEventsObserver ); - - this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - if ( !tableCell ) { - tableSelection.stopSelection(); - tableSelection.clearSelection(); - - return; - } - - tableSelection.startSelectingFrom( tableCell ); - } ); - - this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { - if ( !tableSelection._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - if ( !tableCell ) { - return; - } - - tableSelection.setSelectingTo( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { - if ( !tableSelection._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - tableSelection.stopSelection( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseleave', () => { - if ( !tableSelection._isSelecting ) { - return; - } - - tableSelection.stopSelection(); - } ); - } -} - -mix( MouseSelectionHandler, ObservableMixin ); diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js new file mode 100644 index 00000000..dc6bc3e6 --- /dev/null +++ b/src/tableselection/mouseselectionhandler.js @@ -0,0 +1,87 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module table/tableselection/mouseselectionhandler + */ + +import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; + +import { findAncestor } from '../commands/utils'; +import MouseEventsObserver from './mouseeventsobserver'; + +export default class MouseSelectionHandler { + constructor( tableSelection, editing ) { + const view = editing.view; + const viewDocument = view.document; + const mapper = editing.mapper; + + view.addObserver( MouseEventsObserver ); + + this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + if ( !tableCell ) { + tableSelection.stopSelection(); + tableSelection.clearSelection(); + + return; + } + + tableSelection.startSelectingFrom( tableCell ); + } ); + + this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { + if ( !tableSelection._isSelecting ) { + return; + } + + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + if ( !tableCell ) { + return; + } + + tableSelection.setSelectingTo( tableCell ); + } ); + + this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { + if ( !tableSelection._isSelecting ) { + return; + } + + const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); + + tableSelection.stopSelection( tableCell ); + } ); + + this.listenTo( viewDocument, 'mouseleave', () => { + if ( !tableSelection._isSelecting ) { + return; + } + + tableSelection.stopSelection(); + } ); + } +} + +mix( MouseSelectionHandler, ObservableMixin ); + +// Finds model table cell for given DOM event - ie. for 'mousedown'. +function getModelTableCellFromViewEvent( domEventData, mapper ) { + const viewTargetElement = domEventData.target; + const modelElement = mapper.toModelElement( viewTargetElement ); + + if ( !modelElement ) { + return; + } + + if ( modelElement.is( 'tableCell' ) ) { + return modelElement; + } + + return findAncestor( 'tableCell', modelElement ); +} From 39af70ab894e421dd31ab547369f7b78b6d617e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 13:16:35 +0100 Subject: [PATCH 068/107] Change styles for selected table cells. --- theme/tableediting.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/tableediting.css b/theme/tableediting.css index 5d0224c1..e7b2d120 100644 --- a/theme/tableediting.css +++ b/theme/tableediting.css @@ -12,6 +12,6 @@ .ck-content .table table { & td.selected, & th.selected { - background: hsl(216, 100%, 67%); + box-shadow: inset 0 0 0 1px var(--ck-color-focus-border); } } From 243ca2531a914a61704dea691e976ce49b9377e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 13:18:26 +0100 Subject: [PATCH 069/107] Update visuals styles for the manual tests model contents. --- tests/manual/tableselection.html | 6 +++++- tests/manual/tableselection.js | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/manual/tableselection.html b/tests/manual/tableselection.html index 83001f70..8a2d3db5 100644 --- a/tests/manual/tableselection.html +++ b/tests/manual/tableselection.html @@ -3,6 +3,10 @@ font-family: Helvetica, Arial, sans-serif; font-size: 14px; } + + .print-selected { + background: #b8e3ff; + }
@@ -50,4 +54,4 @@

Model contents:

-
+
diff --git a/tests/manual/tableselection.js b/tests/manual/tableselection.js index 96cef67f..f719277a 100644 --- a/tests/manual/tableselection.js +++ b/tests/manual/tableselection.js @@ -41,13 +41,14 @@ function printModelContents( editor ) { .replace( //g, '>' ) .replace( /\n/g, '
' ) - .replace( /\[/g, '[' ) - .replace( /]/g, ']' ); + .replace( /\[/g, '[' ) + .replace( /]/g, ']' ); } function formatTable( tableString ) { return tableString - .replace( //g, '\n
' ) + .replace( /
/g, '\n/g, '\n\n ' ) .replace( //g, '\n\n ' ) .replace( //g, '\n\n ' ) @@ -56,5 +57,8 @@ function formatTable( tableString ) { .replace( /<\/thead>/g, '\n' ) .replace( /<\/tbody>/g, '\n' ) .replace( /<\/tr>/g, '\n' ) - .replace( /<\/table>/g, '\n
' ); + .replace( /<\/table>/g, '\n' ) + .replace( /tableCell/g, 'cell' ) + .replace( /tableRow/g, 'row' ) + .replace( /paragraph/g, 'p' ); } From 5700d186ba76a6d56f3b40fa43fcf07d30034ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 16:44:54 +0100 Subject: [PATCH 070/107] Fix tests. --- src/tableselection.js | 1 + tests/tableselection.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tableselection.js b/src/tableselection.js index 66025e1b..9e88c73f 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -134,6 +134,7 @@ export default class TableSelection extends Plugin { } this._isSelecting = false; + this._updateModelSelection(); } /** diff --git a/tests/tableselection.js b/tests/tableselection.js index f9b225f0..f9747322 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -13,7 +13,7 @@ import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; import { modelTable, viewTable } from './_utils/utils'; -describe.only( 'table selection', () => { +describe( 'table selection', () => { let editor, model, tableSelection, modelRoot; beforeEach( async () => { From 74f9fceb5c5863c90f4d92fb7d122d8477a82988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 16:52:57 +0100 Subject: [PATCH 071/107] Add tests for getSelectedTableCells(). Add tests for getSelectedTableCells(). --- tests/tableselection.js | 54 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index f9747322..aa37e03f 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -280,7 +280,59 @@ describe( 'table selection', () => { } ); } ); - describe( '* getSelectedTableCells()', () => {} ); + describe( '* getSelectedTableCells()', () => { + it( 'should return nothing if selection is not started', () => { + expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [] ); + } ); + + it( 'should return two table cells', () => { + const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] ); + const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] ); + + tableSelection.startSelectingFrom( firstCell ); + tableSelection.setSelectingTo( lastCell ); + + expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [ + firstCell, lastCell + ] ); + } ); + + it( 'should return four table cells for diagonal selection', () => { + const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] ); + const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); + + tableSelection.startSelectingFrom( firstCell ); + tableSelection.setSelectingTo( lastCell ); + + expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [ + firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell + ] ); + } ); + + it( 'should return row table cells', () => { + const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] ); + const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] ); + + tableSelection.startSelectingFrom( firstCell ); + tableSelection.setSelectingTo( lastCell ); + + expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [ + firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell + ] ); + } ); + + it( 'should return column table cells', () => { + const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] ); + const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] ); + + tableSelection.startSelectingFrom( firstCell ); + tableSelection.setSelectingTo( lastCell ); + + expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [ + firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell + ] ); + } ); + } ); } ); describe( 'mouse selection', () => { From 92fafcc396fae37856b23cd8bbed5c10b905edd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 16:55:01 +0100 Subject: [PATCH 072/107] Group table selection tests. --- tests/tableselection/mouseeventsobserver.js | 74 +++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/tests/tableselection/mouseeventsobserver.js b/tests/tableselection/mouseeventsobserver.js index cd0d537f..48d57443 100644 --- a/tests/tableselection/mouseeventsobserver.js +++ b/tests/tableselection/mouseeventsobserver.js @@ -8,41 +8,43 @@ import View from '@ckeditor/ckeditor5-engine/src/view/view'; import MouseEventsObserver from '../../src/tableselection/mouseeventsobserver'; -describe( 'MouseEventsObserver', () => { - let view, viewDocument, observer; - - beforeEach( () => { - view = new View(); - viewDocument = view.document; - observer = view.addObserver( MouseEventsObserver ); - } ); - - afterEach( () => { - view.destroy(); - } ); - - it( 'should define domEventTypes', () => { - expect( observer.domEventType ).to.deep.equal( [ - 'mousemove', - 'mouseup', - 'mouseleave' - ] ); - } ); - - describe( 'onDomEvent', () => { - for ( const eventName of [ 'mousemove', 'mouseup', 'mouseleave' ] ) { - it( `should fire ${ eventName } with the right event data`, () => { - const spy = sinon.spy(); - - viewDocument.on( eventName, spy ); - - observer.onDomEvent( { type: eventName, target: document.body } ); - - expect( spy.calledOnce ).to.be.true; - - const data = spy.args[ 0 ][ 1 ]; - expect( data.domTarget ).to.equal( document.body ); - } ); - } +describe( 'table selection', () => { + describe( 'MouseEventsObserver', () => { + let view, viewDocument, observer; + + beforeEach( () => { + view = new View(); + viewDocument = view.document; + observer = view.addObserver( MouseEventsObserver ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should define domEventTypes', () => { + expect( observer.domEventType ).to.deep.equal( [ + 'mousemove', + 'mouseup', + 'mouseleave' + ] ); + } ); + + describe( 'onDomEvent', () => { + for ( const eventName of [ 'mousemove', 'mouseup', 'mouseleave' ] ) { + it( `should fire ${ eventName } with the right event data`, () => { + const spy = sinon.spy(); + + viewDocument.on( eventName, spy ); + + observer.onDomEvent( { type: eventName, target: document.body } ); + + expect( spy.calledOnce ).to.be.true; + + const data = spy.args[ 0 ][ 1 ]; + expect( data.domTarget ).to.equal( document.body ); + } ); + } + } ); } ); } ); From e4b8cf6d7889e141882da80e285d6162b164591f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 31 Jan 2020 17:07:53 +0100 Subject: [PATCH 073/107] Rename test method to better describe its role. --- tests/tableselection.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index aa37e03f..53995ca4 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -357,7 +357,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); @@ -380,7 +380,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -388,7 +388,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -408,7 +408,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -416,7 +416,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -436,7 +436,7 @@ describe( 'table selection', () => { [ '10', '[]11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -444,7 +444,7 @@ describe( 'table selection', () => { [ '10', '[]11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -465,10 +465,10 @@ describe( 'table selection', () => { [ '20', '21', '22' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); - selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 2, 2 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -495,7 +495,7 @@ describe( 'table selection', () => { ] ], { asWidget: true } ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -529,7 +529,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -537,7 +537,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); view.document.fire( 'mouseup', domEvtDataStub ); @@ -552,7 +552,7 @@ describe( 'table selection', () => { [ '10', '11' ] ], { asWidget: true } ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -567,7 +567,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -575,7 +575,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); view.document.fire( 'mouseleave', domEvtDataStub ); @@ -590,7 +590,7 @@ describe( 'table selection', () => { [ '10', '11' ] ], { asWidget: true } ) ); - selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -605,7 +605,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) + 'foo' ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousedown', domEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ @@ -613,7 +613,7 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) + 'foo' ); - selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 ); + markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); domEvtDataStub.target = view.document.getRoot().getChild( 1 ); @@ -679,7 +679,7 @@ describe( 'table selection', () => { } } ); -function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { +function markTableCellAsSelectedInEvent( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { domEvtDataStub.target = view.document.getRoot() .getChild( tableIndex ) .getChild( 1 ) // Table is second in widget From a15c4ec9a8e061b005f588d6aca4e61a9fcf921f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 3 Feb 2020 18:49:34 +0100 Subject: [PATCH 074/107] Add missing tests for mouse events in mouse selection tests. --- tests/tableselection.js | 104 ++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/tests/tableselection.js b/tests/tableselection.js index 53995ca4..c5287331 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -391,10 +391,10 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], @@ -419,10 +419,10 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 1, 1 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], @@ -447,10 +447,10 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 1, 1 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], @@ -471,11 +471,11 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 2, 2 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ], - [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ] - ] ) ); + assertSelectedCells( [ + [ 1, 1, 1 ], + [ 1, 1, 1 ], + [ 1, 1, 1 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ @@ -498,11 +498,11 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ], - [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ], - [ '20', '21', '22' ] - ] ) ); + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ @@ -542,10 +542,10 @@ describe( 'table selection', () => { view.document.fire( 'mouseup', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], @@ -555,10 +555,10 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); } ); it( 'should stop selection mode on "mouseleve" event', () => { @@ -580,10 +580,10 @@ describe( 'table selection', () => { view.document.fire( 'mouseleave', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], - [ '10', '11' ] - ] ) ); + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); assertEqualMarkup( getViewData( view ), viewTable( [ [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], @@ -593,10 +593,42 @@ describe( 'table selection', () => { markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); view.document.fire( 'mousemove', domEvtDataStub ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ], + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + } ); + + it( 'should do nothing on "mouseleve" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + view.document.fire( 'mouseleave', domEvtDataStub ); + + assertSelectedCells( [ + [ 0, 0 ], + [ 0, 0 ] + ] ); + } ); + + it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], [ '10', '11' ] ] ) ); + + domEvtDataStub.target = view.document.getRoot() + .getChild( 0 ) + .getChild( 0 ); // selection handler; + + view.document.fire( 'mousedown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), '[' + modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ] ) + ']' ); } ); it( 'should clear view table selection after mouse click outside table', () => { From 22dc61e200a5535c812186ffad73b54227f15205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 08:40:42 +0100 Subject: [PATCH 075/107] Simplify RemoveColumnCommand. --- src/commands/removecolumncommand.js | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index e9fa187b..4cc5e1f6 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -58,23 +58,16 @@ export default class RemoveColumnCommand extends Command { // Get column index of removed column. const cellData = tableMap.find( value => value.cell === tableCell ); const removedColumn = cellData.column; - const removedRow = cellData.row; - - let cellToFocus; - - const tableUtils = this.editor.plugins.get( 'TableUtils' ); - const columns = tableUtils.getColumns( tableCell.parent.parent ); - - const columnToFocus = removedColumn === columns - 1 ? removedColumn - 1 : removedColumn + 1; - const rowToFocus = removedRow; + const selectionRow = cellData.row; + const cellToFocus = getCellToFocus( tableCell ); model.change( writer => { // Update heading columns attribute if removing a row from head section. - if ( headingColumns && removedRow <= headingColumns ) { + if ( headingColumns && selectionRow <= headingColumns ) { writer.setAttribute( 'headingColumns', headingColumns - 1, table ); } - for ( const { cell, row, column, rowspan, colspan } of tableMap ) { + for ( const { cell, column, colspan } of tableMap ) { // If colspaned cell overlaps removed column decrease it's span. if ( column <= removedColumn && colspan > 1 && column + colspan > removedColumn ) { updateNumericAttribute( 'colspan', colspan - 1, cell, writer ); @@ -82,10 +75,6 @@ export default class RemoveColumnCommand extends Command { // The cell in removed column has colspan of 1. writer.remove( cell ); } - - if ( isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) ) { - cellToFocus = cell; - } } writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); @@ -93,6 +82,15 @@ export default class RemoveColumnCommand extends Command { } } -function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { - return ( row <= rowToFocus && row + rowspan >= rowToFocus ) && ( column <= columnToFocus && column + colspan >= columnToFocus ); +// Returns a proper table cell to focus after removing a column. It should be a next sibling to selection visually stay in place but: +// - selection is on last table cell it will return previous cell. +// - table cell is spanned over 2+ columns - it will be truncated so the selection should stay in that cell. +function getCellToFocus( tableCell ) { + const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + + if ( colspan > 1 ) { + return tableCell; + } + + return tableCell.nextSibling ? tableCell.nextSibling : tableCell.previousSibling; } From 5bf9c85fdb22796c6b57457934d675e890d3dc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 09:05:34 +0100 Subject: [PATCH 076/107] Simplify RemoveRowCommand. --- src/commands/removerowcommand.js | 24 +++++++++++++++++------- tests/commands/removerowcommand.js | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index f10336e5..efda49ef 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -56,7 +56,6 @@ export default class RemoveRowCommand extends Command { const headingRows = table.getAttribute( 'headingRows' ) || 0; - const rowToFocus = removedRow; const columnToFocus = cellData.column; model.change( writer => { @@ -100,15 +99,26 @@ export default class RemoveRowCommand extends Command { writer.remove( tableRow ); - const { cell: cellToFocus } = [ ...new TableWalker( table ) ].find( ( { row, column, rowspan, colspan } ) => { - return isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ); - } ); - + const cellToFocus = getCellToFocus( table, removedRow, columnToFocus ); writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); } ); } } -function isCellToFocusAfterRemoving( row, rowToFocus, rowspan, column, columnToFocus, colspan ) { - return ( row <= rowToFocus && row + rowspan >= rowToFocus + 1 ) && ( column <= columnToFocus && column + colspan >= columnToFocus + 1 ); +// Returns a cell to focus on the same column of the focused table cell before removing a row. +function getCellToFocus( table, removedRow, columnToFocus ) { + const row = table.getChild( removedRow ); + + // Default to first table cell. + let cellToFocus = row.getChild( 0 ); + let column = 0; + + for ( const tableCell of row.getChildren() ) { + if ( column > columnToFocus ) { + return cellToFocus; + } + + cellToFocus = tableCell; + column += parseInt( tableCell.getAttribute( 'colspan' ) || 1 ); + } } diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js index 20427beb..c2bb6686 100644 --- a/tests/commands/removerowcommand.js +++ b/tests/commands/removerowcommand.js @@ -113,7 +113,7 @@ describe( 'RemoveRowCommand', () => { assertEqualMarkup( getData( model ), modelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], [ '13', '14' ], - [ '30', '[]31', '32', '33', '34' ] + [ '30', '31', '[]32', '33', '34' ] ] ) ); } ); From ef02380992e10da23f9774faab5fd59c14418d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 09:34:50 +0100 Subject: [PATCH 077/107] Update table selection manual test description. --- tests/manual/tableselection.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/manual/tableselection.md b/tests/manual/tableselection.md index 36376ec6..c42f5bf3 100644 --- a/tests/manual/tableselection.md +++ b/tests/manual/tableselection.md @@ -2,7 +2,6 @@ Selecting table cells: -1. It should be possible to select table cells from the same section (ie.: header) . -2. It should not be possible to extend selection beyond a table section (ie.: header and body). +1. It should be possible to select multiple table cells. Observe selection inn the below model representation - for a block selection the table cells should be selected. From 31ffec869256011fb38b1889a924d6a04220a89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:21:20 +0100 Subject: [PATCH 078/107] Refactor and document MouseSelectionHandler. --- src/tableselection/mouseselectionhandler.js | 186 ++++++++++++++------ 1 file changed, 129 insertions(+), 57 deletions(-) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index dc6bc3e6..4d4acb18 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -13,75 +13,147 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import { findAncestor } from '../commands/utils'; import MouseEventsObserver from './mouseeventsobserver'; +/** + * A mouse selection handler for table selection. + * + * It observes view document mouse events and invokes proper {@link module:table/tableselection~TableSelection} actions. + */ export default class MouseSelectionHandler { + /** + * Creates instance of `MouseSelectionHandler`. + * + * @param {module:table/tableselection~TableSelection} tableSelection + * @param {module:engine/controller/editingcontroller~EditingController} editing + */ constructor( tableSelection, editing ) { + /** + * Table selection. + * + * @private + * @readonly + * @member {module:table/tableselection~TableSelection} + */ + this._tableSelection = tableSelection; + + /** + * Editing mapper. + * + * @private + * @readonly + * @member {module:engine/conversion/mapper~Mapper} + */ + this._mapper = editing.mapper; + const view = editing.view; - const viewDocument = view.document; - const mapper = editing.mapper; + // Currently the MouseObserver only handles `mouseup` events. view.addObserver( MouseEventsObserver ); - this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => { - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - if ( !tableCell ) { - tableSelection.stopSelection(); - tableSelection.clearSelection(); - - return; - } - - tableSelection.startSelectingFrom( tableCell ); - } ); - - this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => { - if ( !tableSelection._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - if ( !tableCell ) { - return; - } - - tableSelection.setSelectingTo( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => { - if ( !tableSelection._isSelecting ) { - return; - } - - const tableCell = getModelTableCellFromViewEvent( domEventData, mapper ); - - tableSelection.stopSelection( tableCell ); - } ); - - this.listenTo( viewDocument, 'mouseleave', () => { - if ( !tableSelection._isSelecting ) { - return; - } - - tableSelection.stopSelection(); - } ); + this.listenTo( view.document, 'mousedown', ( eventInfo, domEventData ) => this._handleMouseDown( domEventData ) ); + this.listenTo( view.document, 'mousemove', ( eventInfo, domEventData ) => this._handleMouseMove( domEventData ) ); + this.listenTo( view.document, 'mouseup', ( eventInfo, domEventData ) => this._handleMouseUp( domEventData ) ); + this.listenTo( view.document, 'mouseleave', () => this._handleMouseLeave() ); } -} -mix( MouseSelectionHandler, ObservableMixin ); + /** + * Handles starting a selection when "mousedown" event has table cell target. + * + * If no table cell in event target it will clear previous selection. + * + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + * @private + */ + _handleMouseDown( domEventData ) { + const tableCell = this._getModelTableCellFromDomEvent( domEventData ); + + if ( !tableCell ) { + this._tableSelection.stopSelection(); + this._tableSelection.clearSelection(); + + return; + } + + this._tableSelection.startSelectingFrom( tableCell ); + } -// Finds model table cell for given DOM event - ie. for 'mousedown'. -function getModelTableCellFromViewEvent( domEventData, mapper ) { - const viewTargetElement = domEventData.target; - const modelElement = mapper.toModelElement( viewTargetElement ); + /** + * Handles updating table selection when "mousemove" event has a table cell target. + * + * Does nothing if no table cell in event target or selection is not started. + * + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + * @private + */ + _handleMouseMove( domEventData ) { + if ( !this._tableSelection._isSelecting ) { + return; + } + + const tableCell = this._getModelTableCellFromDomEvent( domEventData ); + + if ( !tableCell ) { + return; + } + + this._tableSelection.setSelectingTo( tableCell ); + } - if ( !modelElement ) { - return; + /** + * Handles ending (not clearing) table selection on "mouseup" event. + * + * Does nothing if selection is not started. + * + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + * @private + */ + _handleMouseUp( domEventData ) { + if ( !this._tableSelection._isSelecting ) { + return; + } + + const tableCell = this._getModelTableCellFromDomEvent( domEventData ); + + // Selection can be stopped if table cell is undefined. + this._tableSelection.stopSelection( tableCell ); } - if ( modelElement.is( 'tableCell' ) ) { - return modelElement; + /** + * Handles stopping a selection on "mouseleave" event. + * + * Does nothing if selection is not started. + * + * @private + */ + _handleMouseLeave() { + if ( !this._tableSelection._isSelecting ) { + return; + } + + this._tableSelection.stopSelection(); } - return findAncestor( 'tableCell', modelElement ); + /** + * Finds model table cell for given DOM event. + * + * @private + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + * @returns {module:engine/model/element~Element|undefined} Returns model table cell or undefined event target is not + * a mapped table cell. + */ + _getModelTableCellFromDomEvent( domEventData ) { + const viewTargetElement = domEventData.target; + const modelElement = this._mapper.toModelElement( viewTargetElement ); + + if ( !modelElement ) { + return; + } + + if ( modelElement.is( 'tableCell' ) ) { + return modelElement; + } + + return findAncestor( 'tableCell', modelElement ); + } } + +mix( MouseSelectionHandler, ObservableMixin ); From 53d9482e1c32170dd8a768e3497fab6d73cfd541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:23:14 +0100 Subject: [PATCH 079/107] Fix findAncestor docs. --- src/commands/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/utils.js b/src/commands/utils.js index 4b3ffa28..029a9af1 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -13,7 +13,7 @@ import { isObject } from 'lodash-es'; * Returns the parent element of given name. Returns undefined if positionOrElement is not inside desired parent. * * @param {String} parentName Name of parent element to find. - * @param {module:engine/model/position~Position|module:engine/model/element~Position} positionOrElement + * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrElement * Position or parentElement to start searching. * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} */ From 75175ec87e70dd348ced2b749099b36a32833376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:24:58 +0100 Subject: [PATCH 080/107] Rename internal param. --- src/tableselection/mouseselectionhandler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 4d4acb18..d273dc84 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -49,9 +49,9 @@ export default class MouseSelectionHandler { // Currently the MouseObserver only handles `mouseup` events. view.addObserver( MouseEventsObserver ); - this.listenTo( view.document, 'mousedown', ( eventInfo, domEventData ) => this._handleMouseDown( domEventData ) ); - this.listenTo( view.document, 'mousemove', ( eventInfo, domEventData ) => this._handleMouseMove( domEventData ) ); - this.listenTo( view.document, 'mouseup', ( eventInfo, domEventData ) => this._handleMouseUp( domEventData ) ); + this.listenTo( view.document, 'mousedown', ( event, domEventData ) => this._handleMouseDown( domEventData ) ); + this.listenTo( view.document, 'mousemove', ( event, domEventData ) => this._handleMouseMove( domEventData ) ); + this.listenTo( view.document, 'mouseup', ( event, domEventData ) => this._handleMouseUp( domEventData ) ); this.listenTo( view.document, 'mouseleave', () => this._handleMouseLeave() ); } From 3e8a4c5b20a771da0f1ef0a4e44a19b6b4ded114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:30:01 +0100 Subject: [PATCH 081/107] Cross ling MouseSelectionHandler and MouseEventsObserver docs. --- src/tableselection/mouseeventsobserver.js | 2 ++ src/tableselection/mouseselectionhandler.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tableselection/mouseeventsobserver.js b/src/tableselection/mouseeventsobserver.js index aeae1e99..8935978f 100644 --- a/src/tableselection/mouseeventsobserver.js +++ b/src/tableselection/mouseeventsobserver.js @@ -21,6 +21,8 @@ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domev * Note that this observer is not available by default. To make it available it needs to be added to * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. * + * It is registered by {@link module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}. + * * @extends module:engine/view/observer/domeventobserver~DomEventObserver */ export default class MouseEventsObserver extends DomEventObserver { diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index d273dc84..fc6cf402 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -16,7 +16,8 @@ import MouseEventsObserver from './mouseeventsobserver'; /** * A mouse selection handler for table selection. * - * It observes view document mouse events and invokes proper {@link module:table/tableselection~TableSelection} actions. + * It registers the {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} to observe view document mouse events + * and invoke proper {@link module:table/tableselection~TableSelection} actions. */ export default class MouseSelectionHandler { /** From 9c50778a40e904a45007fd46fc4d3da55f3b0196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:50:38 +0100 Subject: [PATCH 082/107] Document TableSelection properties. --- src/tableselection.js | 29 ++++++++++++++++----- src/tableselection/converters.js | 2 +- src/tableselection/mouseselectionhandler.js | 6 ++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 9e88c73f..6f6eeb6d 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -42,7 +42,22 @@ export default class TableSelection extends Plugin { constructor( editor ) { super( editor ); - this._isSelecting = false; + /** + * A flag indicating that the selection is "active". A selection is "active" if it was started and not yet finished. + * A selection can be "active" for instance if user moves a mouse over a table while holding a mouse button down. + * + * @readonly + * @member {Boolean} + */ + this.isSelecting = false; + + /** + * A mouse selection handler. + * + * @private + * @readonly + * @member {module:table/tableselection/mouseselectionhandler~MouseSelectionHandler} + */ this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); } @@ -53,7 +68,7 @@ export default class TableSelection extends Plugin { * @member {Boolean} #isSelectingAndSomethingElse */ get isSelectingAndSomethingElse() { - return this._isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; + return this.isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; } /** @@ -85,7 +100,7 @@ export default class TableSelection extends Plugin { startSelectingFrom( tableCell ) { this.clearSelection(); - this._isSelecting = true; + this.isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; } @@ -102,7 +117,7 @@ export default class TableSelection extends Plugin { */ setSelectingTo( tableCell ) { // Do not update if not in selection mode or no table cell is passed. - if ( !this._isSelecting || !tableCell ) { + if ( !this.isSelecting || !tableCell ) { return; } @@ -129,11 +144,11 @@ export default class TableSelection extends Plugin { * @param {module:engine/model/element~Element} [tableCell] */ stopSelection( tableCell ) { - if ( this._isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } - this._isSelecting = false; + this.isSelecting = false; this._updateModelSelection(); } @@ -149,7 +164,7 @@ export default class TableSelection extends Plugin { clearSelection() { this._startElement = undefined; this._endElement = undefined; - this._isSelecting = false; + this.isSelecting = false; } /** diff --git a/src/tableselection/converters.js b/src/tableselection/converters.js index 4a89f657..aa35b559 100644 --- a/src/tableselection/converters.js +++ b/src/tableselection/converters.js @@ -21,7 +21,7 @@ export function setupTableSelectionHighlighting( editor, tableSelection ) { const viewWriter = conversionApi.writer; const viewSelection = viewWriter.document.selection; - if ( tableSelection.isSelectingAndSomethingElse ) { + if ( tableSelection.isSelecting ) { clearHighlightedTableCells( highlighted, view ); for ( const tableCell of tableSelection.getSelectedTableCells() ) { diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index fc6cf402..6e6589f6 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -86,7 +86,7 @@ export default class MouseSelectionHandler { * @private */ _handleMouseMove( domEventData ) { - if ( !this._tableSelection._isSelecting ) { + if ( !this._tableSelection.isSelecting ) { return; } @@ -108,7 +108,7 @@ export default class MouseSelectionHandler { * @private */ _handleMouseUp( domEventData ) { - if ( !this._tableSelection._isSelecting ) { + if ( !this._tableSelection.isSelecting ) { return; } @@ -126,7 +126,7 @@ export default class MouseSelectionHandler { * @private */ _handleMouseLeave() { - if ( !this._tableSelection._isSelecting ) { + if ( !this._tableSelection.isSelecting ) { return; } From 7078ea6523f7b4f1c9be69be91156056c8de8a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:53:05 +0100 Subject: [PATCH 083/107] Remove redundant TableSelection property. --- src/tableselection.js | 10 ---------- tests/tableselection.js | 36 +----------------------------------- 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 6f6eeb6d..919c7e68 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -61,16 +61,6 @@ export default class TableSelection extends Plugin { this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); } - /** - * Flag indicating that table selection is selecting valid ranges in table cell. - * - * @readonly - * @member {Boolean} #isSelectingAndSomethingElse - */ - get isSelectingAndSomethingElse() { - return this.isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; - } - /** * @inheritDoc */ diff --git a/tests/tableselection.js b/tests/tableselection.js index c5287331..c62965a6 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -37,40 +37,6 @@ describe( 'table selection', () => { } ); describe( 'TableSelection', () => { - describe( 'isSelectingAndSomethingElse()', () => { - it( 'should be false if selection is not started', () => { - expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; - } ); - - it( 'should be true if selection is selecting two different cells', () => { - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - - expect( tableSelection.isSelectingAndSomethingElse ).to.be.true; - } ); - - it( 'should be false if selection start/end is selecting the same table cell', () => { - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - - expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; - } ); - - it( 'should be false if selection has no end cell', () => { - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - - expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; - } ); - - it( 'should be false if selection has ended', () => { - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - tableSelection.stopSelection(); - - expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; - } ); - } ); - describe( 'startSelectingFrom()', () => { it( 'should not change model selection', () => { const spy = sinon.spy(); @@ -263,7 +229,7 @@ describe( 'table selection', () => { tableSelection.clearSelection(); - expect( tableSelection.isSelectingAndSomethingElse ).to.be.false; + expect( tableSelection.isSelecting ).to.be.false; } ); it( 'should not reset model selections', () => { From c04abec94b09c4ad33a924c5fe5bc6ebb89e6b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 4 Feb 2020 10:55:17 +0100 Subject: [PATCH 084/107] Document private property of Table Selection. --- src/tableselection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tableselection.js b/src/tableselection.js index 919c7e68..3e0c79f3 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -59,6 +59,14 @@ export default class TableSelection extends Plugin { * @member {module:table/tableselection/mouseselectionhandler~MouseSelectionHandler} */ this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); + + /** + * A table utilities. + * + * @private + * @readonly + * @member {module:table/tableutils~TableUtils} + */ } /** From 22fa1d8d7c2b326dc352eaac9f80b409b27c7a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 14 Feb 2020 14:32:02 +0100 Subject: [PATCH 085/107] We shouldn't handle selection of a single cell by the new rendering method. --- src/tableselection.js | 10 +++++++++- src/tableselection/converters.js | 2 +- src/tableselection/mouseselectionhandler.js | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 3e0c79f3..0a71777e 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -69,6 +69,10 @@ export default class TableSelection extends Plugin { */ } + get hasMultiCellSelection() { + return this.isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; + } + /** * @inheritDoc */ @@ -177,7 +181,7 @@ export default class TableSelection extends Plugin { * @returns {Iterable.} */ * getSelectedTableCells() { - if ( !this._startElement || !this._endElement ) { + if ( !this.hasMultiCellSelection ) { return; } @@ -203,6 +207,10 @@ export default class TableSelection extends Plugin { * @private */ _updateModelSelection() { + if ( !this.hasMultiCellSelection ) { + return; + } + const editor = this.editor; const model = editor.model; diff --git a/src/tableselection/converters.js b/src/tableselection/converters.js index aa35b559..28dd348a 100644 --- a/src/tableselection/converters.js +++ b/src/tableselection/converters.js @@ -21,7 +21,7 @@ export function setupTableSelectionHighlighting( editor, tableSelection ) { const viewWriter = conversionApi.writer; const viewSelection = viewWriter.document.selection; - if ( tableSelection.isSelecting ) { + if ( tableSelection.hasMultiCellSelection ) { clearHighlightedTableCells( highlighted, view ); for ( const tableCell of tableSelection.getSelectedTableCells() ) { diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 6e6589f6..a139c012 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -68,8 +68,8 @@ export default class MouseSelectionHandler { const tableCell = this._getModelTableCellFromDomEvent( domEventData ); if ( !tableCell ) { - this._tableSelection.stopSelection(); this._tableSelection.clearSelection(); + this._tableSelection.stopSelection(); return; } From f37389b324f5596c5f814a5df62ddef6ac36f659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 14 Feb 2020 15:17:59 +0100 Subject: [PATCH 086/107] Render table selection when table cells are selected and selection process is stopped. --- src/tableselection.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tableselection.js b/src/tableselection.js index 0a71777e..19100a16 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -69,8 +69,13 @@ export default class TableSelection extends Plugin { */ } + /** + * Flag indicating that there are selected table cells. + * + * @type {Boolean} + */ get hasMultiCellSelection() { - return this.isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement; + return !!this._startElement && !!this._endElement && this._startElement !== this._endElement; } /** From 7545eb814d19cb7f8a92cb87d8bad798cc0da8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 14 Feb 2020 16:49:45 +0100 Subject: [PATCH 087/107] Temporally remove selection stop on mouseleave. --- src/tableselection/mouseselectionhandler.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index a139c012..d4ce6033 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -86,9 +86,9 @@ export default class MouseSelectionHandler { * @private */ _handleMouseMove( domEventData ) { - if ( !this._tableSelection.isSelecting ) { - return; - } + // if ( !this._tableSelection.isSelecting ) { + // return; + // } const tableCell = this._getModelTableCellFromDomEvent( domEventData ); @@ -126,11 +126,11 @@ export default class MouseSelectionHandler { * @private */ _handleMouseLeave() { - if ( !this._tableSelection.isSelecting ) { - return; - } + // if ( !this._tableSelection.isSelecting ) { + // return; + // } - this._tableSelection.stopSelection(); + // this._tableSelection.stopSelection(); } /** From 61eceb4e1d4aa38618ada77fe7805366f68ce945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 14 Feb 2020 17:08:53 +0100 Subject: [PATCH 088/107] Stop selection on mouse move if button was depressed outside webpage. --- src/tableselection.js | 16 ++------------- src/tableselection/mouseselectionhandler.js | 22 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 19100a16..81d4d647 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -42,15 +42,6 @@ export default class TableSelection extends Plugin { constructor( editor ) { super( editor ); - /** - * A flag indicating that the selection is "active". A selection is "active" if it was started and not yet finished. - * A selection can be "active" for instance if user moves a mouse over a table while holding a mouse button down. - * - * @readonly - * @member {Boolean} - */ - this.isSelecting = false; - /** * A mouse selection handler. * @@ -107,7 +98,6 @@ export default class TableSelection extends Plugin { startSelectingFrom( tableCell ) { this.clearSelection(); - this.isSelecting = true; this._startElement = tableCell; this._endElement = tableCell; } @@ -124,7 +114,7 @@ export default class TableSelection extends Plugin { */ setSelectingTo( tableCell ) { // Do not update if not in selection mode or no table cell is passed. - if ( !this.isSelecting || !tableCell ) { + if ( !tableCell ) { return; } @@ -151,11 +141,10 @@ export default class TableSelection extends Plugin { * @param {module:engine/model/element~Element} [tableCell] */ stopSelection( tableCell ) { - if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { + if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) { this._endElement = tableCell; } - this.isSelecting = false; this._updateModelSelection(); } @@ -171,7 +160,6 @@ export default class TableSelection extends Plugin { clearSelection() { this._startElement = undefined; this._endElement = undefined; - this.isSelecting = false; } /** diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index d4ce6033..4b72fb88 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -36,6 +36,15 @@ export default class MouseSelectionHandler { */ this._tableSelection = tableSelection; + /** + * A flag indicating that the mouse selection is "active". A selection is "active" if it was started and not yet finished. + * A selection can be "active" for instance if user moves a mouse over a table while holding a mouse button down. + * + * @readonly + * @member {Boolean} + */ + this.isSelecting = false; + /** * Editing mapper. * @@ -74,6 +83,7 @@ export default class MouseSelectionHandler { return; } + this.isSelecting = true; this._tableSelection.startSelectingFrom( tableCell ); } @@ -86,9 +96,11 @@ export default class MouseSelectionHandler { * @private */ _handleMouseMove( domEventData ) { - // if ( !this._tableSelection.isSelecting ) { - // return; - // } + if ( !isButtonPressed( domEventData ) ) { + this._tableSelection.stopSelection(); + + return; + } const tableCell = this._getModelTableCellFromDomEvent( domEventData ); @@ -157,4 +169,8 @@ export default class MouseSelectionHandler { } } +function isButtonPressed( domEventData ) { + return !!domEventData.domEvent.buttons; +} + mix( MouseSelectionHandler, ObservableMixin ); From d8204f388ca59827c87ec1f206b35a9f9bf1c168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Feb 2020 19:07:31 +0100 Subject: [PATCH 089/107] Extract MouseEventObserver tests. --- tests/tableselection.js | 366 +--------------- tests/tableselection/mouseeventobserver.js | 473 +++++++++++++++++++++ 2 files changed, 484 insertions(+), 355 deletions(-) create mode 100644 tests/tableselection/mouseeventobserver.js diff --git a/tests/tableselection.js b/tests/tableselection.js index c62965a6..2688db65 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -5,13 +5,11 @@ 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; -import { modelTable, viewTable } from './_utils/utils'; +import { modelTable } from './_utils/utils'; describe( 'table selection', () => { let editor, model, tableSelection, modelRoot; @@ -50,14 +48,21 @@ describe( 'table selection', () => { } ); describe( 'setSelectingTo()', () => { - it( 'should not change model selection if selection is not started', () => { + it( 'should set model selection on passed cell if startSelectingFrom() was not used', () => { const spy = sinon.spy(); model.document.selection.on( 'change', spy ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - sinon.assert.notCalled( spy ); + sinon.assert.calledOnce( spy ); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); } ); it( 'should change model selection if valid selection will be set', () => { @@ -223,15 +228,6 @@ describe( 'table selection', () => { sinon.assert.notCalled( spy ); } ); - it( 'should stop selecting mode', () => { - tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); - tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - - tableSelection.clearSelection(); - - expect( tableSelection.isSelecting ).to.be.false; - } ); - it( 'should not reset model selections', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); @@ -301,337 +297,6 @@ describe( 'table selection', () => { } ); } ); - describe( 'mouse selection', () => { - let view, domEvtDataStub; - - beforeEach( () => { - view = editor.editing.view; - - domEvtDataStub = { - domEvent: { - buttons: 1 - }, - target: undefined, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - } ); - - it( 'should not start table selection when mouse move is inside one table cell', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - view.document.fire( 'mousemove', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - } ); - - it( 'should start table selection when mouse move expands over two cells', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 0, 0 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - } ); - - it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 1, 1 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] - ], { asWidget: true } ) ); - } ); - - it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '[]11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '00', '01' ], - [ '10', '[]11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 1, 1 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] - ], { asWidget: true } ) ); - } ); - - it( 'should update view selection after changing selection rect', () => { - setModelData( model, modelTable( [ - [ '[]00', '01', '02' ], - [ '10', '11', '12' ], - [ '20', '21', '22' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 2, 2 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1, 1 ], - [ 1, 1, 1 ], - [ 1, 1, 1 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, - { contents: '02', class: 'selected', isSelected: true } - ], - [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, - { contents: '12', class: 'selected', isSelected: true } - ], - [ - { contents: '20', class: 'selected', isSelected: true }, - { contents: '21', class: 'selected', isSelected: true }, - { contents: '22', class: 'selected', isSelected: true } - ] - ], { asWidget: true } ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1, 0 ], - [ 1, 1, 0 ], - [ 0, 0, 0 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, - '02' - ], - [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, - '12' - ], - [ - '20', - '21', - '22' - ] - ], { asWidget: true } ) ); - } ); - - it( 'should stop selecting after "mouseup" event', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - view.document.fire( 'mouseup', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 0, 0 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 0, 0 ] - ] ); - } ); - - it( 'should stop selection mode on "mouseleve" event', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - view.document.fire( 'mouseleave', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 0, 0 ] - ] ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ '10', '11' ] - ], { asWidget: true } ) ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 1, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - assertSelectedCells( [ - [ 1, 1 ], - [ 0, 0 ] - ] ); - } ); - - it( 'should do nothing on "mouseleve" event', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - view.document.fire( 'mouseleave', domEvtDataStub ); - - assertSelectedCells( [ - [ 0, 0 ], - [ 0, 0 ] - ] ); - } ); - - it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) ); - - domEvtDataStub.target = view.document.getRoot() - .getChild( 0 ) - .getChild( 0 ); // selection handler; - - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), '[' + modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ] ) + ']' ); - } ); - - it( 'should clear view table selection after mouse click outside table', () => { - setModelData( model, modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) + 'foo' ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 0 ); - view.document.fire( 'mousedown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[]00', '01' ], - [ '10', '11' ] - ] ) + 'foo' ); - - markTableCellAsSelectedInEvent( domEvtDataStub, view, 0, 0, 0, 1 ); - view.document.fire( 'mousemove', domEvtDataStub ); - - domEvtDataStub.target = view.document.getRoot().getChild( 1 ); - - view.document.fire( 'mousemove', domEvtDataStub ); - view.document.fire( 'mousedown', domEvtDataStub ); - view.document.fire( 'mouseup', domEvtDataStub ); - - // The click in the DOM would trigger selection change and it will set the selection: - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); - } ); - - assertEqualMarkup( getViewData( view ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ] - ], { asWidget: true } ) + '

{}foo

' ); - } ); - } ); - // Helper method for asserting selected table cells. // // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: @@ -676,12 +341,3 @@ describe( 'table selection', () => { expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; } } ); - -function markTableCellAsSelectedInEvent( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) { - domEvtDataStub.target = view.document.getRoot() - .getChild( tableIndex ) - .getChild( 1 ) // Table is second in widget - .getChild( sectionIndex ) - .getChild( rowInSectionIndex ) - .getChild( tableCellIndex ); -} diff --git a/tests/tableselection/mouseeventobserver.js b/tests/tableselection/mouseeventobserver.js new file mode 100644 index 00000000..4f2fa2a9 --- /dev/null +++ b/tests/tableselection/mouseeventobserver.js @@ -0,0 +1,473 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; + +import TableEditing from '../../src/tableediting'; +import TableSelection from '../../src/tableselection'; +import { modelTable, viewTable } from '../_utils/utils'; + +describe( 'table selection', () => { + let editor, model, modelRoot, view; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ TableEditing, TableSelection, Paragraph ] + } ); + + model = editor.model; + modelRoot = model.document.getRoot(); + view = editor.editing.view; + + setModelData( model, modelTable( [ + [ '11[]', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + } ); + + afterEach( async () => { + await editor.destroy(); + } ); + + describe( 'MouseEventObserver', () => { + it( 'should not start table selection when mouse move is inside one table cell', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should start table selection when mouse move expands over two cells', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '01' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '11' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 1, 1 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '11' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01' ], + [ '10', '[]11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '00' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 1, 1 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + ], { asWidget: true } ) ); + } ); + + it( 'should update view selection after changing selection rect', () => { + setModelData( model, modelTable( [ + [ '[]00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + movePressedMouseOver( getTableCell( '22' ) ); + + assertSelectedCells( [ + [ 1, 1, 1 ], + [ 1, 1, 1 ], + [ 1, 1, 1 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + { contents: '02', class: 'selected', isSelected: true } + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + { contents: '12', class: 'selected', isSelected: true } + ], + [ + { contents: '20', class: 'selected', isSelected: true }, + { contents: '21', class: 'selected', isSelected: true }, + { contents: '22', class: 'selected', isSelected: true } + ] + ], { asWidget: true } ) ); + + movePressedMouseOver( getTableCell( '11' ) ); + + assertSelectedCells( [ + [ 1, 1, 0 ], + [ 1, 1, 0 ], + [ 0, 0, 0 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ + { contents: '00', class: 'selected', isSelected: true }, + { contents: '01', class: 'selected', isSelected: true }, + '02' + ], + [ + { contents: '10', class: 'selected', isSelected: true }, + { contents: '11', class: 'selected', isSelected: true }, + '12' + ], + [ + '20', + '21', + '22' + ] + ], { asWidget: true } ) ); + } ); + + it( 'should stop selecting after "mouseup" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '01' ) ); + releaseMouseButtonOver( getTableCell( '01' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + moveReleasedMouseOver( getTableCell( '11' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + } ); + + it( 'should stop selection mode on "mouseleve" event if next "mousemove" has no button pressed', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '01' ) ); + makeMouseLeave(); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + moveReleasedMouseOver( getTableCell( '11' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + } ); + + it( 'should continue selection mode on "mouseleve" and "mousemove" if mouse button is pressed', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + movePressedMouseOver( getTableCell( '01' ) ); + makeMouseLeave(); + + assertSelectedCells( [ + [ 1, 1 ], + [ 0, 0 ] + ] ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ '10', '11' ] + ], { asWidget: true } ) ); + + movePressedMouseOver( getTableCell( '11' ) ); + + assertSelectedCells( [ + [ 1, 1 ], + [ 1, 1 ] + ] ); + } ); + + it( 'should do nothing on "mouseleve" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + makeMouseLeave(); + + assertSelectedCells( [ + [ 0, 0 ], + [ 0, 0 ] + ] ); + } ); + + it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + const uiElement = view.document.getRoot() + .getChild( 0 ) + .getChild( 0 ); // selection handler; + + fireEvent( view, 'mousedown', addTarget( uiElement ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should clear view table selection after mouse click outside table', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + pressMouseButtonOver( getTableCell( '00' ) ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) + 'foo' ); + + movePressedMouseOver( getTableCell( '01' ) ); + + const paragraph = view.document.getRoot().getChild( 1 ); + + fireEvent( view, 'mousemove', addTarget( paragraph ) ); + fireEvent( view, 'mousedown', addTarget( paragraph ) ); + fireEvent( view, 'mouseup', addTarget( paragraph ) ); + + // The click in the DOM would trigger selection change and it will set the selection: + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) ); + } ); + + assertEqualMarkup( getViewData( view ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { asWidget: true } ) + '

{}foo

' ); + } ); + } ); + + // Helper method for asserting selected table cells. + // + // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: + // + // assertSelectedCells( [ + // [ 0, 1 ], + // [ 0, 1 ] + // ] ); + // + // The above call will check if table has second column selected (assuming no spans). + // + // **Note**: This function operates on child indexes - not rows/columns. + function assertSelectedCells( tableMap ) { + const tableIndex = 0; + + for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) { + const row = tableMap[ rowIndex ]; + + for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) { + const expectSelected = row[ cellIndex ]; + + if ( expectSelected ) { + assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] ); + } else { + assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] ); + } + } + } + } + + function assertNodeIsSelected( path ) { + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true; + } + + function assertNodeIsNotSelected( path ) { + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; + } + + function getTableCell( data ) { + for ( const value of view.createRangeIn( view.document.getRoot() ) ) { + if ( value.type === 'text' && value.item.data === data ) { + return value.item.parent.parent; + } + } + } + + function makeMouseLeave() { + fireEvent( view, 'mouseleave' ); + } + + function pressMouseButtonOver( target ) { + fireEvent( view, 'mousemove', addTarget( target ), mouseButtonPressed ); + } + + function movePressedMouseOver( target ) { + moveMouseOver( target, mouseButtonPressed ); + } + + function moveReleasedMouseOver( target ) { + moveMouseOver( target, mouseButtonReleased ); + } + + function moveMouseOver( target, ...decorators ) { + fireEvent( view, 'mousemove', addTarget( target ), ...decorators ); + } + + function releaseMouseButtonOver( target ) { + fireEvent( view, 'mouseup', addTarget( target ), mouseButtonReleased ); + } + + function addTarget( target ) { + return domEventData => { + domEventData.target = target; + }; + } + + function mouseButtonPressed( domEventData ) { + domEventData.domEvent.buttons = 1; + } + + function mouseButtonReleased( domEventData ) { + domEventData.domEvent.buttons = 0; + } + + function fireEvent( view, eventName, ...decorators ) { + const domEvtDataStub = { + domEvent: { + buttons: 0 + }, + target: undefined, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + for ( const decorator of decorators ) { + decorator( domEvtDataStub ); + } + + view.document.fire( eventName, domEvtDataStub ); + } +} ); From 7dd63b21659b1512604383207e418a93cbdbe61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Feb 2020 19:20:48 +0100 Subject: [PATCH 090/107] Fix fluent tests for MouseSelectionHandler. --- src/tableselection/mouseselectionhandler.js | 18 +------ ...tobserver.js => mouseselectionobserver.js} | 49 ++++++++++++++++--- 2 files changed, 42 insertions(+), 25 deletions(-) rename tests/tableselection/{mouseeventobserver.js => mouseselectionobserver.js} (91%) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 4b72fb88..45021929 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -62,7 +62,6 @@ export default class MouseSelectionHandler { this.listenTo( view.document, 'mousedown', ( event, domEventData ) => this._handleMouseDown( domEventData ) ); this.listenTo( view.document, 'mousemove', ( event, domEventData ) => this._handleMouseMove( domEventData ) ); this.listenTo( view.document, 'mouseup', ( event, domEventData ) => this._handleMouseUp( domEventData ) ); - this.listenTo( view.document, 'mouseleave', () => this._handleMouseLeave() ); } /** @@ -120,7 +119,7 @@ export default class MouseSelectionHandler { * @private */ _handleMouseUp( domEventData ) { - if ( !this._tableSelection.isSelecting ) { + if ( !this.isSelecting ) { return; } @@ -130,21 +129,6 @@ export default class MouseSelectionHandler { this._tableSelection.stopSelection( tableCell ); } - /** - * Handles stopping a selection on "mouseleave" event. - * - * Does nothing if selection is not started. - * - * @private - */ - _handleMouseLeave() { - // if ( !this._tableSelection.isSelecting ) { - // return; - // } - - // this._tableSelection.stopSelection(); - } - /** * Finds model table cell for given DOM event. * diff --git a/tests/tableselection/mouseeventobserver.js b/tests/tableselection/mouseselectionobserver.js similarity index 91% rename from tests/tableselection/mouseeventobserver.js rename to tests/tableselection/mouseselectionobserver.js index 4f2fa2a9..4cc254dd 100644 --- a/tests/tableselection/mouseeventobserver.js +++ b/tests/tableselection/mouseselectionobserver.js @@ -14,7 +14,7 @@ import TableSelection from '../../src/tableselection'; import { modelTable, viewTable } from '../_utils/utils'; describe( 'table selection', () => { - let editor, model, modelRoot, view; + let editor, model, modelRoot, view, viewDoc; beforeEach( async () => { editor = await VirtualTestEditor.create( { @@ -24,6 +24,7 @@ describe( 'table selection', () => { model = editor.model; modelRoot = model.document.getRoot(); view = editor.editing.view; + viewDoc = view.document; setModelData( model, modelTable( [ [ '11[]', '12', '13' ], @@ -36,7 +37,7 @@ describe( 'table selection', () => { await editor.destroy(); } ); - describe( 'MouseEventObserver', () => { + describe( 'MouseSelectionObserver', () => { it( 'should not start table selection when mouse move is inside one table cell', () => { setModelData( model, modelTable( [ [ '[]00', '01' ], @@ -231,6 +232,20 @@ describe( 'table selection', () => { ] ); } ); + it( 'should do nothing on "mouseup" event', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + releaseMouseButtonOver( getTableCell( '01' ) ); + + assertSelectedCells( [ + [ 0, 0 ], + [ 0, 0 ] + ] ); + } ); + it( 'should stop selection mode on "mouseleve" event if next "mousemove" has no button pressed', () => { setModelData( model, modelTable( [ [ '[]00', '01' ], @@ -319,11 +334,29 @@ describe( 'table selection', () => { [ '10', '11' ] ] ) ); - const uiElement = view.document.getRoot() + const uiElement = viewDoc.getRoot() + .getChild( 0 ) + .getChild( 0 ); // selection handler; + + fireEvent( view, 'mousedown', addTarget( uiElement ), mouseButtonPressed ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + } ); + + it( 'should do nothing on "mousemove" event over ui element (click on selection handle)', () => { + setModelData( model, modelTable( [ + [ '[]00', '01' ], + [ '10', '11' ] + ] ) ); + + const uiElement = viewDoc.getRoot() .getChild( 0 ) .getChild( 0 ); // selection handler; - fireEvent( view, 'mousedown', addTarget( uiElement ) ); + fireEvent( view, 'mousemove', addTarget( uiElement ), mouseButtonPressed ); assertEqualMarkup( getModelData( model ), modelTable( [ [ '[]00', '01' ], @@ -346,7 +379,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '01' ) ); - const paragraph = view.document.getRoot().getChild( 1 ); + const paragraph = viewDoc.getRoot().getChild( 1 ); fireEvent( view, 'mousemove', addTarget( paragraph ) ); fireEvent( view, 'mousedown', addTarget( paragraph ) ); @@ -409,7 +442,7 @@ describe( 'table selection', () => { } function getTableCell( data ) { - for ( const value of view.createRangeIn( view.document.getRoot() ) ) { + for ( const value of view.createRangeIn( viewDoc.getRoot() ) ) { if ( value.type === 'text' && value.item.data === data ) { return value.item.parent.parent; } @@ -421,7 +454,7 @@ describe( 'table selection', () => { } function pressMouseButtonOver( target ) { - fireEvent( view, 'mousemove', addTarget( target ), mouseButtonPressed ); + fireEvent( view, 'mousedown', addTarget( target ), mouseButtonPressed ); } function movePressedMouseOver( target ) { @@ -468,6 +501,6 @@ describe( 'table selection', () => { decorator( domEvtDataStub ); } - view.document.fire( eventName, domEvtDataStub ); + viewDoc.fire( eventName, domEvtDataStub ); } } ); From 0e01c3faa033f93c28f311b3218884ea7bba6449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 17 Feb 2020 19:27:02 +0100 Subject: [PATCH 091/107] Update TableSelection logic after fixes in MouseSelectionHandler. --- src/tableselection.js | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 81d4d647..5d17294d 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -17,7 +17,24 @@ import MouseSelectionHandler from './tableselection/mouseselectionhandler'; /** * The table selection plugin. * - * It introduces the ability to select table cells using mouse. + * It introduces the ability to select table cells. Table selection is described by two nodes: start and end. + * Both are the oposite corners of an rectangle that spans over them. + * + * Consider a table: + * + * 0 1 2 3 + * +---+---+---+---+ + * 0 | a | b | c | d | + * +-------+ +---+ + * 1 | e | f | | g | + * +---+---+---+---+ + * 2 | h | i | j | + * +---+---+---+---+ + * + * Setting table selection start as table cell "b" and end as table cell "g" will select table cells: "b", "c", "d", "f", and "g". + * The cells that spans over multiple rows or columns can extend over the selection rectangle. For instance setting a selection from + * table cell "a" to table cell "i" will create a selection in which table cell "i" will be extended over a rectangular of the selected + * cell: "a", "b", "e", "f", "h", and "i". * * @extends module:core/plugin~Plugin */ @@ -61,7 +78,7 @@ export default class TableSelection extends Plugin { } /** - * Flag indicating that there are selected table cells. + * Flag indicating that there are selected table cells and the selection has more than one table cell. * * @type {Boolean} */ @@ -113,9 +130,8 @@ export default class TableSelection extends Plugin { * @param {module:engine/model/element~Element} tableCell */ setSelectingTo( tableCell ) { - // Do not update if not in selection mode or no table cell is passed. - if ( !tableCell ) { - return; + if ( !this._startElement ) { + this._startElement = tableCell; } const table = this._startElement.parent.parent; @@ -163,13 +179,13 @@ export default class TableSelection extends Plugin { } /** - * Returns iterator that iterates over all selected table cells. + * Returns iterator for selected table cells. * * tableSelection.startSelectingFrom( startTableCell ); - * tableSelection.stopSelection(); + * tableSelection.stopSelection( endTableCell ); * * const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() ); - * // The above array will consist one table cell. + * // The above array will consist a rectangular table selection. * * @returns {Iterable.} */ From d49786676036e36b27445e6da26d2f4ac6ad735c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Feb 2020 11:54:24 +0100 Subject: [PATCH 092/107] Rename tests for MouseSelectionHandler. --- .../{mouseselectionobserver.js => mouseselectionhandler.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/tableselection/{mouseselectionobserver.js => mouseselectionhandler.js} (99%) diff --git a/tests/tableselection/mouseselectionobserver.js b/tests/tableselection/mouseselectionhandler.js similarity index 99% rename from tests/tableselection/mouseselectionobserver.js rename to tests/tableselection/mouseselectionhandler.js index 4cc254dd..7c81a5a9 100644 --- a/tests/tableselection/mouseselectionobserver.js +++ b/tests/tableselection/mouseselectionhandler.js @@ -37,7 +37,7 @@ describe( 'table selection', () => { await editor.destroy(); } ); - describe( 'MouseSelectionObserver', () => { + describe( 'MouseSelectionHandler', () => { it( 'should not start table selection when mouse move is inside one table cell', () => { setModelData( model, modelTable( [ [ '[]00', '01' ], From 2f1a9dc24887cff659a68e2f1c64ea8bf8d643d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Feb 2020 12:38:54 +0100 Subject: [PATCH 093/107] TableSelection should be cleared on external selection change. --- src/tableselection.js | 21 ++++++++++++++-- src/tableselection/mouseselectionhandler.js | 2 ++ tests/tableselection.js | 28 ++++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 5d17294d..4c7250a3 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -90,9 +90,14 @@ export default class TableSelection extends Plugin { * @inheritDoc */ init() { - this._tableUtils = this.editor.plugins.get( 'TableUtils' ); + const editor = this.editor; + const selection = editor.model.document.selection; + + this._tableUtils = editor.plugins.get( 'TableUtils' ); + + setupTableSelectionHighlighting( editor, this ); - setupTableSelectionHighlighting( this.editor, this ); + selection.on( 'change:range', () => this._clearSelectionOnExternalChange( selection ) ); } /** @@ -234,4 +239,16 @@ export default class TableSelection extends Plugin { writer.setSelection( modelRanges ); } ); } + + /** + * Checks if selection has changed from an external source and it is required to clear internal state. + * + * @param {module:engine/model/documentselection~DocumentSelection} selection + * @private + */ + _clearSelectionOnExternalChange( selection ) { + if ( selection.rangeCount <= 1 && this.hasMultiCellSelection ) { + this.clearSelection(); + } + } } diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 45021929..101f47de 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -153,6 +153,8 @@ export default class MouseSelectionHandler { } } +mix( MouseSelectionHandler, ObservableMixin ); + function isButtonPressed( domEventData ) { return !!domEventData.domEvent.buttons; } diff --git a/tests/tableselection.js b/tests/tableselection.js index 2688db65..d6e455da 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -9,7 +9,9 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; -import { modelTable } from './_utils/utils'; +import { modelTable, viewTable } from './_utils/utils'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; describe( 'table selection', () => { let editor, model, tableSelection, modelRoot; @@ -295,6 +297,30 @@ describe( 'table selection', () => { ] ); } ); } ); + + describe.only( 'behavior', () => { + it( 'should clear selection on external changes', () => { + tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); + tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); + + editor.model.change( writer => { + writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] ), 0 ); + } ); + + assertSelectedCells( [ + [ 0, 0, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 0 ] + ] ); + + expect( editor.editing.view.document.selection.isFake ).to.be.false; + assertEqualMarkup( getViewData( editor.editing.view ), viewTable( [ + [ '{}11', '12', '13' ], + [ '21', '22', '23' ], + [ '31', '32', '33' ] + ], { asWidget: true } ) ); + } ); + } ); } ); // Helper method for asserting selected table cells. From 7619a770833db10e2742e3bf6a40e1453f15b33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 18 Feb 2020 12:46:39 +0100 Subject: [PATCH 094/107] Extract assertSelectedCells() common test util function. --- tests/_utils/utils.js | 50 ++++++++++++- tests/tableselection.js | 72 ++++-------------- tests/tableselection/mouseselectionhandler.js | 75 ++++--------------- 3 files changed, 78 insertions(+), 119 deletions(-) diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 19f45af5..2b7c020b 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -322,11 +322,59 @@ export function assertTableStyle( editor, tableStyle, figureStyle ) { export function assertTableCellStyle( editor, tableCellStyle ) { assertEqualMarkup( editor.getData(), '
' + - `foo` + + `foo` + '
' ); } +/** + * Helper method for asserting selected table cells. + * + * To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: + * + * assertSelectedCells( model, [ + * [ 0, 1 ], + * [ 0, 1 ] + * ] ); + * + * The above call will check if table has second column selected (assuming no spans). + * + * **Note**: This function operates on child indexes - not rows/columns. + */ +export function assertSelectedCells( model, tableMap ) { + const tableIndex = 0; + + for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) { + const row = tableMap[ rowIndex ]; + + for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) { + const expectSelected = row[ cellIndex ]; + + if ( expectSelected ) { + assertNodeIsSelected( model, [ tableIndex, rowIndex, cellIndex ] ); + } else { + assertNodeIsNotSelected( model, [ tableIndex, rowIndex, cellIndex ] ); + } + } + } +} + +function assertNodeIsSelected( model, path ) { + const modelRoot = model.document.getRoot(); + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true; +} + +function assertNodeIsNotSelected( model, path ) { + const modelRoot = model.document.getRoot(); + const node = modelRoot.getNodeByPath( path ); + const selectionRanges = Array.from( model.document.selection.getRanges() ); + + expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; +} + // Formats table cell attributes // // @param {Object} attributes Attributes of a cell. diff --git a/tests/tableselection.js b/tests/tableselection.js index d6e455da..fa2d1ea7 100644 --- a/tests/tableselection.js +++ b/tests/tableselection.js @@ -9,7 +9,7 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import TableEditing from '../src/tableediting'; import TableSelection from '../src/tableselection'; -import { modelTable, viewTable } from './_utils/utils'; +import { assertSelectedCells, modelTable, viewTable } from './_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; @@ -60,7 +60,7 @@ describe( 'table selection', () => { sinon.assert.calledOnce( spy ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -106,7 +106,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -118,7 +118,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 1, 1, 0 ], [ 0, 0, 0 ] @@ -130,7 +130,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 2 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 1 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -142,7 +142,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 1, 0 ], [ 0, 1, 0 ], [ 0, 1, 0 ] @@ -154,7 +154,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 0, 1, 0 ] @@ -162,7 +162,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 1, 0 ], [ 0, 1, 0 ], [ 0, 0, 0 ] @@ -170,7 +170,7 @@ describe( 'table selection', () => { tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 0, 1, 1 ] @@ -185,7 +185,7 @@ describe( 'table selection', () => { tableSelection.stopSelection(); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -208,7 +208,7 @@ describe( 'table selection', () => { tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -236,7 +236,7 @@ describe( 'table selection', () => { tableSelection.clearSelection(); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -298,7 +298,7 @@ describe( 'table selection', () => { } ); } ); - describe.only( 'behavior', () => { + describe( 'behavior', () => { it( 'should clear selection on external changes', () => { tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); @@ -307,7 +307,7 @@ describe( 'table selection', () => { writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] ), 0 ); } ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] @@ -322,48 +322,4 @@ describe( 'table selection', () => { } ); } ); } ); - - // Helper method for asserting selected table cells. - // - // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: - // - // assertSelectedCells( [ - // [ 0, 1 ], - // [ 0, 1 ] - // ] ); - // - // The above call will check if table has second column selected (assuming no spans). - // - // **Note**: This function operates on child indexes - not rows/columns. - function assertSelectedCells( tableMap ) { - const tableIndex = 0; - - for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) { - const row = tableMap[ rowIndex ]; - - for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) { - const expectSelected = row[ cellIndex ]; - - if ( expectSelected ) { - assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] ); - } else { - assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] ); - } - } - } - } - - function assertNodeIsSelected( path ) { - const node = modelRoot.getNodeByPath( path ); - const selectionRanges = Array.from( model.document.selection.getRanges() ); - - expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true; - } - - function assertNodeIsNotSelected( path ) { - const node = modelRoot.getNodeByPath( path ); - const selectionRanges = Array.from( model.document.selection.getRanges() ); - - expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; - } } ); diff --git a/tests/tableselection/mouseselectionhandler.js b/tests/tableselection/mouseselectionhandler.js index 7c81a5a9..b4d77a49 100644 --- a/tests/tableselection/mouseselectionhandler.js +++ b/tests/tableselection/mouseselectionhandler.js @@ -11,10 +11,10 @@ import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils' import TableEditing from '../../src/tableediting'; import TableSelection from '../../src/tableselection'; -import { modelTable, viewTable } from '../_utils/utils'; +import { assertSelectedCells, modelTable, viewTable } from '../_utils/utils'; describe( 'table selection', () => { - let editor, model, modelRoot, view, viewDoc; + let editor, model, view, viewDoc; beforeEach( async () => { editor = await VirtualTestEditor.create( { @@ -22,7 +22,6 @@ describe( 'table selection', () => { } ); model = editor.model; - modelRoot = model.document.getRoot(); view = editor.editing.view; viewDoc = view.document; @@ -74,7 +73,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '01' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -100,7 +99,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '11' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 1, 1 ] ] ); @@ -126,7 +125,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '00' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 1, 1 ] ] ); @@ -147,7 +146,7 @@ describe( 'table selection', () => { pressMouseButtonOver( getTableCell( '00' ) ); movePressedMouseOver( getTableCell( '22' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 1 ], [ 1, 1, 1 ], [ 1, 1, 1 ] @@ -173,7 +172,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '11' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1, 0 ], [ 1, 1, 0 ], [ 0, 0, 0 ] @@ -214,7 +213,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '01' ) ); releaseMouseButtonOver( getTableCell( '01' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -226,7 +225,7 @@ describe( 'table selection', () => { moveReleasedMouseOver( getTableCell( '11' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -240,7 +239,7 @@ describe( 'table selection', () => { releaseMouseButtonOver( getTableCell( '01' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 0 ], [ 0, 0 ] ] ); @@ -262,7 +261,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '01' ) ); makeMouseLeave(); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -274,7 +273,7 @@ describe( 'table selection', () => { moveReleasedMouseOver( getTableCell( '11' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -296,7 +295,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '01' ) ); makeMouseLeave(); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 0, 0 ] ] ); @@ -308,7 +307,7 @@ describe( 'table selection', () => { movePressedMouseOver( getTableCell( '11' ) ); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 1, 1 ], [ 1, 1 ] ] ); @@ -322,7 +321,7 @@ describe( 'table selection', () => { makeMouseLeave(); - assertSelectedCells( [ + assertSelectedCells( model, [ [ 0, 0 ], [ 0, 0 ] ] ); @@ -397,50 +396,6 @@ describe( 'table selection', () => { } ); } ); - // Helper method for asserting selected table cells. - // - // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: - // - // assertSelectedCells( [ - // [ 0, 1 ], - // [ 0, 1 ] - // ] ); - // - // The above call will check if table has second column selected (assuming no spans). - // - // **Note**: This function operates on child indexes - not rows/columns. - function assertSelectedCells( tableMap ) { - const tableIndex = 0; - - for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) { - const row = tableMap[ rowIndex ]; - - for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) { - const expectSelected = row[ cellIndex ]; - - if ( expectSelected ) { - assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] ); - } else { - assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] ); - } - } - } - } - - function assertNodeIsSelected( path ) { - const node = modelRoot.getNodeByPath( path ); - const selectionRanges = Array.from( model.document.selection.getRanges() ); - - expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true; - } - - function assertNodeIsNotSelected( path ) { - const node = modelRoot.getNodeByPath( path ); - const selectionRanges = Array.from( model.document.selection.getRanges() ); - - expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true; - } - function getTableCell( data ) { for ( const value of view.createRangeIn( viewDoc.getRoot() ) ) { if ( value.type === 'text' && value.item.data === data ) { From 3a2e025b7d3132a603d3bd5d814f76f8a5e1bf54 Mon Sep 17 00:00:00 2001 From: Maciej Date: Thu, 20 Feb 2020 16:29:32 +0100 Subject: [PATCH 095/107] Apply suggestions from code review. Co-Authored-By: Aleksander Nowodzinski --- src/tableselection.js | 32 ++++++++++----------- src/tableselection/converters.js | 6 ++-- src/tableselection/mouseselectionhandler.js | 20 ++++++------- tests/_utils/utils.js | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 4c7250a3..098730fb 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -17,7 +17,7 @@ import MouseSelectionHandler from './tableselection/mouseselectionhandler'; /** * The table selection plugin. * - * It introduces the ability to select table cells. Table selection is described by two nodes: start and end. + * It introduces the ability to select table cells. The table selection is described by two nodes: start and end. * Both are the oposite corners of an rectangle that spans over them. * * Consider a table: @@ -31,10 +31,10 @@ import MouseSelectionHandler from './tableselection/mouseselectionhandler'; * 2 | h | i | j | * +---+---+---+---+ * - * Setting table selection start as table cell "b" and end as table cell "g" will select table cells: "b", "c", "d", "f", and "g". - * The cells that spans over multiple rows or columns can extend over the selection rectangle. For instance setting a selection from - * table cell "a" to table cell "i" will create a selection in which table cell "i" will be extended over a rectangular of the selected - * cell: "a", "b", "e", "f", "h", and "i". + * Setting the table selection start in table cell "b" and the end in table cell "g" will select table cells: "b", "c", "d", "f", and "g". + * The cells that span over multiple rows or columns can extend over the selection rectangle. For instance, setting a selection from + * the table cell "a" to the table cell "i" will create a selection in which the table cell "i" will be (partially) outside the rectangle of selected + * cells: "a", "b", "e", "f", "h", and "i". * * @extends module:core/plugin~Plugin */ @@ -69,16 +69,16 @@ export default class TableSelection extends Plugin { this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing ); /** - * A table utilities. + * A reference to the table utilities used across the class. * * @private * @readonly - * @member {module:table/tableutils~TableUtils} + * @member {module:table/tableutils~TableUtils} #_tableUtils */ } /** - * Flag indicating that there are selected table cells and the selection has more than one table cell. + * A flag indicating that there are selected table cells and the selection includes more than one table cell. * * @type {Boolean} */ @@ -109,7 +109,7 @@ export default class TableSelection extends Plugin { } /** - * Starts a selection process. + * Starts the selection process. * * This method enables the table selection process. * @@ -151,13 +151,13 @@ export default class TableSelection extends Plugin { } /** - * Stops selection process (but do not clear the current selection). The selecting process is ended but the selection in model remains. + * Stops the selection process (but do not clear the current selection). The selection process is finished but the selection in the model remains. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); * editor.plugins.get( 'TableSelection' ).stopSelection(); * - * To clear selection use {@link #clearSelection}. + * To clear the selection use {@link #clearSelection}. * * @param {module:engine/model/element~Element} [tableCell] */ @@ -170,7 +170,7 @@ export default class TableSelection extends Plugin { } /** - * Stops current selection process and clears table selection. + * Stops the current selection process and clears the table selection in the model. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); @@ -184,13 +184,13 @@ export default class TableSelection extends Plugin { } /** - * Returns iterator for selected table cells. + * Returns an iterator for selected table cells. * * tableSelection.startSelectingFrom( startTableCell ); * tableSelection.stopSelection( endTableCell ); * * const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() ); - * // The above array will consist a rectangular table selection. + * // The above array will represent a rectangular table selection. * * @returns {Iterable.} */ @@ -216,7 +216,7 @@ export default class TableSelection extends Plugin { } /** - * Set proper model selection for currently selected table cells. + * Synchronizes the model selection with currently selected table cells. * * @private */ @@ -241,7 +241,7 @@ export default class TableSelection extends Plugin { } /** - * Checks if selection has changed from an external source and it is required to clear internal state. + * Checks if the selection has changed via an external change and if it is required to clear the internal state of the plugin. * * @param {module:engine/model/documentselection~DocumentSelection} selection * @private diff --git a/src/tableselection/converters.js b/src/tableselection/converters.js index 28dd348a..1d6750c1 100644 --- a/src/tableselection/converters.js +++ b/src/tableselection/converters.js @@ -8,7 +8,7 @@ */ /** - * Adds a visual highlight style to a selected table cells. + * Adds a visual highlight style to selected table cells. * * @param {module:core/editor/editor~Editor} editor * @param {module:table/tableselection~TableSelection} tableSelection @@ -27,7 +27,7 @@ export function setupTableSelectionHighlighting( editor, tableSelection ) { for ( const tableCell of tableSelection.getSelectedTableCells() ) { const viewElement = conversionApi.mapper.toViewElement( tableCell ); - viewWriter.addClass( 'selected', viewElement ); + viewWriter.addClass( 'ck-editor__editable_selected', viewElement ); highlighted.add( viewElement ); } @@ -43,7 +43,7 @@ function clearHighlightedTableCells( highlighted, view ) { view.change( writer => { for ( const previouslyHighlighted of previous ) { - writer.removeClass( 'selected', previouslyHighlighted ); + writer.removeClass( 'ck-editor__editable_selected', previouslyHighlighted ); } } ); } diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 101f47de..ba7bf59b 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -14,21 +14,21 @@ import { findAncestor } from '../commands/utils'; import MouseEventsObserver from './mouseeventsobserver'; /** - * A mouse selection handler for table selection. + * A mouse selection handler class for the table selection. * * It registers the {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} to observe view document mouse events * and invoke proper {@link module:table/tableselection~TableSelection} actions. */ export default class MouseSelectionHandler { /** - * Creates instance of `MouseSelectionHandler`. + * Creates an instance of the `MouseSelectionHandler`. * * @param {module:table/tableselection~TableSelection} tableSelection * @param {module:engine/controller/editingcontroller~EditingController} editing */ constructor( tableSelection, editing ) { /** - * Table selection. + * The table selection plugin instance. * * @private * @readonly @@ -38,7 +38,7 @@ export default class MouseSelectionHandler { /** * A flag indicating that the mouse selection is "active". A selection is "active" if it was started and not yet finished. - * A selection can be "active" for instance if user moves a mouse over a table while holding a mouse button down. + * A selection can be "active", for instance, if a user moves a mouse over a table while holding a mouse button down. * * @readonly * @member {Boolean} @@ -65,9 +65,9 @@ export default class MouseSelectionHandler { } /** - * Handles starting a selection when "mousedown" event has table cell target. + * Handles starting a selection when "mousedown" event has table cell as a DOM target. * - * If no table cell in event target it will clear previous selection. + * If there is no table cell in the event target, it will clear the previous selection. * * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData * @private @@ -87,9 +87,9 @@ export default class MouseSelectionHandler { } /** - * Handles updating table selection when "mousemove" event has a table cell target. + * Handles updating the table selection when the "mousemove" event has a table cell as a DOM target. * - * Does nothing if no table cell in event target or selection is not started. + * Does nothing if there is no table cell in event target or the selection has not been started yet. * * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData * @private @@ -111,9 +111,9 @@ export default class MouseSelectionHandler { } /** - * Handles ending (not clearing) table selection on "mouseup" event. + * Handles ending (not clearing) the table selection on the "mouseup" event. * - * Does nothing if selection is not started. + * Does nothing if the selection has not been started yet. * * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData * @private diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 2b7c020b..7a2effd0 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -328,7 +328,7 @@ export function assertTableCellStyle( editor, tableCellStyle ) { } /** - * Helper method for asserting selected table cells. + * A helper method for asserting selected table cells. * * To check if a table has expected cells selected pass two dimensional array of truthy and falsy values: * From cf6583d231a0b9d3a245622bc317496566778fd3 Mon Sep 17 00:00:00 2001 From: Maciej Date: Thu, 20 Feb 2020 16:31:05 +0100 Subject: [PATCH 096/107] Update src/commands/removerowcommand.js PR suggestions. Co-Authored-By: Aleksander Nowodzinski --- src/commands/removerowcommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index efda49ef..5c6c1dac 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -105,7 +105,7 @@ export default class RemoveRowCommand extends Command { } } -// Returns a cell to focus on the same column of the focused table cell before removing a row. +// Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell. function getCellToFocus( table, removedRow, columnToFocus ) { const row = table.getChild( removedRow ); From bb8cd22c10a29a025d5d3cefc6af566e49305ff7 Mon Sep 17 00:00:00 2001 From: Maciej Date: Thu, 20 Feb 2020 16:33:16 +0100 Subject: [PATCH 097/107] Apply suggestions from code review Co-Authored-By: Aleksander Nowodzinski --- src/tableselection/mouseeventsobserver.js | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/tableselection/mouseeventsobserver.js b/src/tableselection/mouseeventsobserver.js index 8935978f..fed474bc 100644 --- a/src/tableselection/mouseeventsobserver.js +++ b/src/tableselection/mouseeventsobserver.js @@ -10,16 +10,16 @@ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; /** - * Mouse selection events observer. + * The mouse selection events observer. * - * It register listener for DOM events: + * It registers listeners for DOM events: * * - `'mousemove'` * - `'mouseup'` * - `'mouseleave'` * - * Note that this observer is not available by default. To make it available it needs to be added to - * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method. + * Note that this observer is disabled by default. To enable this observer it needs to be added to + * {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method. * * It is registered by {@link module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}. * @@ -44,13 +44,13 @@ export default class MouseEventsObserver extends DomEventObserver { } /** - * Fired when mouse button is released over one of the editables. + * Fired when the mouse button is released over one of the editables. * * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added - * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added + * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method. * * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mouseup @@ -58,13 +58,13 @@ export default class MouseEventsObserver extends DomEventObserver { */ /** - * Fired when mouse is moved over one of the editables. + * Fired when the mouse is moved over one of the editables. * * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added - * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added + * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method. * * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mousemove @@ -72,13 +72,13 @@ export default class MouseEventsObserver extends DomEventObserver { */ /** - * Fired when mouse is moved away from one of the editables. + * Fired when the mouse is moved out of one of the editables. * * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}. * * Note that this event is not available by default. To make it available - * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added - * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added + * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method. * * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver * @event module:engine/view/document~Document#event:mouseleave From e4cfc97552cb84fe9ff60cea7de735d852eed9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 16:46:23 +0100 Subject: [PATCH 098/107] Improve TableSelection docs. --- src/tableselection.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 098730fb..2dee5720 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -33,8 +33,8 @@ import MouseSelectionHandler from './tableselection/mouseselectionhandler'; * * Setting the table selection start in table cell "b" and the end in table cell "g" will select table cells: "b", "c", "d", "f", and "g". * The cells that span over multiple rows or columns can extend over the selection rectangle. For instance, setting a selection from - * the table cell "a" to the table cell "i" will create a selection in which the table cell "i" will be (partially) outside the rectangle of selected - * cells: "a", "b", "e", "f", "h", and "i". + * the table cell "a" to the table cell "i" will create a selection in which the table cell "i" will be (partially) outside + * the rectangle of selected cells: "a", "b", "e", "f", "h", and "i". * * @extends module:core/plugin~Plugin */ @@ -109,12 +109,13 @@ export default class TableSelection extends Plugin { } /** - * Starts the selection process. - * - * This method enables the table selection process. + * Marks the table cell as a start of a table selection. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell ); * + * This method will clear the previous selection. The model selection will not be updated until + * the {@link #setSelectingTo} method is used. + * * @param {module:engine/model/element~Element} tableCell */ startSelectingFrom( tableCell ) { @@ -125,13 +126,15 @@ export default class TableSelection extends Plugin { } /** - * Updates current table selection end element. Table selection is defined by #start and #end element. - * This method updates the #end element. Must be preceded by {@link #startSelectingFrom}. + * Updates current table selection end element. Table selection is defined by a start and an end element. + * This method updates the end element. Must be preceded by {@link #startSelectingFrom}. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); * * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); * + * This method will update model selection if start and end cells are different and belongs to the same table. + * * @param {module:engine/model/element~Element} tableCell */ setSelectingTo( tableCell ) { @@ -151,7 +154,8 @@ export default class TableSelection extends Plugin { } /** - * Stops the selection process (but do not clear the current selection). The selection process is finished but the selection in the model remains. + * Stops the selection process (but do not clear the current selection). + * The selection process is finished but the selection in the model remains. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell ); From 1eae916af79f1e5965f106cfb9e315f148213c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 16:49:28 +0100 Subject: [PATCH 099/107] Use standard library for basic mathematical operations. --- src/tableselection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tableselection.js b/src/tableselection.js index 2dee5720..4f1d67ac 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -206,11 +206,11 @@ export default class TableSelection extends Plugin { const startLocation = this._tableUtils.getCellLocation( this._startElement ); const endLocation = this._tableUtils.getCellLocation( this._endElement ); - const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row; - const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row; + const startRow = Math.min( startLocation.row, endLocation.row ); + const endRow = Math.max( startLocation.row, endLocation.row ); - const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column; - const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column; + const startColumn = Math.min( startLocation.column, endLocation.column ); + const endColumn = Math.max( startLocation.column, endLocation.column ); for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) { if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) { From f7f39020de8049f341f49c18b425f9dcf346ad70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 16:51:48 +0100 Subject: [PATCH 100/107] Remove duplicated Observable mixin. --- src/tableselection/mouseselectionhandler.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index ba7bf59b..6c0f525c 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -158,5 +158,3 @@ mix( MouseSelectionHandler, ObservableMixin ); function isButtonPressed( domEventData ) { return !!domEventData.domEvent.buttons; } - -mix( MouseSelectionHandler, ObservableMixin ); From 9e87cc8998f3bb5bc4974b5a5a948687152550a1 Mon Sep 17 00:00:00 2001 From: Maciej Date: Thu, 20 Feb 2020 16:53:38 +0100 Subject: [PATCH 101/107] Update src/tableselection/mouseselectionhandler.js Co-Authored-By: Aleksander Nowodzinski --- src/tableselection/mouseselectionhandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tableselection/mouseselectionhandler.js b/src/tableselection/mouseselectionhandler.js index 6c0f525c..fb50b03e 100644 --- a/src/tableselection/mouseselectionhandler.js +++ b/src/tableselection/mouseselectionhandler.js @@ -130,7 +130,7 @@ export default class MouseSelectionHandler { } /** - * Finds model table cell for given DOM event. + * Finds a model table cell for a given DOM event. * * @private * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData From 34df24df49c4a0761524b963efdf73619fa8cfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:07:37 +0100 Subject: [PATCH 102/107] Move selection visual styles to tableselection.css. --- src/tableselection.js | 2 ++ theme/tableediting.css | 7 ------- theme/tableselection.css | 10 ++++++++++ 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 theme/tableselection.css diff --git a/src/tableselection.js b/src/tableselection.js index 4f1d67ac..f8b54987 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -14,6 +14,8 @@ import TableUtils from './tableutils'; import { setupTableSelectionHighlighting } from './tableselection/converters'; import MouseSelectionHandler from './tableselection/mouseselectionhandler'; +import '../theme/tableselection.css'; + /** * The table selection plugin. * diff --git a/theme/tableediting.css b/theme/tableediting.css index e7b2d120..12134c3b 100644 --- a/theme/tableediting.css +++ b/theme/tableediting.css @@ -8,10 +8,3 @@ * it acts as a message to the builder telling that it should look for the corresponding styles * **in the theme** when compiling the editor. */ - -.ck-content .table table { - & td.selected, - & th.selected { - box-shadow: inset 0 0 0 1px var(--ck-color-focus-border); - } -} diff --git a/theme/tableselection.css b/theme/tableselection.css new file mode 100644 index 00000000..12134c3b --- /dev/null +++ b/theme/tableselection.css @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* + * Note: This file should contain the wireframe styles only. But since there are no such styles, + * it acts as a message to the builder telling that it should look for the corresponding styles + * **in the theme** when compiling the editor. + */ From 27ce4421e46cb5d8ebc35f06377d104cc40edc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:07:37 +0100 Subject: [PATCH 103/107] Bring back table selection styles. --- theme/tableselection.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/theme/tableselection.css b/theme/tableselection.css index 12134c3b..25c2bbf1 100644 --- a/theme/tableselection.css +++ b/theme/tableselection.css @@ -8,3 +8,10 @@ * it acts as a message to the builder telling that it should look for the corresponding styles * **in the theme** when compiling the editor. */ + +.ck.ck-editor__editable .table table { + & td.selected, + & th.selected { + box-shadow: inset 0 0 0 1px var(--ck-color-focus-border); + } +} From 4c3c317f48e96c11e3d449cd8bd00b7631b7997f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:10:12 +0100 Subject: [PATCH 104/107] And use proper CSS class for a selected table cell. --- theme/tableselection.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/theme/tableselection.css b/theme/tableselection.css index 25c2bbf1..b3d84551 100644 --- a/theme/tableselection.css +++ b/theme/tableselection.css @@ -10,8 +10,8 @@ */ .ck.ck-editor__editable .table table { - & td.selected, - & th.selected { + & td.ck-editor__editable_selected, + & th.ck-editor__editable_selected { box-shadow: inset 0 0 0 1px var(--ck-color-focus-border); } } From bd67fc95976af87a83872581638165a880a62e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:18:13 +0100 Subject: [PATCH 105/107] Fix tests after changing CSS class for selected table cell. --- tests/_utils/utils.js | 10 ++- tests/tableselection/mouseselectionhandler.js | 66 +++++++++++++------ 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 7a2effd0..48cf187f 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -421,7 +421,7 @@ function makeRows( tableData, options ) { const attributes = isObject ? tableCellData : {}; if ( asWidget ) { - attributes.class = WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' ); + attributes.class = getClassToSet( attributes ); attributes.contenteditable = 'true'; } @@ -443,3 +443,11 @@ function makeRows( tableData, options ) { return `${ previousRowsString }<${ rowElement }>${ tableRowString }`; }, '' ); } + +// Properly handles passed CSS class - editor do sort them. +function getClassToSet( attributes ) { + return ( WIDGET_TABLE_CELL_CLASS + ( attributes.class ? ` ${ attributes.class }` : '' ) ) + .split( ' ' ) + .sort() + .join( ' ' ); +} diff --git a/tests/tableselection/mouseselectionhandler.js b/tests/tableselection/mouseselectionhandler.js index b4d77a49..ec8fab5d 100644 --- a/tests/tableselection/mouseselectionhandler.js +++ b/tests/tableselection/mouseselectionhandler.js @@ -79,7 +79,10 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], [ '10', '11' ] ], { asWidget: true } ) ); } ); @@ -105,8 +108,14 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], + [ + { contents: '10', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '11', class: 'ck-editor__editable_selected', isSelected: true } + ] ], { asWidget: true } ) ); } ); @@ -131,8 +140,14 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], - [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ] + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], + [ + { contents: '10', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '11', class: 'ck-editor__editable_selected', isSelected: true } + ] ], { asWidget: true } ) ); } ); @@ -154,19 +169,19 @@ describe( 'table selection', () => { assertEqualMarkup( getViewData( view ), viewTable( [ [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, - { contents: '02', class: 'selected', isSelected: true } + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '02', class: 'ck-editor__editable_selected', isSelected: true } ], [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, - { contents: '12', class: 'selected', isSelected: true } + { contents: '10', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '11', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '12', class: 'ck-editor__editable_selected', isSelected: true } ], [ - { contents: '20', class: 'selected', isSelected: true }, - { contents: '21', class: 'selected', isSelected: true }, - { contents: '22', class: 'selected', isSelected: true } + { contents: '20', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '21', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '22', class: 'ck-editor__editable_selected', isSelected: true } ] ], { asWidget: true } ) ); @@ -180,13 +195,13 @@ describe( 'table selection', () => { assertEqualMarkup( getViewData( view ), viewTable( [ [ - { contents: '00', class: 'selected', isSelected: true }, - { contents: '01', class: 'selected', isSelected: true }, + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true }, '02' ], [ - { contents: '10', class: 'selected', isSelected: true }, - { contents: '11', class: 'selected', isSelected: true }, + { contents: '10', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '11', class: 'ck-editor__editable_selected', isSelected: true }, '12' ], [ @@ -219,7 +234,10 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -267,7 +285,10 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], [ '10', '11' ] ], { asWidget: true } ) ); @@ -301,7 +322,10 @@ describe( 'table selection', () => { ] ); assertEqualMarkup( getViewData( view ), viewTable( [ - [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ], + [ + { contents: '00', class: 'ck-editor__editable_selected', isSelected: true }, + { contents: '01', class: 'ck-editor__editable_selected', isSelected: true } + ], [ '10', '11' ] ], { asWidget: true } ) ); From c01394bbffab801f62a673d98d60bd240a1a11b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:22:36 +0100 Subject: [PATCH 106/107] Add missing the. --- src/tableselection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tableselection.js b/src/tableselection.js index f8b54987..11b5d414 100644 --- a/src/tableselection.js +++ b/src/tableselection.js @@ -128,7 +128,7 @@ export default class TableSelection extends Plugin { } /** - * Updates current table selection end element. Table selection is defined by a start and an end element. + * Updates the current table selection end element. Table selection is defined by a start and an end element. * This method updates the end element. Must be preceded by {@link #startSelectingFrom}. * * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell ); From 91333494e5916f107aa747c709f7fdadaf753209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 20 Feb 2020 17:27:23 +0100 Subject: [PATCH 107/107] Add complex table to the table selection manual test. --- tests/manual/tableselection.html | 96 +++++++++++++++++++++++++++++++- tests/manual/tableselection.md | 3 +- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/tests/manual/tableselection.html b/tests/manual/tableselection.html index 8a2d3db5..796b3413 100644 --- a/tests/manual/tableselection.html +++ b/tests/manual/tableselection.html @@ -10,7 +10,7 @@
-

A table to test selection:

+

A simple table to test selection:

@@ -52,6 +52,100 @@
+ +

A complex table

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
abcdefghi
0001020304070708
101113171718
20212327
3031333737
40474748
50515253575758
60616768
7071727374757778
80828384858788
+
+

Model contents:

diff --git a/tests/manual/tableselection.md b/tests/manual/tableselection.md index c42f5bf3..e52654fa 100644 --- a/tests/manual/tableselection.md +++ b/tests/manual/tableselection.md @@ -3,5 +3,4 @@ Selecting table cells: 1. It should be possible to select multiple table cells. - -Observe selection inn the below model representation - for a block selection the table cells should be selected. +2. Observe selection inn the below model representation - for a block selection the table cells should be selected.