瀏覽代碼

Merge branch 'master' into i/8204

Kamil Piechaczek 5 年之前
父節點
當前提交
a360837aa2

+ 9 - 0
docs/builds/guides/frameworks/angular.md

@@ -16,6 +16,15 @@ Currently, the CKEditor 5 component for Angular supports integrating CKEditor 5
 	While there is no support to integrate CKEditor 5 from source yet, you can still {@link builds/guides/development/custom-builds create a custom build of CKEditor 5} and include it in your Angular application.
 </info-box>
 
+## Supported Angular versions
+
+Because of the breaking changes in the Angular library output format, the `ckeditor5-angular` package is released in the following versions to support various Angular ecosystems:
+
+* Versions `1.x.x` &ndash; For **Angular 5-8** applications. Support for this version will end when the official support for Angular 8 is dropped (planned date: November 2020),
+* Versions `2.x.x` &ndash; For **Angular 9.1+** applications. This version is currently actively supported.
+
+All available versions are [listed on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular), where they can be pulled from.
+
 ## Quick start
 
 In your existing Angular project, install the [CKEditor 5 WYSIWYG editor component for Angular](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular):

+ 3 - 1
packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js

@@ -23,6 +23,7 @@ import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
 import '../../../theme/textalternativeform.css';
