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

Commit

Permalink
Merge branch 't/ckeditor5-upload/22'
Browse files Browse the repository at this point in the history
Feature: Intorduced the `ImageUpload` feature. It was moved from the `@ckeditor/ckeditor5-upload` package. See ckeditor/ckeditor5-upload#22.
  • Loading branch information
Reinmar committed Feb 2, 2018
2 parents c140b9e + f2defcd commit b974bb0
Show file tree
Hide file tree
Showing 22 changed files with 1,868 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/features/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
* {@link module:image/imagetoolbar~ImageToolbar} adds the image feature's contextual toolbar,
* {@link module:image/imagecaption~ImageCaption} adds support for captions,
* {@link module:image/imagestyle~ImageStyle} adds support for image styles,
* {@link module:upload/imageupload~ImageUpload} adds support for uploading dropped or pasted images (note: it is currently located in the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package but will be moved to the `@ckeditor/ckeditor5-image` package).
* {@link module:image/imageupload~ImageUpload} adds support for uploading dropped or pasted images (note: it is currently located in the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package but will be moved to the `@ckeditor/ckeditor5-image` package).

<info-box info>
The first four features listed above (so all except the upload support) are enabled by default in all builds.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@ckeditor/ckeditor5-ui": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-theme-lark": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-upload": "^1.0.0-alpha.2",
"@ckeditor/ckeditor5-widget": "^1.0.0-alpha.2"
},
"devDependencies": {
Expand Down
39 changes: 39 additions & 0 deletions src/imageupload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module image/imageupload
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ImageUploadUI from './imageupload/imageuploadui';
import ImageUploadProgress from './imageupload/imageuploadprogress';
import ImageUploadEditing from './imageupload/imageuploadediting';

/**
* Image upload plugin.
*
* This plugin do not do anything directly, but loads set of specific plugins to enable image uploading:
* * {@link module:image/imageupload/imageuploadediting~ImageUploadEditing},
* * {@link module:image/imageupload/imageuploadui~ImageUploadUI},
* * {@link module:image/imageupload/imageuploadprogress~ImageUploadProgress}.
*
* @extends module:core/plugin~Plugin
*/
export default class ImageUpload extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'ImageUpload';
}

/**
* @inheritDoc
*/
static get requires() {
return [ ImageUploadEditing, ImageUploadUI, ImageUploadProgress ];
}
}
67 changes: 67 additions & 0 deletions src/imageupload/imageuploadcommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
import Command from '@ckeditor/ckeditor5-core/src/command';

/**
* @module image/imageupload/imageuploadcommand
*/

/**
* Image upload command.
*
* @extends module:core/command~Command
*/
export default class ImageUploadCommand extends Command {
/**
* Executes the command.
*
* @fires execute
* @param {Object} options Options for executed command.
* @param {File} options.file Image file to upload.
* @param {module:engine/model/position~Position} [options.insertAt] Position at which the image should be inserted.
* If the position is not specified the image will be inserted into the current selection.
* Note: You can use the {@link module:upload/utils~findOptimalInsertionPosition} function to calculate
* (e.g. based on the current selection) a position which is more optimal from UX perspective.
*/
execute( options ) {
const editor = this.editor;
const doc = editor.model.document;
const file = options.file;
const fileRepository = editor.plugins.get( FileRepository );

editor.model.change( writer => {
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 = new ModelElement( 'image', {
uploadId: loader.id
} );

let insertAtSelection;

if ( options.insertAt ) {
insertAtSelection = new ModelSelection( [ new ModelRange( options.insertAt ) ] );
} else {
insertAtSelection = doc.selection;
}

editor.model.insertContent( imageElement, insertAtSelection );

// Inserting an image might've failed due to schema regulations.
if ( imageElement.parent ) {
writer.setSelection( ModelRange.createOn( imageElement ) );
}
} );
}
}
214 changes: 214 additions & 0 deletions src/imageupload/imageuploadediting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module image/imageupload/imageuploadediting
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
import { isImageType, findOptimalInsertionPosition } from '../../src/imageupload/utils';

/**
* Image upload editing plugin.
*
* @extends module:core/plugin~Plugin
*/
export default class ImageUploadEditing extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ FileRepository, Notification ];
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const doc = editor.model.document;
const schema = editor.model.schema;
const fileRepository = editor.plugins.get( FileRepository );

// Setup schema to allow uploadId and uploadStatus for images.
schema.extend( 'image', {
allowAttributes: [ 'uploadId', 'uploadStatus' ]
} );

