Bladeren bron

Merge branch 'master' into i/7907

panr 5 jaren geleden
bovenliggende
commit
dae2246351
40 gewijzigde bestanden met toevoegingen van 1322 en 1040 verwijderingen
  1. 13 1
      docs/builds/guides/migrate.md
  2. 1 1
      package.json
  3. 30 6
      packages/ckeditor5-clipboard/src/clipboard.js
  4. 49 0
      packages/ckeditor5-clipboard/src/pasteplaintext.js
  5. 5 3
      packages/ckeditor5-clipboard/src/utils/plaintexttohtml.js
  6. 94 1
      packages/ckeditor5-clipboard/tests/clipboard.js
  7. 96 0
      packages/ckeditor5-clipboard/tests/pasteplaintext.js
  8. 13 5
      packages/ckeditor5-clipboard/tests/utils/plaintexttohtml.js
  9. 2 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.js
  10. 6 1
      packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js
  11. 7 3
      packages/ckeditor5-image/docs/features/image.md
  12. 98 0
      packages/ckeditor5-image/src/imageinsert.js
  13. 151 0
      packages/ckeditor5-image/src/imageinsert/imageinsertui.js
  14. 3 3
      packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.js
  15. 10 10
      packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.js
  16. 77 0
      packages/ckeditor5-image/src/imageinsert/utils.js
  17. 0 55
      packages/ckeditor5-image/src/imageupload.js
  18. 24 139
      packages/ckeditor5-image/src/imageupload/imageuploadui.js
  19. 0 69
      packages/ckeditor5-image/src/imageupload/utils.js
  20. 327 0
      packages/ckeditor5-image/tests/imageinsert/imageinsertui.js
  21. 1 1
      packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js
  22. 4 4
      packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js
  23. 148 0
      packages/ckeditor5-image/tests/imageinsert/utils.js
  24. 109 539
      packages/ckeditor5-image/tests/imageupload/imageuploadui.js
  25. 1 146
      packages/ckeditor5-image/tests/imageupload/utils.js
  26. 1 1
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.html
  27. 4 13
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.js
  28. 18 0
      packages/ckeditor5-image/tests/manual/imageinsertviaurl.md
  29. 0 18
      packages/ckeditor5-image/tests/manual/imageuploadviaurl.md
  30. 9 2
      packages/ckeditor5-image/theme/imageinsert.css
  31. 1 1
      packages/ckeditor5-image/theme/imageinsertformrowview.css
  32. 2 1
      packages/ckeditor5-link/src/link.js
  33. 3 2
      packages/ckeditor5-link/tests/link.js
  34. 1 1
      packages/ckeditor5-list/src/indentcommand.js
  35. 1 1
      packages/ckeditor5-list/src/listcommand.js
  36. 3 3
      packages/ckeditor5-list/src/liststyle.js
  37. 2 2
      packages/ckeditor5-list/src/liststylecommand.js
  38. 4 4
      packages/ckeditor5-list/src/liststyleediting.js
  39. 3 3
      packages/ckeditor5-list/src/liststyleui.js
  40. 1 1
      packages/ckeditor5-list/theme/liststyles.css

File diff suppressed because it is too large
+ 13 - 1
docs/builds/guides/migrate.md


+ 1 - 1
package.json

@@ -131,7 +131,7 @@
   },
   "scripts": {
     "lint": "eslint --quiet '**/*.js'",
-    "stylelint": "stylelint --quiet --allow-empty-input 'packages/**/*.css' 'external/*/packages/**/*.css' 'docs/**/*.css'",
+    "stylelint": "stylelint --quiet --allow-empty-input 'packages/**/*.css' 'docs/**/*.css'",
     "test": "node --max_old_space_size=8192 node_modules/@ckeditor/ckeditor5-dev-tests/bin/test.js",
     "manual": "node --max_old_space_size=8192 node_modules/@ckeditor/ckeditor5-dev-tests/bin/test-manual.js",
     "bootstrap": "yarn install",

+ 30 - 6
packages/ckeditor5-clipboard/src/clipboard.js

@@ -8,6 +8,7 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import PastePlainText from './pasteplaintext';
 
 import ClipboardObserver from './clipboardobserver';
 
@@ -35,6 +36,13 @@ export default class Clipboard extends Plugin {
 		return 'Clipboard';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ PastePlainText ];
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -77,7 +85,11 @@ export default class Clipboard extends Plugin {
 			content = this._htmlDataProcessor.toView( content );
 
 			const eventInfo = new EventInfo( this, 'inputTransformation' );
-			this.fire( eventInfo, { content, dataTransfer } );
+			this.fire( eventInfo, {
+				content,
+				dataTransfer,
+				asPlainText: data.asPlainText
+			} );
 
 			// If CKEditor handled the input, do not bubble the original event any further.
 			// This helps external integrations recognize that fact and act accordingly.
@@ -103,16 +115,27 @@ export default class Clipboard extends Plugin {
 					return;
 				}
 
-				// While pasting plain text, apply selection attributes on the text.
-				if ( isPlainText( modelFragment ) ) {
-					const node = modelFragment.getChild( 0 );
+				// Plain text can be determined based on event flag (#7799) or auto detection (#1006). If detected
+				// preserve selection attributes on pasted items.
+				if ( data.asPlainText || isPlainTextFragment( modelFragment ) ) {
+					// Consider only formatting attributes.
+					const textAttributes = new Map( Array.from( modelDocument.selection.getAttributes() ).filter(
+						keyValuePair => editor.model.schema.getAttributeProperties( keyValuePair[ 0 ] ).isFormatting
+					) );
 
 					model.change( writer => {
-						writer.setAttributes( modelDocument.selection.getAttributes(), node );
+						const range = writer.createRangeIn( modelFragment );
+
+						for ( const item of range.getItems() ) {
+							if ( item.is( '$text' ) || item.is( '$textProxy' ) ) {
+								writer.setAttributes( textAttributes, item );
+							}
+						}
 					} );
 				}
 
 				model.insertContent( modelFragment );
+
 				evt.stop();
 			}
 		}, { priority: 'low' } );
@@ -168,6 +191,7 @@ export default class Clipboard extends Plugin {
  * It can be modified by the event listeners. Read more about the clipboard pipelines in
  * {@glink framework/guides/deep-dive/clipboard "Clipboard" deep dive}.
  * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer Data transfer instance.
+ * @param {Boolean} data.asPlainText If set to `true` content is pasted as plain text.
  */
 
 /**
@@ -212,7 +236,7 @@ export default class Clipboard extends Plugin {
 //
 // @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
 // @returns {Boolean}
-function isPlainText( documentFragment ) {
+function isPlainTextFragment( documentFragment ) {
 	if ( documentFragment.childCount > 1 ) {
 		return false;
 	}

+ 49 - 0
packages/ckeditor5-clipboard/src/pasteplaintext.js

@@ -0,0 +1,49 @@
+/**
+ * @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 clipboard/clipboard
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import ClipboardObserver from './clipboardobserver';
+
+/**
+ * The plugin detects user intentions for pasting plain text.
+ *
+ * For example, it detects <kbd>ctrl/cmd</kbd> + <kbd>shift</kbd> + <kbd>ctrl/v</kbd> keystroke.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class PastePlainText extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'PastePlainText';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const view = this.editor.editing.view;
+		const viewDocument = view.document;
+		let shiftPressed = false;
+
+		view.addObserver( ClipboardObserver );
+
+		this.listenTo( viewDocument, 'keydown', ( evt, data ) => {
+			shiftPressed = data.shiftKey;
+		} );
+
+		this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
+			if ( shiftPressed ) {
+				data.asPlainText = true;
+			}
+		}, { priority: 'high' } );
+	}
+}

+ 5 - 3
packages/ckeditor5-clipboard/src/utils/plaintexttohtml.js

@@ -18,15 +18,17 @@ export default function plainTextToHtml( text ) {
 		// Encode <>.
 		.replace( /</g, '&lt;' )
 		.replace( />/g, '&gt;' )
-		// Creates paragraphs for every line breaks.
-		.replace( /\n/g, '</p><p>' )
+		// Creates a paragraph for each double line break.
+		.replace( /\r?\n\r?\n/g, '</p><p>' )
+		// Creates a line break for each single line break.
+		.replace( /\r?\n/g, '<br>' )
 		// Preserve trailing spaces (only the first and last one – the rest is handled below).
 		.replace( /^\s/, '&nbsp;' )
 		.replace( /\s$/, '&nbsp;' )
 		// Preserve other subsequent spaces now.
 		.replace( /\s\s/g, ' &nbsp;' );
 
-	if ( text.indexOf( '</p><p>' ) > -1 ) {
+	if ( text.includes( '</p><p>' ) || text.includes( '<br>' ) ) {
 		// If we created paragraphs above, add the trailing ones.
 		text = `<p>${ text }</p>`;
 	}

+ 94 - 1
packages/ckeditor5-clipboard/tests/clipboard.js

@@ -112,7 +112,7 @@ describe( 'Clipboard feature', () => {
 				clipboardPlugin.on( 'inputTransformation', ( evt, data ) => {
 					expect( data.content ).is.instanceOf( ViewDocumentFragment );
 					expect( data.dataTransfer ).to.equal( dataTransferMock );
-					expect( stringifyView( data.content ) ).to.equal( '<p>x</p><p></p><p>y  z</p>' );
+					expect( stringifyView( data.content ) ).to.equal( '<p>x</p><p>y  z</p>' );
 
 					done();
 				} );
@@ -351,6 +351,19 @@ describe( 'Clipboard feature', () => {
 				model = editor.model;
 
 				model.schema.extend( '$text', { allowAttributes: 'bold' } );
+				model.schema.extend( '$text', { allowAttributes: 'test' } );
+
+				editor.model.schema.setAttributeProperties( 'bold', { isFormatting: true } );
+
+				model.schema.register( 'softBreak', {
+					allowWhere: '$text',
+					isInline: true
+				} );
+				editor.conversion.for( 'upcast' )
+					.elementToElement( {
+						model: 'softBreak',
+						view: 'br'
+					} );
 			} );
 
 			it( 'should inherit selection attributes (collapsed selection)', () => {
@@ -451,6 +464,86 @@ describe( 'Clipboard feature', () => {
 
 				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
 			} );
+
+			it( 'should inherit selection attributes with data.asPlainText switch set', () => {
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': 'foo',
+					'text/plain': 'foo'
+				} );
+
+				viewDocument.fire( 'clipboardInput', {
+					dataTransfer: dataTransferMock,
+					asPlainText: true,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded foo[]text.</$text></paragraph>' );
+			} );
+
+			it( 'should discard selection attributes with data.asPlainText switch set to false', () => {
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': 'foo<br>bar',
+					'text/plain': 'foo\nbar'
+				} );
+
+				viewDocument.fire( 'clipboardInput', {
+					dataTransfer: dataTransferMock,
+					asPlainText: false,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded </$text>' +
+					'foo<softBreak></softBreak>bar[]' +
+					'<$text bold="true">text.</$text></paragraph>' );
+			} );
+
+			it( 'should work if the insertContent event is cancelled', () => {
+				// (#7887).
+				setModelData( model, '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': 'foo',
+					'text/plain': 'foo'
+				} );
+
+				model.on( 'insertContent', event => {
+					event.stop();
+				}, { priority: 'high' } );
+
+				viewDocument.fire( 'clipboardInput', {
+					dataTransfer: dataTransferMock,
+					asPlainText: false,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bolded []text.</$text></paragraph>' );
+			} );
+
+			it( 'ignores non-formatting text attributes', () => {
+				setModelData( model, '<paragraph><$text test="true">Bolded []text.</$text></paragraph>' );
+
+				const dataTransferMock = createDataTransfer( {
+					'text/html': 'foo',
+					'text/plain': 'foo'
+				} );
+
+				viewDocument.fire( 'clipboardInput', {
+					dataTransfer: dataTransferMock,
+					asPlainText: false,
+					stopPropagation() {},
+					preventDefault() {}
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph><$text test="true">Bolded </$text>foo[]<$text test="true">text.</$text></paragraph>' );
+			} );
 		} );
 
 		function createDataTransfer( data ) {

+ 96 - 0
packages/ckeditor5-clipboard/tests/pasteplaintext.js

@@ -0,0 +1,96 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+import PastePlainText from '../src/pasteplaintext';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+
+/* global document */
+
+describe( 'PastePlainText', () => {
+	let editor, viewDocument;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ PastePlainText, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				viewDocument = editor.editing.view.document;
+			} );
+	} );
+
+	it( 'marks clipboard input as plain text with shift pressed', () => {
+		const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
+
+		viewDocument.on( 'clipboardInput', ( event, data ) => {
+			expect( data.asPlainText ).to.be.true;
+
+			// No need for further execution.
+			event.stop();
+		} );
+
+		viewDocument.fire( 'keydown', {
+			keyCode: getCode( 'v' ),
+			shiftKey: true,
+			ctrlKey: true,
+			preventDefault: () => {},
+			domTarget: document.body
+		} );
+
+		viewDocument.fire( 'clipboardInput', {
+			dataTransfer: dataTransferMock
+		} );
+	} );
+
+	it( 'ignores clipboard input as plain text when shift was released', () => {
+		const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
+
+		viewDocument.on( 'clipboardInput', ( event, data ) => {
+			expect( data.asPlainText ).to.be.undefined;
+
+			// No need for further execution.
+			event.stop();
+		} );
+
+		viewDocument.fire( 'keydown', {
+			keyCode: getCode( 'a' ),
+			shiftKey: true,
+			preventDefault: () => {},
+			domTarget: document.body
+		} );
+
+		viewDocument.fire( 'keyup', {
+			keyCode: getCode( 'a' ),
+			shiftKey: true,
+			preventDefault: () => {},
+			domTarget: document.body
+		} );
+
+		viewDocument.fire( 'keydown', {
+			keyCode: getCode( 'v' ),
+			shiftKey: false,
+			ctrlKey: true,
+			preventDefault: () => {},
+			domTarget: document.body
+		} );
+
+		viewDocument.fire( 'clipboardInput', {
+			dataTransfer: dataTransferMock
+		} );
+	} );
+
+	function createDataTransfer( data ) {
+		return {
+			getData( type ) {
+				return data[ type ];
+			}
+		};
+	}
+} );

