diff --git a/src/table.js b/src/table.js index 64574d0e..e8428e45 100644 --- a/src/table.js +++ b/src/table.js @@ -13,6 +13,7 @@ import TableEditing from './tableediting'; import TableUI from './tableui'; import TableSelection from './tableselection'; import TableClipboard from './tableclipboard'; +import TableNavigation from './tablenavigation'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import '../theme/table.css'; @@ -26,6 +27,7 @@ import '../theme/table.css'; * * * {@link module:table/tableediting~TableEditing editing feature}, * * {@link module:table/tableselection~TableSelection selection feature}, + * * {@link module:table/tablenavigation~TableNavigation keyboard navigation feature}, * * {@link module:table/tableclipboard~TableClipboard clipboard feature}, * * {@link module:table/tableui~TableUI UI feature}. * @@ -36,7 +38,7 @@ export default class Table extends Plugin { * @inheritDoc */ static get requires() { - return [ TableEditing, TableUI, TableSelection, TableClipboard, Widget ]; + return [ TableEditing, TableUI, TableSelection, TableClipboard, TableNavigation, Widget ]; } /** diff --git a/src/tableediting.js b/src/tableediting.js index 4e2375ea..9aaf4286 100644 --- a/src/tableediting.js +++ b/src/tableediting.js @@ -31,7 +31,6 @@ import SetHeaderColumnCommand from './commands/setheadercolumncommand'; import MergeCellsCommand from './commands/mergecellscommand'; import SelectRowCommand from './commands/selectrowcommand'; import SelectColumnCommand from './commands/selectcolumncommand'; -import { getTableCellsContainingSelection } from './utils'; import TableUtils from '../src/tableutils'; import injectTableLayoutPostFixer from './converters/table-layout-post-fixer'; @@ -150,11 +149,6 @@ export default class TableEditing extends Plugin { injectTableLayoutPostFixer( model ); injectTableCellRefreshPostFixer( model ); injectTableCellParagraphPostFixer( model ); - - // Handle Tab key navigation. - 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' } ); } /** @@ -163,102 +157,4 @@ export default class TableEditing extends Plugin { static get requires() { return [ TableUtils ]; } - - /** - * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed - * when the table widget is selected. - * - * @private - * @param {module:utils/eventinfo~EventInfo} eventInfo - * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData - */ - _handleTabOnSelectedTable( domEventData, cancel ) { - const editor = this.editor; - const selection = editor.model.document.selection; - - if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) { - const selectedElement = selection.getSelectedElement(); - - if ( !selectedElement || !selectedElement.is( 'table' ) ) { - return; - } - - cancel(); - - editor.model.change( writer => { - writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) ); - } ); - } - } - - /** - * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed - * inside table cell. - * - * @private - * @param {Boolean} isForward Whether this handler will move the selection to the next or the previous cell. - */ - _getTabHandler( isForward ) { - const editor = this.editor; - - return ( domEventData, cancel ) => { - const selection = editor.model.document.selection; - const tableCell = getTableCellsContainingSelection( selection )[ 0 ]; - - if ( !tableCell ) { - return; - } - - cancel(); - - const tableRow = tableCell.parent; - const table = tableRow.parent; - - const currentRowIndex = table.getChildIndex( tableRow ); - const currentCellIndex = tableRow.getChildIndex( tableCell ); - - const isFirstCellInRow = currentCellIndex === 0; - - if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) { - // It's the first cell of the table - don't do anything (stay in the current position). - return; - } - - const isLastCellInRow = currentCellIndex === tableRow.childCount - 1; - const isLastRow = currentRowIndex === table.childCount - 1; - - if ( isForward && isLastRow && isLastCellInRow ) { - editor.execute( 'insertTableRowBelow' ); - - // Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled - // or it got overwritten) do not change the selection. - if ( currentRowIndex === table.childCount - 1 ) { - return; - } - } - - let cellToFocus; - - // Move to first cell in next row. - if ( isForward && isLastCellInRow ) { - const nextRow = table.getChild( currentRowIndex + 1 ); - - cellToFocus = nextRow.getChild( 0 ); - } - // Move to last cell in a previous row. - else if ( !isForward && isFirstCellInRow ) { - const previousRow = table.getChild( currentRowIndex - 1 ); - - cellToFocus = previousRow.getChild( previousRow.childCount - 1 ); - } - // Move to next/previous cell. - else { - cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) ); - } - - editor.model.change( writer => { - writer.setSelection( writer.createRangeIn( cellToFocus ) ); - } ); - }; - } } diff --git a/src/tablenavigation.js b/src/tablenavigation.js new file mode 100644 index 00000000..26ad8064 --- /dev/null +++ b/src/tablenavigation.js @@ -0,0 +1,516 @@ +/** + * @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/tablenavigation + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import { getSelectedTableCells, getTableCellsContainingSelection } from './utils'; +import { findAncestor } from './commands/utils'; +import TableWalker from './tablewalker'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; + +/** + * This plugin enables keyboard navigation for tables. + * It is loaded automatically by the {@link module:table/table~Table} plugin. + * + * @extends module:core/plugin~Plugin + */ +export default class TableNavigation extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'TableNavigation'; + } + + /** + * @inheritDoc + */ + init() { + const view = this.editor.editing.view; + const viewDocument = view.document; + + // Handle Tab key navigation. + 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' } ); + + // Note: This listener has the "high+1" priority because we would like to avoid collisions with other features + // (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence + // over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected + // but getNearestSelectionRange didn't returned any range). + this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) + 1 } ); + } + + /** + * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed + * when the table widget is selected. + * + * @private + * @param {module:engine/view/observer/keyobserver~KeyEventData} data Key event data. + * @param {Function} cancel The stop/stopPropagation/preventDefault function. + */ + _handleTabOnSelectedTable( data, cancel ) { + const editor = this.editor; + const selection = editor.model.document.selection; + + if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) { + const selectedElement = selection.getSelectedElement(); + + if ( !selectedElement || !selectedElement.is( 'table' ) ) { + return; + } + + cancel(); + + editor.model.change( writer => { + writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) ); + } ); + } + } + + /** + * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the Tab key executed + * inside table cell. + * + * @private + * @param {Boolean} isForward Whether this handler will move the selection to the next or the previous cell. + */ + _getTabHandler( isForward ) { + const editor = this.editor; + + return ( domEventData, cancel ) => { + const selection = editor.model.document.selection; + const tableCell = getTableCellsContainingSelection( selection )[ 0 ]; + + if ( !tableCell ) { + return; + } + + cancel(); + + const tableRow = tableCell.parent; + const table = tableRow.parent; + + const currentRowIndex = table.getChildIndex( tableRow ); + const currentCellIndex = tableRow.getChildIndex( tableCell ); + + const isFirstCellInRow = currentCellIndex === 0; + + if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) { + // It's the first cell of the table - don't do anything (stay in the current position). + return; + } + + const isLastCellInRow = currentCellIndex === tableRow.childCount - 1; + const isLastRow = currentRowIndex === table.childCount - 1; + + if ( isForward && isLastRow && isLastCellInRow ) { + editor.execute( 'insertTableRowBelow' ); + + // Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled + // or it got overwritten) do not change the selection. + if ( currentRowIndex === table.childCount - 1 ) { + return; + } + } + + let cellToFocus; + + // Move to the first cell in the next row. + if ( isForward && isLastCellInRow ) { + const nextRow = table.getChild( currentRowIndex + 1 ); + + cellToFocus = nextRow.getChild( 0 ); + } + // Move to the last cell in the previous row. + else if ( !isForward && isFirstCellInRow ) { + const previousRow = table.getChild( currentRowIndex - 1 ); + + cellToFocus = previousRow.getChild( previousRow.childCount - 1 ); + } + // Move to the next/previous cell. + else { + cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) ); + } + + editor.model.change( writer => { + writer.setSelection( writer.createRangeIn( cellToFocus ) ); + } ); + }; + } + + /** + * Handles {@link module:engine/view/document~Document#event:keydown keydown} events. + * + * @private + * @param {module:utils/eventinfo~EventInfo} eventInfo + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + */ + _onKeydown( eventInfo, domEventData ) { + const keyCode = domEventData.keyCode; + + if ( !isArrowKeyCode( keyCode ) ) { + return; + } + + const wasHandled = this._handleArrowKeys( getDirectionFromKeyCode( keyCode, this.editor.locale.contentLanguageDirection ) ); + + if ( wasHandled ) { + domEventData.preventDefault(); + domEventData.stopPropagation(); + eventInfo.stop(); + } + } + + /** + * Handles arrow keys to move the selection around a table. + * + * @private + * @param {'left'|'up'|'right'|'down'} direction The direction of the arrow key. + * @returns {Boolean} Returns `true` if key was handled. + */ + _handleArrowKeys( direction ) { + const model = this.editor.model; + const selection = model.document.selection; + const isForward = [ 'right', 'down' ].includes( direction ); + + // In case one or more table cells are selected (from outside), + // move the selection to a cell adjacent to the selected table fragment. + const selectedCells = getSelectedTableCells( selection ); + + if ( selectedCells.length ) { + const tableCell = isForward ? selectedCells[ selectedCells.length - 1 ] : selectedCells[ 0 ]; + + this._navigateFromCellInDirection( tableCell, direction ); + + return true; + } + + // Abort if we're not in a table cell. + const tableCell = findAncestor( 'tableCell', selection.focus ); + + if ( !tableCell ) { + return false; + } + + const cellRange = model.createRangeIn( tableCell ); + + // Let's check if the selection is at the beginning/end of the cell. + if ( this._isSelectionAtCellEdge( selection, isForward ) ) { + this._navigateFromCellInDirection( tableCell, direction ); + + return true; + } + + // If this is an object selected and it's not at the start or the end of cell content + // then let's allow widget handler to take care of it. + const objectElement = selection.getSelectedElement(); + + if ( objectElement && model.schema.isObject( objectElement ) ) { + return false; + } + + // If next to the selection there is an object then this is not the cell boundary (widget handler should handle this). + if ( this._isObjectElementNextToSelection( selection, isForward ) ) { + return false; + } + + // If there isn't any $text position between cell edge and selection then we shall move the selection to next cell. + const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward ); + + if ( !textRange ) { + this._navigateFromCellInDirection( tableCell, direction ); + + return true; + } + + // If the navigation is horizontal then we have no more custom cases. + if ( [ 'left', 'right' ].includes( direction ) ) { + return false; + } + + // If the range is a single line then move the selection to the beginning/end of a cell content. + // + // We can't move the selection directly to the another cell because of dual position at the end/beginning + // of wrapped line (it's at the same time at the end of one line and at the start of the next line). + if ( this._isSingleLineRange( textRange, isForward ) ) { + model.change( writer => { + writer.setSelection( isForward ? cellRange.end : cellRange.start ); + } ); + + return true; + } + } + + /** + * Returns true if the selection is at the boundary of a table cell according to the navigation direction. + * + * @private + * @param {module:engine/model/selection~Selection} selection The current selection. + * @param {Boolean} isForward The expected navigation direction. + * @returns {Boolean} + */ + _isSelectionAtCellEdge( selection, isForward ) { + const model = this.editor.model; + const schema = this.editor.model.schema; + + const focus = isForward ? selection.getLastPosition() : selection.getFirstPosition(); + + // If the current limit element is not table cell we are for sure not at the cell edge. + // Also `modifySelection` will not let us out of it. + if ( !schema.getLimitElement( focus ).is( 'tableCell' ) ) { + return false; + } + + const probe = model.createSelection( focus ); + + model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } ); + + // If there was no change in the focus position, then it's not possible to move the selection there. + return focus.isEqual( probe.focus ); + } + + /** + * Checks if there is an {@link module:engine/model/element~Element element} next to the current + * {@link module:engine/model/selection~Selection model selection} marked in + * {@link module:engine/model/schema~Schema schema} as an `object`. + * + * @private + * @param {module:engine/model/selection~Selection} modelSelection The selection. + * @param {Boolean} isForward Direction of checking. + * @returns {Boolean} + */ + _isObjectElementNextToSelection( modelSelection, isForward ) { + const model = this.editor.model; + const schema = model.schema; + + const probe = model.createSelection( modelSelection ); + model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } ); + const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter; + + return objectElement && schema.isObject( objectElement ); + } + + /** + * Truncates the range so that it spans from the last selection position + * to the last allowed $text position (mirrored if isForward is false). + * + * Returns `null` if resulting range can't contain $text element (according to schema). + * + * @private + * @param {module:engine/model/range~Range} range Current table cell content range. + * @param {module:engine/model/selection~Selection} selection The current selection. + * @param {Boolean} isForward The expected navigation direction. + * @returns {module:engine/model/range~Range|null} + */ + _findTextRangeFromSelection( range, selection, isForward ) { + const model = this.editor.model; + + if ( isForward ) { + const position = selection.getLastPosition(); + const lastRangePosition = this._getNearestVisibleTextPosition( range, 'backward' ); + + if ( lastRangePosition && position.isBefore( lastRangePosition ) ) { + return model.createRange( position, lastRangePosition ); + } + + return null; + } else { + const position = selection.getFirstPosition(); + const firstRangePosition = this._getNearestVisibleTextPosition( range, 'forward' ); + + if ( firstRangePosition && position.isAfter( firstRangePosition ) ) { + return model.createRange( firstRangePosition, position ); + } + + return null; + } + } + + /** + * Basing on provided range, finds first/last (depending on `direction`) position inside the range + * that can contain `$text` (according to schema) and is visible in the view. + * + * @param {module:engine/model/range~Range} range The range to find position in. + * @param {'forward'|'backward'} direction Search direction. + * @returns {module:engine/model/position~Position} Nearest selection range. + */ + _getNearestVisibleTextPosition( range, direction ) { + const schema = this.editor.model.schema; + const mapper = this.editor.editing.mapper; + + for ( const { nextPosition, item } of range.getWalker( { direction } ) ) { + if ( schema.checkChild( nextPosition, '$text' ) ) { + const viewElement = mapper.toViewElement( item ); + + if ( viewElement && !viewElement.hasClass( 'ck-hidden' ) ) { + return nextPosition; + } + } + } + } + + /** + * Checks if the DOM range corresponding to provided model range renders as a single line by analyzing DOMRects + * (verifying if they visually wrap content to the next line). + * + * @private + * @param {module:engine/model/range~Range} modelRange Current table cell content range. + * @param {Boolean} isForward The expected navigation direction. + * @returns {Boolean} + */ + _isSingleLineRange( modelRange, isForward ) { + const model = this.editor.model; + const editing = this.editor.editing; + const domConverter = editing.view.domConverter; + + // Wrapped lines contain exactly the same position at the end of current line + // and at the beginning of next line. That position's client rect is at the end + // of current line. In case of caret at first position of the last line that 'dual' + // position would be detected as it's not the last line. + if ( isForward ) { + const probe = model.createSelection( modelRange.start ); + + model.modifySelection( probe ); + + // If the new position is at the end of the container then we can't use this position + // because it would provide incorrect result for eg caption of image and selection + // just before end of it. Also in this case there is no "dual" position. + if ( !probe.focus.isAtEnd && !modelRange.start.isEqual( probe.focus ) ) { + modelRange = model.createRange( probe.focus, modelRange.end ); + } + } + + const viewRange = editing.mapper.toViewRange( modelRange ); + const domRange = domConverter.viewRangeToDom( viewRange ); + const rects = Rect.getDomRangeRects( domRange ); + + let boundaryVerticalPosition; + + for ( const rect of rects ) { + if ( boundaryVerticalPosition === undefined ) { + boundaryVerticalPosition = Math.round( rect.bottom ); + continue; + } + + // Let's check if this rect is in new line. + if ( Math.round( rect.top ) >= boundaryVerticalPosition ) { + return false; + } + + boundaryVerticalPosition = Math.max( boundaryVerticalPosition, Math.round( rect.bottom ) ); + } + + return true; + } + + /** + * Moves the selection from the given table cell in the specified direction. + * + * @private + * @param {module:engine/model/element~Element} tableCell The table cell to start the selection navigation. + * @param {'left'|'up'|'right'|'down'} direction Direction in which selection should move. + */ + _navigateFromCellInDirection( tableCell, direction ) { + const model = this.editor.model; + + const table = findAncestor( 'table', tableCell ); + const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ]; + const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ]; + + const currentCellInfo = tableMap.find( ( { cell } ) => cell == tableCell ); + let { row, column } = currentCellInfo; + + switch ( direction ) { + case 'left': + column--; + break; + + case 'up': + row--; + break; + + case 'right': + column += currentCellInfo.colspan; + break; + + case 'down': + row += currentCellInfo.rowspan; + break; + } + + const isOutsideVertically = row < 0 || row > lastRow; + const isBeforeFirstCell = column < 0 && row <= 0; + const isAfterLastCell = column > lastColumn && row >= lastRow; + + // Note that if the table cell at the end of a row is row-spanned then isAfterLastCell will never be true. + // However, we don't know if user was navigating on the last row or not, so let's stay in the table. + + if ( isOutsideVertically || isBeforeFirstCell || isAfterLastCell ) { + model.change( writer => { + writer.setSelection( writer.createRangeOn( table ) ); + } ); + + return; + } + + if ( column < 0 ) { + column = lastColumn; + row--; + } else if ( column > lastColumn ) { + column = 0; + row++; + } + + const cellToSelect = tableMap.find( cellInfo => cellInfo.row == row && cellInfo.column == column ).cell; + const isForward = [ 'right', 'down' ].includes( direction ); + const positionToSelect = model.createPositionAt( cellToSelect, isForward ? 0 : 'end' ); + + model.change( writer => { + writer.setSelection( positionToSelect ); + } ); + } +} + +// Returns 'true' if provided key code represents one of the arrow keys. +// +// @private +// @param {Number} keyCode +// @returns {Boolean} +function isArrowKeyCode( keyCode ) { + return keyCode == keyCodes.arrowright || + keyCode == keyCodes.arrowleft || + keyCode == keyCodes.arrowup || + keyCode == keyCodes.arrowdown; +} + +// Returns direction name from `keyCode`. +// +// @private +// @param {Number} keyCode +// @param {String} contentLanguageDirection The content language direction. +// @returns {'left'|'up'|'right'|'down'} Arrow direction. +function getDirectionFromKeyCode( keyCode, contentLanguageDirection ) { + const isLtrContent = contentLanguageDirection === 'ltr'; + + switch ( keyCode ) { + case keyCodes.arrowleft: + return isLtrContent ? 'left' : 'right'; + + case keyCodes.arrowright: + return isLtrContent ? 'right' : 'left'; + + case keyCodes.arrowup: + return 'up'; + + case keyCodes.arrowdown: + return 'down'; + } +} diff --git a/tests/table.js b/tests/table.js index b8c0ed45..d37ae22e 100644 --- a/tests/table.js +++ b/tests/table.js @@ -8,11 +8,12 @@ import TableEditing from '../src/tableediting'; import TableUI from '../src/tableui'; import TableSelection from '../src/tableselection'; import TableClipboard from '../src/tableclipboard'; +import TableNavigation from '../src/tablenavigation'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; describe( 'Table', () => { - it( 'requires TableEditing, TableUI, TableSelection, TableClipboard, and Widget', () => { - expect( Table.requires ).to.deep.equal( [ TableEditing, TableUI, TableSelection, TableClipboard, Widget ] ); + it( 'requires TableEditing, TableUI, TableSelection, TableClipboard, TableNavigation and Widget', () => { + expect( Table.requires ).to.deep.equal( [ TableEditing, TableUI, TableSelection, TableClipboard, TableNavigation, Widget ] ); } ); it( 'has proper name', () => { diff --git a/tests/tableediting.js b/tests/tableediting.js index df6731c2..955cfb47 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 { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; import TableEditing from '../src/tableediting'; @@ -224,335 +223,6 @@ describe( 'TableEditing', () => { } ); } ); - describe( 'caret movement', () => { - let domEvtDataStub; - - beforeEach( () => { - domEvtDataStub = { - keyCode: getCode( 'Tab' ), - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - } ); - - it( 'should do nothing if not tab pressed', () => { - setModelData( model, modelTable( [ - [ '11', '12[]' ] - ] ) ); - - domEvtDataStub.keyCode = getCode( 'a' ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.notCalled( domEvtDataStub.preventDefault ); - sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '12[]' ] - ] ) ); - } ); - - it( 'should do nothing if Ctrl+Tab is pressed', () => { - setModelData( model, modelTable( [ - [ '11', '12[]' ] - ] ) ); - - domEvtDataStub.ctrlKey = true; - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.notCalled( domEvtDataStub.preventDefault ); - sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '12[]' ] - ] ) ); - } ); - - describe( 'on TAB', () => { - it( 'should do nothing if selection is not in a table', () => { - setModelData( model, '[]' + modelTable( [ [ '11', '12' ] ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.notCalled( domEvtDataStub.preventDefault ); - sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ - [ '11', '12' ] - ] ) ); - } ); - - it( 'should move to next cell', () => { - setModelData( model, modelTable( [ - [ '11[]', '12' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '[12]' ] - ] ) ); - } ); - - it( 'should create another row and move to first cell in new row', () => { - setModelData( model, modelTable( [ - [ '11', '[12]' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '12' ], - [ '[]', '' ] - ] ) ); - } ); - - it( 'should not create another row and not move the caret if insertTableRowBelow command is disabled', () => { - setModelData( model, modelTable( [ - [ '11', '12[]' ] - ] ) ); - - const insertTableRowBelowCommand = editor.commands.get( 'insertTableRowBelow' ); - - insertTableRowBelowCommand.forceDisabled( 'test' ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '12[]' ] - ] ) ); - } ); - - it( 'should move to the first cell of next row if on end of a row', () => { - setModelData( model, modelTable( [ - [ '11', '12[]' ], - [ '21', '22' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '12' ], - [ '[21]', '22' ] - ] ) ); - } ); - - it( 'should move to the next table cell if part of block content is selected', () => { - setModelData( model, modelTable( [ - [ '11', '12[foo]bar', '13' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ - '11', - '12foobar', - '[13]' - ] - ] ) ); - } ); - - it( 'should move to next cell with an image', () => { - setModelData( model, modelTable( [ - [ '11[]', 'foo' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '[foo]' ] - ] ) ); - } ); - - it( 'should move to next cell with an blockQuote', () => { - model.schema.register( 'blockQuote', { - allowWhere: '$block', - allowContentOf: '$root' - } ); - editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } ); - - setModelData( model, modelTable( [ - [ '11[]', '
foo
' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '
[foo]
' ] - ] ) ); - } ); - - it( 'should listen with lower priority then its children', () => { - // Cancel TAB event. - editor.keystrokes.set( 'Tab', ( data, cancel ) => cancel() ); - - setModelData( model, modelTable( [ - [ '11[]', '12' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11[]', '12' ] - ] ) ); - } ); - - describe( 'on table widget selected', () => { - beforeEach( () => { - editor.model.schema.register( 'block', { - allowWhere: '$block', - allowContentOf: '$block', - isObject: true - } ); - - editor.conversion.elementToElement( { model: 'block', view: 'block' } ); - } ); - - it( 'should move caret to the first table cell on TAB', () => { - const spy = sinon.spy(); - - editor.keystrokes.set( 'Tab', spy, { priority: 'lowest' } ); - - setModelData( model, '[' + modelTable( [ - [ '11', '12' ] - ] ) + ']' ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[11]', '12' ] - ] ) ); - - // Should cancel event - so no other tab handler is called. - sinon.assert.notCalled( spy ); - } ); - - it( 'shouldn\'t do anything on other blocks', () => { - const spy = sinon.spy(); - - editor.editing.view.document.on( 'keydown', spy ); - - setModelData( model, '[foo]' ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.notCalled( domEvtDataStub.preventDefault ); - sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - - assertEqualMarkup( getModelData( model ), '[foo]' ); - - // Should not cancel event. - sinon.assert.calledOnce( spy ); - } ); - } ); - } ); - - describe( 'on SHIFT+TAB', () => { - beforeEach( () => { - domEvtDataStub.shiftKey = true; - } ); - - it( 'should do nothing if selection is not in a table', () => { - setModelData( model, '[]' + modelTable( [ - [ '11', '12' ] - ] ) ); - - domEvtDataStub.keyCode = getCode( 'Tab' ); - domEvtDataStub.shiftKey = true; - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.notCalled( domEvtDataStub.preventDefault ); - sinon.assert.notCalled( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ - [ '11', '12' ] - ] ) ); - } ); - - it( 'should move to previous cell', () => { - setModelData( model, modelTable( [ - [ '11', '12[]' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[11]', '12' ] - ] ) ); - } ); - - it( 'should not move if caret is in first table cell', () => { - setModelData( model, 'foo' + modelTable( [ - [ '[]11', '12' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), - 'foo' + modelTable( [ [ '[]11', '12' ] ] ) - ); - } ); - - it( 'should move to the last cell of previous row if on beginning of a row', () => { - setModelData( model, modelTable( [ - [ '11', '12' ], - [ '[]21', '22' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '11', '[12]' ], - [ '21', '22' ] - ] ) ); - } ); - - it( 'should move to the previous table cell if part of block content is selected', () => { - setModelData( model, modelTable( [ - [ '11', '12[foo]bar', '13' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - assertEqualMarkup( getModelData( model ), modelTable( [ - [ - '[11]', - '12foobar', - '13' - ] - ] ) ); - } ); - - it( 'should move to previous cell with an image', () => { - setModelData( model, modelTable( [ - [ 'foo', 'bar[]' ] - ] ) ); - - editor.editing.view.document.fire( 'keydown', domEvtDataStub ); - - sinon.assert.calledOnce( domEvtDataStub.preventDefault ); - sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); - assertEqualMarkup( getModelData( model ), modelTable( [ - [ '[foo]', 'bar' ] - ] ) ); - } ); - } ); - } ); - describe( 'enter key', () => { let evtDataStub, viewDocument; diff --git a/tests/tablenavigation.js b/tests/tablenavigation.js new file mode 100644 index 00000000..81aefcc9 --- /dev/null +++ b/tests/tablenavigation.js @@ -0,0 +1,2284 @@ +/** + * @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 TableNavigation from '../src/tablenavigation'; +import Table from '../src/table'; +import TableEditing from '../src/tableediting'; +import TableSelection from '../src/tableselection'; +import { modelTable } from './_utils/utils'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; +import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting'; +import ImageCaptionEditing from '@ckeditor/ckeditor5-image/src/imagecaption/imagecaptionediting'; +import HorizontalLineEditing from '@ckeditor/ckeditor5-horizontal-line/src/horizontallineediting'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Image from '@ckeditor/ckeditor5-image/src/image'; +import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; +import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; + +import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import env from '@ckeditor/ckeditor5-utils/src/env'; + +describe( 'TableNavigation', () => { + let editor, model, modelRoot, tableSelection; + + const imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAUCAQAAADRyVAeAAAAKklEQVR42u3PAQ0AAAwCI' + + 'O0f+u/hoAHNZUJFRERERERERERERERERLYiD9N4FAFj2iK6AAAAAElFTkSuQmCC'; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ TableEditing, TableNavigation, TableSelection, Paragraph, ImageEditing, ImageCaptionEditing, MediaEmbedEditing, + HorizontalLineEditing ] + } ) + .then( newEditor => { + editor = newEditor; + + model = editor.model; + modelRoot = model.document.getRoot(); + tableSelection = editor.plugins.get( TableSelection ); + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'should have pluginName', () => { + expect( TableNavigation.pluginName ).to.equal( 'TableNavigation' ); + } ); + + describe( 'Tab key handling', () => { + let domEvtDataStub; + + beforeEach( () => { + domEvtDataStub = { + keyCode: getCode( 'Tab' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + } ); + + it( 'should do nothing if pressed other key', () => { + setModelData( model, modelTable( [ + [ '11', '12[]' ] + ] ) ); + + domEvtDataStub.keyCode = getCode( 'a' ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.notCalled( domEvtDataStub.preventDefault ); + sinon.assert.notCalled( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '12[]' ] + ] ) ); + } ); + + it( 'should do nothing if Ctrl+Tab is pressed', () => { + setModelData( model, modelTable( [ + [ '11', '12[]' ] + ] ) ); + + domEvtDataStub.ctrlKey = true; + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.notCalled( domEvtDataStub.preventDefault ); + sinon.assert.notCalled( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '12[]' ] + ] ) ); + } ); + + describe( 'on Tab key press', () => { + it( 'should do nothing if the selection is not in a table', () => { + setModelData( model, '[]' + modelTable( [ [ '11', '12' ] ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.notCalled( domEvtDataStub.preventDefault ); + sinon.assert.notCalled( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ + [ '11', '12' ] + ] ) ); + } ); + + it( 'should move to the next cell', () => { + setModelData( model, modelTable( [ + [ '11[]', '12' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '[12]' ] + ] ) ); + } ); + + it( 'should create another row and move to the first cell in a new row', () => { + setModelData( model, modelTable( [ + [ '11', '[12]' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '12' ], + [ '[]', '' ] + ] ) ); + } ); + + it( 'should not create another row and not move the caret if the "insertTableRowBelow" command is disabled', () => { + setModelData( model, modelTable( [ + [ '11', '12[]' ] + ] ) ); + + const insertTableRowBelowCommand = editor.commands.get( 'insertTableRowBelow' ); + + insertTableRowBelowCommand.forceDisabled( 'test' ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '12[]' ] + ] ) ); + } ); + + it( 'should move to the first cell of the next row if at the end of a row', () => { + setModelData( model, modelTable( [ + [ '11', '12[]' ], + [ '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '12' ], + [ '[21]', '22' ] + ] ) ); + } ); + + it( 'should move to the next table cell if the block content is partially selected', () => { + setModelData( model, modelTable( [ + [ '11', '12[foo]bar', '13' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ + '11', + '12foobar', + '[13]' + ] + ] ) ); + } ); + + it( 'should move to the next cell containing an image', () => { + setModelData( model, modelTable( [ + [ '11[]', 'foo' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '[foo]' ] + ] ) ); + } ); + + it( 'should move to the next cell containing a block quote', () => { + model.schema.register( 'blockQuote', { + allowWhere: '$block', + allowContentOf: '$root' + } ); + editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } ); + + setModelData( model, modelTable( [ + [ '11[]', '
foo
' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '
[foo]
' ] + ] ) ); + } ); + + it( 'should listen with the lower priority than its children', () => { + // Cancel TAB event. + editor.keystrokes.set( 'Tab', ( data, cancel ) => cancel() ); + + setModelData( model, modelTable( [ + [ '11[]', '12' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11[]', '12' ] + ] ) ); + } ); + + describe( 'on table widget selected', () => { + beforeEach( () => { + editor.model.schema.register( 'block', { + allowWhere: '$block', + allowContentOf: '$block', + isObject: true + } ); + + editor.conversion.elementToElement( { model: 'block', view: 'block' } ); + } ); + + it( 'should move caret to the first table cell on TAB', () => { + const spy = sinon.spy(); + + editor.keystrokes.set( 'Tab', spy, { priority: 'lowest' } ); + + setModelData( model, '[' + modelTable( [ + [ '11', '12' ] + ] ) + ']' ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[11]', '12' ] + ] ) ); + + // Should cancel event - so no other tab handler is called. + sinon.assert.notCalled( spy ); + } ); + + it( 'shouldn\'t do anything on other blocks', () => { + const spy = sinon.spy(); + + editor.editing.view.document.on( 'keydown', spy ); + + setModelData( model, '[foo]' ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.notCalled( domEvtDataStub.preventDefault ); + sinon.assert.notCalled( domEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), '[foo]' ); + + // Should not cancel event. + sinon.assert.calledOnce( spy ); + } ); + } ); + } ); + + describe( 'on Shift+Tab key press', () => { + beforeEach( () => { + domEvtDataStub.shiftKey = true; + } ); + + it( 'should do nothing if the selection is not in a table', () => { + setModelData( model, '[]' + modelTable( [ + [ '11', '12' ] + ] ) ); + + domEvtDataStub.keyCode = getCode( 'Tab' ); + domEvtDataStub.shiftKey = true; + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.notCalled( domEvtDataStub.preventDefault ); + sinon.assert.notCalled( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), '[]' + modelTable( [ + [ '11', '12' ] + ] ) ); + } ); + + it( 'should move to the previous cell', () => { + setModelData( model, modelTable( [ + [ '11', '12[]' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[11]', '12' ] + ] ) ); + } ); + + it( 'should not move if the caret is in the first table cell', () => { + setModelData( model, 'foo' + modelTable( [ + [ '[]11', '12' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), + 'foo' + modelTable( [ [ '[]11', '12' ] ] ) + ); + } ); + + it( 'should move to the last cell of a previous row if at the beginning of a row', () => { + setModelData( model, modelTable( [ + [ '11', '12' ], + [ '[]21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '11', '[12]' ], + [ '21', '22' ] + ] ) ); + } ); + + it( 'should move to the previous table cell if the block content is partially selected', () => { + setModelData( model, modelTable( [ + [ '11', '12[foo]bar', '13' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ + '[11]', + '12foobar', + '13' + ] + ] ) ); + } ); + + it( 'should move to the previous cell containing an image', () => { + setModelData( model, modelTable( [ + [ 'foo', 'bar[]' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', domEvtDataStub ); + + sinon.assert.calledOnce( domEvtDataStub.preventDefault ); + sinon.assert.calledOnce( domEvtDataStub.stopPropagation ); + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '[foo]', 'bar' ] + ] ) ); + } ); + } ); + } ); + + describe( 'Arrow keys handling', () => { + let leftArrowDomEvtDataStub, rightArrowDomEvtDataStub, upArrowDomEvtDataStub, downArrowDomEvtDataStub; + + beforeEach( () => { + leftArrowDomEvtDataStub = { + keyCode: getCode( 'ArrowLeft' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: global.document.body + }; + rightArrowDomEvtDataStub = { + keyCode: getCode( 'ArrowRight' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: global.document.body + }; + upArrowDomEvtDataStub = { + keyCode: getCode( 'ArrowUp' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: global.document.body + }; + downArrowDomEvtDataStub = { + keyCode: getCode( 'ArrowDown' ), + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + domTarget: global.document.body + }; + } ); + + it( 'should do nothing if pressed some non-arrow key', () => { + setModelData( model, modelTable( [ + [ '00', '01[]' ] + ] ) ); + + leftArrowDomEvtDataStub.keyCode = getCode( 'a' ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.notCalled( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]' ] + ] ) ); + } ); + + it( 'should do nothing if the selection is not in a table', () => { + const modelData = '[]foobar' + modelTable( [ [ '00', '01' ] ] ); + + setModelData( model, modelData ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelData ); + } ); + + describe( '#_navigateFromCellInDirection (finding a proper cell to move the selection to)', () => { + let tableNavigation; + + beforeEach( () => { + tableNavigation = editor.plugins.get( TableNavigation ); + } ); + + describe( 'with no col/row-spanned cells', () => { + beforeEach( () => { + setModelData( model, 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + describe( 'from the first table cell', () => { + let tableCell; + + beforeEach( () => { + tableCell = modelRoot.getNodeByPath( [ 1, 0, 0 ] ); + } ); + + it( 'should navigate to the start position of the cell on the right when the direction is "right"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '[]01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the start position of the cell below when the direction is "down"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '[]10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should select a whole table when the direction is "up"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), 'foo[' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + ']bar' ); + } ); + + it( 'should select a whole table when the direction is "left"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), 'foo[' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + ']bar' ); + } ); + } ); + + describe( 'from the last table cell', () => { + let tableCell; + + beforeEach( () => { + tableCell = modelRoot.getNodeByPath( [ 1, 2, 2 ] ); + } ); + + it( 'should navigate to the end position of the cell on the left when the direction is "left"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21[]', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the end position of the cell above when the direction is "up"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12[]' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should select a whole table when the direction is "down"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), 'foo[' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + ']bar' ); + } ); + + it( 'should select a whole table when the direction is "right"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), 'foo[' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + ']bar' ); + } ); + } ); + + describe( 'from a cell in the first column (but not first row)', () => { + let tableCell; + + beforeEach( () => { + tableCell = modelRoot.getNodeByPath( [ 1, 1, 0 ] ); + } ); + + it( 'should navigate to start position of the cell on the right when the direction is "right"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the end position of the cell above when the direction is "up"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00[]', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the start position of the cell below when the direction is "down"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '[]20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the end position of the last cell in the previous row when the direction is "left"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02[]' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + } ); + + describe( 'from a cell in the last column (but not the last row)', () => { + let tableCell; + + beforeEach( () => { + tableCell = modelRoot.getNodeByPath( [ 1, 1, 2 ] ); + } ); + + it( 'should navigate to the end position of the cell on the left when the direction is "left"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11[]', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the end position the cell above when the direction is "up"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02[]' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the start position of the cell below when the direction is "down"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '[]22' ] + ] ) + 'bar' ); + } ); + + it( 'should navigate to the start position of the first cell in the next row when the direction is "right"', () => { + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), 'foo' + modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '[]20', '21', '22' ] + ] ) + 'bar' ); + } ); + } ); + } ); + + describe( 'with col/row-spanned cells', () => { + beforeEach( () => { + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+----+----+----+----+ + // | 10 | 11 | 13 | 14 | + // +----+ + +----+ + // | 20 | | | 24 | + // +----+----+----+----+----+ + // | 30 | 31 | 33 | 34 | + // +----+----+----+----+----+ + // | 40 | 41 | 42 | 43 | 44 | + // +----+----+----+----+----+ + setModelData( model, modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + describe( 'when navigating to the right', () => { + it( 'should navigate to the row-col-spanned cell when approaching from the upper-spanned row', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 0 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '[]11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-col-spanned cell when approaching from the lower-spanned row', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 2, 0 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '[]11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell when approaching from the other row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '[]13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the cell in the upper-spanned row when approaching from the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 2 ] ); // Cell 13. + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '[]14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 0 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '[]31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate from the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'right' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '[]33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + } ); + + describe( 'when navigating to the left', () => { + it( 'should navigate to the row-spanned cell when approaching from the upper-spanned row', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 3 ] ); // Cell 14. + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13[]', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell when approaching from the lower-spanned row', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] ); // Cell 24. + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13[]', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell when approaching from the other row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 2 ] ); // Cell 13. + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11[]', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the cell in the upper-spanned row when approaching from the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); // Cell 11. + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10[]', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 2 ] ); // Cell 33. + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31[]', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate from the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'left' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30[]', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + } ); + + describe( 'when navigating down', () => { + it( 'should navigate to the row-col-spanned cell when approaching from the first spanned column', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '[]11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-col-spanned cell when approaching from the last spanned column', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '[]11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell when approaching from the other col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '[]31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the cell in the first spanned column when approaching from the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); // Cell 11. + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '[]31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 0, 3 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '[]13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate from the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 2 ] ); // Cell 13. + + tableNavigation._navigateFromCellInDirection( tableCell, 'down' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '[]33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + } ); + + describe( 'when navigating up', () => { + it( 'should navigate to the col-spanned cell when approaching from the first spanned column', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 4, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31[]', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the col-spanned cell when approaching from the last spanned column', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 4, 2 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31[]', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-col-spanned cell when approaching from the other col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11[]', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the cell in the first spanned column when approaching from the col-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] ); + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate to the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 3, 2 ] ); // Cell 33. + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13[]', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + + it( 'should navigate from the row-spanned cell', () => { + const tableCell = modelRoot.getNodeByPath( [ 0, 1, 2 ] ); // Cell 13. + + tableNavigation._navigateFromCellInDirection( tableCell, 'up' ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03[]', '04' ], + [ '10', { contents: '11', colspan: 2, rowspan: 2 }, { contents: '13', rowspan: 2 }, '14' ], + [ '20', '24' ], + [ '30', { contents: '31', colspan: 2 }, '33', '34' ], + [ '40', '41', '42', '43', '44' ] + ] ) ); + } ); + } ); + } ); + } ); + + describe( 'with the table cells selected from outside', () => { + describe( 'on a single table cell selected', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + } ); + + it( 'should move to the cell on the left', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell on the right', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell above the selection', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell below the selection', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + + describe( 'on multiple table cells selected vertically', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + } ); + + it( 'should move to the cell on the top left of the selection', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10[]', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell on the bottom right of the selection', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '[]22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell above the selection', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell below the selection', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '[]31', '32', '33' ] + ] ) ); + } ); + } ); + + describe( 'on multiple table cell selected horizontally', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + // Note that this also tests that selection direction doesn't matter. + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 2 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + } ); + + it( 'should move to the cell on the top left of the selection', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10[]', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell on the bottom right of the selection', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '[]13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell above the selection', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell below the selection', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '[]22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + } ); + + describe( 'on multiple table cell selected diagonally', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 2, 2 ] ) + ); + } ); + + it( 'should move to the cell on the top left of selection', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10[]', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell on the bottom right of selection', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '[]23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell above selection', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '32', '33' ] + ] ) ); + } ); + + it( 'should move to the cell below selection', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02', '03' ], + [ '10', '11', '12', '13' ], + [ '20', '21', '22', '23' ], + [ '30', '31', '[]32', '33' ] + ] ) ); + } ); + } ); + } ); + + describe( 'with the selection inside a table cell', () => { + describe( 'with the selection at the boundary of a cell', () => { + describe( 'simple cell text content', () => { + it( 'should navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell below', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + + describe( 'multiple paragraphs in the cell content', () => { + it( 'should navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11x', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '11x', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11x[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11x', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11x', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '11x', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell below', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11x[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11x', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + + describe( 'image widget with caption as only cell content', () => { + it( 'should navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `[]11`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', `11`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `11[]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `11`, '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `[]11`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', `11`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell below', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `11[]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `11`, '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + + describe( 'horizontal line as only cell content (widget without $text positions)', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the left', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the right', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell above', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell below', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + + describe( 'two horizontal lines as only cell content (widget without $text positions)', () => { + it( 'should navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should navigate to the cell below', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + } ); + + describe( 'with selection not at the boundary of a cell', () => { + let editorElement, editor, model, styleElement; + + beforeEach( async () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ Table, Paragraph, Image, ImageCaption, HorizontalLine ] + } ); + + model = editor.model; + + styleElement = global.document.createElement( 'style' ); + styleElement.type = 'text/css'; + styleElement.appendChild( global.document.createTextNode( + ` + * { + font-size: 12px !important; + font-family: serif !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important + } + td { width: 30px !important; } + tr:nth-child(2) td:nth-child(2) { width: 300px !important; } + ` + ) ); + global.document.querySelector( 'head' ).appendChild( styleElement ); + } ); + + afterEach( async () => { + editorElement.remove(); + styleElement.remove(); + await editor.destroy(); + } ); + + describe( 'simple cell text content', () => { + it( 'should not navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '1[]1', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.notCalled( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( leftArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '1[]1', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.notCalled( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( rightArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '1[]1', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not navigate to the cell below', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '1[]1', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + + describe( 'selection inside paragraph', () => { + const text = new Array( 20 ).fill( 0 ).map( () => 'word' ).join( ' ' ); + + it( 'should not navigate if caret is in the middle line of a text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', text + '[] ' + text, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should move caret to beginning of cell content if caret is in the first line of a text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', 'word[] word' + text, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '[]word word' + text, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move caret to end of cell content if caret is in the last line of a text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', text + 'word[] word', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', text + 'word word[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + + if ( !env.isGecko ) { + // These tests fails on Travis. They work correctly when started on local machine. + // Issue is probably related to text rendering and wrapping. + + describe( 'with selection in the wrap area', () => { + const text = new Array( 10 ).fill( 0 ).map( () => 'word' ).join( ' ' ); + + it( 'should move the caret to end if the caret is after the last space in the line next to the last one', () => { + // This is also first position in the last line. + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', text + ' []word word word', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', text + ' word word word[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move the caret to end if ther caret is at the last space in the line next to last one', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', text + '[] word word word', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', text + ' word word word[]', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not move the caret if it\'s just before the last space in the line next to last one', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', text.substring( 0, text.length - 1 ) + '[]d word word word', '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.notCalled( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( downArrowDomEvtDataStub.stopPropagation ); + } ); + } ); + } + + describe( 'with multiple paragraphs of text', () => { + const text = new Array( 100 ).fill( 0 ).map( () => 'word' ).join( ' ' ); + + it( 'should not navigate if caret is in the middle of a line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ text }[]${ text }foobar`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should move the caret to the beginning of a cell content if the caret is in the first line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `word[]${ text }foobar`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `[]word${ text }foobar`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not move the caret to the end of a cell content if the caret is not in the last line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ text }word []wordfoobar`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.notCalled( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( downArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should move the caret to end of a cell content if the caret is in the last line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `foobar${ text }word []word`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `foobar${ text }word word[]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + + describe( 'with horizontal line widget', () => { + const text = new Array( 100 ).fill( 0 ).map( () => 'word' ).join( ' ' ); + + it( 'should not navigate if the caret is in the middle line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ text }[]${ text }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should move the caret to the beginning of cell content if the caret is in the first line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `word[] ${ text }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `[]word ${ text }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move the caret to the end of cell content if the caret is in the last line of text', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ text } word []word`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ text } word word[]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not move the caret to the end of cell content if widget is selected in middle of a cell content', () => { + const paragraph = `${ text }`; + const hr = ''; + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ paragraph }[${ hr }]${ paragraph }${ hr }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ paragraph }${ hr }[]${ text }${ hr }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not move the caret to the end of cell content if widget is next to the selection', () => { + const paragraph = `${ text }`; + const hr = ''; + + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ paragraph }[]${ hr }`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `${ paragraph }[${ hr }]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + + describe( 'contains image widget with caption and selection inside the caption', () => { + it( 'should not navigate to the cell on the left', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `foo[]11`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.notCalled( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( leftArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell on the right', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `11[]foo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.notCalled( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( rightArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell above', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `foo1[]1`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell above but should select the image widget', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `1[]1foo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `[11]foo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not navigate to the cell below when followed by a paragraph', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `1[]1foo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.notCalled( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.notCalled( downArrowDomEvtDataStub.stopPropagation ); + } ); + + it( 'should not navigate to the cell below but should select the image widget', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `foo1[]1`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `foo[11]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not navigate to the cell above but should select the image widget without caption', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `f[]oo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `[]foo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should not navigate to the cell below but should select the image widget without caption', () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', `f[]oo`, '12' ], + [ '20', '21', '22' ] + ] ) ); + + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', `foo[]`, '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + } ); + } ); + } ); + + describe( 'for right-to-left content language', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ TableEditing, TableNavigation, TableSelection, Paragraph, ImageEditing, MediaEmbedEditing ], + language: 'ar' + } ) + .then( newEditor => { + editor = newEditor; + + model = editor.model; + modelRoot = model.document.getRoot(); + tableSelection = editor.plugins.get( TableSelection ); + } ); + } ); + + describe( 'with the table cell selected from outside', () => { + beforeEach( () => { + setModelData( model, modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 1 ] ), + modelRoot.getNodeByPath( [ 0, 1, 1 ] ) + ); + } ); + + it( 'should move to the cell on the right (visually flipped by the browser)', () => { + editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub ); + + sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( leftArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '[]12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell on the left (visually flipped by the browser)', () => { + editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub ); + + sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( rightArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10[]', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell above the selection', () => { + editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub ); + + sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01[]', '02' ], + [ '10', '11', '12' ], + [ '20', '21', '22' ] + ] ) ); + } ); + + it( 'should move to the cell below the selection', () => { + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); + + sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault ); + sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation ); + + assertEqualMarkup( getModelData( model ), modelTable( [ + [ '00', '01', '02' ], + [ '10', '11', '12' ], + [ '20', '[]21', '22' ] + ] ) ); + } ); + } ); + } ); + } ); +} );