This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 't/ckeditor5-upload/22'
Feature: Intorduced the `ImageUpload` feature. It was moved from the `@ckeditor/ckeditor5-upload` package. See ckeditor/ckeditor5-upload#22.
- Loading branch information
Showing
22 changed files
with
1,868 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) ); | ||
} | ||
} ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' ) !== ''; | ||
} |
Oops, something went wrong.