+import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
 
 /**
  * The TextAlternativeFormView class.
@@ -111,7 +112,8 @@ export default class TextAlternativeFormView extends View {
 			attributes: {
 				class: [
 					'ck',
-					'ck-text-alternative-form'
+					'ck-text-alternative-form',
+					'ck-responsive-form'
 				],
 
 				// https://github.com/ckeditor/ckeditor5-image/issues/40

+ 1 - 0
packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js

@@ -29,6 +29,7 @@ describe( 'TextAlternativeFormView', () => {
 
 			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-text-alternative-form' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-responsive-form' ) ).to.be.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 3 - 1
packages/ckeditor5-link/src/ui/linkactionsview.js

@@ -21,6 +21,7 @@ import { ensureSafeUrl } from '../utils';
 import unlinkIcon from '../../theme/icons/unlink.svg';
 import pencilIcon from '@ckeditor/ckeditor5-core/theme/icons/pencil.svg';
 import '../../theme/linkactions.css';
+import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
 
 /**
  * The link actions view class. This view displays the link preview, allows
@@ -117,7 +118,8 @@ export default class LinkActionsView extends View {
 			attributes: {
 				class: [
 					'ck',
-					'ck-link-actions'
+					'ck-link-actions',
+					'ck-responsive-form'
 				],
 
 				// https://github.com/ckeditor/ckeditor5-link/issues/90

+ 3 - 2
packages/ckeditor5-link/src/ui/linkformview.js

@@ -24,6 +24,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
 import '../../theme/linkform.css';
+import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
 
 /**
  * The link form view controller class.
@@ -133,10 +134,10 @@ export default class LinkFormView extends View {
 			}
 		} );
 
-		const classList = [ 'ck', 'ck-link-form' ];
+		const classList = [ 'ck', 'ck-link-form', 'ck-responsive-form' ];
 
 		if ( linkCommand.manualDecorators.length ) {
-			classList.push( 'ck-link-form_layout-vertical' );
+			classList.push( 'ck-link-form_layout-vertical', 'ck-vertical-form' );
 		}
 
 		this.setTemplate( {

+ 1 - 0
packages/ckeditor5-link/tests/ui/linkactionsview.js

@@ -26,6 +26,7 @@ describe( 'LinkActionsView', () => {
 		it( 'should create element from template', () => {
 			expect( view.element.classList.contains( 'ck' ) ).to.true;
 			expect( view.element.classList.contains( 'ck-link-actions' ) ).to.true;
+			expect( view.element.classList.contains( 'ck-responsive-form' ) ).to.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 1 - 0
packages/ckeditor5-link/tests/ui/linkformview.js

@@ -39,6 +39,7 @@ describe( 'LinkFormView', () => {
 		it( 'should create element from template', () => {
 			expect( view.element.classList.contains( 'ck' ) ).to.true;
 			expect( view.element.classList.contains( 'ck-link-form' ) ).to.true;
+			expect( view.element.classList.contains( 'ck-responsive-form' ) ).to.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 1 - 1
packages/ckeditor5-list/src/liststyleediting.js

@@ -213,7 +213,7 @@ function upcastListItemStyle() {
 		dispatcher.on( 'element:li', ( evt, data, conversionApi ) => {
 			const listParent = data.viewItem.parent;
 			const listStyle = listParent.getStyle( 'list-style-type' ) || DEFAULT_LIST_TYPE;
-			const listItem = data.modelRange.start.nodeAfter;
+			const listItem = data.modelRange.start.nodeAfter || data.modelRange.end.nodeBefore;
 
 			conversionApi.writer.setAttribute( 'listStyle', listStyle, listItem );
 		}, { priority: 'low' } );

+ 37 - 0
packages/ckeditor5-list/tests/liststyleediting.js

@@ -333,6 +333,43 @@ describe( 'ListStyleEditing', () => {
 					'<paragraph>Paragraph.</paragraph>'
 				);
 			} );
+
+			// See: #8262.
+			describe( 'list conversion with surrounding text nodes', () => {
+				let editor;
+
+				beforeEach( () => {
+					return VirtualTestEditor
+						.create( {
+							plugins: [ ListStyleEditing ]
+						} )
+						.then( newEditor => {
+							editor = newEditor;
+						} );
+				} );
+
+				afterEach( () => {
+					return editor.destroy();
+				} );
+
+				it( 'should convert a list if raw text is before the list', () => {
+					editor.setData( 'Foo<ul><li>Foo</li></ul>' );
+
+					expect( editor.getData() ).to.equal( '<p>Foo</p><ul><li>Foo</li></ul>' );
+				} );
+
+				it( 'should convert a list if raw text is after the list', () => {
+					editor.setData( '<ul><li>Foo</li></ul>Foo' );
+
+					expect( editor.getData() ).to.equal( '<ul><li>Foo</li></ul><p>Foo</p>' );
+				} );
+
+				it( 'should convert a list if it is surrender by two text nodes', () => {
+					editor.setData( 'Foo<ul><li>Foo</li></ul>Foo' );
+
+					expect( editor.getData() ).to.equal( '<p>Foo</p><ul><li>Foo</li></ul><p>Foo</p>' );
+				} );
+			} );
 		} );
 	} );
 

+ 3 - 1
packages/ckeditor5-media-embed/src/ui/mediaformview.js

@@ -23,6 +23,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
 import '../../theme/mediaform.css';
+import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
 
 /**
  * The media form view controller class.
@@ -132,7 +133,8 @@ export default class MediaFormView extends View {
 			attributes: {
 				class: [
 					'ck',
-					'ck-media-form'
+					'ck-media-form',
+					'ck-responsive-form'
 				],
 
 				tabindex: '-1'

+ 1 - 0
packages/ckeditor5-media-embed/tests/ui/mediaformview.js

@@ -35,6 +35,7 @@ describe( 'MediaFormView', () => {
 		it( 'should create element from template', () => {
 			expect( view.element.classList.contains( 'ck' ) ).to.true;
 			expect( view.element.classList.contains( 'ck-media-form' ) ).to.true;
+			expect( view.element.classList.contains( 'ck-responsive-form' ) ).to.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 186 - 148
packages/ckeditor5-table/src/tableclipboard.js

@@ -56,6 +56,8 @@ export default class TableClipboard extends Plugin {
 		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) );
 		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) );
 		this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } );
+
+		this.decorate( '_replaceTableSlotCell' );
 	}
 
 	/**
@@ -164,7 +166,7 @@ export default class TableClipboard extends Plugin {
 			// Content table to which we insert a pasted table.
 			const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );
 
-			const cellsToSelect = replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
+			const cellsToSelect = this._replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
 
 			if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) {
 				// Selection ranges must be sorted because the first and last selection ranges are considered
@@ -178,6 +180,189 @@ export default class TableClipboard extends Plugin {
 			}
 		} );
 	}
+
+	/**
+	 * Replaces the part of selectedTable with pastedTable.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} pastedTable
+	 * @param {Object} pastedDimensions
+	 * @param {Number} pastedDimensions.height
+	 * @param {Number} pastedDimensions.width
+	 * @param {module:engine/model/element~Element} selectedTable
+	 * @param {Object} selection
+	 * @param {Number} selection.firstColumn
+	 * @param {Number} selection.firstRow
+	 * @param {Number} selection.lastColumn
+	 * @param {Number} selection.lastRow
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @returns {Array.<module:engine/model/element~Element>}
+	 */
+	_replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
+		const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
+
+		// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
+		const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
+
+		const selectedTableMap = [ ...new TableWalker( selectedTable, {
+			startRow: selection.firstRow,
+			endRow: selection.lastRow,
+			startColumn: selection.firstColumn,
+			endColumn: selection.lastColumn,
+			includeAllSlots: true
+		} ) ];
+
+		// Selection must be set to pasted cells (some might be removed or new created).
+		const cellsToSelect = [];
+
+		// Store next cell insert position.
+		let insertPosition;
+
+		// Content table replace cells algorithm iterates over a selected table fragment and:
+		//
+		// - Removes existing table cells at current slot (location).
+		// - Inserts cell from a pasted table for a matched slots.
+		//
+		// This ensures proper table geometry after the paste
+		for ( const tableSlot of selectedTableMap ) {
+			const { row, column } = tableSlot;
+
+			// Save the insert position for current row start.
+			if ( column === selection.firstColumn ) {
+				insertPosition = tableSlot.getPositionBefore();
+			}
+
+			// Map current table slot location to an pasted table slot location.
+			const pastedRow = row - selection.firstRow;
+			const pastedColumn = column - selection.firstColumn;
+			const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
+
+			// Clone cell to insert (to duplicate its attributes and children).
+			// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
+			const cellToInsert = pastedCell ? writer.cloneElement( pastedCell ) : null;
+
+			// Replace the cell from the current slot with new table cell.
+			const newTableCell = this._replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer );
+
+			// The cell was only removed.
+			if ( !newTableCell ) {
+				continue;
+			}
+
+			// Trim the cell if it's row/col-spans would exceed selection area.
+			trimTableCellIfNeeded( newTableCell, row, column, selection.lastRow, selection.lastColumn, writer );
+
+			cellsToSelect.push( newTableCell );
+
+			insertPosition = writer.createPositionAfter( newTableCell );
+		}
+
+		// If there are any headings, all the cells that overlap from heading must be splitted.
+		const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
+		const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
+
+		const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
+		const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
+
+		if ( areHeadingRowsIntersectingSelection ) {
+			const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
+			const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
+
+			cellsToSelect.push( ...newCells );
+		}
+
+		if ( areHeadingColumnsIntersectingSelection ) {
+			const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
+			const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
+
+			cellsToSelect.push( ...newCells );
+		}
+
+		return cellsToSelect;
+	}
+
+	/**
+	 * Replaces a single table slot.
+	 *
+	 * @private
+	 * @param {module:table/tablewalker~TableSlot} tableSlot
+	 * @param {module:engine/model/element~Element} cellToInsert
+	 * @param {module:engine/model/position~Position} insertPosition
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @returns {module:engine/model/element~Element|null} Inserted table cell or null if slot should remain empty.
+	 */
+	_replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer ) {
+		const { cell, isAnchor } = tableSlot;
+
+		// If the slot is occupied by a cell in a selected table - remove it.
+		// The slot of this cell will be either:
+		// - Replaced by a pasted table cell.
+		// - Spanned by a previously pasted table cell.
+		if ( isAnchor ) {
+			writer.remove( cell );
+		}
+
+		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
+		if ( !cellToInsert ) {
+			return null;
+		}
+
+		writer.insert( cellToInsert, insertPosition );
+
+		return cellToInsert;
+	}
+}
+
+/**
+ * Extract table for pasting into table.
+ *
+ * @private
+ * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
+ * @param {module:engine/model/model~Model} model The editor model.
+ * @returns {module:engine/model/element~Element|null}
+ */
+export function getTableIfOnlyTableInContent( content, model ) {
+	if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
+		return null;
+	}
+
+	// Table passed directly.
+	if ( content.is( 'element', 'table' ) ) {
+		return content;
+	}
+
+	// We do not support mixed content when pasting table into table.
+	// See: https://github.com/ckeditor/ckeditor5/issues/6817.
+	if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
+		return content.getChild( 0 );
+	}
+
+	// If there are only whitespaces around a table then use that table for pasting.
+
+	const contentRange = model.createRangeIn( content );
+
+	for ( const element of contentRange.getItems() ) {
+		if ( element.is( 'element', 'table' ) ) {
+			// Stop checking if there is some content before table.
+			const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
+
+			if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
+				return null;
+			}
+
+			// Stop checking if there is some content after table.
+			const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
+
+			if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
+				return null;
+			}
+
+			// There wasn't any content neither before nor after.
+			return element;
+		}
+	}
+
+	return null;
 }
 
 // Prepares a table for pasting and returns adjusted selection dimensions.
