瀏覽代碼

Merge pull request #250 from ckeditor/t/245

Feature: Introduced the `'imageInsert'` command. Closes #245. Closes #251.

BREAKING CHANGE: The `'imageUpload'` command's `files` parameter was renamed to `file`. It still can accept an array of files.
Piotrek Koszuliński 7 年之前
父節點
當前提交
8fc8047f31

+ 2 - 1
packages/ckeditor5-image/docs/features/image.md

@@ -213,7 +213,8 @@ ClassicEditor
 The {@link module:image/image~Image} plugin registers:
 
 * The `'imageTextAlternative'` button.
-* * The {@link module:image/imagetextalternative/imagetextalternativecommand~ImageTextAlternativeCommand `'imageTextAlternative'` command}
+* The {@link module:image/imagetextalternative/imagetextalternativecommand~ImageTextAlternativeCommand `'imageTextAlternative'` command}
+* The {@link module:image/image/imageinsertcommand~ImageInsertCommand `'imageInsert'` command} which accepts a source (e.g. an URL) of an image to insert.
 
 The {@link module:image/imagestyle~ImageStyle} plugin registers:
 

+ 10 - 2
packages/ckeditor5-image/src/image/imageediting.js

@@ -20,11 +20,16 @@ import { toImageWidget } from './utils';
 
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { upcastElementToElement, upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import ImageInsertCommand from './imageinsertcommand';
 
 /**
  * The image engine plugin.
- * It registers `<image>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
- * It also registers converters for editing and data pipelines.
+ *
+ * It registers:
+ *
+ * * `<image>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
+ * * converters for editing and data pipelines.
+ * * `'imageInsert'` command.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -102,6 +107,9 @@ export default class ImageEditing extends Plugin {
 				}
 			} ) )
 			.add( viewFigureToModel() );
+
+		// Register imageUpload command.
+		editor.commands.add( 'imageInsert', new ImageInsertCommand( editor ) );
 	}
 }
 

+ 61 - 0
packages/ckeditor5-image/src/image/imageinsertcommand.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { insertImage, isImageAllowed } from './utils';
+
+/**
+ * @module image/image/imageinsertcommand
+ */
+
+/**
+ * Insert image command.
+ *
+ * The command is registered by the {@link module:image/image/imageediting~ImageEditing} plugin as `'imageInsert'`.
+ *
+ * In order to insert an image at the current selection position
+ * (according to the {@link module:widget/utils~findOptimalInsertionPosition} algorithm),
+ * execute the command and specify the image source:
+ *
+ *		editor.execute( 'imageInsert', { source: 'http://url.to.the/image' } );
+ *
+ * It is also possible to insert multiple images at once:
+ *
+ *		editor.execute( 'imageInsert', {
+ *			source:  [
+ *				'path/to/image.jpg',
+ *				'path/to/other-image.jpg'
+ *			]
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class ImageInsertCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		this.isEnabled = isImageAllowed( this.editor.model );
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @fires execute
+	 * @param {Object} options Options for the executed command.
+	 * @param {String|Array.<String>} options.source The image source or an array of image sources to insert.
+	 */
+	execute( options ) {
+		const model = this.editor.model;
+
+		model.change( writer => {
+			const sources = Array.isArray( options.source ) ? options.source : [ options.source ];
+
+			for ( const src of sources ) {
+				insertImage( writer, model, { src } );
+			}
+		} );
+	}
+}

+ 75 - 1
packages/ckeditor5-image/src/image/utils.js

@@ -7,7 +7,7 @@
  * @module image/image/utils
  */
 
