8
0
فهرست منبع

Merge branch 'master' into i/6126

Marek Lewandowski 5 سال پیش
والد
کامیت
1d54c3e39f
22فایلهای تغییر یافته به همراه855 افزوده شده و 394 حذف شده
  1. 1 0
      packages/ckeditor5-table/package.json
  2. 2 2
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  3. 2 2
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  4. 6 16
      packages/ckeditor5-table/src/commands/splitcellcommand.js
  5. 0 13
      packages/ckeditor5-table/src/commands/utils.js
  6. 1 1
      packages/ckeditor5-table/src/table.js
  7. 4 19
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js
  8. 14 7
      packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
  9. 11 5
      packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
  10. 16 10
      packages/ckeditor5-table/src/tableselection.js
  11. 1 1
      packages/ckeditor5-table/src/tableselection/mouseeventsobserver.js
  12. 0 51
      packages/ckeditor5-table/src/tableselection/utils.js
  13. 1 1
      packages/ckeditor5-table/src/ui/colorinputview.js
  14. 69 0
      packages/ckeditor5-table/src/utils.js
  15. 32 1
      packages/ckeditor5-table/tests/commands/splitcellcommand.js
  16. 7 4
      packages/ckeditor5-table/tests/manual/rtl.js
  17. 25 1
      packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
  18. 24 1
      packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
  19. 229 167
      packages/ckeditor5-table/tests/tableselection-integration.js
  20. 88 91
      packages/ckeditor5-table/tests/tableselection.js
  21. 24 1
      packages/ckeditor5-table/tests/ui/colorinputview.js
  22. 298 0
      packages/ckeditor5-table/tests/utils.js

+ 1 - 0
packages/ckeditor5-table/package.json

@@ -21,6 +21,7 @@
     "@ckeditor/ckeditor5-clipboard": "^17.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^17.0.0",
     "@ckeditor/ckeditor5-engine": "^17.0.0",
+    "@ckeditor/ckeditor5-horizontal-line": "^17.0.0",
     "@ckeditor/ckeditor5-image": "^17.0.0",
     "@ckeditor/ckeditor5-indent": "^17.0.0",
     "@ckeditor/ckeditor5-list": "^17.0.0",

