瀏覽代碼

Merge remote-tracking branch 'origin/master' into i/6128

~ Conflicts:
~	src/tableselection/utils.js
Marek Lewandowski 5 年之前
父節點
當前提交
5142f68f84

+ 3 - 3
packages/ckeditor5-table/src/commands/splitcellcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getTableCellsInSelection } from '../tableselection/utils';
+import { getSelectionAffectedTableCells } from '../utils';
 
 /**
  * The split cell command.
@@ -46,7 +46,7 @@ export default class SplitCellCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const selectedCells = getTableCellsInSelection( this.editor.model.document.selection, true );
+		const selectedCells = getSelectionAffectedTableCells( this.editor.model.document.selection );
 
 		this.isEnabled = selectedCells.length === 1;
 	}
@@ -55,7 +55,7 @@ export default class SplitCellCommand extends Command {
 	 * @inheritDoc
 	 */
 	execute() {
-		const tableCell = getTableCellsInSelection( this.editor.model.document.selection, true )[ 0 ];
+		const tableCell = getSelectionAffectedTableCells( this.editor.model.document.selection )[ 0 ];
 		const isHorizontal = this.direction === 'horizontally';
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 

+ 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 );
-}

+ 8 - 4
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,7 +302,11 @@ 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 ) );
 

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

@@ -1,60 +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 { findAncestor } 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
- * @param {Boolean} [expandSelection=false] If set to `true` expands the selection to entire cell (if possible).
- * @returns {Array.<module:engine/model/element~Element>}
- */
-export function getTableCellsInSelection( selection, expandSelection = false ) {
-	const cells = [];
-
-	for ( const range of selection.getRanges() ) {
-		const element = range.getContainedElement();
-
-		if ( element && element.is( 'tableCell' ) ) {
-			cells.push( element );
-		}
-	}
-
-	if ( expandSelection ) {
-		const cellAncestor = findAncestor( 'tableCell', selection.getFirstPosition() );
-
-		if ( cellAncestor ) {
-			cells.push( cellAncestor );
-		}
-	}
-
-	return cells;
-}

+ 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 );
+}

+ 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;
+		} );
+	} );
+} );