Skip to content

Commit

Permalink
Merge pull request #6728 from ckeditor/i/6115
Browse files Browse the repository at this point in the history
Feature (table): Introduce table cells selection using keyboard. Closes #6115. Closes #3203.
  • Loading branch information
jodator authored May 7, 2020
2 parents a78bca8 + 7aa2f44 commit b567de4
Show file tree
Hide file tree
Showing 17 changed files with 987 additions and 341 deletions.
62 changes: 43 additions & 19 deletions packages/ckeditor5-table/src/tablenavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
* @module table/tablenavigation
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { getSelectedTableCells, getTableCellsContainingSelection } from './utils';
import { findAncestor } from './commands/utils';
import TableSelection from './tableselection';
import TableWalker from './tablewalker';
import { findAncestor } from './commands/utils';
import { getSelectedTableCells, getTableCellsContainingSelection } from './utils';

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
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';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';

/**
* This plugin enables keyboard navigation for tables.
Expand All @@ -29,6 +31,13 @@ export default class TableNavigation extends Plugin {
return 'TableNavigation';
}

/**
* @inheritDoc
*/
static get requires() {
return [ TableSelection ];
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -188,9 +197,15 @@ export default class TableNavigation extends Plugin {
const selectedCells = getSelectedTableCells( selection );

if ( selectedCells.length ) {
const tableCell = isForward ? selectedCells[ selectedCells.length - 1 ] : selectedCells[ 0 ];
let focusCell;

if ( expandSelection ) {
focusCell = this.editor.plugins.get( 'TableSelection' ).getFocusCell();
} else {
focusCell = isForward ? selectedCells[ selectedCells.length - 1 ] : selectedCells[ 0 ];
}

this._navigateFromCellInDirection( tableCell, direction );
this._navigateFromCellInDirection( focusCell, direction, expandSelection );

return true;
}
Expand All @@ -206,7 +221,7 @@ export default class TableNavigation extends Plugin {

// Let's check if the selection is at the beginning/end of the cell.
if ( this._isSelectionAtCellEdge( selection, isForward ) ) {
this._navigateFromCellInDirection( tableCell, direction );
this._navigateFromCellInDirection( tableCell, direction, expandSelection );

return true;
}
Expand All @@ -228,7 +243,7 @@ export default class TableNavigation extends Plugin {
const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );

if ( !textRange ) {
this._navigateFromCellInDirection( tableCell, direction );
this._navigateFromCellInDirection( tableCell, direction, expandSelection );

return true;
}
Expand Down Expand Up @@ -426,18 +441,19 @@ export default class TableNavigation extends Plugin {
/**
* 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.
* @protected
* @param {module:engine/model/element~Element} focusCell The table cell that is current multi-cell selection focus.
* @param {'left'|'up'|'right'|'down'} direction Direction in which selection should move.
* @param {Boolean} [expandSelection=false] If the current selection should be expanded.
*/
_navigateFromCellInDirection( tableCell, direction ) {
_navigateFromCellInDirection( focusCell, direction, expandSelection = false ) {
const model = this.editor.model;

const table = findAncestor( 'table', tableCell );
const table = findAncestor( 'table', focusCell );
const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ];
const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];

const currentCellInfo = tableMap.find( ( { cell } ) => cell == tableCell );
const currentCellInfo = tableMap.find( ( { cell } ) => cell == focusCell );
let { row, column } = currentCellInfo;

switch ( direction ) {
Expand Down Expand Up @@ -474,20 +490,28 @@ export default class TableNavigation extends Plugin {
}

if ( column < 0 ) {
column = lastColumn;
column = expandSelection ? 0 : lastColumn;
row--;
} else if ( column > lastColumn ) {
column = 0;
column = expandSelection ? lastColumn : 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 );
} );
if ( expandSelection ) {
const tableSelection = this.editor.plugins.get( 'TableSelection' );
const anchorCell = tableSelection.getAnchorCell() || focusCell;

tableSelection.setCellSelection( anchorCell, cellToSelect );
} else {
const positionToSelect = model.createPositionAt( cellToSelect, isForward ? 0 : 'end' );

model.change( writer => {
writer.setSelection( positionToSelect );
} );
}
}
}

Expand Down
121 changes: 77 additions & 44 deletions packages/ckeditor5-table/src/tableselection.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import first from '@ckeditor/ckeditor5-utils/src/first';

import TableWalker from './tablewalker';
import TableUtils from './tableutils';
Expand Down Expand Up @@ -106,6 +107,65 @@ export default class TableSelection extends Plugin {
} );
}

/**
* Sets the model selection based on given anchor and target cells (can be the same cell).
* Takes care of setting the backward flag.
*
* const modelRoot = editor.model.document.getRoot();
* const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
* const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
*
* const tableSelection = editor.plugins.get( 'TableSelection' );
* tableSelection.setCellSelection( firstCell, lastCell );
*
* @param {module:engine/model/element~Element} anchorCell
* @param {module:engine/model/element~Element} targetCell
*/
setCellSelection( anchorCell, targetCell ) {
const cellsToSelect = this._getCellsToSelect( anchorCell, targetCell );

this.editor.model.change( writer => {
writer.setSelection(
cellsToSelect.cells.map( cell => writer.createRangeOn( cell ) ),
{ backward: cellsToSelect.backward }
);
} );
}

/**
* Returns the focus cell from the current selection.
*
* @returns {module:engine/model/element~Element}
*/
getFocusCell() {
const selection = this.editor.model.document.selection;
const focusCellRange = [ ...selection.getRanges() ].pop();
const element = focusCellRange.getContainedElement();

if ( element && element.is( 'tableCell' ) ) {
return element;
}

return null;
}

