Ver código fonte

Merge branch 'master' into i/6443

Aleksander Nowodzinski 5 anos atrás
pai
commit
5a3ba5c0cd

+ 1 - 1
CHANGELOG.md

@@ -44,7 +44,7 @@ But we did not stop there, as the release comes with several bug fixes, too:
 * [Potential editor crash when removing a column](https://github.com/ckeditor/ckeditor5/issues/6789).
 * [Editor crash when inserting a table row or column with another widget selected in the cell](https://github.com/ckeditor/ckeditor5/issues/6607).
 
-Blog post coming soon...
+Read more in the blog post: https://ckeditor.com/blog/CKEditor-5-v19.1.1-with-table-enhancements-typing-around-widgets-and-print-to-PDF-feature/
 
 ### Collaboration features
 

+ 1 - 1
packages/ckeditor5-build-decoupled-document/tests/ckeditor.js

@@ -200,7 +200,7 @@ describe( 'DecoupledEditor build', () => {
 
 				editor.setData( data );
 				expect( editor.getData() ).to.equal( data );
-				expect( editor.model.document.selection.getAttribute( 'fontFamily' ) ).to.equal( 'Georgia' );
+				expect( editor.model.document.selection.getAttribute( 'fontFamily' ) ).to.equal( 'Georgia, serif' );
 			} );
 
 			it( 'font background color works', () => {

+ 8 - 8
packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md

@@ -46,7 +46,7 @@ While this would be incorrect:
 
 ## Defining additional semantics
 
-In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties a feature author may declare how a certain element should be treated by other features and the engine.
+In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties, a feature author may declare how a certain element should be treated by other features and the engine.
 
 ### Limit elements
 
@@ -55,9 +55,9 @@ Consider a feature like an image caption. The caption text area should construct
 * A selection that starts inside should not end outside.
 * Pressing <kbd>Backspace</kbd> or <kbd>Delete</kbd> should not delete the area. Pressing <kbd>Enter</kbd> should not split the area.
 
-It should also act as a boundary for external actions. This is mostly enforced by a selection post-fixer that ensures that a selection that starts outside, should not end inside. That means that most actions will either apply to the "outside" of such an element or to a content inside it.
+It should also act as a boundary for external actions. This is mostly enforced by a selection post-fixer that ensures that a selection that starts outside, should not end inside. It means that most actions will either apply to the "outside" of such an element or to the content inside it.
 
-Taken these characteristics, the image caption should be defined as limit element by using the {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} property.
+Taken these characteristics, the image caption should be defined as a limit element by using the {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} property.
 
 ```js
 schema.register( 'myCaption', {
@@ -73,9 +73,9 @@ The engine and various features then check it via {@link module:engine/model/sch
 
 ### Object elements
 
-For the image caption as in the example above it does not make much sense to select the caption box, then copy or drag it somewhere else.
+For an image caption like in the example above it does not make much sense to select the caption box, then copy or drag it somewhere else.
 
-A caption without the image that it describes does not make much sense. However, the image is more self-sufficient. Most likely users should be able to select the entire image (with all its internals), then copy or move it around. {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} should be used to mark such behavior.
+A caption without the image that it describes makes little sense. However, the image is more self-sufficient. Most likely users should be able to select the entire image (with all its internals), then copy or move it around. The {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property should be used to mark such behavior.
 
 ```js
 schema.register( 'myImage', {
@@ -95,7 +95,7 @@ The {@link module:engine/model/schema~Schema#isObject `Schema#isObject()`} can l
 
 Generally speaking, content is usually made out of blocks like paragraphs, list items, images, headings, etc. All these elements should be marked as blocks by using {@link module:engine/model/schema~SchemaItemDefinition#isBlock `isBlock`}.
 
-It is important to remember that a block should not allow another block inside. Container elements like `<blockQuote>` which can contain other block elements should not be marked as blocks.
+It is important to remember that a block should not allow another block inside. Container elements like `<blockQuote>`, which can contain other block elements, should not be marked as blocks.
 
 <info-box>
 	There is also the `$block` generic item which has `isBlock` set to `true`. Most block type items will inherit from `$block` (through `inheritAllFrom`).
@@ -103,11 +103,11 @@ It is important to remember that a block should not allow another block inside.
 
 ### Inline elements
 
-In the editor, all HTML formatting elements such as `<strong>` or `<code>` are represented by text attributes. Therefore, inline model elements are not to be used for this scenarios.
+In the editor, all HTML formatting elements such as `<strong>` or `<code>` are represented by text attributes. Therefore, inline model elements are not supposed to be used for these scenarios.
 
 Currently, the {@link module:engine/model/schema~SchemaItemDefinition#isInline `isInline`} property is used for the `$text` token (so, text nodes) and elements such as `<softBreak>` or placeholder elements such as in the {@link framework/guides/tutorials/implementing-an-inline-widget Implementing an inline widget} tutorial.
 
-The support for inline elements in CKEditor 5 is so far limited to self-contained elements. This is &mdash; all elements marked with `isInline` should also be marked with `isObject`.
+The support for inline elements in CKEditor 5 is so far limited to self-contained elements. Because of this, all elements marked with `isInline` should also be marked with `isObject`.
 
 ## Generic items
 

+ 39 - 39
packages/ckeditor5-engine/src/model/schema.js

@@ -26,9 +26,9 @@ import TreeWalker from './treewalker';
  *
  * Read more about the schema in:
  *
- * * {@glink framework/guides/architecture/editing-engine#schema "Schema"} section of the
- * {@glink framework/guides/architecture/editing-engine Introduction to the "Editing engine architecture"}.
- * * {@glink framework/guides/deep-dive/schema "Schema" deep dive} guide.
+ * * {@glink framework/guides/architecture/editing-engine#schema Schema} section of the
+ * {@glink framework/guides/architecture/editing-engine Introduction to the Editing engine architecture}.
+ * * {@glink framework/guides/deep-dive/schema Schema deep dive} guide.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -171,7 +171,7 @@ export default class Schema {
 	}
 
 	/**
-	 * Returns a definition of the given item or `undefined` if item is not registered.
+	 * Returns a definition of the given item or `undefined` if an item is not registered.
 	 *
 	 * This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
 	 * checking a list of all block elements, etc).
@@ -212,7 +212,7 @@ export default class Schema {
 
 	/**
 	 * Returns `true` if the given item is defined to be
-	 * a block by {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
+	 * a block by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
 	 *
 	 *		schema.isBlock( 'paragraph' ); // -> true
 	 *		schema.isBlock( '$root' ); // -> false
@@ -220,7 +220,7 @@ export default class Schema {
 	 *		const paragraphElement = writer.createElement( 'paragraph' );
 	 *		schema.isBlock( paragraphElement ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -234,18 +234,18 @@ export default class Schema {
 	/**
 	 * Returns `true` if the given item should be treated as a limit element.
 	 *
-	 * It considers the item to be a limit element if its
+	 * It considers an item to be a limit element if its
 	 * {@link module:engine/model/schema~SchemaItemDefinition}'s
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} or
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
-	 * were set to `true`.
+	 * was set to `true`.
 	 *
 	 *		schema.isLimit( 'paragraph' ); // -> false
 	 *		schema.isLimit( '$root' ); // -> true
 	 *		schema.isLimit( editor.model.document.getRoot() ); // -> true
 	 *		schema.isLimit( 'image' ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -261,12 +261,12 @@ export default class Schema {
 	}
 
 	/**
-	 * Returns `true` if the given item is should be treated as an object element.
+	 * Returns `true` if the given item should be treated as an object element.
 	 *
-	 * It considers the item to be an object element if its
+	 * It considers an item to be an object element if its
 	 * {@link module:engine/model/schema~SchemaItemDefinition}'s
 	 * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
-	 * were set to `true`.
+	 * was set to `true`.
 	 *
 	 *		schema.isObject( 'paragraph' ); // -> false
 	 *		schema.isObject( 'image' ); // -> true
@@ -274,7 +274,7 @@ export default class Schema {
 	 *		const imageElement = writer.createElement( 'image' );
 	 *		schema.isObject( imageElement ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -287,7 +287,7 @@ export default class Schema {
 
 	/**
 	 * Returns `true` if the given item is defined to be
-	 * an inline element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
+	 * an inline element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
 	 *
 	 *		schema.isInline( 'paragraph' ); // -> false
 	 *		schema.isInline( 'softBreak' ); // -> true
@@ -295,7 +295,7 @@ export default class Schema {
 	 *		const text = writer.createText('foo' );
 	 *		schema.isInline( text ); // -> true
 	 *
-	 * See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the "Schema" deep dive}
+	 * See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive}
 	 * guide for more details.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
@@ -737,13 +737,13 @@ export default class Schema {
 	}
 
 	/**
-	 * Tries to find position ancestors that allows to insert given node.
+	 * Tries to find position ancestors that allow to insert a given node.
 	 * It starts searching from the given position and goes node by node to the top of the model tree
-	 * as long as {@link module:engine/model/schema~Schema#isLimit limit element},
-	 * {@link module:engine/model/schema~Schema#isObject object element} or top-most ancestor won't be reached.
+	 * as long as a {@link module:engine/model/schema~Schema#isLimit limit element}, an
+	 * {@link module:engine/model/schema~Schema#isObject object element} or a topmost ancestor is not reached.
 	 *
-	 * @param {module:engine/model/position~Position} position Position from searching will start.
-	 * @param {module:engine/model/node~Node|String} node Node for which allowed parent should be found or its name.
+	 * @param {module:engine/model/position~Position} position The position that the search will start from.
+	 * @param {module:engine/model/node~Node|String} node The node for which an allowed parent should be found or its name.
 	 * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
 	 */
 	findAllowedParent( position, node ) {
@@ -873,7 +873,7 @@ export default class Schema {
 	 * This is a helper function for {@link ~Schema#getValidRanges}.
 	 *
 	 * @private
-	 * @param {module:engine/model/range~Range} range Range to process.
+	 * @param {module:engine/model/range~Range} range The range to process.
 	 * @param {String} attribute The name of the attribute to check.
 	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
 	 */
@@ -907,7 +907,7 @@ mix( Schema, ObservableMixin );
 
 /**
  * Event fired when the {@link #checkChild} method is called. It allows plugging in
- * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * additional behavior, for example implementing rules which cannot be defined using the declarative
  * {@link module:engine/model/schema~SchemaItemDefinition} interface.
  *
  * **Note:** The {@link #addChildCheck} method is a more handy way to register callbacks. Internally,
@@ -916,7 +916,7 @@ mix( Schema, ObservableMixin );
  *
  * The {@link #checkChild} method fires an event because it is
  * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
- * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * use this event in various ways, but the most important use case is overriding standard behavior of the
  * `checkChild()` method. Let's see a typical listener template:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
@@ -927,12 +927,12 @@ mix( Schema, ObservableMixin );
  * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
  * parameter contains arguments passed to `checkChild( context, child )`. However, the `context` parameter is already
  * normalized to a {@link module:engine/model/schema~SchemaContext} instance and `child` to a
- * {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you don't have to worry about
+ * {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you do not have to worry about
  * the various ways how `context` and `child` may be passed to `checkChild()`.
  *
  * **Note:** `childDefinition` may be `undefined` if `checkChild()` was called with a non-registered element.
  *
- * So, in order to implement a rule "disallow `heading1` in `blockQuote`" you can add such a listener:
+ * So, in order to implement a rule "disallow `heading1` in `blockQuote`", you can add such a listener:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
  *			const context = args[ 0 ];
@@ -946,8 +946,8 @@ mix( Schema, ObservableMixin );
  *			}
  *		}, { priority: 'high' } );
  *
- * Allowing elements in specific contexts will be a far less common use case, because it's normally handled by
- * `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * Allowing elements in specific contexts will be a far less common use case, because it is normally handled by the
+ * `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario
  * where `listItem` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
  *
  *		schema.on( 'checkChild', ( evt, args ) => {
@@ -968,16 +968,16 @@ mix( Schema, ObservableMixin );
 
 /**
  * Event fired when the {@link #checkAttribute} method is called. It allows plugging in
- * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * additional behavior, for example implementing rules which cannot be defined using the declarative
  * {@link module:engine/model/schema~SchemaItemDefinition} interface.
  *
  * **Note:** The {@link #addAttributeCheck} method is a more handy way to register callbacks. Internally,
  * it registers a listener to this event but comes with a simpler API and it is the recommended choice
  * in most of the cases.
  *
- * The {@link #checkAttribute} method fires an event because it's
+ * The {@link #checkAttribute} method fires an event because it is
  * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
- * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * use this event in various ways, but the most important use case is overriding the standard behavior of the
  * `checkAttribute()` method. Let's see a typical listener template:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
@@ -987,10 +987,10 @@ mix( Schema, ObservableMixin );
  *
  * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
  * parameter contains arguments passed to `checkAttribute( context, attributeName )`. However, the `context` parameter is already
- * normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you don't have to worry about
+ * normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you do not have to worry about
  * the various ways how `context` may be passed to `checkAttribute()`.
  *
- * So, in order to implement a rule "disallow `bold` in a text which is in a `heading1` you can add such a listener:
+ * So, in order to implement a rule "disallow `bold` in a text which is in a `heading1`, you can add such a listener:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
  *			const context = args[ 0 ];
@@ -1004,8 +1004,8 @@ mix( Schema, ObservableMixin );
  *			}
  *		}, { priority: 'high' } );
  *
- * Allowing attributes in specific contexts will be a far less common use case, because it's normally handled by
- * `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * Allowing attributes in specific contexts will be a far less common use case, because it is normally handled by the
+ * `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario
  * where `bold` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
  *
  *		schema.on( 'checkAttribute', ( evt, args ) => {
@@ -1056,7 +1056,7 @@ mix( Schema, ObservableMixin );
  * {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
  *
  * Read more about the meaning of these types in the
- * {@glink framework/guides/deep-dive/schema#defining-additional-semantics Dedicated section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#defining-additional-semantics dedicated section of the Schema deep dive} guide.
  *
  * # Generic items
  *
@@ -1159,14 +1159,14 @@ mix( Schema, ObservableMixin );
  * Most block type items will inherit from `$block` (through `inheritAllFrom`).
  *
  * Read more about the block elements in the
- * {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isInline
  * Whether an item is "text-like" and should be treated as an inline node. Examples of inline elements:
  * `$text`, `softBreak` (`<br>`), etc.
  *
  * Read more about the inline elements in the
- * {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isLimit
  * It can be understood as whether this element should not be split by <kbd>Enter</kbd>.
@@ -1174,7 +1174,7 @@ mix( Schema, ObservableMixin );
  * a limit element are limited to its content.
  *
  * Read more about the limit elements in the
- * {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive} guide.
  *
  * @property {Boolean} isObject
  * Whether an item is "self-contained" and should be treated as a whole. Examples of object elements:
@@ -1184,7 +1184,7 @@ mix( Schema, ObservableMixin );
  * {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
  *
  * Read more about the object elements in the
- * {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the "Schema" deep dive} guide.
+ * {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive} guide.
  */
 
 /**

+ 1 - 1
packages/ckeditor5-font/src/fontfamily.js

@@ -120,7 +120,7 @@ export default class FontFamily extends Plugin {
  *
  * You can preserve pasted font family values by switching the `supportAllValues` option to `true`:
  *
- *		const fontSizeConfig = {
+ *		const fontFamilyConfig = {
  *			supportAllValues: true
  *		};
  *

+ 1 - 1
packages/ckeditor5-font/src/fontfamily/utils.js

@@ -66,7 +66,7 @@ function generateFontPreset( fontDefinition ) {
 
 	return {
 		title: firstFontName,
-		model: firstFontName,
+		model: cssFontNames,
 		view: {
 			name: 'span',
 			styles: {

+ 6 - 4
packages/ckeditor5-font/tests/fontfamily/fontfamilyediting.js

@@ -104,9 +104,9 @@ describe( 'FontFamilyEditing', () => {
 				} );
 
 				it( 'should convert defined fontFamily attribute values', () => {
-					setModelData( doc, '<paragraph>f<$text fontFamily="Arial">o</$text>o</paragraph>' );
+					setModelData( doc, '<paragraph>f<$text fontFamily="Arial, Helvetica, sans-serif">o</$text>o</paragraph>' );
 
-					expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial;">o</span>o</p>' );
+					expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial, Helvetica, sans-serif;">o</span>o</p>' );
 				} );
 			} );
 
@@ -174,10 +174,12 @@ describe( 'FontFamilyEditing', () => {
 		} );
 
 		it( 'should convert fontFamily attribute to configured complex preset', () => {
-			setModelData( doc, '<paragraph>f<$text fontFamily="Lucida Sans Unicode">o</$text>o</paragraph>' );
+			const fontFamily = '\'Lucida Sans Unicode\', \'Lucida Grande\', sans-serif';
+
+			setModelData( doc, `<paragraph>f<$text fontFamily="${ fontFamily }">o</$text>o</paragraph>` );
 
 			expect( editor.getData() )
-				.to.equal( '<p>f<span style="font-family:\'Lucida Sans Unicode\', \'Lucida Grande\', sans-serif;">o</span>o</p>' );
+				.to.equal( `<p>f<span style="font-family:${ fontFamily };">o</span>o</p>` );
 		} );
 
 		it( 'should convert fontFamily attribute from user defined settings', () => {

+ 50 - 28
packages/ckeditor5-font/tests/fontfamily/fontfamilyui.js

@@ -10,9 +10,11 @@ import FontFamilyUI from '../../src/fontfamily/fontfamilyui';
 
 import fontFamilyIcon from '../../theme/icons/font-family.svg';
 
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'FontFamilyUI', () => {
 	let editor, command, element;
@@ -97,47 +99,67 @@ describe( 'FontFamilyUI', () => {
 			expect( listView.items.map( item => item.children.first.isOn ) )
 				.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
 
-			command.value = 'Arial';
+			command.value = 'Arial, Helvetica, sans-serif';
 
 			// The second item is 'Arial' font family.
 			expect( listView.items.map( item => item.children.first.isOn ) )
 				.to.deep.equal( [ false, true, false, false, false, false, false, false, false ] );
 		} );
 
-		it( 'should activate current option in dropdown for full font family definitions', () => {
-			const element = document.createElement( 'div' );
-			document.body.appendChild( element );
+		describe( 'with supportAllValues=true', () => {
+			let editor, element, command, dropdown;
 
-			return ClassicTestEditor
-				.create( element, {
-					plugins: [ FontFamilyEditing, FontFamilyUI ],
-					fontSize: {
-						supportAllValues: true
-					}
-				} )
-				.then( editor => {
-					const command = editor.commands.get( 'fontFamily' );
-					const dropdown = editor.ui.componentFactory.create( 'fontFamily' );
+			beforeEach( async () => {
+				element = document.createElement( 'div' );
+				document.body.appendChild( element );
+
+				editor = await ClassicTestEditor
+					.create( element, {
+						plugins: [ Paragraph, FontFamilyEditing, FontFamilyUI ],
+						fontSize: {
+							supportAllValues: true
+						}
+					} );
+
+				command = editor.commands.get( 'fontFamily' );
+				dropdown = editor.ui.componentFactory.create( 'fontFamily' );
+			} );
+
+			afterEach( async () => {
+				await editor.destroy();
+				element.remove();
+			} );
 
-					const listView = dropdown.listView;
+			it( 'should activate the current option in the dropdown for full font family definitions', () => {
+				const listView = dropdown.listView;
+
+				command.value = undefined;
+
+				// The first item is 'default' font family.
+				expect( listView.items.map( item => item.children.first.isOn ) )
+					.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
+
+				command.value = '\'Courier New\', Courier, monospace';
 
-					command.value = undefined;
+				// The third item is 'Courier New' font family.
+				expect( listView.items.map( item => item.children.first.isOn ) )
+					.to.deep.equal( [ false, false, true, false, false, false, false, false, false ] );
+			} );
+
+			it( 'should apply the complete font-family value (list of font-families)', () => {
+				const listView = dropdown.listView;
+				const fontFamilyArialButton = listView.items.get( 1 ).children.first;
 
-					// The first item is 'default' font family.
-					expect( listView.items.map( item => item.children.first.isOn ) )
-						.to.deep.equal( [ true, false, false, false, false, false, false, false, false ] );
+				setModelData( editor.model, '<paragraph>f[oo]</paragraph>' );
 
-					command.value = '\'Courier New\', Courier, monospace';
+				fontFamilyArialButton.fire( 'execute' );
 
-					// The third item is 'Courier New' font family.
-					expect( listView.items.map( item => item.children.first.isOn ) )
-						.to.deep.equal( [ false, false, true, false, false, false, false, false, false ] );
+				expect( getModelData( editor.model ) ).to.equal(
+					'<paragraph>f[<$text fontFamily="Arial, Helvetica, sans-serif">oo</$text>]</paragraph>'
+				);
 
-					return editor.destroy();
-				} )
-				.then( () => {
-					element.remove();
-				} );
+				expect( editor.getData() ).to.equal( '<p>f<span style="font-family:Arial, Helvetica, sans-serif;">oo</span></p>' );
+			} );
 		} );
 
 		describe( 'model to command binding', () => {

+ 2 - 2
packages/ckeditor5-font/tests/fontfamily/utils.js

@@ -62,7 +62,7 @@ describe( 'FontFamily utils', () => {
 					},
 					{
 						title: 'Comic Sans MS',
-						model: 'Comic Sans MS',
+						model: '\'Comic Sans MS\', sans-serif',
 						view: {
 							name: 'span',
 							styles: {
@@ -73,7 +73,7 @@ describe( 'FontFamily utils', () => {
 					},
 					{
 						title: 'Lucida Console',
-						model: 'Lucida Console',
+						model: '\'Lucida Console\', \'Courier New\', Courier, monospace',
 						view: {
 							name: 'span',
 							styles: {

+ 2 - 1
packages/ckeditor5-font/tests/integration.js

@@ -37,7 +37,8 @@ describe( 'Integration test Font', () => {
 		it( 'should render one span element for all types of font features', () => {
 			setModelData( model,
 				'<paragraph>' +
-					'<$text fontColor="#123456" fontBackgroundColor="rgb(10,20,30)" fontSize="big" fontFamily="Arial">foo</$text>' +
+					'<$text fontColor="#123456" fontBackgroundColor="rgb(10,20,30)" fontSize="big" ' +
+						'fontFamily="Arial, Helvetica, sans-serif">foo</$text>' +
 				'</paragraph>'
 			);
 

+ 2 - 1
packages/ckeditor5-font/tests/manual/font-family.md

@@ -14,7 +14,8 @@ Try to:
 ### Converters mode
 
 The "Restricted value matching" option means that all font-family values that aren't defined in the plugin's configuration will be removed (e.g. when pasted from Google Docs).
-This behaviour can be disabled by selecting the "Disabled value matching" option.
+
+This behaviour can be disabled by selecting the "Disabled value matching" option, which sets ["`supportAllValues: true`"](https://ckeditor.com/docs/ckeditor5/latest/api/module_font_fontfamily-FontFamilyConfig.html#member-supportAllValues) in the font family configuration.
 
 The `Docs-Roboto, Arial` font-family is not specified in the plugin's configuration and should be restored to default font when the "Restricted value matching" option is selected.
 

+ 30 - 5
packages/ckeditor5-table/src/tableclipboard.js

@@ -120,7 +120,7 @@ export default class TableClipboard extends Plugin {
 		}
 
 		// We might need to crop table before inserting so reference might change.
-		let pastedTable = getTableIfOnlyTableInContent( content );
+		let pastedTable = getTableIfOnlyTableInContent( content, model );
 
 		if ( !pastedTable ) {
 			return;
@@ -336,7 +336,7 @@ function expandTableSize( table, expectedHeight, expectedWidth, writer, tableUti
 	}
 }
 
-function getTableIfOnlyTableInContent( content ) {
+function getTableIfOnlyTableInContent( content, model ) {
 	// Table passed directly.
 	if ( content.is( 'table' ) ) {
 		return content;
@@ -344,11 +344,36 @@ function getTableIfOnlyTableInContent( 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( 'table' ) ) {
-		return null;
+	if ( content.childCount == 1 && content.getChild( 0 ).is( 'table' ) ) {
+		return content.getChild( 0 );
 	}
 
-	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( '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.

+ 162 - 3
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -178,7 +178,8 @@ describe( 'table clipboard', () => {
 
 			const table = viewTable( [
 				[ 'aa', 'ab' ],
-				[ 'ba', 'bb' ] ] );
+				[ 'ba', 'bb' ]
+			] );
 
 			const data = {
 				dataTransfer: createDataTransfer(),
@@ -204,7 +205,8 @@ describe( 'table clipboard', () => {
 
 			const table = viewTable( [
 				[ 'aa', 'ab' ],
-				[ 'ba', 'bb' ] ] );
+				[ 'ba', 'bb' ]
+			] );
 
 			const data = {
 				dataTransfer: createDataTransfer(),
@@ -230,7 +232,8 @@ describe( 'table clipboard', () => {
 
 			const table = viewTable( [
 				[ 'aa', 'ab' ],
-				[ 'ba', 'bb' ] ] );
+				[ 'ba', 'bb' ]
+			] );
 
 			const data = {
 				dataTransfer: createDataTransfer(),
@@ -248,6 +251,162 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
+		it( 'should alter model.insertContent if mixed content is pasted (table + empty paragraph)', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const table = viewTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', `${ table }<p>&nbsp;</p>` );
+			viewDocument.fire( 'paste', data );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'aa', 'ab', '02', '03' ],
+				[ 'ba', 'bb', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'should alter model.insertContent if mixed content is pasted (table + br)', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const table = viewTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', `${ table }<br>` );
+			viewDocument.fire( 'paste', data );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'aa', 'ab', '02', '03' ],
+				[ 'ba', 'bb', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'should alter model.insertContent if mixed content is pasted (empty paragraph + table)', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const table = viewTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', `<p>&nbsp;</p>${ table }` );
+			viewDocument.fire( 'paste', data );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'aa', 'ab', '02', '03' ],
+				[ 'ba', 'bb', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'should alter model.insertContent if mixed content is pasted (br + table)', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const table = viewTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', `<br>${ table }` );
+			viewDocument.fire( 'paste', data );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'aa', 'ab', '02', '03' ],
+				[ 'ba', 'bb', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'should alter model.insertContent if mixed content is pasted (p + p + table + p + br)', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			const table = viewTable( [
+				[ 'aa', 'ab' ],
+				[ 'ba', 'bb' ]
+			] );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', `<p>&nbsp;</p><p>&nbsp;</p>${ table }<p>&nbsp;</p><br>` );
+			viewDocument.fire( 'paste', data );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'aa', 'ab', '02', '03' ],
+				[ 'ba', 'bb', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
+		it( 'should not alter model.insertContent if element other than a table is passed directly', () => {
+			tableSelection.setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+			);
+
+			model.change( writer => {
+				const element = writer.createElement( 'paragraph' );
+
+				writer.insertText( 'foo', element, 0 );
+				model.insertContent( element );
+			} );
+
+			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+				[ 'foo', '', '02', '03' ],
+				[ '', '', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
 		it( 'should alter model.insertContent if selectable is a document selection', () => {
 			tableSelection.setCellSelection(
 				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),