8
0
Просмотр исходного кода

Implement the 'imageInsert' command.

Maciej Gołaszewski 7 лет назад
Родитель
Сommit
63882471fb

+ 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 { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { upcastElementToElement, upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import { upcastElementToElement, upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import ImageInsertCommand from './imageinsertcommand';
 
 
 /**
 /**
  * The image engine plugin.
  * 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
  * @extends module:core/plugin~Plugin
  */
  */
@@ -102,6 +107,9 @@ export default class ImageEditing extends Plugin {
 				}
 				}
 			} ) )
 			} ) )
 			.add( viewFigureToModel() );
 			.add( viewFigureToModel() );
+
+		// Register imageUpload command.
+		editor.commands.add( 'imageInsert', new ImageInsertCommand( editor ) );
 	}
 	}
 }
 }
 
 

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

@@ -0,0 +1,44 @@
+/**
+ * @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.
+ *
+ * @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.sources The image source or an array of image sources to insert.
+	 */
+	execute( options ) {
+		const editor = this.editor;
+
+		editor.model.change( writer => {
+			const sources = Array.isArray( options.sources ) ? options.sources : [ options.sources ];
+
+			for ( const src of sources ) {
+				insertImage( writer, editor, { src } );
+			}
+		} );
+	}
+}

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

@@ -7,7 +7,7 @@
  * @module image/image/utils
  * @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' );
 const imageSymbol = Symbol( 'isImage' );
 
 
@@ -65,3 +65,76 @@ export function isImageWidgetSelected( selection ) {
 export function isImage( modelElement ) {
 export function isImage( modelElement ) {
 	return !!modelElement && modelElement.is( 'image' );
 	return !!modelElement && modelElement.is( 'image' );
 }
 }
+
+/**
+ * Handles inserting single file.
+ *
+ * @param {module:engine/model/writer~writer} writer
+ * @param {module:core/editor/editor~Editor} editor
+ * @param {Object} attributes
+ */
+export function insertImage( writer, editor, attributes ) {
+	const model = editor.model;
+	const doc = model.document;
+
+	const imageElement = writer.createElement( 'image', attributes );
+
+	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 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;
+}

+ 7 - 54
packages/ckeditor5-image/src/imageupload/imageuploadcommand.js

@@ -5,7 +5,7 @@
 
 
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 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
  * @module image/imageupload/imageuploadcommand
@@ -21,11 +21,7 @@ export default class ImageUploadCommand extends Command {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	refresh() {
 	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 );
 	}
 	}
 
 
 	/**
 	/**
@@ -38,11 +34,13 @@ export default class ImageUploadCommand extends Command {
 	execute( options ) {
 	execute( options ) {
 		const editor = this.editor;
 		const editor = this.editor;
 
 
+		const fileRepository = editor.plugins.get( FileRepository );
+
 		editor.model.change( writer => {
 		editor.model.change( writer => {
 			const filesToUpload = Array.isArray( options.files ) ? options.files : [ options.files ];
 			const filesToUpload = Array.isArray( options.files ) ? options.files : [ options.files ];
 
 
 			for ( const file of filesToUpload ) {
 			for ( const file of filesToUpload ) {
-				uploadImage( writer, editor, file );
+				uploadImage( writer, editor, fileRepository, file );
 			}
 			}
 		} );
 		} );
 	}
 	}
@@ -53,11 +51,7 @@ export default class ImageUploadCommand extends Command {
 // @param {module:engine/model/writer~writer} writer
 // @param {module:engine/model/writer~writer} writer
 // @param {module:core/editor/editor~Editor} editor
 // @param {module:core/editor/editor~Editor} editor
 // @param {File} file
 // @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, editor, fileRepository, file ) {
 	const loader = fileRepository.createLoader( file );
 	const loader = fileRepository.createLoader( file );
 
 
 	// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
 	// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
@@ -65,46 +59,5 @@ function uploadImage( writer, editor, file ) {
 		return;
 		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, editor, { uploadId: loader.id } );
 }
 }

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

@@ -15,7 +15,7 @@ import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import { isImageType } from '../../src/imageupload/utils';
 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
  * @extends module:core/plugin~Plugin
  */
  */

+ 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 ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageEditing from '../../src/image/imageediting';
 import ImageEditing from '../../src/image/imageediting';
 import ImageLoadObserver from '../../src/image/imageloadobserver';
 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 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 { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { isImageWidget } from '../../src/image/utils';
 import { isImageWidget } from '../../src/image/utils';
@@ -58,6 +59,10 @@ describe( 'ImageEditing', () => {
 		expect( view.getObserver( ImageLoadObserver ) ).to.be.instanceOf( ImageLoadObserver );
 		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.
 	// See https://github.com/ckeditor/ckeditor5-image/issues/142.
 	it( 'should update the ui after image has been loaded in the DOM', () => {
 	it( 'should update the ui after image has been loaded in the DOM', () => {
 		const element = document.createElement( 'div' );
 		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( { sources: 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( { sources: [ 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( { sources: imgSrc } );
+
+			expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
+		} );
+	} );
+} );