-import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { findOptimalInsertionPosition, isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 
 const imageSymbol = Symbol( 'isImage' );
 
@@ -65,3 +65,77 @@ export function isImageWidgetSelected( selection ) {
 export function isImage( modelElement ) {
 	return !!modelElement && modelElement.is( 'image' );
 }
+
+/**
+ * Handles inserting single file. This method unifies image insertion using {@link module:widget/utils~findOptimalInsertionPosition} method.
+ *
+ *		model.change( writer => {
+ *			insertImage( writer, model, { src: 'path/to/image.jpg' } );
+ *		} );
+ *
+ * @param {module:engine/model/writer~Writer} writer
+ * @param {module:engine/model/model~Model} model
+ * @param {Object} [attributes={}] Attributes of inserted image
+ */
+export function insertImage( writer, model, attributes = {} ) {
+	const imageElement = writer.createElement( 'image', attributes );
+
+	const insertAtSelection = findOptimalInsertionPosition( model.document.selection, model );
+
+	model.insertContent( imageElement, insertAtSelection );
+
+	// Inserting an image might've failed due to schema regulations.
+	if ( imageElement.parent ) {
+		writer.setSelection( imageElement, 'on' );
+	}
+}
+
+/**
+ * Checks if image can be inserted at current model selection.
+ *
+ * @param {module:engine/model/model~Model} model
+ * @returns {Boolean}
+ */
+export function isImageAllowed( model ) {
+	const schema = model.schema;
+	const selection = model.document.selection;
+
+	return isImageAllowedInParent( selection, schema, model ) && checkSelectionWithObject( selection, schema );
+}
+
+// Checks if image is allowed by schema in optimal insertion parent.
+//
+// @returns {Boolean}
+function isImageAllowedInParent( selection, schema, model ) {
+	const parent = getInsertImageParent( selection, model );
+
+	return schema.checkChild( parent, 'image' );
+}
+
+// Check used in image commands for additional cases when the command should be disabled:
+//
+// - selection is on object
+// - selection is inside object
+//
+// @returns {Boolean}
+function checkSelectionWithObject( selection, schema ) {
+	const selectedElement = selection.getSelectedElement();
+
+	const isSelectionOnObject = !!selectedElement && schema.isObject( selectedElement );
+	const isSelectionInObject = !![ ...selection.focus.getAncestors() ].find( ancestor => schema.isObject( ancestor ) );
+
+	return !isSelectionOnObject && !isSelectionInObject;
+}
+
+// Returns a node that will be used to insert image with `model.insertContent` to check if image can be placed there.
+function getInsertImageParent( selection, model ) {
+	const insertAt = findOptimalInsertionPosition( selection, model );
+
+	let parent = insertAt.parent;
+
+	if ( !parent.is( '$root' ) ) {
+		parent = parent.parent;
+	}
+
+	return parent;
+}

+ 35 - 58
packages/ckeditor5-image/src/imageupload/imageuploadcommand.js

@@ -5,7 +5,7 @@
 
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import { insertImage, isImageAllowed } from '../image/utils';
 
 /**
  * @module image/imageupload/imageuploadcommand
@@ -14,6 +14,29 @@ import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/uti
 /**
  * Image upload command.
  *
+ * The command is registered by the {@link module:image/imageupload/imageuploadediting~ImageUploadEditing} plugin as `'imageUpload'`.
+ *
+ * In order to upload an image at the current selection position
+ * (according to the {@link module:widget/utils~findOptimalInsertionPosition} algorithm),
+ * execute the command and pass the native image file instance:
+ *
+ *		this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
+ *			// Assuming that only images were pasted:
+ *			const images = Array.from( data.dataTransfer.files );
+ *
+ *			// Upload the first image:
+ *			editor.execute( 'imageUpload', { file: images[ 0 ] } );
+ *		} );
+ *
+ * It is also possible to insert multiple images at once:
+ *
+ *		editor.execute( 'imageUpload', {
+ *			file: [
+ *				file1,
+ *				file2
+ *			]
+ *		} );
+ *
  * @extends module:core/command~Command
  */
 export default class ImageUploadCommand extends Command {
@@ -21,11 +44,7 @@ export default class ImageUploadCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const model = this.editor.model;
-		const selection = model.document.selection;
-		const schema = model.schema;
-
-		this.isEnabled = isImageAllowedInParent( selection, schema, model ) && checkSelectionWithObject( selection, schema );
+		this.isEnabled = isImageAllowed( this.editor.model );
 	}
 
 	/**
@@ -33,16 +52,19 @@ export default class ImageUploadCommand extends Command {
 	 *
 	 * @fires execute
 	 * @param {Object} options Options for the executed command.
-	 * @param {File|Array.<File>} options.files The image file or an array of image files to upload.
+	 * @param {File|Array.<File>} options.file The image file or an array of image files to upload.
 	 */
 	execute( options ) {
 		const editor = this.editor;
+		const model = editor.model;
 
-		editor.model.change( writer => {
-			const filesToUpload = Array.isArray( options.files ) ? options.files : [ options.files ];
+		const fileRepository = editor.plugins.get( FileRepository );
+
+		model.change( writer => {
+			const filesToUpload = Array.isArray( options.file ) ? options.file : [ options.file ];
 
 			for ( const file of filesToUpload ) {
-				uploadImage( writer, editor, file );
+				uploadImage( writer, model, fileRepository, file );
 			}
 		} );
 	}
@@ -51,13 +73,9 @@ export default class ImageUploadCommand extends Command {
 // Handles uploading single file.
 //
 // @param {module:engine/model/writer~writer} writer
-// @param {module:core/editor/editor~Editor} editor
+// @param {module:engine/model/model~Model} model
 // @param {File} file
-function uploadImage( writer, editor, file ) {
-	const model = editor.model;
-	const doc = model.document;
-	const fileRepository = editor.plugins.get( FileRepository );
-
+function uploadImage( writer, model, fileRepository, file ) {
 	const loader = fileRepository.createLoader( file );
 
 	// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
@@ -65,46 +83,5 @@ function uploadImage( writer, editor, file ) {
 		return;
 	}
 
-	const imageElement = writer.createElement( 'image', { uploadId: loader.id } );
-
-	const insertAtSelection = findOptimalInsertionPosition( doc.selection, model );
-
-	model.insertContent( imageElement, insertAtSelection );
-
-	// Inserting an image might've failed due to schema regulations.
-	if ( imageElement.parent ) {
-		writer.setSelection( imageElement, 'on' );
-	}
-}
-
-// Checks if image is allowed by schema in optimal insertion parent.
-function isImageAllowedInParent( selection, schema, model ) {
-	const parent = getInsertImageParent( selection, model );
-
-	return schema.checkChild( parent, 'image' );
-}
-
-// Additional check for when the command should be disabled:
-// - selection is on object
-// - selection is inside object
-function checkSelectionWithObject( selection, schema ) {
-	const selectedElement = selection.getSelectedElement();
-
-	const isSelectionOnObject = !!selectedElement && schema.isObject( selectedElement );
-	const isSelectionInObject = !![ ...selection.focus.getAncestors() ].find( ancestor => schema.isObject( ancestor ) );
-
-	return !isSelectionOnObject && !isSelectionInObject;
-}
-
-// Returns a node that will be used to insert image with `model.insertContent` to check if image can be placed there.
-function getInsertImageParent( selection, model ) {
-	const insertAt = findOptimalInsertionPosition( selection, model );
-
-	let parent = insertAt.parent;
-
-	if ( !parent.is( '$root' ) ) {
-		parent = parent.parent;
-	}
-
-	return parent;
+	insertImage( writer, model, { uploadId: loader.id } );
 }

+ 2 - 2
packages/ckeditor5-image/src/imageupload/imageuploadediting.js

@@ -15,7 +15,7 @@ import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import { isImageType } from '../../src/imageupload/utils';
 
 /**
- * The editing part of the image upload feature.
+ * The editing part of the image upload feature. It registers the `'imageUpload'` command.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -75,7 +75,7 @@ export default class ImageUploadEditing extends Plugin {
 
 					// Upload images after the selection has changed in order to ensure the command's state is refreshed.
 					editor.model.enqueueChange( 'default', () => {
-						editor.execute( 'imageUpload', { files: images } );
+						editor.execute( 'imageUpload', { file: images } );
 					} );
 				}
 			} );

+ 1 - 1
packages/ckeditor5-image/src/imageupload/imageuploadui.js

@@ -51,7 +51,7 @@ export default class ImageUploadUI extends Plugin {
 				const imagesToUpload = Array.from( files ).filter( isImageType );
 
 				if ( imagesToUpload.length ) {
-					editor.execute( 'imageUpload', { files: imagesToUpload } );
+					editor.execute( 'imageUpload', { file: imagesToUpload } );
 				}
 			} );
 

+ 5 - 0
packages/ckeditor5-image/tests/image/imageediting.js

@@ -9,6 +9,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageEditing from '../../src/image/imageediting';
 import ImageLoadObserver from '../../src/image/imageloadobserver';
+import ImageInsertCommand from '../../src/image/imageinsertcommand';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { isImageWidget } from '../../src/image/utils';
@@ -58,6 +59,10 @@ describe( 'ImageEditing', () => {
 		expect( view.getObserver( ImageLoadObserver ) ).to.be.instanceOf( ImageLoadObserver );
 	} );
 
+	it( 'should register imageInsert command', () => {
+		expect( editor.commands.get( 'imageInsert' ) ).to.be.instanceOf( ImageInsertCommand );
+	} );
+
 	// See https://github.com/ckeditor/ckeditor5-image/issues/142.
 	it( 'should update the ui after image has been loaded in the DOM', () => {
 		const element = document.createElement( 'div' );

+ 159 - 0
packages/ckeditor5-image/tests/image/imageinsertcommand.js

@@ -0,0 +1,159 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+import ImageInsertCommand from '../../src/image/imageinsertcommand';
+import Image from '../../src/image/imageediting';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'ImageInsertCommand', () => {
+	let editor, command, model;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Image, Paragraph ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				command = new ImageInsertCommand( editor );
+
+				const schema = model.schema;
+				schema.extend( 'image', { allowAttributes: 'uploadId' } );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be true when the selection directly in the root', () => {
+			model.enqueueChange( 'transparent', () => {
+				setModelData( model, '[]' );
+
+				command.refresh();
+				expect( command.isEnabled ).to.be.true;
+			} );
+		} );
+
+		it( 'should be true when the selection is in empty block', () => {
+			setModelData( model, '<paragraph>[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true when the selection directly in a paragraph', () => {
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true when the selection directly in a block', () => {
+			model.schema.register( 'block', { inheritAllFrom: '$block' } );
+			model.schema.extend( '$text', { allowIn: 'block' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'block', view: 'block' } ) );
+
+			setModelData( model, '<block>foo[]</block>' );
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false when the selection is on other image', () => {
+			setModelData( model, '[<image></image>]' );
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false when the selection is inside other image', () => {
+			model.schema.register( 'caption', {
+				allowIn: 'image',
+				allowContentOf: '$block',
+				isLimit: true
+			} );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'caption', view: 'figcaption' } ) );
+			setModelData( model, '<image><caption>[]</caption></image>' );
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false when the selection is on other object', () => {
+			model.schema.register( 'object', { isObject: true, allowIn: '$root' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'object', view: 'object' } ) );
+			setModelData( model, '[<object></object>]' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false when the selection is inside other object', () => {
+			model.schema.register( 'object', { isObject: true, allowIn: '$root' } );
+			model.schema.extend( '$text', { allowIn: 'object' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'object', view: 'object' } ) );
+			setModelData( model, '<object>[]</object>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false when schema disallows image', () => {
+			model.schema.register( 'block', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'paragraph', { allowIn: 'block' } );
+			// Block image in block.
+			model.schema.addChildCheck( ( context, childDefinition ) => {
+				if ( childDefinition.name === 'image' && context.last.name === 'block' ) {
+					return false;
+				}
+			} );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'block', view: 'block' } ) );
+
+			setModelData( model, '<block><paragraph>[]</paragraph></block>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should insert image at selection position as other widgets', () => {
+			const imgSrc = 'foo/bar.jpg';
+
+			setModelData( model, '<paragraph>f[o]o</paragraph>' );
+
+			command.execute( { source: imgSrc } );
+
+			expect( getModelData( model ) ).to.equal( `[<image src="${ imgSrc }"></image>]<paragraph>foo</paragraph>` );
+		} );
+
+		it( 'should insert multiple images at selection position as other widgets', () => {
+			const imgSrc1 = 'foo/bar.jpg';
+			const imgSrc2 = 'foo/baz.jpg';
+
+			setModelData( model, '<paragraph>f[o]o</paragraph>' );
+
+			command.execute( { source: [ imgSrc1, imgSrc2 ] } );
+
+			expect( getModelData( model ) )
+				.to.equal( `<image src="${ imgSrc1 }"></image>[<image src="${ imgSrc2 }"></image>]<paragraph>foo</paragraph>` );
+		} );
+
+		it( 'should not insert image nor crash when image could not be inserted', () => {
+			const imgSrc = 'foo/bar.jpg';
+
+			model.schema.register( 'other', {
+				allowIn: '$root',
+				isLimit: true
+			} );
+			model.schema.extend( '$text', { allowIn: 'other' } );
+
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'other', view: 'p' } ) );
+
+			setModelData( model, '<other>[]</other>' );
+
+			command.execute( { source: imgSrc } );
+
+			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
+		} );
+	} );
+} );

+ 158 - 2
packages/ckeditor5-image/tests/image/utils.js

@@ -7,8 +7,13 @@ import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfr
 import ViewDowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter';
 import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import { toImageWidget, isImageWidget, isImageWidgetSelected, isImage } from '../../src/image/utils';
+import { toImageWidget, isImageWidget, isImageWidgetSelected, isImage, isImageAllowed, insertImage } from '../../src/image/utils';
 import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import Image from '../../src/image/imageediting';
 
 describe( 'image widget utils', () => {
 	let element, image, writer;
@@ -87,7 +92,7 @@ describe( 'image widget utils', () => {
 		} );
 	} );
 
-	describe( 'isImage', () => {
+	describe( 'isImage()', () => {
 		it( 'should return true for image element', () => {
 			const image = new ModelElement( 'image' );
 
@@ -105,4 +110,155 @@ describe( 'image widget utils', () => {
 			expect( isImage( undefined ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'isImageAllowed()', () => {
+		let editor, model;
+
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ Image, Paragraph ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+
+					const schema = model.schema;
+					schema.extend( 'image', { allowAttributes: 'uploadId' } );
+				} );
+		} );
+
+		it( 'should return true when the selection directly in the root', () => {
+			model.enqueueChange( 'transparent', () => {
+				setModelData( model, '[]' );
+
+				expect( isImageAllowed( model ) ).to.be.true;
+			} );
+		} );
+
+		it( 'should return true when the selection is in empty block', () => {
+			setModelData( model, '<paragraph>[]</paragraph>' );
+
+			expect( isImageAllowed( model ) ).to.be.true;
+		} );
+
+		it( 'should return true when the selection directly in a paragraph', () => {
+			setModelData( model, '<paragraph>foo[]</paragraph>' );
+			expect( isImageAllowed( model ) ).to.be.true;
+		} );
+
+		it( 'should return true when the selection directly in a block', () => {
+			model.schema.register( 'block', { inheritAllFrom: '$block' } );
+			model.schema.extend( '$text', { allowIn: 'block' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'block', view: 'block' } ) );
+
+			setModelData( model, '<block>foo[]</block>' );
+			expect( isImageAllowed( model ) ).to.be.true;
+		} );
+
+		it( 'should return false when the selection is on other image', () => {
+			setModelData( model, '[<image></image>]' );
+			expect( isImageAllowed( model ) ).to.be.false;
+		} );
+
+		it( 'should return false when the selection is inside other image', () => {
+			model.schema.register( 'caption', {
+				allowIn: 'image',
+				allowContentOf: '$block',
+				isLimit: true
+			} );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'caption', view: 'figcaption' } ) );
+			setModelData( model, '<image><caption>[]</caption></image>' );
+			expect( isImageAllowed( model ) ).to.be.false;
+		} );
+
+		it( 'should return false when the selection is on other object', () => {
+			model.schema.register( 'object', { isObject: true, allowIn: '$root' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'object', view: 'object' } ) );
+			setModelData( model, '[<object></object>]' );
+
+			expect( isImageAllowed( model ) ).to.be.false;
+		} );
+
+		it( 'should return false when the selection is inside other object', () => {
+			model.schema.register( 'object', { isObject: true, allowIn: '$root' } );
+			model.schema.extend( '$text', { allowIn: 'object' } );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'object', view: 'object' } ) );
+			setModelData( model, '<object>[]</object>' );
+
+			expect( isImageAllowed( model ) ).to.be.false;
+		} );
+
+		it( 'should return false when schema disallows image', () => {
+			model.schema.register( 'block', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'paragraph', { allowIn: 'block' } );
+			// Block image in block.
+			model.schema.addChildCheck( ( context, childDefinition ) => {
+				if ( childDefinition.name === 'image' && context.last.name === 'block' ) {
+					return false;
+				}
+			} );
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'block', view: 'block' } ) );
+
+			setModelData( model, '<block><paragraph>[]</paragraph></block>' );
+
+			expect( isImageAllowed( model ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'insertImage()', () => {
+		let editor, model;
+
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ Image, Paragraph ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+
+					const schema = model.schema;
+					schema.extend( 'image', { allowAttributes: 'uploadId' } );
+				} );
+		} );
+
+		it( 'should insert image at selection position as other widgets', () => {
+			setModelData( model, '<paragraph>f[o]o</paragraph>' );
+
+			model.change( writer => {
+				insertImage( writer, model );
+			} );
+
+			expect( getModelData( model ) ).to.equal( '[<image></image>]<paragraph>foo</paragraph>' );
+		} );
+
+		it( 'should insert image with given attributes', () => {
+			setModelData( model, '<paragraph>f[o]o</paragraph>' );
+
+			model.change( writer => {
+				insertImage( writer, model, { src: 'bar' } );
+			} );
+
+			expect( getModelData( model ) ).to.equal( '[<image src="bar"></image>]<paragraph>foo</paragraph>' );
+		} );
+
+		it( 'should not insert image nor crash when image could not be inserted', () => {
+			model.schema.register( 'other', {
+				allowIn: '$root',
+				isLimit: true
+			} );
+			model.schema.extend( '$text', { allowIn: 'other' } );
+
+			editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'other', view: 'p' } ) );
+
+			setModelData( model, '<other>[]</other>' );
+
+			model.change( writer => {
+				insertImage( writer, model );
+			} );
+
+			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
+		} );
+	} );
 } );

+ 4 - 4
packages/ckeditor5-image/tests/imageupload/imageuploadcommand.js

@@ -139,7 +139,7 @@ describe( 'ImageUploadCommand', () => {
 			const file = createNativeFileMock();
 			setModelData( model, '<paragraph>f[o]o</paragraph>' );
 
-			command.execute( { files: file } );
+			command.execute( { file } );
 
 			const id = fileRepository.getLoader( file ).id;
 			expect( getModelData( model ) )
@@ -154,7 +154,7 @@ describe( 'ImageUploadCommand', () => {
 			model.change( writer => {
 				expect( writer.batch.operations ).to.length( 0 );
 
-				command.execute( { files: file } );
+				command.execute( { file } );
 
 				expect( writer.batch.operations ).to.length.above( 0 );
 			} );
@@ -173,7 +173,7 @@ describe( 'ImageUploadCommand', () => {
 
 			setModelData( model, '<other>[]</other>' );
 
-			command.execute( { files: file } );
+			command.execute( { file } );
 
 			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
 		} );
@@ -188,7 +188,7 @@ describe( 'ImageUploadCommand', () => {
 			setModelData( model, '<paragraph>fo[]o</paragraph>' );
 
 			expect( () => {
-				command.execute( { files: file } );
+				command.execute( { file } );
 			} ).to.not.throw();
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[]o</paragraph>' );

+ 10 - 10
packages/ckeditor5-image/tests/imageupload/imageuploadediting.js

@@ -308,7 +308,7 @@ describe( 'ImageUploadEditing', () => {
 	it( 'should use read data once it is present', done => {
 		const file = createNativeFileMock();
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		model.once( '_change', () => {
 			expect( getViewData( view ) ).to.equal(
@@ -328,7 +328,7 @@ describe( 'ImageUploadEditing', () => {
 	it( 'should replace read data with server response once it is present', done => {
 		const file = createNativeFileMock();
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		model.document.once( 'change', () => {
 			model.document.once( 'change', () => {
@@ -359,7 +359,7 @@ describe( 'ImageUploadEditing', () => {
 		}, { priority: 'high' } );
 
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		nativeReaderMock.mockError( 'Reading error.' );
 	} );
@@ -375,7 +375,7 @@ describe( 'ImageUploadEditing', () => {
 		}, { priority: 'high' } );
 
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 		nativeReaderMock.abort();
 
 		setTimeout( () => {
@@ -399,7 +399,7 @@ describe( 'ImageUploadEditing', () => {
 		} );
 
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		sinon.assert.calledOnce( loadSpy );
 
@@ -436,7 +436,7 @@ describe( 'ImageUploadEditing', () => {
 			evt.stop();
 		}, { priority: 'high' } );
 
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		model.document.once( 'change', () => {
 			model.document.once( 'change', () => {
@@ -453,7 +453,7 @@ describe( 'ImageUploadEditing', () => {
 	it( 'should abort upload if image is removed', () => {
 		const file = createNativeFileMock();
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		const abortSpy = testUtils.sinon.spy( loader, 'abort' );
 
@@ -472,7 +472,7 @@ describe( 'ImageUploadEditing', () => {
 	it( 'should not abort and not restart upload when image is moved', () => {
 		const file = createNativeFileMock();
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		const abortSpy = testUtils.sinon.spy( loader, 'abort' );
 		const loadSpy = testUtils.sinon.spy( loader, 'read' );
@@ -497,7 +497,7 @@ describe( 'ImageUploadEditing', () => {
 			evt.stop();
 		}, { priority: 'high' } );
 
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		model.document.once( 'change', () => {
 			// This is called after "manual" remove.
@@ -533,7 +533,7 @@ describe( 'ImageUploadEditing', () => {
 	it( 'should create responsive image if server return multiple images', done => {
 		const file = createNativeFileMock();
 		setModelData( model, '<paragraph>{}foo bar</paragraph>' );
-		editor.execute( 'imageUpload', { files: file } );
+		editor.execute( 'imageUpload', { file } );
 
 		model.document.once( 'change', () => {
 			model.document.once( 'change', () => {

+ 7 - 7
packages/ckeditor5-image/tests/imageupload/imageuploadprogress.js

@@ -74,7 +74,7 @@ describe( 'ImageUploadProgress', () => {
 
 	it( 'should convert image\'s "reading" uploadStatus attribute', () => {
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		expect( getViewData( view ) ).to.equal(
 			'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
@@ -86,7 +86,7 @@ describe( 'ImageUploadProgress', () => {
 
 	it( 'should convert image\'s "uploading" uploadStatus attribute', done => {
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		model.document.once( 'change', () => {
 			expect( getViewData( view ) ).to.equal(
@@ -167,7 +167,7 @@ describe( 'ImageUploadProgress', () => {
 
 	it( 'should update progressbar width on progress', done => {
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		model.document.once( 'change', () => {
 			adapterMock.mockProgress( 40, 100 );
@@ -189,7 +189,7 @@ describe( 'ImageUploadProgress', () => {
 		const clock = testUtils.sinon.useFakeTimers();
 
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		model.document.once( 'change', () => {
 			model.document.once( 'change', () => {
@@ -222,7 +222,7 @@ describe( 'ImageUploadProgress', () => {
 		uploadProgress.placeholder = base64Sample;
 
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		expect( getViewData( view ) ).to.equal(
 			'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
@@ -238,7 +238,7 @@ describe( 'ImageUploadProgress', () => {
 		}, { priority: 'highest' } );
 
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		expect( getViewData( view ) ).to.equal(
 			'[<figure class="ck-widget image" contenteditable="false"><img></img></figure>]<p>foo</p>'
@@ -276,7 +276,7 @@ describe( 'ImageUploadProgress', () => {
 		testUtils.sinon.stub( env, 'isEdge' ).get( () => true );
 
 		setModelData( model, '<paragraph>[]foo</paragraph>' );
-		editor.execute( 'imageUpload', { files: createNativeFileMock() } );
+		editor.execute( 'imageUpload', { file: createNativeFileMock() } );
 
 		model.document.once( 'change', () => {
 			model.document.once( 'change', () => {

+ 3 - 3
packages/ckeditor5-image/tests/imageupload/imageuploadui.js

@@ -99,7 +99,7 @@ describe( 'ImageUploadUI', () => {
 		button.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-		expect( executeStub.firstCall.args[ 1 ].files ).to.deep.equal( files );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
 	} );
 
 	it( 'should execute imageUpload command with multiple files', () => {
@@ -110,7 +110,7 @@ describe( 'ImageUploadUI', () => {
 		button.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-		expect( executeStub.firstCall.args[ 1 ].files ).to.deep.equal( files );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
 	} );
 
 	it( 'should optimize the insertion position', () => {
@@ -171,6 +171,6 @@ describe( 'ImageUploadUI', () => {
 		button.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
-		expect( executeStub.firstCall.args[ 1 ].files ).to.deep.equal( [ files[ 0 ] ] );
+		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( [ files[ 0 ] ] );
 	} );
 } );