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

T/245: The "imageInsert" command #250

Merged
merged 8 commits into from
Nov 22, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
44 changes: 44 additions & 0 deletions src/image/imageinsertcommand.js
Original file line number Diff line number Diff line change
@@ -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 model = this.editor.model;

model.change( writer => {
const sources = Array.isArray( options.sources ) ? options.sources : [ options.sources ];

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;
}
66 changes: 10 additions & 56 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 @@ -21,11 +21,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 );
}

/**
Expand All @@ -37,12 +33,15 @@ export default class ImageUploadCommand extends Command {
*/
execute( options ) {
const editor = this.editor;
const model = editor.model;

const fileRepository = editor.plugins.get( FileRepository );

editor.model.change( writer => {
model.change( writer => {
const filesToUpload = Array.isArray( options.files ) ? options.files : [ options.files ];

for ( const file of filesToUpload ) {
uploadImage( writer, editor, file );
uploadImage( writer, model, fileRepository, file );
}
} );
}
Expand All @@ -51,60 +50,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 } );
}
2 changes: 1 addition & 1 deletion 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
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