Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #250 from ckeditor/t/245
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Reinmar committed Nov 22, 2018
2 parents 1761cbf + f458c25 commit cc1e7a3
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 91 deletions.
3 changes: 2 additions & 1 deletion docs/features/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
12 changes: 10 additions & 2 deletions src/image/imageediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -102,6 +107,9 @@ export default class ImageEditing extends Plugin {
}
} ) )
.add( viewFigureToModel() );

// Register imageUpload command.
editor.commands.add( 'imageInsert', new ImageInsertCommand( editor ) );
}
}

Expand Down
61 changes: 61 additions & 0 deletions src/image/imageinsertcommand.js
Original file line number Diff line number Diff line change
@@ -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 } );
}
} );
}
}
76 changes: 75 additions & 1 deletion src/image/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );

Expand Down Expand Up @@ -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;
}
93 changes: 35 additions & 58 deletions src/imageupload/imageuploadcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,35 +14,57 @@ 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 {
/**
* @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 );
}

/**
* Executes the 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 );
}
} );
}
Expand All @@ -51,60 +73,15 @@ 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.
if ( !loader ) {
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 } );
}
4 changes: 2 additions & 2 deletions src/imageupload/imageuploadediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 } );
} );
}
} );
Expand Down
2 changes: 1 addition & 1 deletion src/imageupload/imageuploadui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } );
}
} );

Expand Down
5 changes: 5 additions & 0 deletions tests/image/imageediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' );
Expand Down
Loading

0 comments on commit cc1e7a3

Please sign in to comment.