+ 2 - 2
packages/ckeditor5-table/src/commands/insertcolumncommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getRangeContainedElement, findAncestor } from './utils';
+import { findAncestor } from './utils';
 
 /**
  * The insert column command.
@@ -75,7 +75,7 @@ export default class InsertColumnCommand extends Command {
 		const referencePosition = insertBefore ? selection.getFirstPosition() : selection.getLastPosition();
 		const referenceRange = insertBefore ? selection.getFirstRange() : selection.getLastRange();
 
-		const tableCell = getRangeContainedElement( referenceRange ) || findAncestor( 'tableCell', referencePosition );
+		const tableCell = referenceRange.getContainedElement() || findAncestor( 'tableCell', referencePosition );
 		const table = tableCell.parent.parent;
 
 		const { column } = tableUtils.getCellLocation( tableCell );

+ 2 - 2
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getRangeContainedElement, findAncestor } from './utils';
+import { findAncestor } from './utils';
 
 /**
  * The insert row command.
@@ -74,7 +74,7 @@ export default class InsertRowCommand extends Command {
 		const referencePosition = insertAbove ? selection.getFirstPosition() : selection.getLastPosition();
 		const referenceRange = insertAbove ? selection.getFirstRange() : selection.getLastRange();
 
-		const tableCell = getRangeContainedElement( referenceRange ) || findAncestor( 'tableCell', referencePosition );
+		const tableCell = referenceRange.getContainedElement() || findAncestor( 'tableCell', referencePosition );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 

+ 6 - 16
packages/ckeditor5-table/src/commands/splitcellcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { findAncestor } from './utils';
+import { getSelectionAffectedTableCells } from '../utils';
 
 /**
  * The split cell command.
@@ -46,30 +46,20 @@ export default class SplitCellCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const model = this.editor.model;
-		const doc = model.document;
+		const selectedCells = getSelectionAffectedTableCells( this.editor.model.document.selection );
 
-		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
-
-		this.isEnabled = !!tableCell;
+		this.isEnabled = selectedCells.length === 1;
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	execute() {
-		const model = this.editor.model;
-		const document = model.document;
-		const selection = document.selection;
-
-		const firstPosition = selection.getFirstPosition();
-		const tableCell = findAncestor( 'tableCell', firstPosition );
-
-		const isHorizontally = this.direction === 'horizontally';
-
+		const tableCell = getSelectionAffectedTableCells( this.editor.model.document.selection )[ 0 ];
+		const isHorizontal = this.direction === 'horizontally';
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 
-		if ( isHorizontally ) {
+		if ( isHorizontal ) {
 			tableUtils.splitCellHorizontally( tableCell, 2 );
 		} else {
 			tableUtils.splitCellVertically( tableCell, 2 );

+ 0 - 13
packages/ckeditor5-table/src/commands/utils.js

@@ -29,19 +29,6 @@ export function findAncestor( parentName, positionOrElement ) {
 	}
 }
 
-/**
- * Returns an element contained by a range, if it's the only one element contained by the range and if it's fully contained.
- *
- * @param {module:engine/model/range~Range} range
- * @returns {module:engine/model/element~Element|null}
- */
-export function getRangeContainedElement( range ) {
-	const nodeAfterStart = range.start.nodeAfter;
-	const nodeBeforeEnd = range.end.nodeBefore;
-
-	return ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
-}
-
 /**
  * A common method to update the numeric value. If a value is the default one, it will be unset.
  *

+ 1 - 1
packages/ckeditor5-table/src/table.js

@@ -26,7 +26,7 @@ import '../theme/table.css';
  *
  * * {@link module:table/tableediting~TableEditing editing feature},
  * * {@link module:table/tableselection~TableSelection selection feature},
- * * {@link module:table/tableclipboar~TableClipboard clipboard feature},
+ * * {@link module:table/tableclipboard~TableClipboard clipboard feature},
  * * {@link module:table/tableui~TableUI UI feature}.
  *
  * @extends module:core/plugin~Plugin

+ 4 - 19
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -8,8 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-
-import { findAncestor } from '../../commands/utils';
+import { getSelectionAffectedTableCells } from '../../utils';
 
 /**
  * The table cell attribute command.
@@ -36,8 +35,7 @@ export default class TableCellPropertyCommand extends Command {
 	 */
 	refresh() {
 		const editor = this.editor;
-
-		const selectedTableCells = getSelectedTableCells( editor.model );
+		const selectedTableCells = getSelectionAffectedTableCells( editor.model.document.selection );
 
 		this.isEnabled = !!selectedTableCells.length;
 		this.value = this._getSingleValue( selectedTableCells );
@@ -54,11 +52,9 @@ export default class TableCellPropertyCommand extends Command {
 	 * for example to allow a single undo step for multiple executions.
 	 */
 	execute( options = {} ) {
-		const model = this.editor.model;
-
 		const { value, batch } = options;
-
-		const tableCells = getSelectedTableCells( model );
+		const model = this.editor.model;
+		const tableCells = getSelectionAffectedTableCells( model.document.selection );
 		const valueToSet = this._getValueToSet( value );
 
 		model.enqueueChange( batch || 'default', writer => {
@@ -112,14 +108,3 @@ export default class TableCellPropertyCommand extends Command {
 		return everyCellHasAttribute ? firstCellValue : undefined;
 	}
 }
-
-// Returns all selected table cells.
-// The implementation of this function is incorrect as it may return a single cell twice.
-// See https://github.com/ckeditor/ckeditor5/issues/6358.
-function getSelectedTableCells( model ) {
-	const selection = model.document.selection;
-
-	return Array.from( selection.getSelectedBlocks() )
-		.map( element => findAncestor( 'tableCell', model.createPositionAt( element, 0 ) ) )
-		.filter( tableCell => !!tableCell );
-}

+ 14 - 7
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -682,6 +682,7 @@ export default class TableCellPropertiesView extends View {
 		// -- Horizontal ---------------------------------------------------
 
 		const horizontalAlignmentToolbar = new ToolbarView( locale );
+		const isContentRTL = this.locale.contentLanguageDirection === 'rtl';
 
 		horizontalAlignmentToolbar.set( {
 			isCompact: true,
@@ -695,7 +696,7 @@ export default class TableCellPropertiesView extends View {
 			labels: this._horizontalAlignmentLabels,
 			propertyName: 'horizontalAlignment',
 			nameToValue: name => {
-				return name === 'left' ? '' : name;
+				return name === ( isContentRTL ? 'right' : 'left' ) ? '' : name;
 			}
 		} );
 
@@ -781,14 +782,20 @@ export default class TableCellPropertiesView extends View {
 	 * @type {Object.<String,String>}
 	 */
 	get _horizontalAlignmentLabels() {
+		const locale = this.locale;
 		const t = this.t;
 
-		return {
-			left: t( 'Align cell text to the left' ),
-			center: t( 'Align cell text to the center' ),
-			right: t( 'Align cell text to the right' ),
-			justify: t( 'Justify cell text' )
-		};
+		const left = t( 'Align cell text to the left' );
+		const center = t( 'Align cell text to the center' );
+		const right = t( 'Align cell text to the right' );
+		const justify = t( 'Justify cell text' );
+
+		// Returns object with a proper order of labels.
+		if ( locale.uiLanguageDirection === 'rtl' ) {
+			return { right, center, left, justify };
+		} else {
+			return { left, center, right, justify };
+		}
 	}
 
 	/**

+ 11 - 5
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -681,13 +681,19 @@ export default class TablePropertiesView extends View {
 	 * @type {Object.<String,String>}
 	 */
 	get _alignmentLabels() {
+		const locale = this.locale;
 		const t = this.t;
 
-		return {
-			left: t( 'Align table to the left' ),
-			center: t( 'Center table' ),
-			right: t( 'Align table to the right' )
-		};
+		const left = t( 'Align table to the left' );
+		const center = t( 'Center table' );
+		const right = t( 'Align table to the right' );
+
+		// Returns object with a proper order of labels.
+		if ( locale.uiLanguageDirection === 'rtl' ) {
+			return { right, center, left };
+		} else {
+			return { left, center, right };
+		}
 	}
 }
 

+ 16 - 10
packages/ckeditor5-table/src/tableselection.js

@@ -12,7 +12,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import TableWalker from './tablewalker';
 import TableUtils from './tableutils';
 import MouseEventsObserver from './tableselection/mouseeventsobserver';
-import { getTableCellsInSelection, clearTableCellsContents } from './tableselection/utils';
+import { getSelectedTableCells } from './utils';
 import { findAncestor } from './commands/utils';
 import cropTable from './tableselection/croptable';
 
@@ -64,7 +64,7 @@ export default class TableSelection extends Plugin {
 	getSelectedTableCells() {
 		const selection = this.editor.model.document.selection;
 
-		const selectedCells = getTableCellsInSelection( selection );
+		const selectedCells = getSelectedTableCells( selection );
 
 		if ( selectedCells.length == 0 ) {
 			return null;
@@ -291,7 +291,7 @@ export default class TableSelection extends Plugin {
 		const [ selection, options ] = args;
 		const model = this.editor.model;
 		const isBackward = !options || options.direction == 'backward';
-		const selectedTableCells = getTableCellsInSelection( selection );
+		const selectedTableCells = getSelectedTableCells( selection );
 
 		if ( !selectedTableCells.length ) {
 			return;
@@ -302,14 +302,20 @@ export default class TableSelection extends Plugin {
 		model.change( writer => {
 			const tableCellToSelect = selectedTableCells[ isBackward ? selectedTableCells.length - 1 : 0 ];
 
-			clearTableCellsContents( model, selectedTableCells );
+			model.change( writer => {
+				for ( const tableCell of selectedTableCells ) {
+					model.deleteContent( writer.createSelection( tableCell, 'in' ) );
+				}
+			} );
+
+			const rangeToSelect = model.schema.getNearestSelectionRange( writer.createPositionAt( tableCellToSelect, 0 ) );
 
-			// The insertContent() helper passes the actual DocumentSelection,
-			// while the deleteContent() helper always operates on the abstract clones.
-			if ( selection.is( 'documentSelection' ) ) {
-				writer.setSelection( tableCellToSelect, 'in' );
-			} else {
-				selection.setTo( tableCellToSelect, 'in' );
+			if ( rangeToSelect ) {
+				if ( selection.is( 'documentSelection' ) ) {
+					writer.setSelection( rangeToSelect );
+				} else {
+					selection.setTo( rangeToSelect );
+				}
 			}
 		} );
 	}

+ 1 - 1
packages/ckeditor5-table/src/tableselection/mouseeventsobserver.js

@@ -21,7 +21,7 @@ import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domev
  * Note that this observer is disabled by default. To enable this observer it needs to be added to
  * {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
  *
- * It is registered by {@link module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}.
+ * It is registered by the {@link module:table/tableselection~TableSelection} plugin.
  *
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */

+ 0 - 51
packages/ckeditor5-table/src/tableselection/utils.js

@@ -1,51 +0,0 @@
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module table/tableselection/utils
- */
-
-import { getRangeContainedElement } from '../commands/utils';
-
-/**
- * Clears contents of the passed table cells.
- *
- * This is to be used with table selection
- *
- *		tableSelection.startSelectingFrom( startCell )
- *		tableSelection.setSelectingFrom( endCell )
- *
- *		clearTableCellsContents( editor.model, tableSelection.getSelectedTableCells() );
- *
- * @param {module:engine/model/model~Model} model
- * @param {Iterable.<module:engine/model/element~Element>} tableCells
- */
-export function clearTableCellsContents( model, tableCells ) {
-	model.change( writer => {
-		for ( const tableCell of tableCells ) {
-			model.deleteContent( writer.createSelection( tableCell, 'in' ) );
-		}
-	} );
-}
-
-/**
- * Returns all model cells within the provided model selection.
- *
- * @param {Iterable.<module:engine/model/selection~Selection>} selection
- * @returns {Array.<module:engine/model/element~Element>}
- */
-export function getTableCellsInSelection( selection ) {
-	const cells = [];
-
-	for ( const range of selection.getRanges() ) {
-		const element = getRangeContainedElement( range );
-
-		if ( element && element.is( 'tableCell' ) ) {
-			cells.push( element );
-		}
-	}
-
-	return cells;
-}

+ 1 - 1
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -175,7 +175,7 @@ export default class ColorInputView extends View {
 
 		dropdown.buttonView.children.add( colorPreview );
 
-		dropdown.panelPosition = 'sw';
+		dropdown.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';
 		dropdown.panelView.children.add( removeColorButton );
 		dropdown.panelView.children.add( colorGrid );
 		dropdown.bind( 'isEnabled' ).to( this, 'isReadOnly', value => !value );

+ 69 - 0
packages/ckeditor5-table/src/utils.js

@@ -67,3 +67,72 @@ export function getTableWidgetAncestor( selection ) {
 
 	return null;
 }
+
+/**
+ * Returns all model table cells that are fully selected (from the outside)
+ * within the provided model selection's ranges.
+ *
+ * To obtain the cells selected from the inside, use
+ * {@link module:table/utils~getTableCellsContainingSelection}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getSelectedTableCells( selection ) {
+	const cells = [];
+
+	for ( const range of selection.getRanges() ) {
+		const element = range.getContainedElement();
+
+		if ( element && element.is( 'tableCell' ) ) {
+			cells.push( element );
+		}
+	}
+
+	return cells;
+}
+
+/**
+ * Returns all model table cells the provided model selection's ranges
+ * {@link module:engine/model/range~Range#start} inside.
+ *
+ * To obtain the cells selected from the outside, use
+ * {@link module:table/utils~getSelectedTableCells}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getTableCellsContainingSelection( selection ) {
+	const cells = [];
+
+	for ( const range of selection.getRanges() ) {
+		const cellWithSelection = findAncestor( 'tableCell', range.start );
+
+		if ( cellWithSelection ) {
+			cells.push( cellWithSelection );
+		}
+	}
+
+	return cells;
+}
+
+/**
+ * Returns all model table cells that are either completely selected
+ * by selection ranges or host selection range
+ * {@link module:engine/model/range~Range#start start positions} inside them.
+ *
+ * Combines {@link module:table/utils~getTableCellsContainingSelection} and
+ * {@link module:table/utils~getSelectedTableCells}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getSelectionAffectedTableCells( selection ) {
+	const selectedCells = getSelectedTableCells( selection );
+
+	if ( selectedCells.length ) {
+		return selectedCells;
+	}
+
+	return getTableCellsContainingSelection( selection );
+}

+ 32 - 1
packages/ckeditor5-table/tests/commands/splitcellcommand.js

@@ -8,6 +8,7 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
 
 import SplitCellCommand from '../../src/commands/splitcellcommand';
 import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import TableSelection from '../../src/tableselection';
 import TableUtils from '../../src/tableutils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
@@ -17,7 +18,7 @@ describe( 'SplitCellCommand', () => {
 	beforeEach( () => {
 		return ModelTestEditor
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ TableUtils, TableSelection ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -47,6 +48,36 @@ describe( 'SplitCellCommand', () => {
 				expect( command.isEnabled ).to.be.true;
 			} );
 
+			it( 'should be true if in an entire cell is selected', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
+				);
+
+				expect( command.isEnabled ).to.be.true;
+			} );
+
+			it( 'should be false if multiple cells are selected', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				expect( command.isEnabled ).to.be.false;
+			} );
+
 			it( 'should be false if not in cell', () => {
 				setData( model, '<paragraph>11[]</paragraph>' );
 

+ 7 - 4
packages/ckeditor5-table/tests/manual/rtl.js

@@ -8,18 +8,21 @@
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 
+import TableProperties from '../../src/tableproperties';
+import TableCellProperties from '../../src/tablecellproperties';
+
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet ],
+		plugins: [ ArticlePluginSet, TableProperties, TableCellProperties ],
 		language: {
-			ui: 'en',
-			content: 'ar'
+			ui: 'ar',
+			content: 'en'
 		},
 		toolbar: [
 			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
 		],
 		table: {
-			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ],
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ],
 			tableToolbar: [ 'bold', 'italic' ]
 		}
 	} )

+ 25 - 1
packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js

@@ -474,7 +474,7 @@ describe( 'table cell properties', () => {
 							expect( toolbar.ariaLabel ).to.equal( 'Horizontal text alignment toolbar' );
 						} );
 
-						it( 'should bring alignment buttons', () => {
+						it( 'should bring alignment buttons in the right order (left-to-right UI)', () => {
 							expect( toolbar.items.map( ( { label } ) => label ) ).to.have.ordered.members( [
 								'Align cell text to the left',
 								'Align cell text to the center',
@@ -487,6 +487,30 @@ describe( 'table cell properties', () => {
 							] );
 						} );
 
+						it( 'should bring alignment buttons in the right order (right-to-left UI)', () => {
+							// Creates its own local instances of locale, view and toolbar.
+							const locale = {
+								t: val => val,
+								uiLanguageDirection: 'rtl',
+								contentLanguageDirection: 'rtl'
+							};
+							const view = new TableCellPropertiesView( locale, VIEW_OPTIONS );
+							const toolbar = view.horizontalAlignmentToolbar;
+
+							expect( toolbar.items.map( ( { label } ) => label ) ).to.have.ordered.members( [
+								'Align cell text to the right',
+								'Align cell text to the center',
+								'Align cell text to the left',
+								'Justify cell text'
+							] );
+
+							expect( toolbar.items.map( ( { isOn } ) => isOn ) ).to.have.ordered.members( [
+								true, false, false, false
+							] );
+
+							view.destroy();
+						} );
+
 						it( 'should change the #horizontalAlignment value', () => {
 							toolbar.items.last.fire( 'execute' );
 							expect( view.horizontalAlignment ).to.equal( 'justify' );

+ 24 - 1
packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

@@ -439,7 +439,7 @@ describe( 'table properties', () => {
 							expect( toolbar.ariaLabel ).to.equal( 'Table alignment toolbar' );
 						} );
 
-						it( 'should bring alignment buttons', () => {
+						it( 'should bring alignment buttons in the right order (left-to-right UI)', () => {
 							expect( toolbar.items.map( ( { label } ) => label ) ).to.have.ordered.members( [
 								'Align table to the left',
 								'Center table',
@@ -451,6 +451,29 @@ describe( 'table properties', () => {
 							] );
 						} );
 
+						it( 'should bring alignment buttons in the right order (right-to-left UI)', () => {
+							// Creates its own local instances of locale, view and toolbar.
+							const locale = {
+								t: val => val,
+								uiLanguageDirection: 'rtl',
+								contentLanguageDirection: 'rtl'
+							};
+							const view = new TablePropertiesView( locale, VIEW_OPTIONS );
+							const toolbar = view.alignmentToolbar;
+
+							expect( toolbar.items.map( ( { label } ) => label ) ).to.have.ordered.members( [
+								'Align table to the right',
+								'Center table',
+								'Align table to the left'
+							] );
+
+							expect( toolbar.items.map( ( { isOn } ) => isOn ) ).to.have.ordered.members( [
+								false, true, false
+							] );
+
+							view.destroy();
+						} );
+
 						it( 'should change the #horizontalAlignment value', () => {
 							toolbar.items.last.fire( 'execute' );
 							expect( view.alignment ).to.equal( 'right' );

+ 229 - 167
packages/ckeditor5-table/tests/tableselection-integration.js

@@ -5,207 +5,269 @@
 
 /* globals document */
 
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
 
 import TableEditing from '../src/tableediting';
 import TableSelection from '../src/tableselection';
+import TableClipboard from '../src/tableclipboard';
+
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
 import { modelTable } from './_utils/utils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
-import Delete from '@ckeditor/ckeditor5-typing/src/delete';
-import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import Input from '@ckeditor/ckeditor5-typing/src/input';
 import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 
-describe( 'table selection', () => {
+describe( 'TableSelection - integration', () => {
 	let editor, model, tableSelection, modelRoot, element, viewDocument;
 
-	describe( 'TableSelection - input integration', () => {
-		afterEach( async () => {
-			element.remove();
-			await editor.destroy();
+	afterEach( async () => {
+		element.remove();
+		await editor.destroy();
+	} );
+
+	describe( 'on delete', () => {
+		beforeEach( async () => {
+			await setupEditor( [ Delete ] );
 		} );
 
-		describe( 'on delete', () => {
-			beforeEach( async () => {
-				await setupEditor( [ Delete ] );
+		it( 'should clear contents of the selected table cells and put selection in last cell on backward delete', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const domEventData = new DomEventData( viewDocument, {
+				preventDefault: sinon.spy()
+			}, {
+				direction: 'backward',
+				unit: 'character',
+				sequence: 1
 			} );
+			viewDocument.fire( 'delete', domEventData );
 
-			it( 'should clear contents of the selected table cells and put selection in last cell on backward delete', () => {
-				tableSelection._setCellSelection(
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
-				);
-
-				const domEventData = new DomEventData( viewDocument, {
-					preventDefault: sinon.spy()
-				}, {
-					direction: 'backward',
-					unit: 'character',
-					sequence: 1
-				} );
-				viewDocument.fire( 'delete', domEventData );
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '', '', '13' ],
+				[ '', '[]', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
 
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '', '', '13' ],
-					[ '', '[]', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
+		it( 'should clear contents of the selected table cells and put selection in last cell on forward delete', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const domEventData = new DomEventData( viewDocument, {
+				preventDefault: sinon.spy()
+			}, {
+				direction: 'forward',
+				unit: 'character',
+				sequence: 1
 			} );
+			viewDocument.fire( 'delete', domEventData );
 
-			it( 'should clear contents of the selected table cells and put selection in last cell on forward delete', () => {
-				tableSelection._setCellSelection(
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
-				);
-
-				const domEventData = new DomEventData( viewDocument, {
-					preventDefault: sinon.spy()
-				}, {
-					direction: 'forward',
-					unit: 'character',
-					sequence: 1
-				} );
-				viewDocument.fire( 'delete', domEventData );
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]', '', '13' ],
+				[ '', '', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
 
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '[]', '', '13' ],
-					[ '', '', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
+		it( 'should not interfere with default key handler if no table selection', () => {
+			setModelData( model, modelTable( [
+				[ '11[]', '12', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+
+			const domEventData = new DomEventData( viewDocument, {
+				preventDefault: sinon.spy()
+			}, {
+				direction: 'backward',
+				unit: 'character',
+				sequence: 1
 			} );
+			viewDocument.fire( 'delete', domEventData );
 
-			it( 'should not interfere with default key handler if no table selection', () => {
-				setModelData( model, modelTable( [
-					[ '11[]', '12', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
-
-				const domEventData = new DomEventData( viewDocument, {
-					preventDefault: sinon.spy()
-				}, {
-					direction: 'backward',
-					unit: 'character',
-					sequence: 1
-				} );
-				viewDocument.fire( 'delete', domEventData );
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '1[]', '12', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
 
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '1[]', '12', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
+		it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete backwards)', () => {
+			const selection = model.createSelection( [
+				model.createRange(
+					model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
+					model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
+				),
+				model.createRange(
+					model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
+					model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
+				)
+			] );
+
+			model.change( writer => {
+				model.deleteContent( selection );
+				writer.setSelection( selection );
 			} );
 
-			it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete backwards)', () => {
-				const selection = model.createSelection( [
-					model.createRange(
-						model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
-						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
-					),
-					model.createRange(
-						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
-						model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
-					)
-				] );
-
-				model.change( writer => {
-					model.deleteContent( selection );
-					writer.setSelection( selection );
-				} );
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '', '[]', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
 
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '', '[]', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
+		it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete forwards)', () => {
+			const selection = model.createSelection( [
+				model.createRange(
+					model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
+					model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
+				),
+				model.createRange(
+					model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
+					model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
+				)
+			] );
+
+			model.change( writer => {
+				model.deleteContent( selection, {
+					direction: 'forward'
+				} );
+				writer.setSelection( selection );
 			} );
 
-			it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete forwards)', () => {
-				const selection = model.createSelection( [
-					model.createRange(
-						model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
-						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
-					),
-					model.createRange(
-						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
-						model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
-					)
-				] );
-
-				model.change( writer => {
-					model.deleteContent( selection, {
-						direction: 'forward'
-					} );
-					writer.setSelection( selection );
-				} );
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '[]', '', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
+	} );
 
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '[]', '', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
-			} );
+	describe( 'on user input', () => {
+		beforeEach( async () => {
+			await setupEditor( [ Input ] );
 		} );
 
-		describe( 'on user input', () => {
-			beforeEach( async () => {
-				await setupEditor( [ Input ] );
-			} );
+		it( 'should clear contents of the selected table cells and put selection in last cell on user input', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
+
+			// Mutate at the place where the document selection was put; it's more realistic
+			// than mutating at some arbitrary position.
+			const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
+
+			viewDocument.fire( 'mutations', [
+				{
+					type: 'children',
+					oldChildren: [],
+					newChildren: [ new ViewText( viewDocument, 'x' ) ],
+					node: placeOfMutation
+				}
+			] );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ '', '', '13' ],
+				[ '', 'x[]', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
 
-			it( 'should clear contents of the selected table cells and put selection in last cell on user input', () => {
-				tableSelection._setCellSelection(
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
-				);
-
-				viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
-
-				// Mutate at the place where the document selection was put; it's more realistic
-				// than mutating at some arbitrary position.
-				const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
-
-				viewDocument.fire( 'mutations', [
-					{
-						type: 'children',
-						oldChildren: [],
-						newChildren: [ new ViewText( viewDocument, 'x' ) ],
-						node: placeOfMutation
-					}
-				] );
-
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ '', '', '13' ],
-					[ '', 'x[]', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
-			} );
+		it( 'should not interfere with default key handler if no table selection', () => {
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
+
+			// Mutate at the place where the document selection was put; it's more realistic
+			// than mutating at some arbitrary position.
+			const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
+
+			viewDocument.fire( 'mutations', [
+				{
+					type: 'children',
+					oldChildren: [],
+					newChildren: [ new ViewText( viewDocument, 'x' ) ],
+					node: placeOfMutation
+				}
+			] );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ 'x[]11', '12', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
+	} );
+
+	describe( 'with other features', () => {
+		beforeEach( async () => {
+			await setupEditor( [ Clipboard, HorizontalLine ] );
+		} );
+
+		it( 'allows pasting over multi-cell selection', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
 
-			it( 'should not interfere with default key handler if no table selection', () => {
-				viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
-
-				// Mutate at the place where the document selection was put; it's more realistic
-				// than mutating at some arbitrary position.
-				const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
-
-				viewDocument.fire( 'mutations', [
-					{
-						type: 'children',
-						oldChildren: [],
-						newChildren: [ new ViewText( viewDocument, 'x' ) ],
-						node: placeOfMutation
-					}
-				] );
-
-				assertEqualMarkup( getModelData( model ), modelTable( [
-					[ 'x[]11', '12', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				] ) );
+			const dataTransferMock = {
+				getData: sinon.stub().withArgs( 'text/plain' ).returns( 'foo' )
+			};
+
+			editor.editing.view.document.fire( 'clipboardInput', {
+				dataTransfer: dataTransferMock,
+				stop: sinon.spy()
 			} );
+
+			assertEqualMarkup( getModelData( model ), modelTable( [
+				[ 'foo[]', '', '13' ],
+				[ '', '', '23' ],
+				[ '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'allows inserting a horizontal line over a multi-range selection', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			editor.execute( 'horizontalLine' );
+
+			assertEqualMarkup(
+				getModelData( model ),
+				'<table>' +
+					'<tableRow>' +
+						'<tableCell><horizontalLine></horizontalLine><paragraph>[]</paragraph></tableCell>' +
+						'<tableCell><paragraph></paragraph></tableCell>' +
+						'<tableCell><paragraph>13</paragraph></tableCell>' +
+					'</tableRow>' +
+					'<tableRow>' +
+						'<tableCell><paragraph></paragraph></tableCell>' +
+						'<tableCell><paragraph></paragraph></tableCell>' +
+						'<tableCell><paragraph>23</paragraph></tableCell>' +
+					'</tableRow>' +
+					'<tableRow>' +
+						'<tableCell><paragraph>31</paragraph></tableCell>' +
+						'<tableCell><paragraph>32</paragraph></tableCell>' +
+						'<tableCell><paragraph>33</paragraph></tableCell>' +
+					'</tableRow>' +
+				'</table>'
+			);
 		} );
 	} );
 
@@ -214,7 +276,7 @@ describe( 'table selection', () => {
 		document.body.appendChild( element );
 
 		editor = await ClassicTestEditor.create( element, {
-			plugins: [ TableEditing, TableSelection, Paragraph, ...plugins ]
+			plugins: [ TableEditing, TableSelection, TableClipboard, Paragraph, ...plugins ]
 		} );
 
 		model = editor.model;

+ 88 - 91
packages/ckeditor5-table/tests/tableselection.js

@@ -37,110 +37,107 @@ describe( 'table selection', () => {
 	afterEach( async () => {
 		await editor.destroy();
 	} );
+	describe( 'selection by shift+click', () => {
+		it( 'should...', () => {
+			// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+			// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+
+			// tableSelection.stopSelection();
+
+			// assertSelectedCells( model, [
+			// 	[ 1, 1, 0 ],
+			// 	[ 0, 0, 0 ],
+			// 	[ 0, 0, 0 ]
+			// ] );
+		} );
+	} );
+
+	describe( 'selection by mouse drag', () => {
+		it( 'should...', () => {
+			// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+			// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
 
-	describe( 'TableSelection', () => {
-		describe( 'selection by shift+click', () => {
-			it( 'should...', () => {
-				// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+			// tableSelection.stopSelection();
 
-				// tableSelection.stopSelection();
+			// assertSelectedCells( model, [
+			// 	[ 1, 1, 0 ],
+			// 	[ 0, 0, 0 ],
+			// 	[ 0, 0, 0 ]
+			// ] );
+		} );
+	} );
 
-				// assertSelectedCells( model, [
-				// 	[ 1, 1, 0 ],
-				// 	[ 0, 0, 0 ],
-				// 	[ 0, 0, 0 ]
-				// ] );
-			} );
+	describe( 'getSelectedTableCells()', () => {
+		it( 'should return nothing if selection is not started', () => {
+			expect( tableSelection.getSelectedTableCells() ).to.be.null;
 		} );
 
-		describe( 'selection by mouse drag', () => {
-			it( 'should...', () => {
-				// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
+		it( 'should return two table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
 
-				// tableSelection.stopSelection();
+			tableSelection._setCellSelection(
+				firstCell,
+				lastCell
+			);
 
-				// assertSelectedCells( model, [
-				// 	[ 1, 1, 0 ],
-				// 	[ 0, 0, 0 ],
-				// 	[ 0, 0, 0 ]
-				// ] );
-			} );
+			expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+				firstCell, lastCell
+			] );
 		} );
 
-		describe( 'getSelectedTableCells()', () => {
-			it( 'should return nothing if selection is not started', () => {
-				expect( tableSelection.getSelectedTableCells() ).to.be.null;
-			} );
-
-			it( 'should return two table cells', () => {
-				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
-				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
-
-				tableSelection._setCellSelection(
-					firstCell,
-					lastCell
-				);
-
-				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
-					firstCell, lastCell
-				] );
-			} );
-
-			it( 'should return four table cells for diagonal selection', () => {
-				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
-				const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
-
-				tableSelection._setCellSelection(
-					firstCell,
-					lastCell
-				);
-
-				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
-					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell
-				] );
-			} );
-
-			it( 'should return row table cells', () => {
-				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
-				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
-
-				tableSelection._setCellSelection(
-					firstCell,
-					lastCell
-				);
-
-				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
-					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell
-				] );
-			} );
-
-			it( 'should return column table cells', () => {
-				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
-				const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
-
-				tableSelection._setCellSelection( firstCell, lastCell );
-
-				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
-					firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell
-				] );
-			} );
+		it( 'should return four table cells for diagonal selection', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
+
+			tableSelection._setCellSelection(
+				firstCell,
+				lastCell
+			);
+
+			expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+				firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell
+			] );
 		} );
 
-		describe( 'getSelectionAsFragment()', () => {
-			it( 'should return undefined if no table cells are selected', () => {
-				expect( tableSelection.getSelectionAsFragment() ).to.be.null;
-			} );
+		it( 'should return row table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
+
+			tableSelection._setCellSelection(
+				firstCell,
+				lastCell
+			);
+
+			expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+				firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell
+			] );
+		} );
+
+		it( 'should return column table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
+				firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell
+			] );
+		} );
+	} );
+
+	describe( 'getSelectionAsFragment()', () => {
+		it( 'should return undefined if no table cells are selected', () => {
+			expect( tableSelection.getSelectionAsFragment() ).to.be.null;
+		} );
 
-			it( 'should return document fragment for selected table cells', () => {
-				tableSelection._setCellSelection(
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
-				);
+		it( 'should return document fragment for selected table cells', () => {
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
 
-				expect( tableSelection.getSelectionAsFragment() ).to.be.instanceOf( DocumentFragment );
-			} );
+			expect( tableSelection.getSelectionAsFragment() ).to.be.instanceOf( DocumentFragment );
 		} );
 	} );
 } );

+ 24 - 1
packages/ckeditor5-table/tests/ui/colorinputview.js

@@ -73,7 +73,6 @@ describe( 'ColorInputView', () => {
 			it( 'should be created', () => {
 				expect( view._dropdownView ).to.be.instanceOf( DropdownView );
 				expect( view._dropdownView.buttonView.element.classList.contains( 'ck-input-color__button' ) ).to.be.true;
-				expect( view._dropdownView.panelPosition ).to.equal( 'sw' );
 			} );
 
 			it( 'should bind #isEnabled to the view\'s #isReadOnly', () => {
@@ -107,6 +106,30 @@ describe( 'ColorInputView', () => {
 			it( 'should have the remove color button', () => {
 				expect( view._dropdownView.panelView.children.first ).to.be.instanceOf( ButtonView );
 			} );
+
+			describe( 'position', () => {
+				it( 'should be SouthWest in LTR', () => {
+					locale.uiLanguageDirection = 'ltr';
+					view = new ColorInputView( locale, {
+						colorDefinitions: DEFAULT_COLORS,
+						columns: 5
+					} );
+					view.render();
+
+					expect( view._dropdownView.panelPosition ).to.equal( 'sw' );
+				} );
+
+				it( 'should be SouthEast in RTL', () => {
+					locale.uiLanguageDirection = 'rtl';
+					view = new ColorInputView( locale, {
+						colorDefinitions: DEFAULT_COLORS,
+						columns: 5
+					} );
+					view.render();
+
+					expect( view._dropdownView.panelPosition ).to.equal( 'se' );
+				} );
+			} );
 		} );
 
 		describe( 'color grid', () => {

+ 298 - 0
packages/ckeditor5-table/tests/utils.js

@@ -0,0 +1,298 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import TableSelection from '../src/tableselection';
+import TableEditing from '../src/tableediting';
+
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { modelTable } from './_utils/utils';
+import {
+	getSelectedTableCells,
+	getTableCellsContainingSelection,
+	getSelectionAffectedTableCells
+} from '../src/utils';
+
+describe( 'table utils', () => {
+	let editor, model, tableSelection, modelRoot;
+
+	beforeEach( async () => {
+		editor = await VirtualTestEditor.create( {
+			plugins: [ TableEditing, TableSelection, Paragraph ]
+		} );
+
+		model = editor.model;
+		modelRoot = model.document.getRoot();
+		tableSelection = editor.plugins.get( TableSelection );
+
+		setModelData( model, modelTable( [
+			[ '11[]', '12', '13' ],
+			[ '21', '22', '23' ],
+			[ '31', '32', '33' ]
+		] ) );
+	} );
+
+	afterEach( async () => {
+		await editor.destroy();
+	} );
+
+	describe( 'getSelectedTableCells()', () => {
+		let selection;
+
+		beforeEach( () => {
+			selection = model.document.selection;
+		} );
+
+		it( 'should return an empty array when a collapsed selection is anchored in a cell', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRange( writer.createPositionAt( firstCell, 0 ) ) );
+			} );
+
+			expect( getSelectedTableCells( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return an empty array when a non-collapsed selection is anchored in a cell', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeIn( firstCell ) );
+			} );
+
+			expect( getSelectedTableCells( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return an empty array when a non-cell node is selected', () => {
+			const paragraph = modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] );
+
+			expect( paragraph.is( 'paragraph' ) ).to.be.true;
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( paragraph ) );
+			} );
+
+			expect( getSelectedTableCells( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return an empty array when an entire table is selected', () => {
+			const table = modelRoot.getNodeByPath( [ 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( table ) );
+			} );
+
+			expect( getSelectedTableCells( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return two table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( getSelectedTableCells( selection ) ) ).to.have.ordered.members( [
+				firstCell, lastCell
+			] );
+		} );
+
+		it( 'should return four table cells for diagonal selection', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( getSelectedTableCells( selection ) ) ).to.have.ordered.members( [
+				firstCell,
+				modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+				lastCell
+			] );
+		} );
+
+		it( 'should return row table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( getSelectedTableCells( selection ) ) ).to.have.ordered.members( [
+				firstCell,
+				modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+				lastCell
+			] );
+		} );
+
+		it( 'should return column table cells', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( getSelectedTableCells( selection ) ) ).to.have.ordered.members( [
+				firstCell,
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+				lastCell
+			] );
+		} );
+	} );
+
+	describe( 'getTableCellsContainingSelection()', () => {
+		let selection;
+
+		beforeEach( () => {
+			selection = model.document.selection;
+		} );
+
+		it( 'should return an array with a cell when a selection is anchored in it', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRange( writer.createPositionAt( firstCell, 0 ) ) );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.have.ordered.members( [ firstCell ] );
+		} );
+
+		it( 'should return an array with a cell when a selection range is anchored in its descendant', () => {
+			const cell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const paragraph = modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( paragraph, 0 ),
+					writer.createPositionAt( paragraph, 1 ),
+				) );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.have.ordered.members( [
+				cell
+			] );
+		} );
+
+		it( 'should return an array with cells when multiple collapsed selection ranges are anchored in them', () => {
+			const cellA = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const cellB = modelRoot.getNodeByPath( [ 0, 1, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( [
+					writer.createRange( writer.createPositionAt( cellA, 0 ) ),
+					writer.createRange( writer.createPositionAt( cellB, 0 ) )
+				] );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.have.ordered.members( [
+				cellA,
+				cellB
+			] );
+		} );
+
+		it( 'should return an array with cells when multiple non–collapsed selection ranges are anchored in them', () => {
+			const cellA = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const cellB = modelRoot.getNodeByPath( [ 0, 1, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( [
+					writer.createRangeIn( cellA ),
+					writer.createRangeIn( cellB )
+				] );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.have.ordered.members( [
+				cellA,
+				cellB
+			] );
+		} );
+
+		it( 'should return an empty array when an entire cell is selected', () => {
+			const cell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( cell ) );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return an empty array when an entire table is selected', () => {
+			const table = modelRoot.getNodeByPath( [ 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( table ) );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.be.empty;
+		} );
+
+		it( 'should return an empty array when unrelated elements host selection ranges', () => {
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const paragraph = modelRoot.getNodeByPath( [ 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRange( writer.createPositionAt( paragraph, 1 ) ) );
+			} );
+
+			expect( getTableCellsContainingSelection( selection ) ).to.be.empty;
+		} );
+	} );
+
+	describe( 'getSelectionAffectedTableCells()', () => {
+		let selection;
+
+		beforeEach( () => {
+			selection = model.document.selection;
+		} );
+
+		it( 'should return completely selected cells (if there are any)', () => {
+			const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
+
+			tableSelection._setCellSelection( firstCell, lastCell );
+
+			expect( Array.from( getSelectionAffectedTableCells( selection ) ) ).to.have.ordered.members( [
+				firstCell, lastCell
+			] );
+		} );
+
+		it( 'should return cells when selection ranges are starting in them', () => {
+			const cellA = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
+			const cellB = modelRoot.getNodeByPath( [ 0, 1, 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( [
+					writer.createRange( writer.createPositionAt( cellA, 0 ) ),
+					writer.createRange( writer.createPositionAt( cellB, 0 ) )
+				] );
+			} );
+
+			expect( getSelectionAffectedTableCells( selection ) ).to.have.ordered.members( [
+				cellA,
+				cellB
+			] );
+		} );
+
+		it( 'should return an empty array if no cells are selected and no selection ranges start in any cell', () => {
+			const table = modelRoot.getNodeByPath( [ 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( table ) );
+			} );
+
+			expect( getSelectionAffectedTableCells( selection ) ).to.be.empty;
+
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const paragraph = modelRoot.getNodeByPath( [ 0 ] );
+
+			model.change( writer => {
+				writer.setSelection( writer.createRange( writer.createPositionAt( paragraph, 1 ) ) );
+			} );
+
+			expect( getSelectionAffectedTableCells( selection ) ).to.be.empty;
+		} );
+	} );
+} );