diff --git a/docs/features/image.md b/docs/features/image.md index d3841124..034d4e7f 100644 --- a/docs/features/image.md +++ b/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: diff --git a/src/image/imageediting.js b/src/image/imageediting.js index 789060c8..1dd38fe7 100644 --- a/src/image/imageediting.js +++ b/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 `` 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: + * + * * `` 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 ) ); } } diff --git a/src/image/imageinsertcommand.js b/src/image/imageinsertcommand.js new file mode 100644 index 00000000..7dcef633 --- /dev/null +++ b/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.} 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 } ); + } + } ); + } +} diff --git a/src/image/utils.js b/src/image/utils.js index 37728e90..b8c29b04 100644 --- a/src/image/utils.js +++ b/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; +} diff --git a/src/imageupload/imageuploadcommand.js b/src/imageupload/imageuploadcommand.js index 4e18bc3a..2958b399 100644 --- a/src/imageupload/imageuploadcommand.js +++ b/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.} options.files The image file or an array of image files to upload. + * @param {File|Array.} 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 } ); } diff --git a/src/imageupload/imageuploadediting.js b/src/imageupload/imageuploadediting.js index 440ddaa4..4a9d4b81 100644 --- a/src/imageupload/imageuploadediting.js +++ b/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 } ); } ); } } ); diff --git a/src/imageupload/imageuploadui.js b/src/imageupload/imageuploadui.js index d6912d76..57469b4d 100644 --- a/src/imageupload/imageuploadui.js +++ b/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 } ); } } ); diff --git a/tests/image/imageediting.js b/tests/image/imageediting.js index 35da6eaa..dab907f3 100644 --- a/tests/image/imageediting.js +++ b/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' ); diff --git a/tests/image/imageinsertcommand.js b/tests/image/imageinsertcommand.js new file mode 100644 index 00000000..2fc0df54 --- /dev/null +++ b/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, '[]' ); + + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be true when the selection directly in a paragraph', () => { + setModelData( model, 'foo[]' ); + 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, 'foo[]' ); + expect( command.isEnabled ).to.be.true; + } ); + + it( 'should be false when the selection is on other image', () => { + setModelData( model, '[]' ); + 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, '[]' ); + 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, '[]' ); + + 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, '[]' ); + + 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, '[]' ); + + 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, 'f[o]o' ); + + command.execute( { source: imgSrc } ); + + expect( getModelData( model ) ).to.equal( `[]foo` ); + } ); + + it( 'should insert multiple images at selection position as other widgets', () => { + const imgSrc1 = 'foo/bar.jpg'; + const imgSrc2 = 'foo/baz.jpg'; + + setModelData( model, 'f[o]o' ); + + command.execute( { source: [ imgSrc1, imgSrc2 ] } ); + + expect( getModelData( model ) ) + .to.equal( `[]foo` ); + } ); + + 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, '[]' ); + + command.execute( { source: imgSrc } ); + + expect( getModelData( model ) ).to.equal( '[]' ); + } ); + } ); +} ); diff --git a/tests/image/utils.js b/tests/image/utils.js index 85985a71..1471054a 100644 --- a/tests/image/utils.js +++ b/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, '[]' ); + + expect( isImageAllowed( model ) ).to.be.true; + } ); + + it( 'should return true when the selection directly in a paragraph', () => { + setModelData( model, 'foo[]' ); + 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, 'foo[]' ); + expect( isImageAllowed( model ) ).to.be.true; + } ); + + it( 'should return false when the selection is on other image', () => { + setModelData( model, '[]' ); + 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, '[]' ); + 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, '[]' ); + + 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, '[]' ); + + 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, '[]' ); + + 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, 'f[o]o' ); + + model.change( writer => { + insertImage( writer, model ); + } ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + } ); + + it( 'should insert image with given attributes', () => { + setModelData( model, 'f[o]o' ); + + model.change( writer => { + insertImage( writer, model, { src: 'bar' } ); + } ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + } ); + + 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, '[]' ); + + model.change( writer => { + insertImage( writer, model ); + } ); + + expect( getModelData( model ) ).to.equal( '[]' ); + } ); + } ); } ); diff --git a/tests/imageupload/imageuploadcommand.js b/tests/imageupload/imageuploadcommand.js index eaeb7dfd..5207da68 100644 --- a/tests/imageupload/imageuploadcommand.js +++ b/tests/imageupload/imageuploadcommand.js @@ -139,7 +139,7 @@ describe( 'ImageUploadCommand', () => { const file = createNativeFileMock(); setModelData( model, 'f[o]o' ); - 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, '[]' ); - command.execute( { files: file } ); + command.execute( { file } ); expect( getModelData( model ) ).to.equal( '[]' ); } ); @@ -188,7 +188,7 @@ describe( 'ImageUploadCommand', () => { setModelData( model, 'fo[]o' ); expect( () => { - command.execute( { files: file } ); + command.execute( { file } ); } ).to.not.throw(); expect( getModelData( model ) ).to.equal( 'fo[]o' ); diff --git a/tests/imageupload/imageuploadediting.js b/tests/imageupload/imageuploadediting.js index 6d647dc2..c2c0fc35 100644 --- a/tests/imageupload/imageuploadediting.js +++ b/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, '{}foo bar' ); - 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, '{}foo bar' ); - 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, '{}foo bar' ); - editor.execute( 'imageUpload', { files: file } ); + editor.execute( 'imageUpload', { file } ); nativeReaderMock.mockError( 'Reading error.' ); } ); @@ -375,7 +375,7 @@ describe( 'ImageUploadEditing', () => { }, { priority: 'high' } ); setModelData( model, '{}foo bar' ); - editor.execute( 'imageUpload', { files: file } ); + editor.execute( 'imageUpload', { file } ); nativeReaderMock.abort(); setTimeout( () => { @@ -399,7 +399,7 @@ describe( 'ImageUploadEditing', () => { } ); setModelData( model, '{}foo bar' ); - 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, '{}foo bar' ); - 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, '{}foo bar' ); - 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, '{}foo bar' ); - editor.execute( 'imageUpload', { files: file } ); + editor.execute( 'imageUpload', { file } ); model.document.once( 'change', () => { model.document.once( 'change', () => { diff --git a/tests/imageupload/imageuploadprogress.js b/tests/imageupload/imageuploadprogress.js index ffef57b9..6715af20 100644 --- a/tests/imageupload/imageuploadprogress.js +++ b/tests/imageupload/imageuploadprogress.js @@ -74,7 +74,7 @@ describe( 'ImageUploadProgress', () => { it( 'should convert image\'s "reading" uploadStatus attribute', () => { setModelData( model, '[]foo' ); - editor.execute( 'imageUpload', { files: createNativeFileMock() } ); + editor.execute( 'imageUpload', { file: createNativeFileMock() } ); expect( getViewData( view ) ).to.equal( '[
' + @@ -86,7 +86,7 @@ describe( 'ImageUploadProgress', () => { it( 'should convert image\'s "uploading" uploadStatus attribute', done => { setModelData( model, '[]foo' ); - 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, '[]foo' ); - 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, '[]foo' ); - 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, '[]foo' ); - editor.execute( 'imageUpload', { files: createNativeFileMock() } ); + editor.execute( 'imageUpload', { file: createNativeFileMock() } ); expect( getViewData( view ) ).to.equal( '[
' + @@ -238,7 +238,7 @@ describe( 'ImageUploadProgress', () => { }, { priority: 'highest' } ); setModelData( model, '[]foo' ); - editor.execute( 'imageUpload', { files: createNativeFileMock() } ); + editor.execute( 'imageUpload', { file: createNativeFileMock() } ); expect( getViewData( view ) ).to.equal( '[
]

foo

' @@ -276,7 +276,7 @@ describe( 'ImageUploadProgress', () => { testUtils.sinon.stub( env, 'isEdge' ).get( () => true ); setModelData( model, '[]foo' ); - editor.execute( 'imageUpload', { files: createNativeFileMock() } ); + editor.execute( 'imageUpload', { file: createNativeFileMock() } ); model.document.once( 'change', () => { model.document.once( 'change', () => { diff --git a/tests/imageupload/imageuploadui.js b/tests/imageupload/imageuploadui.js index 68326d4a..7ae0f074 100644 --- a/tests/imageupload/imageuploadui.js +++ b/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 ] ] ); } ); } );