@@ -246,109 +431,6 @@ function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, t
 	return selection;
 }
 
-// Replaces the part of selectedTable with pastedTable.
-//
-// @param {module:engine/model/element~Element} pastedTable
-// @param {Object} pastedDimensions
-// @param {Number} pastedDimensions.height
-// @param {Number} pastedDimensions.width
-// @param {module:engine/model/element~Element} selectedTable
-// @param {Object} selection
-// @param {Number} selection.firstColumn
-// @param {Number} selection.firstRow
-// @param {Number} selection.lastColumn
-// @param {Number} selection.lastRow
-// @param {module:engine/model/writer~Writer} writer
-// @returns {Array.<module:engine/model/element~Element>}
-function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
-	const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
-
-	// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
-	const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
-
-	const selectedTableMap = [ ...new TableWalker( selectedTable, {
-		startRow: selection.firstRow,
-		endRow: selection.lastRow,
-		startColumn: selection.firstColumn,
-		endColumn: selection.lastColumn,
-		includeAllSlots: true
-	} ) ];
-
-	// Selection must be set to pasted cells (some might be removed or new created).
-	const cellsToSelect = [];
-
-	// Store next cell insert position.
-	let insertPosition;
-
-	// Content table replace cells algorithm iterates over a selected table fragment and:
-	//
-	// - Removes existing table cells at current slot (location).
-	// - Inserts cell from a pasted table for a matched slots.
-	//
-	// This ensures proper table geometry after the paste
-	for ( const tableSlot of selectedTableMap ) {
-		const { row, column, cell, isAnchor } = tableSlot;
-
-		// Save the insert position for current row start.
-		if ( column === selection.firstColumn ) {
-			insertPosition = tableSlot.getPositionBefore();
-		}
-
-		// If the slot is occupied by a cell in a selected table - remove it.
-		// The slot of this cell will be either:
-		// - Replaced by a pasted table cell.
-		// - Spanned by a previously pasted table cell.
-		if ( isAnchor ) {
-			writer.remove( cell );
-		}
-
-		// Map current table slot location to an pasted table slot location.
-		const pastedRow = row - selection.firstRow;
-		const pastedColumn = column - selection.firstColumn;
-		const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
-
-		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
-		if ( !pastedCell ) {
-			continue;
-		}
-
-		// Clone cell to insert (to duplicate its attributes and children).
-		// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
-		const cellToInsert = writer.cloneElement( pastedCell );
-
-		// Trim the cell if it's row/col-spans would exceed selection area.
-		trimTableCellIfNeeded( cellToInsert, row, column, selection.lastRow, selection.lastColumn, writer );
-
-		writer.insert( cellToInsert, insertPosition );
-		cellsToSelect.push( cellToInsert );
-
-		insertPosition = writer.createPositionAfter( cellToInsert );
-	}
-
-	// If there are any headings, all the cells that overlap from heading must be splitted.
-	const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
-	const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
-
-	const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
-	const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
-
-	if ( areHeadingRowsIntersectingSelection ) {
-		const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
-		const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
-
-		cellsToSelect.push( ...newCells );
-	}
-
-	if ( areHeadingColumnsIntersectingSelection ) {
-		const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
-		const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
-
-		cellsToSelect.push( ...newCells );
-	}
-
-	return cellsToSelect;
-}
-
 // Expand table (in place) to expected size.
 function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
 	const tableWidth = tableUtils.getColumns( table );