/**
* Returns the anchor cell from the current selection.
*
* @returns {module:engine/model/element~Element} anchorCell
*/
getAnchorCell() {
const selection = this.editor.model.document.selection;
const anchorCellRange = first( selection.getRanges() );
const element = anchorCellRange.getContainedElement();

if ( element && element.is( 'tableCell' ) ) {
return element;
}

return null;
}

/**
* Defines a selection converter which marks the selected cells with a specific class.
*
Expand Down Expand Up @@ -181,7 +241,7 @@ export default class TableSelection extends Plugin {

if ( targetCell && haveSameTableParent( anchorCell, targetCell ) ) {
blockSelectionChange = true;
this._setCellSelection( anchorCell, targetCell );
this.setCellSelection( anchorCell, targetCell );

domEventData.preventDefault();
}
Expand Down Expand Up @@ -272,7 +332,7 @@ export default class TableSelection extends Plugin {
}

blockSelectionChange = true;
this._setCellSelection( anchorCell, targetCell );
this.setCellSelection( anchorCell, targetCell );

domEventData.preventDefault();
} );
Expand Down Expand Up @@ -363,25 +423,6 @@ export default class TableSelection extends Plugin {
} );
}

/**
* Sets the model selection based on given anchor and target cells (can be the same cell).
* Takes care of setting the backward flag.
*
* @protected
* @param {module:engine/model/element~Element} anchorCell
* @param {module:engine/model/element~Element} targetCell
*/
_setCellSelection( anchorCell, targetCell ) {
const cellsToSelect = this._getCellsToSelect( anchorCell, targetCell );

this.editor.model.change( writer => {
writer.setSelection(
cellsToSelect.cells.map( cell => writer.createRangeOn( cell ) ),
{ backward: cellsToSelect.backward }
);
} );
}

/**
* Returns the model table cell element based on the target element of the passed DOM event.
*
Expand Down Expand Up @@ -425,39 +466,31 @@ export default class TableSelection extends Plugin {
const startColumn = Math.min( startLocation.column, endLocation.column );
const endColumn = Math.max( startLocation.column, endLocation.column );

const cells = [];
// 2-dimensional array of the selected cells to ease flipping the order of cells for backward selections.
const selectionMap = new Array( endRow - startRow + 1 ).fill( null ).map( () => [] );

for ( const cellInfo of new TableWalker( findAncestor( 'table', anchorCell ), { startRow, endRow } ) ) {
if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
cells.push( cellInfo.cell );
selectionMap[ cellInfo.row - startRow ].push( cellInfo.cell );
}
}

// A selection started in the bottom right corner and finished in the upper left corner
// should have it ranges returned in the reverse order.
// However, this is only half of the story because the selection could be made to the left (then the left cell is a focus)
// or to the right (then the right cell is a focus), while being a forward selection in both cases
// (because it was made from top to bottom). This isn't handled.
// This method would need to be smarter, but the ROI is microscopic, so I skip this.
if ( checkIsBackward( startLocation, endLocation ) ) {
return { cells: cells.reverse(), backward: true };
}
const flipVertically = endLocation.row < startLocation.row;
const flipHorizontally = endLocation.column < startLocation.column;

return { cells, backward: false };
}
}
if ( flipVertically ) {
selectionMap.reverse();
}

// Naively check whether the selection should be backward or not. See the longer explanation where this function is used.
function checkIsBackward( startLocation, endLocation ) {
if ( startLocation.row > endLocation.row ) {
return true;
}
if ( flipHorizontally ) {
selectionMap.forEach( row => row.reverse() );
}

if ( startLocation.row == endLocation.row && startLocation.column > endLocation.column ) {
return true;
return {
cells: selectionMap.flat(),
backward: flipVertically || flipHorizontally
};
}

return false;
}

function haveSameTableParent( cellA, cellB ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe( 'InsertColumnCommand', () => {
const tableSelection = editor.plugins.get( TableSelection );
const modelRoot = model.document.getRoot();

tableSelection._setCellSelection(
tableSelection.setCellSelection(
modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
modelRoot.getNodeByPath( [ 0, 1, 1 ] )
);
Expand Down Expand Up @@ -251,7 +251,7 @@ describe( 'InsertColumnCommand', () => {
const tableSelection = editor.plugins.get( TableSelection );
const modelRoot = model.document.getRoot();

tableSelection._setCellSelection(
tableSelection.setCellSelection(
modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
modelRoot.getNodeByPath( [ 0, 1, 1 ] )
);
Expand Down
4 changes: 2 additions & 2 deletions packages/ckeditor5-table/tests/commands/insertrowcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe( 'InsertRowCommand', () => {
const tableSelection = editor.plugins.get( TableSelection );
const modelRoot = model.document.getRoot();

tableSelection._setCellSelection(
tableSelection.setCellSelection(
modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
modelRoot.getNodeByPath( [ 0, 1, 1 ] )
);
Expand Down Expand Up @@ -328,7 +328,7 @@ describe( 'InsertRowCommand', () => {
const tableSelection = editor.plugins.get( TableSelection );
const modelRoot = model.document.getRoot();

tableSelection._setCellSelection(
tableSelection.setCellSelection(
modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
modelRoot.getNodeByPath( [ 0, 1, 1 ] )
);
Expand Down
Loading

0 comments on commit b567de4

Please sign in to comment.