// Register imageUpload command.
editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );

// Execute imageUpload command when image is dropped or pasted.
editor.editing.view.on( 'clipboardInput', ( evt, data ) => {
// Skip if non empty HTML data is included.
// https://github.com/ckeditor/ckeditor5-upload/issues/68
if ( isHtmlIncluded( data.dataTransfer ) ) {
return;
}

let targetModelSelection = new ModelSelection(
data.targetRanges.map( viewRange => editor.editing.mapper.toModelRange( viewRange ) )
);

for ( const file of data.dataTransfer.files ) {
const insertAt = findOptimalInsertionPosition( targetModelSelection );

if ( isImageType( file ) ) {
editor.execute( 'imageUpload', { file, insertAt } );
evt.stop();
}

// Use target ranges only for the first image. Then, use that image position
// so we keep adding the next ones after the previous one.
targetModelSelection = doc.selection;
}
} );

// Prevents from browser redirecting to the dropped image.
editor.editing.view.on( 'dragover', ( evt, data ) => {
data.preventDefault();
} );

doc.on( 'change', () => {
const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } );

for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == 'image' ) {
const item = entry.position.nodeAfter;
const isInGraveyard = entry.position.root.rootName == '$graveyard';

// Check if the image element still has upload id.
const uploadId = item.getAttribute( 'uploadId' );

if ( !uploadId ) {
continue;
}

// Check if the image is loaded on this client.
const loader = fileRepository.loaders.get( uploadId );

if ( !loader ) {
continue;
}

if ( isInGraveyard ) {
// If the image was inserted to the graveyard - abort the loading process.
loader.abort();
} else if ( loader.status == 'idle' ) {
// If the image was inserted into content and has not been loaded, start loading it.
this._load( loader, item );
}
}
}
} );
}

/**
* Performs image loading. Image is read from the disk and temporary data is displayed, after uploading process
* is complete we replace temporary data with target image from the server.
*
* @private
* @param {module:upload/filerepository~FileLoader} loader
* @param {module:engine/model/element~Element} imageElement
*/
_load( loader, imageElement ) {
const editor = this.editor;
const model = editor.model;
const t = editor.locale.t;
const fileRepository = editor.plugins.get( FileRepository );
const notification = editor.plugins.get( Notification );

model.enqueueChange( 'transparent', writer => {
writer.setAttribute( 'uploadStatus', 'reading', imageElement );
} );

loader.read()
.then( data => {
const viewFigure = editor.editing.mapper.toViewElement( imageElement );
const viewImg = viewFigure.getChild( 0 );
const promise = loader.upload();

viewImg.setAttribute( 'src', data );
editor.editing.view.render();

model.enqueueChange( 'transparent', writer => {
writer.setAttribute( 'uploadStatus', 'uploading', imageElement );
} );

return promise;
} )
.then( data => {
model.enqueueChange( 'transparent', writer => {
writer.setAttributes( { uploadStatus: 'complete', src: data.default }, imageElement );

// Srcset attribute for responsive images support.
let maxWidth = 0;
const srcsetAttribute = Object.keys( data )
// Filter out keys that are not integers.
.filter( key => {
const width = parseInt( key, 10 );

if ( !isNaN( width ) ) {
maxWidth = Math.max( maxWidth, width );

return true;
}
} )

// Convert each key to srcset entry.
.map( key => `${ data[ key ] } ${ key }w` )

// Join all entries.
.join( ', ' );

if ( srcsetAttribute != '' ) {
writer.setAttribute( 'srcset', {
data: srcsetAttribute,
width: maxWidth
}, imageElement );
}
} );

clean();
} )
.catch( msg => {
// Might be 'aborted'.
if ( loader.status == 'error' ) {
notification.showWarning( msg, {
title: t( 'Upload failed' ),
namespace: 'upload'
} );
}

clean();

// Permanently remove image from insertion batch.
model.enqueueChange( 'transparent', writer => {
writer.remove( imageElement );
} );
} );

function clean() {
model.enqueueChange( 'transparent', writer => {
writer.removeAttribute( 'uploadId', imageElement );
writer.removeAttribute( 'uploadStatus', imageElement );
} );

fileRepository.destroyLoader( loader );
}
}
}

// Returns true if non-empty `text/html` is included in data transfer.
//
// @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
// @returns {Boolean}
export function isHtmlIncluded( dataTransfer ) {
return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
}
Loading

0 comments on commit b974bb0

Please sign in to comment.