@@ -369,50 +451,6 @@ function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
 	}
 }
 
-function getTableIfOnlyTableInContent( content, model ) {
-	if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
-		return null;
-	}
-
-	// Table passed directly.
-	if ( content.is( 'element', 'table' ) ) {
-		return content;
-	}
-
-	// We do not support mixed content when pasting table into table.
-	// See: https://github.com/ckeditor/ckeditor5/issues/6817.
-	if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
-		return content.getChild( 0 );
-	}
-
-	// If there are only whitespaces around a table then use that table for pasting.
-
-	const contentRange = model.createRangeIn( content );
-
-	for ( const element of contentRange.getItems() ) {
-		if ( element.is( 'element', 'table' ) ) {
-			// Stop checking if there is some content before table.
-			const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
-
-			if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
-				return null;
-			}
-
-			// Stop checking if there is some content after table.
-			const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
-
-			if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
-				return null;
-			}
-
-			// There wasn't any content neither before nor after.
-			return element;
-		}
-	}
-
-	return null;
-}
-
 // Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
 //
 // At given row & column location it might be one of:

+ 8 - 0
packages/ckeditor5-table/src/tableutils.js

@@ -27,6 +27,14 @@ export default class TableUtils extends Plugin {
 	}
 
 	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.decorate( 'insertColumns' );
