瀏覽代碼

Merge pull request #8372 from ckeditor/i/8236

Feature (image): Inserting images by pasting URL to an image directly into the editor. Closes #8236.

MINOR BREAKING CHANGE (image): The `insertImage()` Image's utility function parameters have changed. The removed `writer` instance is no longer needed. Additionaly, you can specify `insertPosition` as an optional parameter.
Maciej 5 年之前
父節點
當前提交
908a35ac38
共有 26 個文件被更改,包括 854 次插入85 次删除
  1. 2 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.js
  2. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-caption.js
  3. 3 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.html
  4. 20 0
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js
  5. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js
  6. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-link.js
  7. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js
  8. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js
  9. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js
  10. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-resize.js
  11. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js
  12. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-style.js
  13. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js
  14. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image.js
  15. 1 0
      packages/ckeditor5-image/docs/api/image.md
  16. 17 0
      packages/ckeditor5-image/docs/features/image.md
  17. 174 0
      packages/ckeditor5-image/src/autoimage.js
  18. 4 6
      packages/ckeditor5-image/src/image/imageinsertcommand.js
  19. 13 12
      packages/ckeditor5-image/src/image/utils.js
  20. 6 9
      packages/ckeditor5-image/src/imageupload/imageuploadcommand.js
  21. 555 0
      packages/ckeditor5-image/tests/autoimage.js
  22. 3 9
      packages/ckeditor5-image/tests/image/utils.js
  23. 3 2
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.js
  24. 5 0
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.md
  25. 9 11
      packages/ckeditor5-media-embed/tests/automediaembed.js
  26. 28 25
      tests/manual/all-features.js

+ 2 - 0
packages/ckeditor5-image/docs/_snippets/features/build-image-source.js

@@ -9,10 +9,12 @@ import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
 
 import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
 import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert';
+import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage';
 import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
 
 ClassicEditor.builtinPlugins.push( ImageResize );
 ClassicEditor.builtinPlugins.push( ImageInsert );
 ClassicEditor.builtinPlugins.push( LinkImage );
+ClassicEditor.builtinPlugins.push( AutoImage );
 
 window.ClassicEditor = ClassicEditor;

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-caption.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-caption' ), {
-		removePlugins: [ 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

+ 3 - 0
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.html

@@ -0,0 +1,3 @@
+<div id="snippet-image-insert-via-pasting-url-into-editor">
+	<p>Insert an URL of an image: </p>
+</div>

+ 20 - 0
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js

@@ -0,0 +1,20 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-image-insert-via-pasting-url-into-editor' ), {
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		cloudServices: CS_CONFIG
+	} )
+	.then( editor => {
+		window.editorInsertImageViaPastingUrlIntoEditor = editor;
+	} )
+	.catch( err => {
+		console.error( err );
+	} );

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js