+ 13 - 5
packages/ckeditor5-clipboard/tests/utils/plaintexttohtml.js

@@ -10,16 +10,24 @@ describe( 'plainTextToHtml()', () => {
 		expect( plainTextToHtml( 'x y <z>' ) ).to.equal( 'x y &lt;z&gt;' );
 	} );
 
-	it( 'turns a single line break into paragraphs', () => {
-		expect( plainTextToHtml( 'x\ny\nz' ) ).to.equal( '<p>x</p><p>y</p><p>z</p>' );
+	it( 'turns double line breaks into paragraphs (Linux/Mac EOL style)', () => {
+		expect( plainTextToHtml( 'x\n\ny\n\nz' ) ).to.equal( '<p>x</p><p>y</p><p>z</p>' );
 	} );
 
-	it( 'turns double line breaks into paragraphs', () => {
-		expect( plainTextToHtml( 'x\n\ny\n\nz' ) ).to.equal( '<p>x</p><p></p><p>y</p><p></p><p>z</p>' );
+	it( 'turns double line breaks into paragraphs (Windows EOL style)', () => {
+		expect( plainTextToHtml( 'x\r\n\r\ny\r\n\r\nz' ) ).to.equal( '<p>x</p><p>y</p><p>z</p>' );
+	} );
+
+	it( 'turns single line breaks into soft breaks (Linux/Mac EOL style)', () => {
+		expect( plainTextToHtml( 'x\ny\nz' ) ).to.equal( '<p>x<br>y<br>z</p>' );
+	} );
+
+	it( 'turns single line breaks into soft breaks (Windows EOL style)', () => {
+		expect( plainTextToHtml( 'x\r\ny\r\nz' ) ).to.equal( '<p>x<br>y<br>z</p>' );
 	} );
 
 	it( 'turns combination of different amount of line breaks to paragraphs', () => {
-		expect( plainTextToHtml( 'a\nb\n\nc\n\n\nd' ) ).to.equal( '<p>a</p><p>b</p><p></p><p>c</p><p></p><p></p><p>d</p>' );
+		expect( plainTextToHtml( 'a\n\nb\nc\n\n\n\nd\ne' ) ).to.equal( '<p>a</p><p>b<br>c</p><p></p><p>d<br>e</p>' );
 	} );
 
 	it( 'preserves trailing spaces', () => {

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

@@ -8,9 +8,11 @@
 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 LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
 
 ClassicEditor.builtinPlugins.push( ImageResize );
+ClassicEditor.builtinPlugins.push( ImageInsert );
 ClassicEditor.builtinPlugins.push( LinkImage );
 
 window.ClassicEditor = ClassicEditor;

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

@@ -7,10 +7,15 @@
 
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js';
 
+const toolbarItems = [ ...ClassicEditor.defaultConfig.toolbar.items ];
+
+toolbarItems.splice( toolbarItems.indexOf( 'imageUpload' ), 1, 'imageInsert' );
+
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-insert-via-url' ), {
 		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize', 'LinkImage' ],
 		toolbar: {
+			items: toolbarItems,
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},
 		image: {
@@ -24,7 +29,7 @@ ClassicEditor
 		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
-		window.imageViaUrl = editor;
+		window.editorInsertImageViaUrl = editor;
 	} )
 	.catch( err => {
 		console.error( err );

+ 7 - 3
packages/ckeditor5-image/docs/features/image.md

@@ -13,6 +13,7 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
 * {@link module:image/imagestyle~ImageStyle} adds support for [image styles](#image-styles).
 * {@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/imageresize~ImageResize} adds support for [resizing images](#resizing-images).
 * {@link module:link/linkimage~LinkImage} adds support for [linking images](#linking-images).
 
@@ -86,12 +87,15 @@ See the {@link features/image-upload Image upload} guide.
 
 Besides the ability to insert images by uploading them directly from your disk or via CKFinder, you can also configure CKEditor 5 to allow inserting images via source URL.
 
-In order to enable this option, configure {@link module:image/imageupload~ImageUploadPanelConfig#items `image.upload.panel.items`} like below:
+In order to enable this option, install the `ImageInsert` plugin and configure {@link module:image/imageupload~ImageUploadPanelConfig#items `image.upload.panel.items`} like below:
 
 ```js
+import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert';
+
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		// ...
+		plugins: [ ... , ImageInsert ],
+		toolbar: [ ... , 'imageInsert' ],
 		image: {
 			// ...
 			upload: {
@@ -103,7 +107,7 @@ ClassicEditor
 	} )
 ```
 
-This will extend the standalone **Insert image** button in the toolbar by adding a dropdown panel with the new feature. To open the panel and add the image URL, click the arrow next to the image button. Check the demo below to insert a new image via URL or update an existing image by selecting it, opening the dropdown panel and pasting a new URL.
+This will add a new **Insert image** dropdown in the toolbar. To open the panel and add the image URL, click the arrow next to the image button. Check the demo below to insert a new image via URL or update an existing image by selecting it, opening the dropdown panel and pasting a new URL.
 
 {@snippet features/image-insert-via-url}
 

+ 98 - 0
packages/ckeditor5-image/src/imageinsert.js

@@ -0,0 +1,98 @@
+/**
+ * @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/imageinsert
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageUpload from './imageupload';
+import ImageInsertUI from './imageinsert/imageinsertui';
+
+/**
+ * The image insert plugin.
+ *
+ * For a detailed overview, check the {@glink features/image-upload/image-upload Image upload feature}
+ * and {@glink features/image#inserting-images-via-source-url Insert images via source URL} documentation.
+ *
+ * This plugin does not do anything directly, but it loads a set of specific plugins
+ * to enable image uploading or inserting via implemented integrations:
+ *
+ * * {@link module:image/imageupload~ImageUpload}
+ * * {@link module:image/imageinsert/imageinsertui~ImageInsertUI},
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageInsert extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageInsert';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageUpload, ImageInsertUI ];
+	}
+}
+
+/**
+ * Image insert panel view configuration.
+ * **NOTE:** Currently the panel settings are configurable through the `image.upload` property.
+ *
+ * @protected
+ * @member {module:image/imageupload~ImageUploadPanelConfig} module:image/imageupload~ImageUploadConfig#panel
+ */
+
+/**
+ * The configuration of the image insert dropdown panel view. Used by the image insert feature in the `@ckeditor/ckeditor5-image` package.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ * 				image: {
+ * 					upload: {
+ * 						panel: ... // panel settings for "imageInsert" view goes here
+ * 					}
+ * 				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @protected
+ * @interface module:image/imageupload~ImageUploadPanelConfig
+ */
+
+/**
+ * The list of {@link module:image/imageinsert~ImageInsert} integrations.
+ *
+ * The option accepts string tokens.
+ * * for predefined integrations, we have two special strings: `insertImageViaUrl` and `openCKFinder`.
+ * The former adds the **Insert image via URL** feature, while the latter adds the built-in **CKFinder** integration.
+ * * for custom integrations, each string should be a name of the component registered in the
+ * {@link module:ui/componentfactory~ComponentFactory component factory}.
+ * If you have a plugin `PluginX` that registers `pluginXButton` component, then the integration token
+ * in that case should be `pluginXButton`.
+ *
+ *		// Add `insertImageViaUrl`, `openCKFinder` and custom `pluginXButton` integrations.
+ *		const imageUploadConfig = {
+ *			upload: {
+ *				panel: {
+ *					items: [
+ *						'insertImageViaUrl',
+ *						'openCKFinder',
+ *						'pluginXButton'
+ *					]
+ *				}
+ *			}
+ *		};
+ *
+ * @member {Array.<String>} module:image/imageupload~ImageUploadPanelConfig#items
+ * @default [ 'insertImageViaUrl' ]
+ */

+ 151 - 0
packages/ckeditor5-image/src/imageinsert/imageinsertui.js

@@ -0,0 +1,151 @@
+/**
+ * @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/imageinsert/imageinsertui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageInsertPanelView from './ui/imageinsertpanelview';
+import { prepareIntegrations } from './utils';
+
+import { isImage } from '../image/utils';
+
+/**
+ * The image insert dropdown plugin.
+ *
+ * For a detailed overview, check the {@glink features/image-upload/image-upload Image upload feature}
+ * and {@glink features/image#inserting-images-via-source-url Insert images via source URL} documentation.
+ *
+ * Adds the `'imageInsert'` dropdown to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageInsertUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageInsertUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( 'imageInsert', locale => {
+			return this._createDropdownView( locale );
+		} );
+	}
+
+	/**
+	 * Creates the dropdown view.
+	 *
+	 * @param {module:utils/locale~Locale} locale The localization services instance.
+	 *
+	 * @private
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_createDropdownView( locale ) {
+		const editor = this.editor;
+		const imageInsertView = new ImageInsertPanelView( locale, prepareIntegrations( editor ) );
+		const command = editor.commands.get( 'imageUpload' );
+
+		const dropdownView = imageInsertView.dropdownView;
+		const splitButtonView = dropdownView.buttonView;
+
+		splitButtonView.actionView = editor.ui.componentFactory.create( 'imageUpload' );
+		// After we replaced action button with `imageUpload` component,
+		// we have lost a proper styling and some minor visual quirks have appeared.
+		// Brining back original split button classes helps fix the button styling
+		// See https://github.com/ckeditor/ckeditor5/issues/7986.
+		splitButtonView.actionView.extendTemplate( {
+			attributes: {
+				class: 'ck ck-button ck-splitbutton__action'
+			}
+		} );
+
+		return this._setUpDropdown( dropdownView, imageInsertView, command );
+	}
+
+	/**
+	 * Sets up the dropdown view.
+	 *
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdownView.
+	 * @param {module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView} imageInsertView An imageInsertView.
+	 * @param {module:core/command~Command} command An imageInsert command
+	 *
+	 * @private
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_setUpDropdown( dropdownView, imageInsertView, command ) {
+		const editor = this.editor;
+		const t = editor.t;
+		const insertButtonView = imageInsertView.insertButtonView;
+		const insertImageViaUrlForm = imageInsertView.getIntegration( 'insertImageViaUrl' );
+		const panelView = dropdownView.panelView;
+
+		dropdownView.bind( 'isEnabled' ).to( command );
+
+		// Defer the children injection to improve initial performance.
+		// See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652.
+		dropdownView.buttonView.once( 'open', () => {
+			panelView.children.add( imageInsertView );
+		} );
+
+		dropdownView.on( 'change:isOpen', () => {
+			const selectedElement = editor.model.document.selection.getSelectedElement();
+
+			if ( dropdownView.isOpen ) {
+				imageInsertView.focus();
+
+				if ( isImage( selectedElement ) ) {
+					imageInsertView.imageURLInputValue = selectedElement.getAttribute( 'src' );
+					insertButtonView.label = t( 'Update' );
+					insertImageViaUrlForm.label = t( 'Update image URL' );
+				} else {
+					imageInsertView.imageURLInputValue = '';
+					insertButtonView.label = t( 'Insert' );
+					insertImageViaUrlForm.label = t( 'Insert image via URL' );
+				}
+			}
+		} );
+
+		imageInsertView.delegate( 'submit', 'cancel' ).to( dropdownView );
+		this.delegate( 'cancel' ).to( dropdownView );
+
+		dropdownView.on( 'submit', () => {
+			closePanel();
+			onSubmit();
+		} );
+
+		dropdownView.on( 'cancel', () => {
+			closePanel();
+		} );
+
+		function onSubmit() {
+			const selectedElement = editor.model.document.selection.getSelectedElement();
+
+			if ( isImage( selectedElement ) ) {
+				editor.model.change( writer => {
+					writer.setAttribute( 'src', imageInsertView.imageURLInputValue, selectedElement );
+					writer.removeAttribute( 'srcset', selectedElement );
+					writer.removeAttribute( 'sizes', selectedElement );
+				} );
+			} else {
+				editor.execute( 'imageInsert', { source: imageInsertView.imageURLInputValue } );
+			}
+		}
+
+		function closePanel() {
+			editor.editing.view.focus();
+			dropdownView.isOpen = false;
+		}
+
+		return dropdownView;
+	}
+}

+ 3 - 3
packages/ckeditor5-image/src/imageupload/ui/imageuploadformrowview.js → packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.js

@@ -4,16 +4,16 @@
  */
 
 /**
- * @module image/imageupload/ui/imageuploadformrowview
+ * @module image/imageinsert/ui/imageinsertformrowview
  */
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
-import '../../../theme/imageuploadformrowview.css';
+import '../../../theme/imageinsertformrowview.css';
 
 /**
  * The class representing a single row in a complex form,
- * used by {@link module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView}.
+ * used by {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}.
  *
  * **Note**: For now this class is private. When more use cases appear (beyond `ckeditor5-table` and `ckeditor5-image`),
  * it will become a component in `ckeditor5-ui`.

+ 10 - 10
packages/ckeditor5-image/src/imageupload/ui/imageuploadpanelview.js → packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.js

@@ -4,14 +4,14 @@
  */
 
 /**
- * @module image/imageupload/ui/imageuploadpanelview
+ * @module image/imageinsert/ui/imageinsertpanelview
  */
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
-import ImageUploadFormRowView from './imageuploadformrowview';
+import ImageInsertFormRowView from './imageinsertformrowview';
 import { createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
@@ -25,18 +25,18 @@ import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
 import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
 import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
 
-import '../../../theme/imageupload.css';
+import '../../../theme/imageinsert.css';
 
 /**
  * The insert an image via URL view controller class.
  *
- * See {@link module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView}.
+ * See {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}.
  *
  * @extends module:ui/view~View
  */
-export default class ImageUploadPanelView extends View {
+export default class ImageInsertPanelView extends View {
 	/**
-	 * Creates a view for the dropdown panel of {@link module:image/imageupload/imageuploadui~ImageUploadUI}.
+	 * Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}.
 	 *
 	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
 	 * @param {Object} [integrations] An integrations object that contains
@@ -151,7 +151,7 @@ export default class ImageUploadPanelView extends View {
 			attributes: {
 				class: [
 					'ck',
-					'ck-image-upload-form'
+					'ck-image-insert-form'
 				],
 
 				tabindex: '-1'
@@ -159,12 +159,12 @@ export default class ImageUploadPanelView extends View {
 
 			children: [
 				...this._integrations,
-				new ImageUploadFormRowView( locale, {
+				new ImageInsertFormRowView( locale, {
 					children: [
 						this.insertButtonView,
 						this.cancelButtonView
 					],
-					class: 'ck-image-upload-form__action-row'
+					class: 'ck-image-insert-form__action-row'
 				} )
 			]
 		} );
@@ -247,7 +247,7 @@ export default class ImageUploadPanelView extends View {
 
 		panelView.extendTemplate( {
 			attributes: {
-				class: 'ck-image-upload__panel'
+				class: 'ck-image-insert__panel'
 			}
 		} );
 

+ 77 - 0
packages/ckeditor5-image/src/imageinsert/utils.js

@@ -0,0 +1,77 @@
+/**
+ * @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/imageinsert/utils
+ */
+
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
+
+/**
+ * Creates integrations object that will be passed to the
+ * {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}.
+ *
+ * @param {module:core/editor/editor~Editor} editor Editor instance.
+ *
+ * @returns {Object.<String, module:ui/view~View>} Integrations object.
+ */
+export function prepareIntegrations( editor ) {
+	const panelItems = editor.config.get( 'image.upload.panel.items' );
+	const imageInsertUIPlugin = editor.plugins.get( 'ImageInsertUI' );
+
+	const PREDEFINED_INTEGRATIONS = {
+		'insertImageViaUrl': createLabeledInputView( editor.locale )
+	};
+
+	if ( !panelItems ) {
+		return PREDEFINED_INTEGRATIONS;
+	}
+
+	// Prepares ckfinder component for the `openCKFinder` integration token.
+	if ( panelItems.find( item => item === 'openCKFinder' ) && editor.ui.componentFactory.has( 'ckfinder' ) ) {
+		const ckFinderButton = editor.ui.componentFactory.create( 'ckfinder' );
+		ckFinderButton.set( {
+			withText: true,
+			class: 'ck-image-insert__ck-finder-button'
+		} );
+
+		// We want to close the dropdown panel view when user clicks the ckFinderButton.
+		ckFinderButton.delegate( 'execute' ).to( imageInsertUIPlugin, 'cancel' );
+
+		PREDEFINED_INTEGRATIONS.openCKFinder = ckFinderButton;
+	}
+
+	// Creates integrations object of valid views to pass it to the ImageInsertPanelView.
+	return panelItems.reduce( ( object, key ) => {
+		if ( PREDEFINED_INTEGRATIONS[ key ] ) {
+			object[ key ] = PREDEFINED_INTEGRATIONS[ key ];
+		} else if ( editor.ui.componentFactory.has( key ) ) {
+			object[ key ] = editor.ui.componentFactory.create( key );
+		}
+
+		return object;
+	}, {} );
+}
+
+/**
+ * Creates labeled field view.
+ *
+ * @param {module:utils/locale~Locale} locale The localization services instance.
+ *
+ * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
+ */
+export function createLabeledInputView( locale ) {
+	const t = locale.t;
+	const labeledInputView = new LabeledFieldView( locale, createLabeledInputText );
+
+	labeledInputView.set( {
+		label: t( 'Insert image via URL' )
+	} );
+	labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
+	labeledInputView.infoText = t( 'Paste the image source URL.' );
+
+	return labeledInputView;
+}

+ 0 - 55
packages/ckeditor5-image/src/imageupload.js

@@ -83,58 +83,3 @@ export default class ImageUpload extends Plugin {
  * @member {Array.<String>} module:image/imageupload~ImageUploadConfig#types
  * @default [ 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff' ]
  */
-
-/**
- * The image upload panel view configuration.
- *
- * @protected
- * @member {module:image/imageupload~ImageUploadPanelConfig} module:image/imageupload~ImageUploadConfig#panel
- */
-
-/**
- * The configuration of the image upload dropdown panel view. Used by the image upload feature in the `@ckeditor/ckeditor5-image` package.
- *
- *		ClassicEditor
- *			.create( editorElement, {
- * 				image: {
- * 					upload: {
- * 						panel: ... // Panel settings go here.
- * 					}
- * 				}
- *			} )
- *			.then( ... )
- *			.catch( ... );
- *
- * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
- *
- * @protected
- * @interface module:image/imageupload~ImageUploadPanelConfig
- */
-
-/**
- * The list of {@link module:image/imageupload~ImageUpload} integrations.
- *
- * This option accepts string tokens:
- * * For predefined integrations, there are two special strings: `insertImageViaUrl` and `openCKFinder`.
- * The former adds the {@glink features/image#inserting-images-via-source-url Insert image via URL} feature, while the latter adds the
- * built-in {@glink features/image-upload/ckfinder CKFinder} integration.
- * * For custom integrations, each string should be a name of the already registered component.
- * If you have a plugin `PluginX` that registers the `pluginXButton` component, then in that case the integration token
- * should be `pluginXButton`.
- *
- *		// Add `insertImageViaUrl`, `openCKFinder` and custom `pluginXButton` integrations.
- *		const imageUploadConfig = {
- *			upload: {
- *				panel: {
- *					items: [
- *						'insertImageViaUrl',
- *						'openCKFinder',
- *						'pluginXButton'
- *					]
- *				}
- *			}
- *		};
- *
- * @member {Array.<String>} module:image/imageupload~ImageUploadPanelConfig#items
- * @default [ 'insertImageViaUrl' ]
- */

+ 24 - 139
packages/ckeditor5-image/src/imageupload/imageuploadui.js

@@ -8,21 +8,17 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageUploadPanelView from './ui/imageuploadpanelview';
-
 import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
-import { createImageTypeRegExp, prepareIntegrations } from './utils';
+import { createImageTypeRegExp } from './utils';
 
 import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
 
-import { isImage } from '../image/utils';
-
 /**
  * The image upload button plugin.
  *
  * For a detailed overview, check the {@glink features/image-upload/image-upload Image upload feature} documentation.
  *
- * Adds the `'imageUpload'` dropdown to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
+ * Adds the `'imageUpload'` button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -39,148 +35,37 @@ export default class ImageUploadUI extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
-		const isImageUploadPanelViewEnabled = !!editor.config.get( 'image.upload.panel.items' );
+		const t = editor.t;
 
+		// Setup `imageUpload` button.
 		editor.ui.componentFactory.add( 'imageUpload', locale => {
-			if ( isImageUploadPanelViewEnabled ) {
-				return this._createDropdownView( locale );
-			} else {
-				return this._createFileDialogButtonView( locale );
-			}
-		} );
-	}
+			const view = new FileDialogButtonView( locale );
+			const command = editor.commands.get( 'imageUpload' );
+			const imageTypes = editor.config.get( 'image.upload.types' );
+			const imageTypesRegExp = createImageTypeRegExp( imageTypes );
 
-	/**
-	 * Sets up the dropdown view.
-	 *
-	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdownView.
-	 * @param {module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView} imageUploadView An imageUploadView.
-	 * @param {module:core/command~Command} command An imageUpload command
-	 *
-	 * @private
-	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
-	 */
-	_setUpDropdown( dropdownView, imageUploadView, command ) {
-		const editor = this.editor;
-		const t = editor.t;
-		const insertButtonView = imageUploadView.insertButtonView;
-		const insertImageViaUrlForm = imageUploadView.getIntegration( 'insertImageViaUrl' );
+			view.set( {
+				acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ),
+				allowMultipleFiles: true
+			} );
 
-		dropdownView.bind( 'isEnabled' ).to( command );
+			view.buttonView.set( {
+				label: t( 'Insert image' ),
+				icon: imageIcon,
+				tooltip: true
+			} );
 
-		dropdownView.on( 'change:isOpen', () => {
-			const selectedElement = editor.model.document.selection.getSelectedElement();
+			view.buttonView.bind( 'isEnabled' ).to( command );
 
-			if ( dropdownView.isOpen ) {
-				imageUploadView.focus();
+			view.on( 'done', ( evt, files ) => {
+				const imagesToUpload = Array.from( files ).filter( file => imageTypesRegExp.test( file.type ) );
 
-				if ( isImage( selectedElement ) ) {
-					imageUploadView.imageURLInputValue = selectedElement.getAttribute( 'src' );
-					insertButtonView.label = t( 'Update' );
-					insertImageViaUrlForm.label = t( 'Update image URL' );
-				} else {
-					imageUploadView.imageURLInputValue = '';
-					insertButtonView.label = t( 'Insert' );
-					insertImageViaUrlForm.label = t( 'Insert image via URL' );
+				if ( imagesToUpload.length ) {
+					editor.execute( 'imageUpload', { file: imagesToUpload } );
 				}
-			}
-		} );
-
-		imageUploadView.delegate( 'submit', 'cancel' ).to( dropdownView );
-		this.delegate( 'cancel' ).to( dropdownView );
-
-		dropdownView.on( 'submit', () => {
-			closePanel();
-			onSubmit();
-		} );
-
-		dropdownView.on( 'cancel', () => {
-			closePanel();
-		} );
-
-		function onSubmit() {
-			const selectedElement = editor.model.document.selection.getSelectedElement();
-
-			if ( isImage( selectedElement ) ) {
-				editor.model.change( writer => {
-					writer.setAttribute( 'src', imageUploadView.imageURLInputValue, selectedElement );
-					writer.removeAttribute( 'srcset', selectedElement );
-					writer.removeAttribute( 'sizes', selectedElement );
-				} );
-			} else {
-				editor.execute( 'imageInsert', { source: imageUploadView.imageURLInputValue } );
-			}
-		}
-
-		function closePanel() {
-			editor.editing.view.focus();
-			dropdownView.isOpen = false;
-		}
-
-		return dropdownView;
-	}
+			} );
 
-	/**
-	 * Creates the dropdown view.
-	 *
-	 * @param {module:utils/locale~Locale} locale The localization services instance.
-	 *
-	 * @private
-	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
-	 */
-	_createDropdownView( locale ) {
-		const editor = this.editor;
-		const imageUploadView = new ImageUploadPanelView( locale, prepareIntegrations( editor ) );
-		const command = editor.commands.get( 'imageUpload' );
-
-		const dropdownView = imageUploadView.dropdownView;
-		const panelView = dropdownView.panelView;
-		const splitButtonView = dropdownView.buttonView;
-
-		splitButtonView.actionView = this._createFileDialogButtonView( locale );
-
-		panelView.children.add( imageUploadView );
-
-		return this._setUpDropdown( dropdownView, imageUploadView, command );
-	}
-
-	/**
-	 * Creates and sets up the file dialog button view.
-	 *
-	 * @param {module:utils/locale~Locale} locale The localization services instance.
-	 *
-	 * @private
-	 * @returns {module:upload/ui/filedialogbuttonview~FileDialogButtonView}
-	 */
-	_createFileDialogButtonView( locale ) {
-		const editor = this.editor;
-		const t = locale.t;
-		const imageTypes = editor.config.get( 'image.upload.types' );
-		const fileDialogButtonView = new FileDialogButtonView( locale );
-		const imageTypesRegExp = createImageTypeRegExp( imageTypes );
-		const command = editor.commands.get( 'imageUpload' );
-
-		fileDialogButtonView.set( {
-			acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ),
-			allowMultipleFiles: true
-		} );
-
-		fileDialogButtonView.buttonView.set( {
-			label: t( 'Insert image' ),
-			icon: imageIcon,
-			tooltip: true
+			return view;
 		} );
-
-		fileDialogButtonView.buttonView.bind( 'isEnabled' ).to( command );
-
-		fileDialogButtonView.on( 'done', ( evt, files ) => {
-			const imagesToUpload = Array.from( files ).filter( file => imageTypesRegExp.test( file.type ) );
-
-			if ( imagesToUpload.length ) {
-				editor.execute( 'imageUpload', { file: imagesToUpload } );
-			}
-		} );
-
-		return fileDialogButtonView;
 	}
 }

+ 0 - 69
packages/ckeditor5-image/src/imageupload/utils.js

@@ -9,9 +9,6 @@
 
 /* global fetch, File */
 
-import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
-import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils';
-
 /**
  * Creates a regular expression used to test for image files.
  *
@@ -85,69 +82,3 @@ function getImageMimeType( blob, src ) {
 		return 'image/jpeg';
 	}
 }
-
-/**
- * Creates an integrations object that will be passed to the
- * {@link module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView}.
- *
- * @param {module:core/editor/editor~Editor} editor The editor instance.
- *
- * @returns {Object.<String, module:ui/view~View>} The integrations object.
- */
-export function prepareIntegrations( editor ) {
-	const panelItems = editor.config.get( 'image.upload.panel.items' );
-	const imageUploadUIPlugin = editor.plugins.get( 'ImageUploadUI' );
-
-	const PREDEFINED_INTEGRATIONS = {
-		'insertImageViaUrl': createLabeledInputView( editor.locale )
-	};
-
-	if ( !panelItems ) {
-		return PREDEFINED_INTEGRATIONS;
-	}
-
-	// Prepares ckfinder component for the `openCKFinder` integration token.
-	if ( panelItems.find( item => item === 'openCKFinder' ) && editor.ui.componentFactory.has( 'ckfinder' ) ) {
-		const ckFinderButton = editor.ui.componentFactory.create( 'ckfinder' );
-		ckFinderButton.set( {
-			withText: true,
-			class: 'ck-image-upload__ck-finder-button'
-		} );
-
-		// We want to close the dropdown panel view when user clicks the ckFinderButton.
-		ckFinderButton.delegate( 'execute' ).to( imageUploadUIPlugin, 'cancel' );
-
-		PREDEFINED_INTEGRATIONS.openCKFinder = ckFinderButton;
-	}
-
-	// Creates integrations object of valid views to pass it to the ImageUploadPanelView.
-	return panelItems.reduce( ( object, key ) => {
-		if ( PREDEFINED_INTEGRATIONS[ key ] ) {
-			object[ key ] = PREDEFINED_INTEGRATIONS[ key ];
-		} else if ( editor.ui.componentFactory.has( key ) ) {
-			object[ key ] = editor.ui.componentFactory.create( key );
-		}
-
-		return object;
-	}, {} );
-}
-
-/**
- * Creates a labeled field view.
- *
- * @param {module:utils/locale~Locale} locale The localization services instance.
- *
- * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
- */
-export function createLabeledInputView( locale ) {
-	const t = locale.t;
-	const labeledInputView = new LabeledFieldView( locale, createLabeledInputText );
-
-	labeledInputView.set( {
-		label: t( 'Insert image via URL' )
-	} );
-	labeledInputView.fieldView.placeholder = 'https://example.com/src/image.png';
-	labeledInputView.infoText = t( 'Paste the image source URL.' );
-
-	return labeledInputView;
-}

+ 327 - 0
packages/ckeditor5-image/tests/imageinsert/imageinsertui.js

@@ -0,0 +1,327 @@
+/**
+ * @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 document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Image from '../../src/image';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
+import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
+import ImageInsert from '../../src/imageinsert';
+import ImageInsertUI from '../../src/imageinsert/imageinsertui';
+import ImageInsertPanelView from '../../src/imageinsert/ui/imageinsertpanelview';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
+
+describe( 'ImageInsertUI', () => {
+	let editor, editorElement, fileRepository, dropdown;
+
+	class UploadAdapterPluginMock extends Plugin {
+		init() {
+			fileRepository = this.editor.plugins.get( FileRepository );
+			fileRepository.createUploadAdapter = loader => {
+				return new UploadAdapterMock( loader );
+			};
+		}
+	}
+
+	describe( 'dropdown', () => {
+		beforeEach( () => {
+			editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicEditor
+				.create( editorElement, {
+					plugins: [ Paragraph, Image, ImageInsert, ImageInsertUI, FileRepository, UploadAdapterPluginMock, Clipboard ],
+					toolbar: [ 'imageInsert' ],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl'
+								]
+							}
+						}
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					dropdown = editor.ui.view.toolbar.children.first.children.first;
+
+					// Hide all notifications (prevent alert() calls).
+					const notification = editor.plugins.get( Notification );
+					notification.on( 'show', evt => evt.stop() );
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should register the "imageInsert" dropdown', () => {
+			const button = editor.ui.componentFactory.create( 'imageInsert' );
+
+			expect( button ).to.be.instanceOf( DropdownView );
+		} );
+
+		it( 'should not insert panel view children until dropdown is not open for the first time', () => {
+			expect( dropdown.panelView.children.length ).to.equal( 0 );
+
+			dropdown.buttonView.fire( 'open' );
+
+			expect( dropdown.panelView.children.length ).to.equal( 1 );
+			expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView );
+		} );
+
+		describe( 'dropdown action button', () => {
+			it( 'should be an instance of FileDialogButtonView', () => {
+				const dropdown = editor.ui.componentFactory.create( 'imageInsert' );
+
+				expect( dropdown.buttonView.actionView ).to.be.instanceOf( FileDialogButtonView );
+			} );
+		} );
+
+		describe( 'dropdown panel buttons', () => {
+			it( 'should have "Update" label on submit button when URL input is already filled', () => {
+				const viewDocument = editor.editing.view.document;
+
+				editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
+
+				editor.editing.view.change( writer => {
+					writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+				} );
+
+				const img = viewDocument.selection.getSelectedElement();
+
+				const data = fakeEventData();
+				const eventInfo = new EventInfo( img, 'click' );
+				const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+				viewDocument.fire( 'click', domEventDataMock );
+
+				dropdown.buttonView.fire( 'open' );
+
+				const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+
+				expect( inputValue ).to.equal( '/assets/sample.png' );
+				expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' );
+			} );
+
+			it( 'should have "Insert" label on submit button on uploading a new image', () => {
+				const viewDocument = editor.editing.view.document;
+
+				editor.setData( '<p>test</p>' );
+
+				editor.editing.view.change( writer => {
+					writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
+				} );
+
+				const el = viewDocument.selection.getSelectedElement();
+
+				const data = fakeEventData();
+				const eventInfo = new EventInfo( el, 'click' );
+				const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+				viewDocument.fire( 'click', domEventDataMock );
+
+				dropdown.buttonView.fire( 'open' );
+
+				const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+
+				expect( dropdown.isOpen ).to.be.true;
+				expect( inputValue ).to.equal( '' );
+				expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' );
+			} );
+		} );
+
+		describe( 'dropdown panel integrations', () => {
+			describe( 'insert image via URL form', () => {
+				it( 'should have "Insert image via URL" label on inserting new image', () => {
+					const viewDocument = editor.editing.view.document;
+
+					editor.setData( '<p>test</p>' );
+
+					editor.editing.view.change( writer => {
+						writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
+					} );
+
+					const el = viewDocument.selection.getSelectedElement();
+
+					const data = fakeEventData();
+					const eventInfo = new EventInfo( el, 'click' );
+					const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+					viewDocument.fire( 'click', domEventDataMock );
+
+					dropdown.buttonView.fire( 'open' );
+
+					const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+
+					const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
+
+					expect( dropdown.isOpen ).to.be.true;
+					expect( inputValue ).to.equal( '' );
+					expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' );
+				} );
+
+				it( 'should have "Update image URL" label on updating the image source URL', () => {
+					const viewDocument = editor.editing.view.document;
+
+					editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
+
+					editor.editing.view.change( writer => {
+						writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+					} );
+
+					const el = viewDocument.selection.getSelectedElement();
+
+					const data = fakeEventData();
+					const eventInfo = new EventInfo( el, 'click' );
+					const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+					viewDocument.fire( 'click', domEventDataMock );
+
+					dropdown.buttonView.fire( 'open' );
+
+					const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+					const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
+
+					expect( dropdown.isOpen ).to.be.true;
+					expect( inputValue ).to.equal( '/assets/sample.png' );
+					expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' );
+				} );
+			} );
+		} );
+
+		it( 'should remove all attributes from model except "src" when updating the image source URL', () => {
+			const viewDocument = editor.editing.view.document;
+			const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+			const submitSpy = sinon.spy();
+
+			dropdown.buttonView.fire( 'open' );
+
+			const insertButtonView = dropdown.panelView.children.first.insertButtonView;
+
+			editor.setData( '<figure><img src="image-url-800w.jpg"' +
+			'srcset="image-url-480w.jpg 480w,image-url-800w.jpg 800w"' +
+			'sizes="(max-width: 600px) 480px,800px"' +
+			'alt="test-image"></figure>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+			} );
+
+			const selectedElement = editor.model.document.selection.getSelectedElement();
+
+			expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' );
+			expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true;
+
+			dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png';
+
+			dropdown.on( 'submit', submitSpy );
+
+			insertButtonView.fire( 'execute' );
+
+			sinon.assert.notCalled( commandSpy );
+			sinon.assert.calledOnce( submitSpy );
+			expect( dropdown.isOpen ).to.be.false;
+			expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' );
+			expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false;
+			expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false;
+		} );
+
+		describe( 'events', () => {
+			it( 'should emit "submit" event when clicking on submit button', () => {
+				const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+				const submitSpy = sinon.spy();
+
+				dropdown.buttonView.fire( 'open' );
+
+				dropdown.on( 'submit', submitSpy );
+
+				const insertButtonView = dropdown.panelView.children.first.insertButtonView;
+
+				insertButtonView.fire( 'execute' );
+
+				expect( dropdown.isOpen ).to.be.false;
+				sinon.assert.calledOnce( commandSpy );
+				sinon.assert.calledOnce( submitSpy );
+			} );
+
+			it( 'should emit "cancel" event when clicking on cancel button', () => {
+				const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+				const cancelSpy = sinon.spy();
+
+				dropdown.buttonView.fire( 'open' );
+
+				dropdown.on( 'cancel', cancelSpy );
+
+				const cancelButtonView = dropdown.panelView.children.first.cancelButtonView;
+
+				cancelButtonView.fire( 'execute' );
+
+				expect( dropdown.isOpen ).to.be.false;
+				sinon.assert.notCalled( commandSpy );
+				sinon.assert.calledOnce( cancelSpy );
+			} );
+		} );
+
+		it( 'should inject integrations to the dropdown panel view from the config', async () => {
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						CKFinder,
+						Paragraph,
+						Image,
+						ImageInsert,
+						ImageInsertUI,
+						FileRepository,
+						UploadAdapterPluginMock,
+						Clipboard
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl',
+									'openCKFinder'
+								]
+							}
+						}
+					}
+				} );
+
+			const dropdown = editor.ui.componentFactory.create( 'imageInsert' );
+
+			dropdown.buttonView.fire( 'open' );
+
+			expect( dropdown.panelView.children.first._integrations.length ).to.equal( 2 );
+			expect( dropdown.panelView.children.first._integrations.first ).to.be.instanceOf( LabeledFieldView );
+			expect( dropdown.panelView.children.first._integrations.last ).to.be.instanceOf( ButtonView );
+
+			editor.destroy();
+		} );
+	} );
+} );
+
+function fakeEventData() {
+	return {
+		preventDefault: sinon.spy()
+	};
+}

+ 1 - 1
packages/ckeditor5-image/tests/imageupload/ui/imageuploadformrowview.js → packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js

@@ -4,7 +4,7 @@
  */
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
-import ImageUploadFormRowView from '../../../src/imageupload/ui/imageuploadformrowview';
+import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview';
 import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
 
 describe( 'ImageUploadFormRowView', () => {

+ 4 - 4
packages/ckeditor5-image/tests/imageupload/ui/imageuploadpanelview.js → packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js

@@ -8,8 +8,8 @@
 import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
 
-import ImageUploadPanelView from '../../../src/imageupload/ui/imageuploadpanelview';
-import ImageUploadFormRowView from '../../../src/imageupload/ui/imageuploadformrowview';
+import ImageUploadPanelView from '../../../src/imageinsert/ui/imageinsertpanelview';
+import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
@@ -21,7 +21,7 @@ import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
-import { createLabeledInputView } from '../../../src/imageupload/utils';
+import { createLabeledInputView } from '../../../src/imageinsert/utils';
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
@@ -140,7 +140,7 @@ describe( 'ImageUploadPanelView', () => {
 	describe( 'template', () => {
 		it( 'should create element from the template', () => {
 			expect( view.element.classList.contains( 'ck' ) ).to.true;
-			expect( view.element.classList.contains( 'ck-image-upload-form' ) ).to.true;
+			expect( view.element.classList.contains( 'ck-image-insert-form' ) ).to.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 148 - 0
packages/ckeditor5-image/tests/imageinsert/utils.js

@@ -0,0 +1,148 @@
+/**
+ * @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 document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Image from '../../src/image';
+import ImageUploadUI from '../../src/imageinsert/imageinsertui';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import { prepareIntegrations, createLabeledInputView } from '../../src/imageinsert/utils';
+
+describe( 'Upload utils', () => {
+	describe( 'prepareIntegrations()', () => {
+		it( 'should return "insetImageViaUrl" and "openCKFinder" integrations', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						CKFinder,
+						Paragraph,
+						Image,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl',
+									'openCKFinder'
+								]
+							}
+						}
+					}
+				} );
+
+			const openCKFinderExtendedView = Object.values( prepareIntegrations( editor ) )[ 1 ];
+
+			expect( openCKFinderExtendedView.class ).contains( 'ck-image-insert__ck-finder-button' );
+			expect( openCKFinderExtendedView.label ).to.equal( 'Insert image or file' );
+			expect( openCKFinderExtendedView.withText ).to.be.true;
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should return only "insertImageViaUrl" integration and throw warning ' +
+			'for "image-upload-integrations-invalid-view" error', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Image,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl',
+									'openCKFinder'
+								]
+							}
+						}
+					}
+				} );
+
+			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should return only "link" integration', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Link,
+						Image,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'link'
+								]
+							}
+						}
+					}
+				} );
+
+			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
+			expect( Object.values( prepareIntegrations( editor ) )[ 0 ].label ).to.equal( 'Link' );
+			expect( Object.values( prepareIntegrations( editor ) )[ 0 ] ).to.be.instanceOf( ButtonView );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should return "insertImageViaUrl" integration, when no integrations were configured', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Image,
+						ImageUploadUI
+					]
+				} );
+
+			expect( Object.keys( prepareIntegrations( editor ) ).length ).to.equal( 1 );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+	} );
+
+	describe( 'createLabeledInputView()', () => {
+		describe( 'image URL input view', () => {
+			it( 'should have placeholder', () => {
+				const view = createLabeledInputView( { t: val => val } );
+				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
+			} );
+
+			it( 'should have info text', () => {
+				const view = createLabeledInputView( { t: val => val } );
+				expect( view.infoText ).to.match( /^Paste the image source URL/ );
+			} );
+		} );
+	} );
+} );

+ 109 - 539
packages/ckeditor5-image/tests/imageupload/imageuploadui.js

@@ -9,7 +9,6 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Image from '../../src/image';
-import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import ImageUploadUI from '../../src/imageupload/imageuploadui';
@@ -17,11 +16,6 @@ import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
-import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
-import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
-import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -38,578 +32,154 @@ describe( 'ImageUploadUI', () => {
 		}
 	}
 
-	describe( 'file dialog button', () => {
-		beforeEach( () => {
-			editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			return ClassicEditor
-				.create( editorElement, {
-					plugins: [ Paragraph, Image, ImageUploadEditing, ImageUploadUI, FileRepository, UploadAdapterPluginMock, Clipboard ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					model = editor.model;
-
-					// Hide all notifications (prevent alert() calls).
-					const notification = editor.plugins.get( Notification );
-					notification.on( 'show', evt => evt.stop() );
-				} );
-		} );
-
-		afterEach( () => {
-			editorElement.remove();
-
-			return editor.destroy();
-		} );
-		it( 'should register imageUpload file dialog button', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-
-			expect( button ).to.be.instanceOf( FileDialogButtonView );
-		} );
-
-		it( 'should set proper accepted mime-types for imageUpload button as defined in configuration', () => {
-			editor.config.set( 'image.upload.types', [ 'svg+xml', 'jpeg', 'vnd.microsoft.icon', 'x-xbitmap' ] );
-
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-
-			expect( button.acceptedType ).to.equal( 'image/svg+xml,image/jpeg,image/vnd.microsoft.icon,image/x-xbitmap' );
-		} );
-
-		it( 'should be disabled while ImageUploadCommand is disabled', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const command = editor.commands.get( 'imageUpload' );
-
-			command.isEnabled = true;
-
-			expect( button.buttonView.isEnabled ).to.true;
-
-			command.isEnabled = false;
-
-			expect( button.buttonView.isEnabled ).to.false;
-		} );
-
-		// ckeditor5-upload/#77
-		it( 'should be properly bound with ImageUploadCommand', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const command = editor.commands.get( 'imageUpload' );
-			const spy = sinon.spy();
-
-			button.render();
-
-			button.buttonView.on( 'execute', spy );
-
-			command.isEnabled = false;
-
-			button.buttonView.element.dispatchEvent( new Event( 'click' ) );
-
-			sinon.assert.notCalled( spy );
-		} );
-
-		it( 'should execute imageUpload command', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = [ createNativeFileMock() ];
-
-			button.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
-		} );
-
-		it( 'should execute imageUpload command with multiple files', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = [ createNativeFileMock(), createNativeFileMock(), createNativeFileMock() ];
-
-			button.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
-		} );
-
-		it( 'should optimize the insertion position', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = [ createNativeFileMock() ];
-
-			setModelData( model, '<paragraph>f[]oo</paragraph>' );
-
-			button.fire( 'done', files );
-
-			const id = fileRepository.getLoader( files[ 0 ] ).id;
-
-			expect( getModelData( model ) ).to.equal(
-				`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
-			'<paragraph>foo</paragraph>'
-			);
-		} );
-
-		it( 'should correctly insert multiple files', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = [ createNativeFileMock(), createNativeFileMock() ];
-
-			setModelData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
-
-			button.fire( 'done', files );
-
-			const id1 = fileRepository.getLoader( files[ 0 ] ).id;
-			const id2 = fileRepository.getLoader( files[ 1 ] ).id;
-
-			expect( getModelData( model ) ).to.equal(
-				'<paragraph>foo</paragraph>' +
-			`<image uploadId="${ id1 }" uploadStatus="reading"></image>` +
-			`[<image uploadId="${ id2 }" uploadStatus="reading"></image>]` +
-			'<paragraph>bar</paragraph>'
-			);
-		} );
-
-		it( 'should not execute imageUpload if the file is not an image', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const file = {
-				type: 'media/mp3',
-				size: 1024
-			};
-
-			button.fire( 'done', [ file ] );
-			sinon.assert.notCalled( executeStub );
-		} );
-
-		it( 'should work even if the FileList does not support iterators', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = {
-				0: createNativeFileMock(),
-				length: 1
-			};
-
-			button.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( [ files[ 0 ] ] );
-		} );
-
-		it( 'should add the new image after the selected one, without replacing the selected image', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const files = [ createNativeFileMock() ];
-
-			setModelData( model, '[<image src="/assets/sample.png"></image>]<paragraph>bar</paragraph>' );
-
-			button.fire( 'done', files );
-
-			const id1 = fileRepository.getLoader( files[ 0 ] ).id;
-
-			expect( getModelData( model ) ).to.equal(
-				'<image src="/assets/sample.png"></image>' +
-				`[<image uploadId="${ id1 }" uploadStatus="reading"></image>]` +
-				'<paragraph>bar</paragraph>'
-			);
-		} );
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicEditor
+			.create( editorElement, {
+				plugins: [ Paragraph, Image, ImageUploadEditing, ImageUploadUI, FileRepository, UploadAdapterPluginMock, Clipboard ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				// Hide all notifications (prevent alert() calls).
+				const notification = editor.plugins.get( Notification );
+				notification.on( 'show', evt => evt.stop() );
+			} );
 	} );
 
-	describe( 'dropdown', () => {
-		beforeEach( () => {
-			editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			return ClassicEditor
-				.create( editorElement, {
-					plugins: [ Paragraph, Image, ImageUploadEditing, ImageUploadUI, FileRepository, UploadAdapterPluginMock, Clipboard ],
-					image: {
-						upload: {
-							panel: {
-								items: [
-									'insertImageViaUrl'
-								]
-							}
-						}
-					}
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					model = editor.model;
+	afterEach( () => {
+		editorElement.remove();
 
-					// Hide all notifications (prevent alert() calls).
-					const notification = editor.plugins.get( Notification );
-					notification.on( 'show', evt => evt.stop() );
-				} );
-		} );
-
-		afterEach( () => {
-			editorElement.remove();
+		return editor.destroy();
+	} );
 
-			return editor.destroy();
-		} );
-		it( 'should register imageUpload dropdown', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
+	it( 'should register imageUpload button', () => {
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
 
-			expect( button ).to.be.instanceOf( DropdownView );
-		} );
+		expect( button ).to.be.instanceOf( FileDialogButtonView );
+	} );
 
-		it( 'should set proper accepted mime-types for imageUpload button as defined in configuration', () => {
-			editor.config.set( 'image.upload.types', [ 'svg+xml', 'jpeg', 'vnd.microsoft.icon', 'x-xbitmap' ] );
+	it( 'should set proper accepted mime-types for imageUpload button as defined in configuration', () => {
+		editor.config.set( 'image.upload.types', [ 'svg+xml', 'jpeg', 'vnd.microsoft.icon', 'x-xbitmap' ] );
 
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
 
-			expect( fileDialogButton.acceptedType ).to.equal( 'image/svg+xml,image/jpeg,image/vnd.microsoft.icon,image/x-xbitmap' );
-		} );
+		expect( button.acceptedType ).to.equal( 'image/svg+xml,image/jpeg,image/vnd.microsoft.icon,image/x-xbitmap' );
+	} );
 
-		it( 'should be disabled while ImageUploadCommand is disabled', () => {
-			const button = editor.ui.componentFactory.create( 'imageUpload' );
-			const command = editor.commands.get( 'imageUpload' );
+	it( 'should be disabled while ImageUploadCommand is disabled', () => {
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const command = editor.commands.get( 'imageUpload' );
 
-			command.isEnabled = true;
+		command.isEnabled = true;
 
-			expect( button.buttonView.isEnabled ).to.true;
+		expect( button.buttonView.isEnabled ).to.true;
 
-			command.isEnabled = false;
+		command.isEnabled = false;
 
-			expect( button.buttonView.isEnabled ).to.false;
-		} );
+		expect( button.buttonView.isEnabled ).to.false;
+	} );
 
-		// ckeditor5-upload/#77
-		it( 'should be properly bound with ImageUploadCommand', () => {
-			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-			const command = editor.commands.get( 'imageUpload' );
-			const spy = sinon.spy();
+	// ckeditor5-upload/#77
+	it( 'should be properly bound with ImageUploadCommand', () => {
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const command = editor.commands.get( 'imageUpload' );
+		const spy = sinon.spy();
 
-			dropdown.render();
+		button.render();
 
-			dropdown.buttonView.on( 'execute', spy );
+		button.buttonView.on( 'execute', spy );
 
-			command.isEnabled = false;
+		command.isEnabled = false;
 
-			dropdown.buttonView.element.dispatchEvent( new Event( 'click' ) );
+		button.buttonView.element.dispatchEvent( new Event( 'click' ) );
 
-			sinon.assert.notCalled( spy );
-		} );
+		sinon.assert.notCalled( spy );
+	} );
 
-		it( 'should execute imageUpload command', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const files = [ createNativeFileMock() ];
+	it( 'should execute imageUpload command', () => {
+		const executeStub = sinon.stub( editor, 'execute' );
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const files = [ createNativeFileMock() ];
 
-			fileDialogButton.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
-		} );
+		button.fire( 'done', files );
+		sinon.assert.calledOnce( executeStub );
+		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
+	} );
 
-		it( 'should execute imageUpload command with multiple files', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const files = [ createNativeFileMock(), createNativeFileMock(), createNativeFileMock() ];
+	it( 'should execute imageUpload command with multiple files', () => {
+		const executeStub = sinon.stub( editor, 'execute' );
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const files = [ createNativeFileMock(), createNativeFileMock(), createNativeFileMock() ];
 
-			fileDialogButton.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
-		} );
+		button.fire( 'done', files );
+		sinon.assert.calledOnce( executeStub );
+		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
+	} );
 
-		it( 'should optimize the insertion position', () => {
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const files = [ createNativeFileMock() ];
+	it( 'should optimize the insertion position', () => {
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const files = [ createNativeFileMock() ];
 
-			setModelData( model, '<paragraph>f[]oo</paragraph>' );
+		setModelData( model, '<paragraph>f[]oo</paragraph>' );
 
-			fileDialogButton.fire( 'done', files );
+		button.fire( 'done', files );
 
-			const id = fileRepository.getLoader( files[ 0 ] ).id;
+		const id = fileRepository.getLoader( files[ 0 ] ).id;
 
-			expect( getModelData( model ) ).to.equal(
-				`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
+		expect( getModelData( model ) ).to.equal(
+			`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
 			'<paragraph>foo</paragraph>'
-			);
-		} );
+		);
+	} );
 
-		it( 'should correctly insert multiple files', () => {
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const files = [ createNativeFileMock(), createNativeFileMock() ];
+	it( 'should correctly insert multiple files', () => {
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const files = [ createNativeFileMock(), createNativeFileMock() ];
 
-			setModelData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
+		setModelData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
 
-			fileDialogButton.fire( 'done', files );
+		button.fire( 'done', files );
 
-			const id1 = fileRepository.getLoader( files[ 0 ] ).id;
-			const id2 = fileRepository.getLoader( files[ 1 ] ).id;
+		const id1 = fileRepository.getLoader( files[ 0 ] ).id;
+		const id2 = fileRepository.getLoader( files[ 1 ] ).id;
 
-			expect( getModelData( model ) ).to.equal(
-				'<paragraph>foo</paragraph>' +
+		expect( getModelData( model ) ).to.equal(
+			'<paragraph>foo</paragraph>' +
 			`<image uploadId="${ id1 }" uploadStatus="reading"></image>` +
 			`[<image uploadId="${ id2 }" uploadStatus="reading"></image>]` +
 			'<paragraph>bar</paragraph>'
-			);
-		} );
-
-		it( 'should not execute imageUpload if the file is not an image', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const file = {
-				type: 'media/mp3',
-				size: 1024
-			};
-
-			fileDialogButton.fire( 'done', [ file ] );
-			sinon.assert.notCalled( executeStub );
-		} );
-
-		it( 'should work even if the FileList does not support iterators', () => {
-			const executeStub = sinon.stub( editor, 'execute' );
-			const plugin = editor.plugins.get( 'ImageUploadUI' );
-			const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
-			const files = {
-				0: createNativeFileMock(),
-				length: 1
-			};
-
-			fileDialogButton.fire( 'done', files );
-			sinon.assert.calledOnce( executeStub );
-			expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-			expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( [ files[ 0 ] ] );
-		} );
-
-		describe( 'dropdown action button', () => {
-			it( 'should be an instance of FileDialogButtonView', () => {
-				const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-
-				expect( dropdown.buttonView.actionView ).to.be.instanceOf( FileDialogButtonView );
-			} );
-		} );
-
-		describe( 'dropdown panel buttons', () => {
-			it( 'should have "Update" label on submit button when URL input is already filled', () => {
-				const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-				const viewDocument = editor.editing.view.document;
-
-				editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
-
-				editor.editing.view.change( writer => {
-					writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
-				} );
-
-				const img = viewDocument.selection.getSelectedElement();
-
-				const data = fakeEventData();
-				const eventInfo = new EventInfo( img, 'click' );
-				const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
-
-				viewDocument.fire( 'click', domEventDataMock );
-
-				dropdown.isOpen = true;
-
-				const inputValue = dropdown.panelView.children.first.imageURLInputValue;
-
-				expect( dropdown.isOpen ).to.be.true;
-				expect( inputValue ).to.equal( '/assets/sample.png' );
-				expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' );
-			} );
-
-			it( 'should have "Insert" label on submit button on uploading a new image', () => {
-				const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-				const viewDocument = editor.editing.view.document;
-
-				editor.setData( '<p>test</p>' );
-
-				editor.editing.view.change( writer => {
-					writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
-				} );
-
-				const el = viewDocument.selection.getSelectedElement();
-
-				const data = fakeEventData();
-				const eventInfo = new EventInfo( el, 'click' );
-				const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
-
-				viewDocument.fire( 'click', domEventDataMock );
-
-				dropdown.isOpen = true;
-
-				const inputValue = dropdown.panelView.children.first.imageURLInputValue;
-
-				expect( dropdown.isOpen ).to.be.true;
-				expect( inputValue ).to.equal( '' );
-				expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' );
-			} );
-		} );
-
-		describe( 'dropdown panel integrations', () => {
-			describe( 'insert image via URL form', () => {
-				it( 'should have "Insert image via URL" label on inserting new image', () => {
-					const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-					const viewDocument = editor.editing.view.document;
-
-					editor.setData( '<p>test</p>' );
-
-					editor.editing.view.change( writer => {
-						writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
-					} );
-
-					const el = viewDocument.selection.getSelectedElement();
-
-					const data = fakeEventData();
-					const eventInfo = new EventInfo( el, 'click' );
-					const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
-
-					viewDocument.fire( 'click', domEventDataMock );
-
-					dropdown.isOpen = true;
-
-					const inputValue = dropdown.panelView.children.first.imageURLInputValue;
-
-					const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
-
-					expect( dropdown.isOpen ).to.be.true;
-					expect( inputValue ).to.equal( '' );
-					expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' );
-				} );
-
-				it( 'should have "Update image URL" label on updating the image source URL', () => {
-					const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-					const viewDocument = editor.editing.view.document;
-
-					editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
-
-					editor.editing.view.change( writer => {
-						writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
-					} );
-
-					const el = viewDocument.selection.getSelectedElement();
-
-					const data = fakeEventData();
-					const eventInfo = new EventInfo( el, 'click' );
-					const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
-
-					viewDocument.fire( 'click', domEventDataMock );
-
-					dropdown.isOpen = true;
-
-					const inputValue = dropdown.panelView.children.first.imageURLInputValue;
-					const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
-
-					expect( dropdown.isOpen ).to.be.true;
-					expect( inputValue ).to.equal( '/assets/sample.png' );
-					expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' );
-				} );
-			} );
-		} );
-
-		it( 'should remove all attributes from model except "src" when updating the image source URL', () => {
-			const viewDocument = editor.editing.view.document;
-			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-			const insertButtonView = dropdown.panelView.children.first.insertButtonView;
-			const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
-			const submitSpy = sinon.spy();
-
-			dropdown.isOpen = true;
-
-			editor.setData( '<figure><img src="image-url-800w.jpg"' +
-			'srcset="image-url-480w.jpg 480w,image-url-800w.jpg 800w"' +
-			'sizes="(max-width: 600px) 480px,800px"' +
-			'alt="test-image"></figure>' );
-
-			editor.editing.view.change( writer => {
-				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
-			} );
-
-			const selectedElement = editor.model.document.selection.getSelectedElement();
-
-			expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' );
-			expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true;
-
-			dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png';
-
-			dropdown.on( 'submit', submitSpy );
-
-			insertButtonView.fire( 'execute' );
-
-			sinon.assert.notCalled( commandSpy );
-			sinon.assert.calledOnce( submitSpy );
-			expect( dropdown.isOpen ).to.be.false;
-			expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' );
-			expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false;
-			expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false;
-		} );
-
-		describe( 'events', () => {
-			it( 'should emit "submit" event when clicking on submit button', () => {
-				const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-				const insertButtonView = dropdown.panelView.children.first.insertButtonView;
-				const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
-				const submitSpy = sinon.spy();
-
-				dropdown.isOpen = true;
-
-				dropdown.on( 'submit', submitSpy );
-
-				insertButtonView.fire( 'execute' );
-
-				expect( dropdown.isOpen ).to.be.false;
-				sinon.assert.calledOnce( commandSpy );
-				sinon.assert.calledOnce( submitSpy );
-			} );
-
-			it( 'should emit "cancel" event when clicking on cancel button', () => {
-				const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-				const cancelButtonView = dropdown.panelView.children.first.cancelButtonView;
-				const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
-				const cancelSpy = sinon.spy();
-
-				dropdown.isOpen = true;
+		);
+	} );
 
-				dropdown.on( 'cancel', cancelSpy );
+	it( 'should not execute imageUpload if the file is not an image', () => {
+		const executeStub = sinon.stub( editor, 'execute' );
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const file = {
+			type: 'media/mp3',
+			size: 1024
+		};
 
-				cancelButtonView.fire( 'execute' );
+		button.fire( 'done', [ file ] );
+		sinon.assert.notCalled( executeStub );
+	} );
 
-				expect( dropdown.isOpen ).to.be.false;
-				sinon.assert.notCalled( commandSpy );
-				sinon.assert.calledOnce( cancelSpy );
-			} );
-		} );
-
-		it( 'should inject integrations to the dropdown panel view from the config', async () => {
-			const editor = await ClassicEditor
-				.create( editorElement, {
-					plugins: [
-						CKFinder,
-						Paragraph,
-						Image,
-						ImageUploadEditing,
-						ImageUploadUI,
-						FileRepository,
-						UploadAdapterPluginMock,
-						Clipboard
-					],
-					image: {
-						upload: {
-							panel: {
-								items: [
-									'insertImageViaUrl',
-									'openCKFinder'
-								]
-							}
-						}
-					}
-				} );
-
-			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
-
-			expect( dropdown.panelView.children.first._integrations.length ).to.equal( 2 );
-			expect( dropdown.panelView.children.first._integrations.first ).to.be.instanceOf( LabeledFieldView );
-			expect( dropdown.panelView.children.first._integrations.last ).to.be.instanceOf( ButtonView );
-
-			editor.destroy();
-		} );
+	it( 'should work even if the FileList does not support iterators', () => {
+		const executeStub = sinon.stub( editor, 'execute' );
+		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const files = {
+			0: createNativeFileMock(),
+			length: 1
+		};
+
+		button.fire( 'done', files );
+		sinon.assert.calledOnce( executeStub );
+		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( [ files[ 0 ] ] );
 	} );
 } );
-
-function fakeEventData() {
-	return {
-		preventDefault: sinon.spy()
-	};
-}

+ 1 - 146
packages/ckeditor5-image/tests/imageupload/utils.js

@@ -3,18 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document */
-
-import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import Image from '../../src/image';
-import ImageUploadUI from '../../src/imageupload/imageuploadui';
-import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Link from '@ckeditor/ckeditor5-link/src/link';
-import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
-import { createImageTypeRegExp, prepareIntegrations, createLabeledInputView } from '../../src/imageupload/utils';
+import { createImageTypeRegExp } from '../../src/imageupload/utils';
 
 describe( 'Upload utils', () => {
 	describe( 'createImageTypeRegExp()', () => {
@@ -42,138 +31,4 @@ describe( 'Upload utils', () => {
 			expect( createImageTypeRegExp( [ 'png' ] ).test( 'svg+xml' ) ).to.be.false;
 		} );
 	} );
-
-	describe( 'prepareIntegrations()', () => {
-		it( 'should return "insetImageViaUrl" and "openCKFinder" integrations', async () => {
-			const editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			const editor = await ClassicEditor
-				.create( editorElement, {
-					plugins: [
-						CKFinder,
-						Paragraph,
-						Image,
-						ImageUploadEditing,
-						ImageUploadUI
-					],
-					image: {
-						upload: {
-							panel: {
-								items: [
-									'insertImageViaUrl',
-									'openCKFinder'
-								]
-							}
-						}
-					}
-				} );
-
-			const openCKFinderExtendedView = Object.values( prepareIntegrations( editor ) )[ 1 ];
-
-			expect( openCKFinderExtendedView.class ).contains( 'ck-image-upload__ck-finder-button' );
-			expect( openCKFinderExtendedView.label ).to.equal( 'Insert image or file' );
-			expect( openCKFinderExtendedView.withText ).to.be.true;
-
-			editor.destroy();
-			editorElement.remove();
-		} );
-
-		it( 'should return only "insertImageViaUrl" integration and throw warning' +
-			'for "image-upload-integrations-invalid-view" error', async () => {
-			const editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			const editor = await ClassicEditor
-				.create( editorElement, {
-					plugins: [
-						Paragraph,
-						Image,
-						ImageUploadEditing,
-						ImageUploadUI
-					],
-					image: {
-						upload: {
-							panel: {
-								items: [
-									'insertImageViaUrl',
-									'openCKFinder'
-								]
-							}
-						}
-					}
-				} );
-
-			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
-
-			editor.destroy();
-			editorElement.remove();
-		} );
-
-		it( 'should return only "link" integration', async () => {
-			const editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			const editor = await ClassicEditor
-				.create( editorElement, {
-					plugins: [
-						Paragraph,
-						Link,
-						Image,
-						ImageUploadEditing,
-						ImageUploadUI
-					],
-					image: {
-						upload: {
-							panel: {
-								items: [
-									'link'
-								]
-							}
-						}
-					}
-				} );
-
-			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
-			expect( Object.values( prepareIntegrations( editor ) )[ 0 ].label ).to.equal( 'Link' );
-			expect( Object.values( prepareIntegrations( editor ) )[ 0 ] ).to.be.instanceOf( ButtonView );
-
-			editor.destroy();
-			editorElement.remove();
-		} );
-
-		it( 'should return "insertImageViaUrl" integration, when no integrations were configured', async () => {
-			const editorElement = document.createElement( 'div' );
-			document.body.appendChild( editorElement );
-
-			const editor = await ClassicEditor
-				.create( editorElement, {
-					plugins: [
-						Paragraph,
-						Image,
-						ImageUploadEditing,
-						ImageUploadUI
-					]
-				} );
-
-			expect( Object.keys( prepareIntegrations( editor ) ).length ).to.equal( 1 );
-
-			editor.destroy();
-			editorElement.remove();
-		} );
-	} );
-
-	describe( 'createLabeledInputView()', () => {
-		describe( 'image URL input view', () => {
-			it( 'should have placeholder', () => {
-				const view = createLabeledInputView( { t: val => val } );
-				expect( view.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
-			} );
-
-			it( 'should have info text', () => {
-				const view = createLabeledInputView( { t: val => val } );
-				expect( view.infoText ).to.match( /^Paste the image source URL/ );
-			} );
-		} );
-	} );
 } );

+ 1 - 1
packages/ckeditor5-image/tests/manual/imageuploadviaurl.html → packages/ckeditor5-image/tests/manual/imageinsertviaurl.html

@@ -10,7 +10,7 @@
 	}
 </style>
 
-<div id="editor2">
+<div id="editor">
 	<h2>Image upload via URL with CKFinder integration</h2>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.</p>
 </div>

+ 4 - 13
packages/ckeditor5-image/tests/manual/imageuploadviaurl.js → packages/ckeditor5-image/tests/manual/imageinsertviaurl.js

@@ -7,14 +7,12 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ImageUpload from '../../src/imageupload';
+import ImageInsert from '../../src/imageinsert';
 import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
 
-import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
-
 ClassicEditor
-	.create( document.querySelector( '#editor2' ), {
-		plugins: [ ArticlePluginSet, ImageUpload, CKFinder ],
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, ImageInsert, CKFinder ],
 		toolbar: [
 			'heading',
 			'|',
@@ -24,7 +22,7 @@ ClassicEditor
 			'bulletedList',
 			'numberedList',
 			'blockQuote',
-			'imageUpload',
+			'imageInsert',
 			'insertTable',
 			'mediaEmbed',
 			'undo',
@@ -48,13 +46,6 @@ ClassicEditor
 	} )
 	.then( editor => {
 		window.editor = editor;
-
-		// Register fake adapter.
-		editor.plugins.get( 'FileRepository' ).createUploadAdapter = loader => {
-			const adapterMock = new UploadAdapterMock( loader );
-
-			return adapterMock;
-		};
 	} )
 	.catch( err => {
 		console.error( err.stack );

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

@@ -0,0 +1,18 @@
+## Image insert via URL
+
+1. Click on the arrow button in `imageInsert` plugin to reveal the image insert panel.
+1. Paste the URL to the input (eg: `https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg`).
+1. Click `Insert` button.
+
+## Image replace via URL
+
+1. Click on the image in the editor.
+1. Click on the arrow button in `imageInsert` plugin to reveal the image insert panel.
+1. Edit the value of the input.
+1. Click `Update` button.
+
+## Image insert via integrations
+
+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.

+ 0 - 18
packages/ckeditor5-image/tests/manual/imageuploadviaurl.md

@@ -1,18 +0,0 @@
-## Image upload via URL
-
-1. Click on the arrow button in `imageUpload` plugin to reveal the image upload panel.
-1. Paste the URL to the input (eg: `https://ckeditor.com/docs/ckeditor5/latest/assets/img/malta.jpg`).
-1. Click `Insert` button.
-
-## Image replace via URL
-
-1. Click on the image in the editor.
-1. Click on the arrow button in `imageUpload` plugin to reveal the image upload panel.
-1. Edit the value of the input.
-1. Click `Update` button.
-
-## Image upload via integrations
-
-1. In the **Editor 2** click on the arrow button in `imageUpload` plugin to reveal the image upload panel.
-1. Click on the **CKFinder** button.
-1. Choose image and confirm.

+ 9 - 2
packages/ckeditor5-image/theme/imageupload.css → packages/ckeditor5-image/theme/imageinsert.css

@@ -3,14 +3,21 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-.ck.ck-image-upload__panel {
+.ck.ck-image-insert__panel {
 	padding: var(--ck-spacing-standard);
 }
 
-.ck.ck-image-upload__ck-finder-button {
+.ck.ck-image-insert__ck-finder-button {
 	display: block;
 	width: 100%;
 	margin: var(--ck-spacing-standard) auto;
 	border: 1px solid hsl(0, 0%, 80%);
 	border-radius: var(--ck-border-radius);
 }
+
+/* https://github.com/ckeditor/ckeditor5/issues/7986 */
+.ck.ck-splitbutton > .ck-file-dialog-button.ck-button {
+	padding: 0;
+	margin: 0;
+	border: none;
+}

+ 1 - 1
packages/ckeditor5-image/theme/imageuploadformrowview.css → packages/ckeditor5-image/theme/imageinsertformrowview.css

@@ -14,7 +14,7 @@
 		flex-grow: 1;
 	}
 
-	&.ck-image-upload-form__action-row {
+	&.ck-image-insert-form__action-row {
 		margin-top: var(--ck-spacing-standard);
 
 		& .ck-button-save,

+ 2 - 1
packages/ckeditor5-link/src/link.js

@@ -10,6 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import LinkEditing from './linkediting';
 import LinkUI from './linkui';
+import AutoLink from './autolink';
 
 /**
  * The link plugin.
@@ -24,7 +25,7 @@ export default class Link extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ LinkEditing, LinkUI ];
+		return [ LinkEditing, LinkUI, AutoLink ];
 	}
 
 	/**

+ 3 - 2
packages/ckeditor5-link/tests/link.js

@@ -4,12 +4,13 @@
  */
 
 import Link from '../src/link';
+import AutoLink from '../src/autolink';
 import LinkEditing from '../src/linkediting';
 import LinkUI from '../src/linkui';
 
 describe( 'Link', () => {
-	it( 'should require LinkEditing and LinkUI', () => {
-		expect( Link.requires ).to.deep.equal( [ LinkEditing, LinkUI ] );
+	it( 'should require LinkEditing, LinkUI and AutoLink', () => {
+		expect( Link.requires ).to.deep.equal( [ LinkEditing, LinkUI, AutoLink ] );
 	} );
 
 	it( 'should be named', () => {

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

@@ -95,7 +95,7 @@ export default class IndentCommand extends Command {
 			/**
 			 * Event fired by the {@link #execute} method.
 			 *
-			 * It allows to execute an action after executing the {@link ~IndentCommand#execute} method, e.g. adjusting
+			 * It allows to execute an action after executing the {@link ~IndentCommand#execute} method, for example adjusting
 			 * attributes of changed list items.
 			 *
 			 * @protected

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

@@ -215,7 +215,7 @@ export default class ListCommand extends Command {
 			/**
 			 * Event fired by the {@link #execute} method.
 			 *
-			 * It allows to execute an action after executing the {@link ~ListCommand#execute} method, e.g. adjusting
+			 * It allows to execute an action after executing the {@link ~ListCommand#execute} method, for example adjusting
 			 * attributes of changed blocks.
 			 *
 			 * @protected

+ 3 - 3
packages/ckeditor5-list/src/liststyle.js

@@ -12,10 +12,10 @@ import ListStyleEditing from './liststyleediting';
 import ListStyleUI from './liststyleui';
 
 /**
- * The list styles feature.
+ * The list style feature.
  *
- * This is a "glue" plugin that loads the {@link module:list/liststyleediting~ListStyleEditing list styles editing feature}
- * and the {@link module:list/liststyleui~ListStyleUI list styles UI feature}.
+ * This is a "glue" plugin that loads the {@link module:list/liststyleediting~ListStyleEditing list style editing feature}
+ * and the {@link module:list/liststyleui~ListStyleUI list style UI feature}.
  *
  * @extends module:core/plugin~Plugin
  */

+ 2 - 2
packages/ckeditor5-list/src/liststylecommand.js

@@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 import { getSiblingNodes } from './utils';
 
 /**
- * The list style command. It is used by the {@link module:list/liststyle~ListStyle list styles feature}.
+ * The list style command. It is used by the {@link module:list/liststyle~ListStyle list style feature}.
  *
  * @extends module:core/command~Command
  */
@@ -47,7 +47,7 @@ export default class ListStyleCommand extends Command {
 	 * Executes the command.
 	 *
 	 * @param {Object} options
-	 * @param {String|null} options.type The type of the list styles, e.g. 'disc' or 'square'. If `null` specified, the default
+	 * @param {String|null} options.type The type of the list style, e.g. `'disc'` or `'square'`. If `null` is specified, the default
 	 * style will be applied.
 	 * @protected
 	 */

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

@@ -15,10 +15,10 @@ import { getSiblingListItem, getSiblingNodes } from './utils';
 const DEFAULT_LIST_TYPE = 'default';
 
 /**
- * The list styles engine feature.
+ * The list style engine feature.
  *
- * It sets value for the `listItem` attribute for the {@link module:list/list~List `<listItem>`} element that
- * allows modifying list style type.
+ * It sets the value for the `listItem` attribute of the {@link module:list/list~List `<listItem>`} element that
+ * allows modifying the list style type.
  *
  * It registers the `'listStyle'` command.
  *
@@ -186,7 +186,7 @@ export default class ListStyleEditing extends Plugin {
 	}
 }
 
-// Returns a converter that consumes the `style` attribute and search for `list-style-type` definition.
+// Returns a converter that consumes the `style` attribute and searches for the `list-style-type` definition.
 // If not found, the `"default"` value will be used.
 //
 // @returns {Function}

+ 3 - 3
packages/ckeditor5-list/src/liststyleui.js

@@ -31,10 +31,10 @@ import listStyleUpperLatinIcon from '../theme/icons/liststyleupperlatin.svg';
 import '../theme/liststyles.css';
 
 /**
- * The list styles UI plugin. It introduces the extended `'bulletedList'` and `'numberedList'` toolbar
- * buttons that allow users change styles of individual lists in the content.
+ * The list style UI plugin. It introduces the extended `'bulletedList'` and `'numberedList'` toolbar
+ * buttons that allow users to change styles of individual lists in the content.
  *
- * **Note**: Buttons introduces by this plugin override implementations from the {@link module:list/listui~ListUI}
+ * **Note**: Buttons introduced by this plugin override implementations from the {@link module:list/listui~ListUI}
  * (because they share the same names).
  *
  * @extends module:core/plugin~Plugin

+ 1 - 1
packages/ckeditor5-list/theme/liststyles.css

@@ -6,7 +6,7 @@
 .ck.ck-list-styles-dropdown > .ck-dropdown__panel > .ck-toolbar > .ck-toolbar__items {
 	/*
 	 * Use the benefits of the toolbar (e.g. out-of-the-box keyboard navigation) but make it look
-	 * like panel with thumbnails (previews).
+	 * like a panel with thumbnails (previews).
 	 */
 	display: grid;
 }

Some files were not shown because too many files changed in this diff