+		this.decorate( 'insertRows' );
+	}
+
+	/**
 	 * Returns the table cell location as an object with table row and table column indexes.
 	 *
 	 * For instance, in the table below:

+ 146 - 1
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -23,7 +23,7 @@ import TableEditing from '../src/tableediting';
 import TableCellPropertiesEditing from '../src/tablecellproperties/tablecellpropertiesediting';
 import TableWalker from '../src/tablewalker';
 
-import TableClipboard from '../src/tableclipboard';
+import TableClipboard, { getTableIfOnlyTableInContent } from '../src/tableclipboard';
 
 describe( 'table clipboard', () => {
 	let editor, model, modelRoot, tableSelection, viewDocument, element;
@@ -478,6 +478,30 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
+		it( '#_replaceTableSlotCell() should be overridable', () => {
+			const tableClipboard = editor.plugins.get( 'TableClipboard' );
+
+			tableClipboard.on( '_replaceTableSlotCell', ( evt, args ) => {
+				const [ /* tableSlot */, cellToInsert, /* insertPosition */, writer ] = args;
+
+				if ( cellToInsert ) {
+					writer.setAttribute( 'foo', 'bar', cellToInsert );
+				}
+			}, { priority: 'high' } );
+
+			pasteTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ { contents: 'aa', foo: 'bar' }, { contents: 'ab', foo: 'bar' }, '02', '03' ],
+				[ { contents: 'ba', foo: 'bar' }, { contents: 'bb', foo: 'bar' }, '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
 		describe( 'single cell selected', () => {
 			beforeEach( () => {
 				setModelData( model, modelTable( [
@@ -3949,6 +3973,127 @@ describe( 'table clipboard', () => {
 		} );
 	} );
 
+	describe( 'getTableIfOnlyTableInContent helper', () => {
+		beforeEach( async () => {
+			await createEditor();
+		} );
+
+		it( 'should return null for no table provided', () => {
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const content = modelRoot.getChild( 0 );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for a text node provided', async () => {
+			setModelData( model, '<paragraph>foo</paragraph>' );
+
+			const content = modelRoot.getNodeByPath( [ 0, 0 ] );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for mixed content provided (table + paragraph)', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph>foo</paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return null for mixed content provided (paragraph + table)', () => {
+			setModelData( model,
+				'<paragraph>foo</paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+
+			expect( getTableIfOnlyTableInContent( content, model ) ).to.be.null;
+		} );
+
+		it( 'should return table element for mixed content provided (table + empty paragraph)', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph></paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for mixed content provided (table + empty paragraph)', () => {
+			setModelData( model,
+				'<paragraph></paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for mixed content provided (p + p + table + p)', () => {
+			setModelData( model,
+				'<paragraph></paragraph>' +
+				'<paragraph></paragraph>' +
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>' +
+				'<paragraph></paragraph>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for if table is the only element provided in document fragment', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = documentFragmentFromChildren( modelRoot );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		it( 'should return table element for if table is the only element provided directly', () => {
+			setModelData( model,
+				'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
+
+			const content = modelRoot.getChild( 0 );
+			const result = getTableIfOnlyTableInContent( content, model );
+
+			expect( result ).to.be.not.null;
+			expect( result.is( 'element', 'table' ) ).to.be.true;
+		} );
+
+		function documentFragmentFromChildren( element ) {
+			return model.change( writer => {
+				const documentFragment = writer.createDocumentFragment();
+
+				for ( const child of element.getChildren() ) {
+					writer.insert( writer.cloneElement( child ), documentFragment, 'end' );
+				}
+
+				return documentFragment;
+			} );
+		}
+	} );
+
 	async function createEditor( extraPlugins = [] ) {
 		editor = await ClassicTestEditor.create( element, {
 			plugins: [ TableEditing, TableClipboard, Paragraph, Clipboard, ...extraPlugins ]

+ 44 - 0
packages/ckeditor5-table/tests/tableutils.js

@@ -5,6 +5,7 @@
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
@@ -16,6 +17,8 @@ import TableUtils from '../src/tableutils';
 describe( 'TableUtils', () => {
 	let editor, model, root, tableUtils;
 
+	testUtils.createSinonSandbox();
+
 	beforeEach( () => {
 		return ModelTestEditor.create( {
 			plugins: [ Paragraph, TableEditing, TableUtils ]
@@ -51,6 +54,27 @@ describe( 'TableUtils', () => {
 	} );
 
 	describe( 'insertRows()', () => {
+		it( 'should be decorated', () => {
+			const spy = sinon.spy();
+
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.on( 'insertRows', spy );
+
+			tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '11[]', '12' ],
+				[ '', '' ],
+				[ '21', '22' ]
+			] ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
 		it( 'should insert row in given table at given index', () => {
 			setData( model, modelTable( [
 				[ '11[]', '12' ],
@@ -335,6 +359,26 @@ describe( 'TableUtils', () => {
 	} );
 
 	describe( 'insertColumns()', () => {
+		it( 'should be decorated', () => {
+			const spy = sinon.spy();
+
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			tableUtils.on( 'insertColumns', spy );
+
+			tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '11[]', '', '12' ],
+				[ '21', '', '22' ]
+			] ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
 		it( 'should insert column in given table at given index', () => {
 			setData( model, modelTable( [
 				[ '11[]', '12' ],

+ 0 - 67
packages/ckeditor5-theme-lark/theme/ckeditor5-image/textalternativeform.css

@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-@import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";
-@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";
-
-.ck.ck-text-alternative-form {
-	padding: var(--ck-spacing-standard);
-
-	&:focus {
-		/* https://github.com/ckeditor/ckeditor5-link/issues/90 */
-		outline: none;
-	}
-
-	@mixin ck-dir ltr {
-		& > :not(:first-child) {
-			margin-left: var(--ck-spacing-standard);
-		}
-	}
-
-	@mixin ck-dir rtl {
-		& > :not(:last-child) {
-			margin-left: var(--ck-spacing-standard);
-		}
-	}
-
-	@mixin ck-media-phone {
-		padding: 0;
-		width: calc(.8 * var(--ck-input-text-width));
-
-		& .ck-labeled-field-view {
-			margin: var(--ck-spacing-standard) var(--ck-spacing-standard) 0;
-
-			& .ck-input-text {
-				min-width: 0;
-				width: 100%;
-			}
-		}
-
-		& .ck-button {
-			padding: var(--ck-spacing-standard);
-			margin-top: var(--ck-spacing-standard);
-
-			border-radius: 0;
-			border: 0;
-			border-top: 1px solid var(--ck-color-base-border);
-
-			@mixin ck-dir ltr {
-				margin-left: 0;
-
-				&:first-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
-			}
-
-			@mixin ck-dir rtl {
-				margin-left: 0;
-
-				&:last-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
-			}
-		}
-	}
-}

+ 0 - 25
packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkactions.css

@@ -11,8 +11,6 @@
 @import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";
 
 .ck.ck-link-actions {
-	padding: var(--ck-spacing-standard);
-
 	& .ck-button.ck-link-actions__preview {
 		padding-left: 0;
 		padding-right: 0;
@@ -52,11 +50,6 @@
 		}
 	}
 
-	&:focus {
-		/* https://github.com/ckeditor/ckeditor5-link/issues/90 */
-		outline: none;
-	}
-
 	@mixin ck-dir ltr {
 		& .ck-button:not(:first-child) {
 			margin-left: var(--ck-spacing-standard);
@@ -70,9 +63,6 @@
 	}
 
 	@mixin ck-media-phone {
-		padding: 0;
-		width: calc(.8 * var(--ck-input-text-width));
-
 		& .ck-button.ck-link-actions__preview {
 			margin: var(--ck-spacing-standard) var(--ck-spacing-standard) 0;
 
@@ -83,27 +73,12 @@
 		}
 
 		& .ck-button:not(.ck-link-actions__preview) {
-			padding: var(--ck-spacing-standard);
-			margin-top: var(--ck-spacing-standard);
-
-			border-radius: 0;
-			border: 0;
-			border-top: 1px solid var(--ck-color-base-border);
-
 			@mixin ck-dir ltr {
 				margin-left: 0;
-
-				&:first-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
 			}
 
 			@mixin ck-dir rtl {
 				margin-left: 0;
-
-				&:last-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
 			}
 		}
 	}

+ 0 - 65
packages/ckeditor5-theme-lark/theme/ckeditor5-link/linkform.css

@@ -3,69 +3,8 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-@import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";
 @import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";
 
-.ck.ck-link-form {
-	padding: var(--ck-spacing-standard);
-
-	&:focus {
-		/* https://github.com/ckeditor/ckeditor5-link/issues/90 */
-		outline: none;
-	}
-
-	@mixin ck-dir ltr {
-		& > :not(:first-child) {
-			margin-left: var(--ck-spacing-standard);
-		}
-	}
-
-	@mixin ck-dir rtl {
-		& > :not(:last-child) {
-			margin-left: var(--ck-spacing-standard);
-		}
-	}
-
-	@mixin ck-media-phone {
-		padding: 0;
-		width: calc(.8 * var(--ck-input-text-width));
-
-		& .ck-labeled-field-view {
-			margin: var(--ck-spacing-standard) var(--ck-spacing-standard) 0;
-
-			& .ck-input-text {
-				min-width: 0;
-				width: 100%;
-			}
-		}
-
-		& .ck-button {
-			padding: var(--ck-spacing-standard);
-			margin-top: var(--ck-spacing-standard);
-
-			border-radius: 0;
-			border: 0;
-			border-top: 1px solid var(--ck-color-base-border);
-
-			@mixin ck-dir ltr {
-				margin-left: 0;
-
-				&:first-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
-			}
-
-			@mixin ck-dir rtl {
-				margin-left: 0;
-
-				&:last-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
-			}
-		}
-	}
-}
-
 /*
  * Style link form differently when manual decorators are available.
  * See: https://github.com/ckeditor/ckeditor5-link/issues/186.
@@ -93,10 +32,6 @@
 
 		@mixin ck-dir ltr {
 			margin-left: 0;
-
-			&:first-of-type {
-				border-right: 1px solid var(--ck-color-base-border);
-			}
 		}
 
 		@mixin ck-dir rtl {

+ 12 - 6
packages/ckeditor5-theme-lark/theme/ckeditor5-media-embed/mediaform.css

@@ -6,10 +6,15 @@
 @import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";
 @import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";
 
-.ck.ck-media-form {
+.ck-vertical-form .ck-button::after {
+	border-right: 1px solid var(--ck-color-base-border);
+}
+
+.ck.ck-responsive-form {
 	padding: var(--ck-spacing-standard);
 
 	&:focus {
+		/* https://github.com/ckeditor/ckeditor5-link/issues/90 */
 		outline: none;
 	}
 
@@ -43,7 +48,8 @@
 			}
 		}
 
-		& .ck-button {
+		/* 'button' selector must be used because those styles shouldn't be added to other elements, like 'a'. Example in LinkActionsView. */
+		& button.ck-button {
 			padding: var(--ck-spacing-standard);
 			margin-top: var(--ck-spacing-standard);
 
@@ -53,10 +59,6 @@
 
 			@mixin ck-dir ltr {
 				margin-left: 0;
-
-				&:first-of-type {
-					border-right: 1px solid var(--ck-color-base-border);
-				}
 			}
 
 			@mixin ck-dir rtl {
@@ -66,6 +68,10 @@
 					border-right: 1px solid var(--ck-color-base-border);
 				}
 			}
+
+			&::after {
+				border-right: 1px solid var(--ck-color-base-border);
+			}
 		}
 	}
 }

+ 30 - 0
packages/ckeditor5-ui/theme/components/responsive-form/responsiveform.css

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+@import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";
+
+.ck-vertical-form .ck-button::after {
+	content: "";
+	width: 0;
+	position: absolute;
+	right: -1px;
+	top: var(--ck-spacing-small);
+	bottom: var(--ck-spacing-small);
+	z-index: 1;
+}
+
+.ck.ck-responsive-form {
+	@mixin ck-media-phone {
+		& .ck-button::after {
+			content: "";
+			width: 0;
+			position: absolute;
+			right: -1px;
+			top: var(--ck-spacing-small);
+			bottom: var(--ck-spacing-small);
+			z-index: 1;
+		}
+	}
+}