@@ -13,7 +13,7 @@ toolbarItems.splice( toolbarItems.indexOf( 'imageUpload' ), 1, 'imageInsert' );
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-insert-via-url' ), {
-		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			items: toolbarItems,
 			viewportTopOffset: window.getViewportTopOffsetConfig()

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-link.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-link' ), {
-		removePlugins: [ 'ImageResize' ],
+		removePlugins: [ 'ImageResize', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-buttons-dropdown' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-buttons' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize-px' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		image: {
 			resizeUnit: 'px',
 			resizeOptions: [

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-resize.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-resize' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
-		removePlugins: [ 'LinkImage' ],
+		removePlugins: [ 'LinkImage', 'AutoImage' ],
 		image: {
 			styles: [
 				'alignLeft',

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-style.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style' ), {
-		removePlugins: [ 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-toolbar' ), {
-		removePlugins: [ 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image' ), {
-		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage', 'AutoImage' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 0
packages/ckeditor5-image/docs/api/image.md

@@ -22,6 +22,7 @@ See the {@link features/image Image feature} guide and the documentation of the
 * {@link module:image/imagetextalternative~ImageTextAlternative}
 * {@link module:image/imagetoolbar~ImageToolbar}
 * {@link module:image/imageupload~ImageUpload}
+* {@link module:image/autoimage~AutoImage}
 
 ## Installation
 

+ 17 - 0
packages/ckeditor5-image/docs/features/image.md

@@ -14,6 +14,7 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
 * {@link module:image/imagetextalternative~ImageTextAlternative} adds support for adding text alternative.
 * {@link module:image/imageupload~ImageUpload} adds support for {@link features/image-upload uploading dropped or pasted images}.
 * {@link module:image/imageinsert~ImageInsert} adds support for [inserting images via URL](#inserting-images-via-source-url) and other custom integrations.
+* {@link module:image/autoimage~AutoImage} adds support for [inserting images via pasting URL into editor](#inserting-images-via-pasting-url-into-editor).
 * {@link module:image/imageresize~ImageResize} adds support for [resizing images](#resizing-images).
 * {@link module:link/linkimage~LinkImage} adds support for [linking images](#linking-images).
 
@@ -105,6 +106,22 @@ This will add a new **Insert image** dropdown in the toolbar. To open the panel
 
 {@snippet features/image-insert-via-url}
 
+## Inserting images via pasting URL into editor
+
+The {@link module:image/autoimage~AutoImage} plugin recognizes image links in the pasted content and embeds them shortly after they are injected into the document to speed up the editing. Accepted image extensions: jpg, jpeg, png, gif, ico.
+
+<info-box>
+	The image URL must be the only content pasted to be properly embedded. Multiple links (`"http://image.url http://another.image.url"`) as well as bigger chunks of content (`"This link http://image.url will not be auto–embedded when pasted."`) are ignored.
+</info-box>
+
+If the automatic embedding was unexpected, for instance when the link was meant to remain in the content as text, simply undo the action (by clicking the "Undo" button in the toolbar or using the <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> keystrokes).
+
+You can paste the image URL directly into the editor content, and it will be automatically embedded.
+
+<input class="example-input" type="text" value="https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg">
+
+{@snippet features/image-insert-via-pasting-url-into-editor}
+
 ## Responsive images
 
 Support for responsive images in CKEditor 5 is brought by the {@link features/easy-image Easy Image} feature without any additional configuration. Refer to the {@link features/easy-image#responsive-images Easy Image integration} guide to learn how to use the feature in your project.

+ 174 - 0
packages/ckeditor5-image/src/autoimage.js

@@ -0,0 +1,174 @@
+/**
+ * @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 image/autoimage
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
+import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { insertImage } from './image/utils';
+
+// Implements the pattern: http(s)://(www.)example.com/path/to/resource.ext?query=params&maybe=too.
+const IMAGE_URL_REGEXP = new RegExp( String( /^(http(s)?:\/\/)?[\w-]+(\.[\w-]+)+[\w._~:/?#[\]@!$&'()*+,;=%-]+/.source +
+	/\.(jpg|jpeg|png|gif|ico|JPG|JPEG|PNG|GIF|ICO)\??[\w._~:/#[\]@!$&'()*+,;=%-]*$/.source ) );
+
+/**
+ * The auto-image plugin. It recognizes image links in the pasted content and embeds
+ * them shortly after they are injected into the document.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class AutoImage extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ Clipboard, Undo ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'AutoImage';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * The paste–to–embed `setTimeout` ID. Stored as a property to allow
+		 * cleaning of the timeout.
+		 *
+		 * @private
+		 * @member {Number} #_timeoutId
+		 */
+		this._timeoutId = null;
+
+		/**
+		 * The position where the `<image>` element will be inserted after the timeout,
+		 * determined each time the new content is pasted into the document.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition} #_positionToInsert
+		 */
+		this._positionToInsert = null;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const modelDocument = editor.model.document;
+
+		// We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
+		// After pasting, the content between those positions will be checked for a URL that could be transformed
+		// into image.
+		this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', () => {
+			const firstRange = modelDocument.selection.getFirstRange();
+
+			const leftLivePosition = LivePosition.fromPosition( firstRange.start );
+			leftLivePosition.stickiness = 'toPrevious';
+
+			const rightLivePosition = LivePosition.fromPosition( firstRange.end );
+			rightLivePosition.stickiness = 'toNext';
+
+			modelDocument.once( 'change:data', () => {
+				this._embedImageBetweenPositions( leftLivePosition, rightLivePosition );
+
+				leftLivePosition.detach();
+				rightLivePosition.detach();
+			}, { priority: 'high' } );
+		} );
+
+		editor.commands.get( 'undo' ).on( 'execute', () => {
+			if ( this._timeoutId ) {
+				global.window.clearTimeout( this._timeoutId );
+				this._positionToInsert.detach();
+
+				this._timeoutId = null;
+				this._positionToInsert = null;
+			}
+		}, { priority: 'high' } );
+	}
+
+	/**
+	 * Analyzes the part of the document between provided positions in search for an URL representing an image.
+	 * When the URL is found, it is automatically converted into an image.
+	 *
+	 * @protected
+	 * @param {module:engine/model/liveposition~LivePosition} leftPosition Left position of the selection.
+	 * @param {module:engine/model/liveposition~LivePosition} rightPosition Right position of the selection.
+	 */
+	_embedImageBetweenPositions( leftPosition, rightPosition ) {
+		const editor = this.editor;
+		// TODO: Use marker instead of LiveRange & LivePositions.
+		const urlRange = new LiveRange( leftPosition, rightPosition );
+		const walker = urlRange.getWalker( { ignoreElementEnd: true } );
+
+		let src = '';
+
+		for ( const node of walker ) {
+			if ( node.item.is( '$textProxy' ) ) {
+				src += node.item.data;
+			}
+		}
+
+		src = src.trim();
+
+		// If the URL does not match to image URL regexp, let's skip that.
+		if ( !src.match( IMAGE_URL_REGEXP ) ) {
+			urlRange.detach();
+
+			return;
+		}
+
+		// Position won't be available in the `setTimeout` function so let's clone it.
+		this._positionToInsert = LivePosition.fromPosition( leftPosition );
+
+		// This action mustn't be executed if undo was called between pasting and auto-embedding.
+		this._timeoutId = global.window.setTimeout( () => {
+			// Don't do anything if image element cannot be inserted at the current position.
+			// See https://github.com/ckeditor/ckeditor5/issues/2763.
+			// Condition must be checked after timeout - pasting may take place on an element, replacing it. The final position matters.
+			const imageCommand = editor.commands.get( 'imageInsert' );
+
+			if ( !imageCommand.isEnabled ) {
+				urlRange.detach();
+
+				return;
+			}
+
+			editor.model.change( writer => {
+				this._timeoutId = null;
+
+				writer.remove( urlRange );
+				urlRange.detach();
+
+				let insertionPosition;
+
+				// Check if position where the element should be inserted is still valid.
+				// Otherwise leave it as undefined to use the logic of insertImage().
+				if ( this._positionToInsert.root.rootName !== '$graveyard' ) {
+					insertionPosition = this._positionToInsert.toPosition();
+				}
+
+				insertImage( editor.model, { src }, insertionPosition );
+
+				this._positionToInsert.detach();
+				this._positionToInsert = null;
+			} );
+		}, 100 );
+	}
+}

+ 4 - 6
packages/ckeditor5-image/src/image/imageinsertcommand.js

@@ -50,12 +50,10 @@ export default class ImageInsertCommand extends Command {
 	execute( options ) {
 		const model = this.editor.model;
 
-		model.change( writer => {
-			const sources = Array.isArray( options.source ) ? options.source : [ options.source ];
+		const sources = Array.isArray( options.source ) ? options.source : [ options.source ];
 
-			for ( const src of sources ) {
-				insertImage( writer, model, { src } );
-			}
-		} );
+		for ( const src of sources ) {
+			insertImage( model, { src } );
+		}
 	}
 }

+ 13 - 12
packages/ckeditor5-image/src/image/utils.js

@@ -71,25 +71,26 @@ export function isImage( modelElement ) {
 /**
  * Handles inserting single file. This method unifies image insertion using {@link module:widget/utils~findOptimalInsertionPosition} method.
  *
- *		model.change( writer => {
- *			insertImage( writer, model, { src: 'path/to/image.jpg' } );
- *		} );
+ *		insertImage( model, { src: 'path/to/image.jpg' } );
  *
- * @param {module:engine/model/writer~Writer} writer
  * @param {module:engine/model/model~Model} model
  * @param {Object} [attributes={}] Attributes of inserted image
+ * @param {module:engine/model/position~Position} [insertPosition] Position to insert the image. If not specified,
+ * the {@link module:widget/utils~findOptimalInsertionPosition} logic will be applied.
  */
-export function insertImage( writer, model, attributes = {} ) {
-	const imageElement = writer.createElement( 'image', attributes );
+export function insertImage( model, attributes = {}, insertPosition = null ) {
+	model.change( writer => {
+		const imageElement = writer.createElement( 'image', attributes );
 
-	const insertAtSelection = findOptimalInsertionPosition( model.document.selection, model );
+		const insertAtSelection = insertPosition || findOptimalInsertionPosition( model.document.selection, model );
 
-	model.insertContent( imageElement, insertAtSelection );
+		model.insertContent( imageElement, insertAtSelection );
 
-	// Inserting an image might've failed due to schema regulations.
-	if ( imageElement.parent ) {
-		writer.setSelection( imageElement, 'on' );
-	}
+		// Inserting an image might've failed due to schema regulations.
+		if ( imageElement.parent ) {
+			writer.setSelection( imageElement, 'on' );
+		}
+	} );
 }
 
 /**

+ 6 - 9
packages/ckeditor5-image/src/imageupload/imageuploadcommand.js

@@ -63,22 +63,19 @@ export default class ImageUploadCommand extends Command {
 
 		const fileRepository = editor.plugins.get( FileRepository );
 
-		model.change( writer => {
-			const filesToUpload = Array.isArray( options.file ) ? options.file : [ options.file ];
+		const filesToUpload = Array.isArray( options.file ) ? options.file : [ options.file ];
 
-			for ( const file of filesToUpload ) {
-				uploadImage( writer, model, fileRepository, file );
-			}
-		} );
+		for ( const file of filesToUpload ) {
+			uploadImage( model, fileRepository, file );
+		}
 	}
 }
 
 // Handles uploading single file.
 //
-// @param {module:engine/model/writer~writer} writer
 // @param {module:engine/model/model~Model} model
 // @param {File} file
-function uploadImage( writer, model, fileRepository, file ) {
+function uploadImage( model, fileRepository, file ) {
 	const loader = fileRepository.createLoader( file );
 
 	// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
@@ -86,5 +83,5 @@ function uploadImage( writer, model, fileRepository, file ) {
 		return;
 	}
 
-	insertImage( writer, model, { uploadId: loader.id } );
+	insertImage( model, { uploadId: loader.id } );
 }

+ 555 - 0
packages/ckeditor5-image/tests/autoimage.js

@@ -0,0 +1,555 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global setTimeout */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import Table from '@ckeditor/ckeditor5-table/src/table';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import Image from '../src/image';
+import ImageCaption from '../src/imagecaption';
+import AutoImage from '../src/autoimage';
+
+describe( 'AutoImage - integration', () => {
+	let editorElement, editor;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ Typing, Paragraph, Link, Image, ImageCaption, AutoImage ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should load Clipboard plugin', () => {
+		expect( editor.plugins.get( Clipboard ) ).to.instanceOf( Clipboard );
+	} );
+
+	it( 'should load Undo plugin', () => {
+		expect( editor.plugins.get( Undo ) ).to.instanceOf( Undo );
+	} );
+
+	it( 'has proper name', () => {
+		expect( AutoImage.pluginName ).to.equal( 'AutoImage' );
+	} );
+
+	describe( 'use fake timers', () => {
+		let clock;
+
+		beforeEach( () => {
+			clock = sinon.useFakeTimers();
+		} );
+
+		afterEach( () => {
+			clock.restore();
+		} );
+
+		it( 'replaces pasted text with image element after 100ms', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'can undo auto-embeding', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			clock.tick( 100 );
+
+			editor.commands.execute( 'undo' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+		} );
+
+		describe( 'supported URL', () => {
+			const supportedURLs = [
+				'example.com/image.png',
+				'www.example.com/image.png',
+				'https://www.example.com/image.png',
+				'https://example.com/image.png',
+				'http://www.example.com/image.png',
+				'http://example.com/image.png',
+				'http://example.com/image.jpg',
+				'http://example.com/image.jpeg',
+				'http://example.com/image.gif',
+				'http://example.com/image.ico',
+				'http://example.com/image.JPG',
+				'http://example.com%20Fimage.png',
+				'http://example.com/image.png?foo=bar',
+				'http://example.com/image.png#foo'
+			];
+
+			for ( const supportedURL of supportedURLs ) {
+				it( `should detect "${ supportedURL }" as a valid URL`, () => {
+					setData( editor.model, '<paragraph>[]</paragraph>' );
+					pasteHtml( editor, supportedURL );
+
+					clock.tick( 100 );
+
+					expect( getData( editor.model ) ).to.equal(
+						`[<image src="${ supportedURL }"><caption></caption></image>]`
+					);
+				} );
+			}
+		} );
+
+		describe( 'unsupported URL', () => {
+			const unsupportedOrInvalid = [
+				'http://www.example.com',
+				'https://example.com',
+				'http://www.example.com/image.svg',
+				'http://www.example.com/image.webp',
+				'http://www.example.com/image.mp3',
+				'http://www.example.com/image.exe',
+				'http://www.example.com/image.txt',
+				'http://example.com/image.png http://example.com/image.png',
+				'Foo bar http://example.com/image.png bar foo.',
+				'https://example.com/im age.png'
+			];
+
+			for ( const unsupportedURL of unsupportedOrInvalid ) {
+				it( `should not detect "${ unsupportedURL }" as a valid URL`, () => {
+					setData( editor.model, '<paragraph>[]</paragraph>' );
+					pasteHtml( editor, unsupportedURL );
+
+					clock.tick( 100 );
+
+					expect( getData( editor.model ) ).to.equal(
+						`<paragraph>${ unsupportedURL }[]</paragraph>`
+					);
+				} );
+			}
+		} );
+
+		it( 'works for URL that was pasted as a link', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<a href="http://example.com/image.png">' +
+				'http://example.com/image.png</a>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for URL that contains some inline styles', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<b>http://example.com/image.png</b>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for not collapsed selection inside single element', () => {
+			setData( editor.model, '<paragraph>[Foo]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'works for not collapsed selection over a few elements', () => {
+			setData( editor.model, '<paragraph>Fo[o</paragraph><paragraph>Ba]r</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Fo</paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph>r</paragraph>'
+			);
+		} );
+
+		it( 'inserts image in-place (collapsed selection)', () => {
+			setData( editor.model, '<paragraph>Foo []Bar</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo </paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'inserts image in-place (non-collapsed selection)', () => {
+			setData( editor.model, '<paragraph>Foo [Bar] Baz</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo </paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph> Baz</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted more than single node', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor,
+				'http://example.com/image.png ' +
+				'<a href="http://example.com/image.png">' +
+				'http://example.com/image.png</a>'
+			);
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
+				'<paragraph>http://example.com/image.png ' +
+				'<$text linkHref="http://example.com/image.png">' +
+				'http://example.com/image.png</$text>' +
+				'</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted a paragraph with the url', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<p>http://example.com/image.png</p>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+		} );
+
+		it( 'does nothing if pasted a block of content that looks like a URL', () => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, '<p>https://</p><p>example.com/image</p><p>.png</p>' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>https://</paragraph><paragraph>example.com/image</paragraph><paragraph>.png[]</paragraph>'
+			);
+		} );
+
+		// #47
+		it( 'does not transform a valid URL into a image if the element cannot be placed in the current position', () => {
+			setData( editor.model, '<image src="/assets/sample.png"><caption>Foo.[]</caption></image>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<image src="/assets/sample.png"><caption>' +
+				'Foo.http://example.com/image.png[]' +
+				'</caption></image>'
+			);
+		} );
+
+		it( 'replaces a URL in image if pasted a link when other image element was selected', () => {
+			setData(
+				editor.model,
+				'[<image src="http://example.com/image.png"><caption></caption></image>]'
+			);
+
+			pasteHtml( editor, 'http://example.com/image2.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'[<image src="http://example.com/image2.png"><caption></caption></image>]'
+			);
+		} );
+
+		it( 'inserts a new image element if pasted a link when other image element was selected in correct place', () => {
+			setData(
+				editor.model,
+				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
+				'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+				'<paragraph><$text>Bar</$text>.</paragraph>'
+			);
+
+			pasteHtml( editor, 'http://example.com/image2.png' );
+
+			clock.tick( 100 );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
+				'[<image src="http://example.com/image2.png"><caption></caption></image>]' +
+				'<paragraph>Bar.</paragraph>'
+			);
+		} );
+	} );
+
+	describe( 'use real timers', () => {
+		const characters = Array( 10 ).fill( 1 ).map( ( x, i ) => String.fromCharCode( 65 + i ) );
+
+		it( 'undo breaks the auto-image feature (undo was done before auto-image)', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			expect( getData( editor.model ) ).to.equal(
+				'<paragraph>http://example.com/image.png[]</paragraph>'
+			);
+
+			setTimeout( () => {
+				editor.commands.execute( 'undo' );
+
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>[]</paragraph>'
+				);
+
+				done();
+			} );
+		} );
+
+		// Checking whether paste+typing calls the auto-image handler once.
+		it( 'pasting handler should be executed once', done => {
+			const AutoImagePlugin = editor.plugins.get( AutoImage );
+			const autoImageHandler = AutoImagePlugin._embedImageBetweenPositions;
+			let counter = 0;
+
+			AutoImagePlugin._embedImageBetweenPositions = function( ...args ) {
+				counter += 1;
+
+				return autoImageHandler.apply( this, args );
+			};
+
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+			pasteHtml( editor, 'http://example.com/image.png' );
+			simulateTyping( 'Foo. Bar.' );
+
+			setTimeout( () => {
+				AutoImagePlugin._embedImageBetweenPositions = autoImageHandler;
+
+				expect( counter ).to.equal( 1 );
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'typing before pasted link during collaboration should not blow up', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			for ( let i = 0; i < 10; ++i ) {
+				const rootEl = editor.model.document.getRoot();
+
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], writer.createPositionFromPath( rootEl, [ 0, i ] ) );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'typing after pasted link during collaboration should not blow up', done => {
+			setData( editor.model, '<paragraph>[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if parent element where the URL was pasted has been deleted', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				writer.remove( writer.createRangeOn( editor.model.document.getRoot().getChild( 1 ) ) );
+			} );
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if new element appeared above the pasted URL', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+				writer.insert( paragraph, writer.createPositionAfter( editor.model.document.getRoot().getChild( 0 ) ) );
+				writer.setSelection( paragraph, 'in' );
+			} );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'<paragraph>ABCDEFGHIJ</paragraph>' +
+					'<paragraph>Bar.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]'
+				);
+
+				done();
+			}, 100 );
+		} );
+
+		it( 'should insert the image element even if new element appeared below the pasted URL', done => {
+			setData( editor.model, '<paragraph>Foo.</paragraph><paragraph>Bar.[]</paragraph>' );
+
+			pasteHtml( editor, 'http://example.com/image.png' );
+
+			editor.model.enqueueChange( 'transparent', writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+				writer.insert( paragraph, writer.createPositionAfter( editor.model.document.getRoot().getChild( 1 ) ) );
+				writer.setSelection( paragraph, 'in' );
+			} );
+
+			for ( let i = 0; i < 10; ++i ) {
+				setTimeout( () => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.insertText( characters[ i ], editor.model.document.selection.getFirstPosition() );
+					} );
+				}, i * 5 );
+			}
+
+			setTimeout( () => {
+				expect( getData( editor.model ) ).to.equal(
+					'<paragraph>Foo.</paragraph>' +
+					'<paragraph>Bar.</paragraph>' +
+					'[<image src="http://example.com/image.png"><caption></caption></image>]' +
+					'<paragraph>ABCDEFGHIJ</paragraph>'
+				);
+
+				done();
+			}, 100 );
+		} );
+	} );
+
+	it( 'should detach LiveRange', async () => {
+		const editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ Typing, Paragraph, Link, Image, ImageCaption, Table, AutoImage ]
+		} );
+
+		setData(
+			editor.model,
+			'<table>' +
+				'<tableRow>' +
+					'[<tableCell><paragraph>foo</paragraph></tableCell>]' +
+					'[<tableCell><paragraph>bar</paragraph></tableCell>]' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		pasteHtml( editor, '<table><tr><td>one</td><td>two</td></tr></table>' );
+
+		expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+				'<tableRow>' +
+					'<tableCell><paragraph>one</paragraph></tableCell>' +
+					'<tableCell><paragraph>two</paragraph></tableCell>' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		expect( () => {
+			editor.setData( '' );
+		} ).not.to.throw();
+
+		await editor.destroy();
+	} );
+
+	function simulateTyping( text ) {
+		// While typing, every character is an atomic change.
+		text.split( '' ).forEach( character => {
+			editor.execute( 'input', {
+				text: character
+			} );
+		} );
+	}
+
+	function pasteHtml( editor, html ) {
+		editor.editing.view.document.fire( 'paste', {
+			dataTransfer: createDataTransfer( { 'text/html': html } ),
+			stopPropagation() {},
+			preventDefault() {}
+		} );
+	}
+
+	function createDataTransfer( data ) {
+		return {
+			getData( type ) {
+				return data[ type ];
+			}
+		};
+	}
+} );

+ 3 - 9
packages/ckeditor5-image/tests/image/utils.js

@@ -240,9 +240,7 @@ describe( 'image widget utils', () => {
 		it( 'should insert image at selection position as other widgets', () => {
 			setModelData( model, '<paragraph>f[o]o</paragraph>' );
 
-			model.change( writer => {
-				insertImage( writer, model );
-			} );
+			insertImage( model );
 
 			expect( getModelData( model ) ).to.equal( '[<image></image>]<paragraph>foo</paragraph>' );
 		} );
@@ -250,9 +248,7 @@ describe( 'image widget utils', () => {
 		it( 'should insert image with given attributes', () => {
 			setModelData( model, '<paragraph>f[o]o</paragraph>' );
 
-			model.change( writer => {
-				insertImage( writer, model, { src: 'bar' } );
-			} );
+			insertImage( model, { src: 'bar' } );
 
 			expect( getModelData( model ) ).to.equal( '[<image src="bar"></image>]<paragraph>foo</paragraph>' );
 		} );
@@ -268,9 +264,7 @@ describe( 'image widget utils', () => {
 
 			setModelData( model, '<other>[]</other>' );
 
-			model.change( writer => {
-				insertImage( writer, model );
-			} );
+			insertImage( model );
 
 			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
 		} );

+ 3 - 2
packages/ckeditor5-image/tests/manual/imageinsertviaurl.js

@@ -7,12 +7,13 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ImageInsert from '../../src/imageinsert';
 import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import ImageInsert from '../../src/imageinsert';
+import AutoImage from '../../src/autoimage';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, ImageInsert, CKFinder ],
+		plugins: [ ArticlePluginSet, ImageInsert, AutoImage, CKFinder ],
 		toolbar: [
 			'heading',
 			'|',

+ 5 - 0
packages/ckeditor5-image/tests/manual/imageinsertviaurl.md

@@ -16,3 +16,8 @@
 1. In the **Editor 2** click on the arrow button in `imageInsert` plugin to reveal the image insert panel.
 1. Click on the **CKFinder** button.
 1. Choose image and confirm.
+
+## Image insert via pasting URL into editor
+
+1. Copy image URL to clipboard (eg: `https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg`).
+1. Paste the URL to the editor.

+ 9 - 11
packages/ckeditor5-media-embed/tests/automediaembed.js

@@ -11,8 +11,6 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Link from '@ckeditor/ckeditor5-link/src/link';
-import List from '@ckeditor/ckeditor5-list/src/list';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Image from '@ckeditor/ckeditor5-image/src/image';
@@ -30,7 +28,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ MediaEmbed, AutoMediaEmbed, Link, List, Bold, Typing, Image, ImageCaption ]
+				plugins: [ Typing, Paragraph, Link, Image, ImageCaption, MediaEmbed, AutoMediaEmbed ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -109,7 +107,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			);
 		} );
 
-		it( 'works for a full URL (https without "wwww" sub-domain)', () => {
+		it( 'works for a full URL (https without "www" sub-domain)', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
 			pasteHtml( editor, 'https://youtube.com/watch?v=H08tGjXNHO4' );
 
@@ -131,7 +129,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			);
 		} );
 
-		it( 'works for a full URL (http without "wwww" sub-domain)', () => {
+		it( 'works for a full URL (http without "www" sub-domain)', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
 			pasteHtml( editor, 'http://youtube.com/watch?v=H08tGjXNHO4' );
 
@@ -307,13 +305,13 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 		it( 'does nothing if pasted a block of content that looks like a URL', () => {
 			setData( editor.model, '<paragraph>[]</paragraph>' );
-			pasteHtml( editor, '<ul><li>https://</li><li>youtube.com/watch?</li></ul><p>v=H08tGjXNHO4</p>' );
+			pasteHtml( editor, '<p>https://</p><p>youtube.com/watch?</p><p>v=H08tGjXNHO4</p>' );
 
 			clock.tick( 100 );
 
 			expect( getData( editor.model ) ).to.equal(
-				'<listItem listIndent="0" listType="bulleted">https://</listItem>' +
-				'<listItem listIndent="0" listType="bulleted">youtube.com/watch?</listItem>' +
+				'<paragraph>https://</paragraph>' +
+				'<paragraph>youtube.com/watch?</paragraph>' +
 				'<paragraph>v=H08tGjXNHO4[]</paragraph>'
 			);
 		} );
@@ -361,7 +359,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 				editor.model,
 				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
 				'[<media url="https://open.spotify.com/album/2IXlgvecaDqOeF3viUZnPI?si=ogVw7KlcQAGZKK4Jz9QzvA"></media>]' +
-				'<paragraph><$text bold="true">Bar</$text>.</paragraph>'
+				'<paragraph>Bar.</paragraph>'
 			);
 
 			pasteHtml( editor, 'https://www.youtube.com/watch?v=H08tGjXNHO4' );
@@ -371,7 +369,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 			expect( getData( editor.model ) ).to.equal(
 				'<paragraph>Foo. <$text linkHref="https://cksource.com">Bar</$text></paragraph>' +
 				'[<media url="https://www.youtube.com/watch?v=H08tGjXNHO4"></media>]' +
-				'<paragraph><$text bold="true">Bar</$text>.</paragraph>'
+				'<paragraph>Bar.</paragraph>'
 			);
 		} );
 
@@ -587,7 +585,7 @@ describe( 'AutoMediaEmbed - integration', () => {
 
 	it( 'should detach LiveRange', async () => {
 		const editor = await ClassicTestEditor.create( editorElement, {
-			plugins: [ MediaEmbed, AutoMediaEmbed, Link, List, Bold, Typing, Image, ImageCaption, Table ]
+			plugins: [ Typing, Paragraph, Link, Image, ImageCaption, Table, MediaEmbed, AutoMediaEmbed ]
 		} );
 
 		setData(

+ 28 - 25
tests/manual/all-features.js

@@ -6,47 +6,50 @@
 /* globals console, window, document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
-import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage';
+import AutoLink from '@ckeditor/ckeditor5-link/src/autolink';
 import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
-import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
-import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
-import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript';
-import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor';
+import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
 import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor';
+import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor';
 import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily';
 import FontSize from '@ckeditor/ckeditor5-font/src/fontsize';
-import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
-import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount';
-import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
-import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
-import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
-import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak';
-import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
 import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight';
+import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
+import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
 import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
-import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
 import Mention from '@ckeditor/ckeditor5-mention/src/mention';
+import PageBreak from '@ckeditor/ckeditor5-page-break/src/pagebreak';
 import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat';
-import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
+import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
+import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
+import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
+import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript';
+import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
 import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation';
-import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
-import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
+import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount';
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [
-			ArticlePluginSet, CodeBlock, Alignment,
-			TableProperties, TableCellProperties, SpecialCharacters, SpecialCharactersEssentials,
-			Code, Underline, Strikethrough, Superscript, Subscript,
-			Highlight, FontColor, FontBackgroundColor, FontFamily, FontSize,
-			IndentBlock, WordCount, EasyImage,
-			TodoList, PageBreak, HorizontalLine, Mention, RemoveFormat, TextTransformation,
-			ImageResize, LinkImage
+			ArticlePluginSet, Underline, Strikethrough, Superscript, Subscript, Code, RemoveFormat,
+			FontColor, FontBackgroundColor, FontFamily, FontSize, Highlight,
+			CodeBlock, TodoList, TableProperties, TableCellProperties,
+			EasyImage, ImageResize, LinkImage, AutoImage,
+			AutoLink, Mention, TextTransformation,
+			Alignment, IndentBlock,
+			PageBreak, HorizontalLine,
+			SpecialCharacters, SpecialCharactersEssentials, WordCount
 		],
 		toolbar: [
 			'heading',