Socket
Socket
Sign inDemoInstall

@ckeditor/ckeditor5-upload

Package Overview
Dependencies
Maintainers
1
Versions
614
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-upload - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

.eslintrc.js

31

CHANGELOG.md
Changelog
=========
## [0.2.0](https://github.com/ckeditor/ckeditor5-upload/compare/v0.1.0...v0.2.0) (2017-09-03)
### Bug fixes
* [Safari, Edge] The image upload (button) feature will not throw an error anymore when trying to access picked files. The feature should not use `for...of` loop on native `FileList` because Safari and Edge do not support `Symbol.iterator` for it yet. Closes [#35](https://github.com/ckeditor/ckeditor5-upload/issues/35). ([f4efd9b](https://github.com/ckeditor/ckeditor5-upload/commit/f4efd9b))
* An image dropped on another image will not redirect the browser to the file's path. Closes [#32](https://github.com/ckeditor/ckeditor5-upload/issues/32). ([4f533be](https://github.com/ckeditor/ckeditor5-upload/commit/4f533be))
* Bound `ImageUploadButton#isEnabled` to `ImageUploadCommand#isEnabled`. Closes [#43](https://github.com/ckeditor/ckeditor5-upload/issues/43). ([ba6de66](https://github.com/ckeditor/ckeditor5-upload/commit/ba6de66))
* Fixed two issues related to dropping images. First, when dropping a file into an empty paragraph, that paragraph should be replaced with that image. Second, drop position should be read correctly when the editor is focused upon drop. Closes [#42](https://github.com/ckeditor/ckeditor5-upload/issues/42). Closes [#29](https://github.com/ckeditor/ckeditor5-upload/issues/29). ([fec452d](https://github.com/ckeditor/ckeditor5-upload/commit/fec452d))
* Image will be inserted after the block if the selection is placed at the block's end. Closes [#7](https://github.com/ckeditor/ckeditor5-upload/issues/7). ([70742f9](https://github.com/ckeditor/ckeditor5-upload/commit/70742f9))
* When image upload is aborted, now the "image placeholder" element is permanently removed so it is not reinserted on undo. Closes [#38](https://github.com/ckeditor/ckeditor5-upload/issues/38). ([aff6382](https://github.com/ckeditor/ckeditor5-upload/commit/aff6382))
### Features
* Responsive images support in image upload. Closes [#34](https://github.com/ckeditor/ckeditor5-upload/issues/34). ([9a022a2](https://github.com/ckeditor/ckeditor5-upload/commit/9a022a2))
* The `ImageUploadCommand` now accepts `insertAt` position which allows customizing where the image will be inserted. Closes [#45](https://github.com/ckeditor/ckeditor5-upload/issues/45). ([b90c8d7](https://github.com/ckeditor/ckeditor5-upload/commit/b90c8d7))
### Other changes
* Aborting upload when image is removed and removing image on upload error. Closes [#2](https://github.com/ckeditor/ckeditor5-upload/issues/2). ([c3bbb57](https://github.com/ckeditor/ckeditor5-upload/commit/c3bbb57))
* Aligned the implementation to the new Command API (see https://github.com/ckeditor/ckeditor5-core/issues/88). ([3d97b81](https://github.com/ckeditor/ckeditor5-upload/commit/3d97b81))
* Changed from original to default image. Closes [#49](https://github.com/ckeditor/ckeditor5-upload/issues/49). ([d8d61f3](https://github.com/ckeditor/ckeditor5-upload/commit/d8d61f3))
* Cleaned up SVG icons. ([ab81012](https://github.com/ckeditor/ckeditor5-upload/commit/ab81012))
* Optional notification title when upload fails. Closes [#30](https://github.com/ckeditor/ckeditor5-upload/issues/30). ([1a6306c](https://github.com/ckeditor/ckeditor5-upload/commit/1a6306c))
### BREAKING CHANGES
* `UploadImageCommand` doesn't optimize the drop position itself anymore. Instead, a separate `findOptimalInsertionPosition()` function was introduced.
* `UploadImageCommand` doesn't verify the type of file anymore. This needs to be done by the caller.
* The command API has been changed.
## 0.1.0 (2017-05-07)

@@ -5,0 +36,0 @@

11

gulpfile.js

@@ -6,3 +6,3 @@ /**

/* jshint browser: false, node: true, strict: true */
/* eslint-env node */

@@ -12,3 +12,4 @@ 'use strict';

const gulp = require( 'gulp' );
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' )( {
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' );
const options = {
// Files ignored by `gulp lint` task.

@@ -19,6 +20,6 @@ // Files from .gitignore will be added automatically during task execution.

]
} );
};
gulp.task( 'lint', ckeditor5Lint.lint );
gulp.task( 'lint-staged', ckeditor5Lint.lintStaged );
gulp.task( 'lint', () => ckeditor5Lint.lint( options ) );
gulp.task( 'lint-staged', () => ckeditor5Lint.lintStaged( options ) );
gulp.task( 'pre-commit', [ 'lint-staged' ] );
{
"Insert image": "Label for the insert image toolbar button."
"Insert image": "Label for the insert image toolbar button.",
"Upload failed": "Title of the notification displayed when upload fails."
}
{
"name": "@ckeditor/ckeditor5-upload",
"version": "0.1.0",
"version": "0.2.0",
"description": "Upload Feature for CKEditor 5.",
"keywords": [],
"dependencies": {
"@ckeditor/ckeditor5-core": "^0.8.1",
"@ckeditor/ckeditor5-engine": "^0.10.0",
"@ckeditor/ckeditor5-ui": "^0.9.0",
"@ckeditor/ckeditor5-utils": "^0.9.1"
"@ckeditor/ckeditor5-core": "^0.9.0",
"@ckeditor/ckeditor5-engine": "^0.11.0",
"@ckeditor/ckeditor5-ui": "^0.10.0",
"@ckeditor/ckeditor5-utils": "^0.10.0"
},
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "^0.8.1",
"@ckeditor/ckeditor5-clipboard": "^0.6.0",
"@ckeditor/ckeditor5-dev-lint": "^2.0.2",
"@ckeditor/ckeditor5-editor-classic": "^0.7.3",
"@ckeditor/ckeditor5-enter": "^0.9.1",
"@ckeditor/ckeditor5-heading": "^0.9.1",
"@ckeditor/ckeditor5-image": "^0.6.0",
"@ckeditor/ckeditor5-list": "^0.6.1",
"@ckeditor/ckeditor5-paragraph": "^0.8.0",
"@ckeditor/ckeditor5-typing": "^0.9.1",
"@ckeditor/ckeditor5-undo": "^0.8.1",
"@ckeditor/ckeditor5-basic-styles": "^0.9.0",
"@ckeditor/ckeditor5-clipboard": "^0.7.0",
"@ckeditor/ckeditor5-dev-lint": "^3.1.0",
"@ckeditor/ckeditor5-editor-classic": "^0.8.0",
"@ckeditor/ckeditor5-enter": "^0.10.0",
"@ckeditor/ckeditor5-heading": "^0.10.0",
"@ckeditor/ckeditor5-image": "^0.7.0",
"@ckeditor/ckeditor5-list": "^0.7.0",
"@ckeditor/ckeditor5-paragraph": "^0.9.0",
"@ckeditor/ckeditor5-typing": "^0.10.0",
"@ckeditor/ckeditor5-undo": "^0.9.0",
"eslint-config-ckeditor5": "^1.0.5",
"gulp": "^3.9.1",

@@ -25,0 +26,0 @@ "guppy-pre-commit": "^0.4.0"

@@ -1,4 +0,5 @@

CKEditor 5 Upload Feature
CKEditor 5 upload feature
========================================
[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-upload.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload)

@@ -5,0 +6,0 @@ [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-upload.svg)](https://travis-ci.org/ckeditor/ckeditor5-upload)

@@ -32,3 +32,3 @@ /**

static get pluginName() {
return 'upload/filerepository';
return 'FileRepository';
}

@@ -432,3 +432,3 @@

*
* editor.plugins.get( 'upload/filerepository' ).createAdapter = function( loader ) {
* editor.plugins.get( 'FileRepository' ).createAdapter = function( loader ) {
* return new Adapter( loader );

@@ -446,5 +446,14 @@ * };

* {
* original: 'http://server/orginal-size.image.png'
* default: 'http://server/default-size.image.png'
* }
*
* Additionally, other image sizes can be provided:
*
* {
* default: 'http://server/default-size.image.png',
* '160': 'http://server/size-160.image.png',
* '500': 'http://server/size-500.image.png',
* '1000': 'http://server/size-1000.image.png'
* }
*
* Take a look at {@link module:upload/filerepository~Adapter example Adapter implementation} and

@@ -451,0 +460,0 @@ * {@link module:upload/filerepository~FileRepository#createAdapter createAdapter method}.

@@ -14,2 +14,3 @@ /**

import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
import { isImageType, findOptimalInsertionPosition } from './utils';

@@ -38,4 +39,5 @@ /**

// Setup `insertImage` button.
editor.ui.componentFactory.add( 'insertImage', ( locale ) => {
editor.ui.componentFactory.add( 'insertImage', locale => {
const view = new FileDialogButtonView( locale );
const command = editor.commands.get( 'imageUpload' );

@@ -50,5 +52,11 @@ view.set( {

view.bind( 'isEnabled' ).to( command );
view.on( 'done', ( evt, files ) => {
for ( const file of files ) {
editor.execute( 'imageUpload', { file: file } );
for ( const file of Array.from( files ) ) {
const insertAt = findOptimalInsertionPosition( editor.document.selection );
if ( isImageType( file ) ) {
editor.execute( 'imageUpload', { file, insertAt } );
}
}

@@ -55,0 +63,0 @@ } );

@@ -6,10 +6,7 @@ /**

import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
import FileRepository from './filerepository';
import { isImageType } from './utils';
import Command from '@ckeditor/ckeditor5-core/src/command/command';
import Command from '@ckeditor/ckeditor5-core/src/command';

@@ -23,15 +20,19 @@ /**

*
* @extends module:core/command/command~Command
* @extends module:core/command~Command
*/
export default class ImageUploadCommand extends Command {
/**
* Executes command.
* Executes the command.
*
* @protected
* @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.
* @param {module:engine/model/batch~Batch} [options.batch] Batch to collect all the change steps.
* New batch will be created if this option is not set.
*/
_doExecute( options ) {
execute( options ) {
const editor = this.editor;

@@ -44,39 +45,29 @@ const doc = editor.document;

if ( !isImageType( file ) ) {
return;
}
doc.enqueueChanges( () => {
let insertPosition;
const selectedElement = selection.getSelectedElement();
const imageElement = new ModelElement( 'image', {
uploadId: fileRepository.createLoader( file ).id
} );
// If selected element is placed directly in root - put image after it.
if ( selectedElement && selectedElement.parent.is( 'rootElement' ) ) {
insertPosition = ModelPosition.createAfter( selectedElement );
let insertAtSelection;
if ( options.insertAt ) {
insertAtSelection = new ModelSelection( [ new ModelRange( options.insertAt ) ] );
} else {
// If selection is inside some block - put image before it.
const firstBlock = doc.selection.getSelectedBlocks().next().value;
if ( firstBlock ) {
insertPosition = ModelPosition.createBefore( firstBlock );
}
insertAtSelection = doc.selection;
}
// No position to insert.
if ( !insertPosition ) {
return;
editor.data.insertContent( imageElement, insertAtSelection, batch );
// Inserting an image might've failed due to schema regulations.
if ( imageElement.parent ) {
selection.setRanges( [ ModelRange.createOn( imageElement ) ] );
}
const imageElement = new ModelElement( 'image', {
uploadId: fileRepository.createLoader( file ).id
} );
const documentFragment = new ModelDocumentFragment( [ imageElement ] );
const range = new ModelRange( insertPosition );
const insertSelection = new ModelSelection();
insertSelection.setRanges( [ range ] );
editor.data.insertContent( documentFragment, insertSelection, batch );
selection.setRanges( [ ModelRange.createOn( imageElement ) ] );
} );
}
}
// Returns correct image insertion position.
//
// @param {module:engine/model/document~Document} doc
// @returns {module:engine/model/position~Position|undefined}

@@ -14,3 +14,4 @@ /**

import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
import { isImageType } from './utils';
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
import { isImageType, findOptimalInsertionPosition } from './utils';

@@ -37,2 +38,3 @@ /**

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

@@ -45,17 +47,34 @@ // Setup schema to allow uploadId for images.

// Register imageUpload command.
editor.commands.set( 'imageUpload', new ImageUploadCommand( editor ) );
editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
// Execute imageUpload command when image is dropped or pasted.
editor.editing.view.on( 'clipboardInput', ( evt, data ) => {
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 } );
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;
}
} );
// Listen on document changes and start upload process when image with `uploadId` attribute is present.
doc.on( 'change', ( evt, type, data, batch ) => {
if ( type === 'insert' ) {
// Prevents from browser redirecting to drag-end-dropped image.
editor.editing.view.on( 'dragover', ( evt, data ) => {
data.preventDefault();
} );
doc.on( 'change', ( evt, type, data ) => {
// Listen on document changes and:
// * start upload process when image with `uploadId` attribute is inserted,
// * abort upload process when image `uploadId` attribute is removed.
if ( type === 'insert' || type === 'reinsert' || type === 'remove' ) {
for ( const value of data.range ) {

@@ -65,3 +84,2 @@ if ( value.type === 'elementStart' && value.item.name === 'image' ) {

const uploadId = imageElement.getAttribute( 'uploadId' );
const fileRepository = editor.plugins.get( FileRepository );

@@ -71,4 +89,10 @@ if ( uploadId ) {

if ( loader && loader.status == 'idle' ) {
this.load( loader, batch, imageElement );
if ( loader ) {
if ( type === 'insert' && loader.status == 'idle' ) {
this.load( loader, imageElement );
}
if ( type === 'remove' ) {
loader.abort();
}
}

@@ -88,7 +112,7 @@ }

* @param {module:upload/filerepository~FileLoader} loader
* @param {module:engine/model/batch~Batch} batch
* @param {module:engine/model/element~Element} imageElement
*/
load( loader, batch, imageElement ) {
load( loader, imageElement ) {
const editor = this.editor;
const t = editor.locale.t;
const doc = editor.document;

@@ -99,3 +123,3 @@ const fileRepository = editor.plugins.get( FileRepository );

doc.enqueueChanges( () => {
batch.setAttribute( imageElement, 'uploadStatus', 'reading' );
doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'reading' );
} );

@@ -113,3 +137,3 @@

doc.enqueueChanges( () => {
batch.setAttribute( imageElement, 'uploadStatus', 'uploading' );
doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'uploading' );
} );

@@ -121,4 +145,19 @@

doc.enqueueChanges( () => {
batch.setAttribute( imageElement, 'uploadStatus', 'complete' );
batch.setAttribute( imageElement, 'src', data.original );
doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'complete' );
doc.batch( 'transparent' ).setAttribute( imageElement, 'src', data.default );
// Srcset attribute for responsive images support.
const srcsetAttribute = Object.keys( data )
// Filter out keys that are not integers.
.filter( key => !isNaN( parseInt( key, 10 ) ) )
// Convert each key to srcset entry.
.map( key => `${ data[ key ] } ${ key }w` )
// Join all entries.
.join( ', ' );
if ( srcsetAttribute != '' ) {
doc.batch( 'transparent' ).setAttribute( imageElement, 'srcset', srcsetAttribute );
}
} );

@@ -131,6 +170,14 @@

if ( loader.status == 'error' ) {
notification.showWarning( msg, { namespace: 'upload' } );
notification.showWarning( msg, {
title: t( 'Upload failed' ),
namespace: 'upload'
} );
}
clean();
// Permanently remove image from insertion batch.
doc.enqueueChanges( () => {
doc.batch( 'transparent' ).remove( imageElement );
} );
} );

@@ -140,4 +187,4 @@

doc.enqueueChanges( () => {
batch.removeAttribute( imageElement, 'uploadId' );
batch.removeAttribute( imageElement, 'uploadStatus' );
doc.batch( 'transparent' ).removeAttribute( imageElement, 'uploadId' );
doc.batch( 'transparent' ).removeAttribute( imageElement, 'uploadStatus' );
} );

@@ -144,0 +191,0 @@

@@ -62,19 +62,14 @@ /**

const editor = this.editor;
const fileRepository = editor.plugins.get( FileRepository );
const placeholder = this.placeholder;
const modelImage = data.item;
const uploadId = modelImage.getAttribute( 'uploadId' );
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) || !uploadId ) {
return;
}
const fileRepository = editor.plugins.get( FileRepository );
const placeholder = this.placeholder;
const status = data.attributeNewValue;
const modelImage = data.item;
const viewFigure = editor.editing.mapper.toViewElement( modelImage );
const uploadId = modelImage.getAttribute( 'uploadId' );
const loader = fileRepository.loaders.get( uploadId );
if ( !loader ) {
return;
}
// Show placeholder with infinite progress bar on the top while image is read from disk.

@@ -91,12 +86,17 @@ if ( status == 'reading' ) {

if ( status == 'uploading' ) {
viewFigure.removeClass( 'ck-infinite-progress' );
const progressBar = createProgressBar();
viewFigure.appendChildren( progressBar );
const loader = fileRepository.loaders.get( uploadId );
// Update progress bar width when uploadedPercent is changed.
loader.on( 'change:uploadedPercent', ( evt, name, value ) => {
progressBar.setStyle( 'width', value + '%' );
editor.editing.view.render();
} );
if ( loader ) {
const progressBar = createProgressBar();
viewFigure.removeClass( 'ck-infinite-progress' );
viewFigure.appendChildren( progressBar );
// Update progress bar width when uploadedPercent is changed.
loader.on( 'change:uploadedPercent', ( evt, name, value ) => {
progressBar.setStyle( 'width', value + '%' );
editor.editing.view.render();
} );
}
return;

@@ -108,6 +108,9 @@ }

if ( progressBar ) {
progressBar.remove();
} else {
viewFigure.removeClass( 'ck-infinite-progress' );
}
viewFigure.removeClass( 'ck-appear' );
progressBar.remove();
editor.editing.view.render();
}

@@ -114,0 +117,0 @@ }

@@ -32,3 +32,3 @@ /**

* @protected
* @member module:upload/ui/filedialogbuttonview~FileInputView #fileInputView
* @member {module:upload/ui/filedialogbuttonview~FileInputView}
*/

@@ -84,3 +84,3 @@ this.fileInputView = new FileInputView( locale );

return super.destroy();
super.destroy();
}

@@ -87,0 +87,0 @@ }

@@ -10,2 +10,4 @@ /**

import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
/**

@@ -23,1 +25,45 @@ * Checks if given file is an image.

/**
* Returns a model position which is optimal (in terms of UX) for inserting an image.
*
* For instance, if a selection is in a middle of a paragraph, position before this paragraph
* will be returned, so that it's not split. If the selection is at the end of a paragraph,
* position after this paragraph will be returned.
*
* Note: If selection is placed in an empty block, that block will be returned. If that position
* is then passed to {@link module:engine/controller/datacontroller~DataController#insertContent}
* that block will be fully replaced by the image.
*
* @param {module:engine/model/selection~Selection} selection Selection based on which the
* insertion position should be calculated.
* @returns {module:engine/model/position~Position} The optimal position.
*/
export function findOptimalInsertionPosition( selection ) {
const selectedElement = selection.getSelectedElement();
if ( selectedElement ) {
return ModelPosition.createAfter( selectedElement );
}
const firstBlock = selection.getSelectedBlocks().next().value;
if ( firstBlock ) {
// If inserting into an empty block – return position in that block. It will get
// replaced with the image by insertContent(). #42.
if ( firstBlock.isEmpty ) {
return ModelPosition.createAt( firstBlock );
}
const positionAfter = ModelPosition.createAfter( firstBlock );
// If selection is at the end of the block - return position after the block.
if ( selection.focus.isTouching( positionAfter ) ) {
return positionAfter;
}
// Otherwise return position before the block.
return ModelPosition.createBefore( firstBlock );
}
return selection.focus;
}

@@ -17,3 +17,3 @@ /**

beforeEach( () => {
testUtils.sinon.stub( window, 'FileReader', () => {
testUtils.sinon.stub( window, 'FileReader' ).callsFake( () => {
nativeReaderMock = new NativeFileReaderMock();

@@ -61,3 +61,3 @@

throw new Error( 'Reader should not resolve.' );
}, ( status ) => {
}, status => {
expect( status ).to.equal( 'error' );

@@ -76,3 +76,3 @@ expect( reader.error ).to.equal( 'Error during file reading.' );

throw new Error( 'Reader should not resolve.' );
}, ( status ) => {
}, status => {
expect( status ).to.equal( 'aborted' );

@@ -92,3 +92,3 @@ } );

throw new Error( 'Reader should not resolve.' );
}, ( status ) => {
}, status => {
expect( status ).to.equal( 'aborted' );

@@ -95,0 +95,0 @@ } );

@@ -26,3 +26,3 @@ /**

editor = newEditor;
fileRepository = editor.plugins.get( 'upload/filerepository' );
fileRepository = editor.plugins.get( 'FileRepository' );
fileRepository.createAdapter = loader => {

@@ -45,3 +45,3 @@ adapterMock = new AdapterMock( loader );

it( 'should initialize uploaded observable', ( done ) => {
it( 'should initialize uploaded observable', done => {
expect( fileRepository.uploaded ).to.equal( 0 );

@@ -57,3 +57,3 @@

it( 'should initialize uploadTotal', ( done ) => {
it( 'should initialize uploadTotal', done => {
expect( fileRepository.uploadTotal ).to.be.null;

@@ -69,3 +69,3 @@

it( 'should initialize uploadedPercent', ( done ) => {
it( 'should initialize uploadedPercent', done => {
expect( fileRepository.uploadedPercent ).to.equal( 0 );

@@ -90,3 +90,6 @@

sinon.assert.calledOnce( stub );
sinon.assert.calledWithExactly( stub, 'FileRepository: no createAdapter method found. Please define it before creating a loader.' );
sinon.assert.calledWithExactly(
stub,
'FileRepository: no createAdapter method found. Please define it before creating a loader.'
);
} );

@@ -157,3 +160,3 @@

beforeEach( () => {
testUtils.sinon.stub( window, 'FileReader', () => {
testUtils.sinon.stub( window, 'FileReader' ).callsFake( () => {
nativeReaderMock = new NativeFileReaderMock();

@@ -170,3 +173,3 @@

it( 'should initialize id', () => {
expect( loader.id ).to.be.number;
expect( loader.id ).to.be.a( 'string' );
} );

@@ -186,3 +189,3 @@

it( 'should initialize status observable', ( done ) => {
it( 'should initialize status observable', done => {
expect( loader.status ).to.equal( 'idle' );

@@ -198,3 +201,3 @@

it( 'should initialize uploaded observable', ( done ) => {
it( 'should initialize uploaded observable', done => {
expect( loader.uploaded ).to.equal( 0 );

@@ -210,3 +213,3 @@

it( 'should initialize uploadTotal observable', ( done ) => {
it( 'should initialize uploadTotal observable', done => {
expect( loader.uploadTotal ).to.equal( null );

@@ -222,3 +225,3 @@

it( 'should initialize uploadedPercent observable', ( done ) => {
it( 'should initialize uploadedPercent observable', done => {
expect( loader.uploadedPercent ).to.equal( 0 );

@@ -235,3 +238,5 @@

it( 'should initialize uploadResponse observable', ( done ) => {
it( 'should initialize uploadResponse observable', done => {
const response = {};
expect( loader.uploadResponse ).to.equal( null );

@@ -244,3 +249,2 @@

const response = {};
loader.uploadResponse = response;

@@ -247,0 +251,0 @@ } );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Image from '@ckeditor/ckeditor5-image/src/image';

@@ -22,8 +22,9 @@ import ImageUpload from '../src/imageupload';

return ClassicEditor.create( editorElement, {
plugins: [ Image, ImageUpload ]
} )
.then( newEditor => {
editor = newEditor;
} );
return ClassicEditor
.create( editorElement, {
plugins: [ Image, ImageUpload ]
} )
.then( newEditor => {
editor = newEditor;
} );
} );

@@ -30,0 +31,0 @@

@@ -8,24 +8,47 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Image from '@ckeditor/ckeditor5-image/src/image';
import FileDialogButtonView from '../src/ui/filedialogbuttonview';
import FileRepository from '../src/filerepository';
import ImageUploadButton from '../src/imageuploadbutton';
import ImageUploadEngine from '../src/imageuploadengine';
import { createNativeFileMock } from './_utils/mocks';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
import { createNativeFileMock, AdapterMock } from './_utils/mocks';
import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
describe( 'ImageUploadButton', () => {
let editor;
let editor, doc, editorElement, fileRepository;
beforeEach( () => {
const editorElement = document.createElement( 'div' );
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
return ClassicEditor.create( editorElement, {
plugins: [ Image, ImageUploadButton ]
} )
return ClassicEditor
.create( editorElement, {
plugins: [ Paragraph, Image, ImageUploadButton, FileRepository ]
} )
.then( newEditor => {
editor = newEditor;
doc = editor.document;
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = loader => {
return new AdapterMock( loader );
};
// Hide all notifications (prevent alert() calls).
const notification = editor.plugins.get( Notification );
notification.on( 'show', evt => evt.stop() );
} );
} );
afterEach( () => {
editorElement.remove();
return editor.destroy();
} );
it( 'should include ImageUploadEngine', () => {

@@ -41,2 +64,15 @@ expect( editor.plugins.get( ImageUploadEngine ) ).to.be.instanceOf( ImageUploadEngine );

it( 'should be disabled while ImageUploadCommand is disabled', () => {
const button = editor.ui.componentFactory.create( 'insertImage' );
const command = editor.commands.get( 'imageUpload' );
command.isEnabled = true;
expect( button.isEnabled ).to.true;
command.isEnabled = false;
expect( button.isEnabled ).to.false;
} );
it( 'should execute imageUpload command', () => {

@@ -52,3 +88,64 @@ const executeStub = sinon.stub( editor, 'execute' );

} );
it( 'should optimize the insertion position', () => {
const button = editor.ui.componentFactory.create( 'insertImage' );
const files = [ createNativeFileMock() ];
setModelData( doc, '<paragraph>f[]oo</paragraph>' );
button.fire( 'done', files );
const id = fileRepository.getLoader( files[ 0 ] ).id;
expect( getModelData( doc ) ).to.equal(
`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
'<paragraph>foo</paragraph>'
);
} );
it( 'should correctly insert multiple files', () => {
const button = editor.ui.componentFactory.create( 'insertImage' );
const files = [ createNativeFileMock(), createNativeFileMock() ];
setModelData( doc, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
button.fire( 'done', files );
const id1 = fileRepository.getLoader( files[ 0 ] ).id;
const id2 = fileRepository.getLoader( files[ 1 ] ).id;
expect( getModelData( doc ) ).to.equal(
'<paragraph>foo</paragraph>' +
`<image uploadId="${ id1 }" uploadStatus="reading"></image>` +
`[<image uploadId="${ id2 }" uploadStatus="reading"></image>]` +
'<paragraph>bar</paragraph>'
);
} );
it( 'should not execute imageUpload if the file is not an image', () => {
const executeStub = sinon.stub( editor, 'execute' );
const button = editor.ui.componentFactory.create( 'insertImage' );
const file = {
type: 'media/mp3',
size: 1024
};
button.fire( 'done', [ file ] );
sinon.assert.notCalled( executeStub );
} );
it( 'should work even if the FileList does not support iterators', () => {
const executeStub = sinon.stub( editor, 'execute' );
const button = editor.ui.componentFactory.create( 'insertImage' );
const files = {
0: createNativeFileMock(),
length: 1
};
button.fire( 'done', files );
sinon.assert.calledOnce( executeStub );
expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
expect( executeStub.firstCall.args[ 1 ].file ).to.equal( files[ 0 ] );
} );
} );

@@ -14,87 +14,88 @@ /**

import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
describe( 'ImageUploadCommand', () => {
let editor, command, adapterMock, document, fileRepository;
let editor, command, doc, fileRepository;
beforeEach( () => {
return VirtualTestEditor.create( {
plugins: [ FileRepository, Image, Paragraph ]
} )
.then( newEditor => {
editor = newEditor;
command = new ImageUploadCommand( editor );
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = loader => {
adapterMock = new AdapterMock( loader );
return VirtualTestEditor
.create( {
plugins: [ FileRepository, Image, Paragraph ]
} )
.then( newEditor => {
editor = newEditor;
command = new ImageUploadCommand( editor );
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = loader => {
return new AdapterMock( loader );
};
return adapterMock;
};
doc = editor.document;
document = editor.document;
const schema = doc.schema;
schema.allow( { name: 'image', attributes: [ 'uploadId' ], inside: '$root' } );
schema.requireAttributes( 'image', [ 'uploadId' ] );
} );
} );
const schema = document.schema;
schema.allow( { name: 'image', attributes: [ 'uploadId' ], inside: '$root' } );
schema.requireAttributes( 'image', [ 'uploadId' ] );
} );
afterEach( () => {
return editor.destroy();
} );
describe( '_doExecute', () => {
it( 'should insert image', () => {
describe( 'execute()', () => {
it( 'should insert image at selection position (includes deleting selected content)', () => {
const file = createNativeFileMock();
setModelData( document, '<paragraph>foo[]</paragraph>' );
setModelData( doc, '<paragraph>f[o]o</paragraph>' );
command._doExecute( { file } );
command.execute( { file } );
const id = fileRepository.getLoader( file ).id;
expect( getModelData( document ) ).to.equal( `[<image uploadId="${ id }"></image>]<paragraph>foo</paragraph>` );
expect( getModelData( doc ) )
.to.equal( `<paragraph>f</paragraph>[<image uploadId="${ id }"></image>]<paragraph>o</paragraph>` );
} );
it( 'should insert image after other image', () => {
it( 'should insert directly at specified position (options.insertAt)', () => {
const file = createNativeFileMock();
setModelData( document, '[<image src="image.png"></image>]' );
setModelData( doc, '<paragraph>f[]oo</paragraph>' );
command._doExecute( { file } );
const insertAt = new ModelPosition( doc.getRoot(), [ 0, 2 ] ); // fo[]o
command.execute( { file, insertAt } );
const id = fileRepository.getLoader( file ).id;
expect( getModelData( document ) ).to.equal( `<image src="image.png"></image>[<image uploadId="${ id }"></image>]` );
expect( getModelData( doc ) )
.to.equal( `<paragraph>fo</paragraph>[<image uploadId="${ id }"></image>]<paragraph>o</paragraph>` );
} );
it( 'should not insert image when proper insert position cannot be found', () => {
it( 'should allow to provide batch instance (options.batch)', () => {
const batch = doc.batch();
const file = createNativeFileMock();
document.schema.registerItem( 'other' );
document.schema.allow( { name: 'other', inside: '$root' } );
buildModelConverter().for( editor.editing.modelToView )
.fromElement( 'other' )
.toElement( 'span' );
const spy = sinon.spy( batch, 'insert' );
setModelData( document, '<other>[]</other>' );
setModelData( doc, '<paragraph>[]foo</paragraph>' );
command._doExecute( { file } );
command.execute( { batch, file } );
const id = fileRepository.getLoader( file ).id;
expect( getModelData( document ) ).to.equal( '<other>[]</other>' );
expect( getModelData( doc ) ).to.equal( `[<image uploadId="${ id }"></image>]<paragraph>foo</paragraph>` );
sinon.assert.calledOnce( spy );
} );
it( 'should not insert non-image', () => {
it( 'should not insert image nor crash when image could not be inserted', () => {
const file = createNativeFileMock();
file.type = 'audio/mpeg3';
setModelData( document, '<paragraph>foo[]</paragraph>' );
command._doExecute( { file } );
doc.schema.registerItem( 'other' );
doc.schema.allow( { name: '$text', inside: 'other' } );
doc.schema.allow( { name: 'other', inside: '$root' } );
doc.schema.limits.add( 'other' );
buildModelConverter().for( editor.editing.modelToView )
.fromElement( 'other' )
.toElement( 'p' );
expect( getModelData( document ) ).to.equal( '<paragraph>foo[]</paragraph>' );
} );
setModelData( doc, '<other>[]</other>' );
it( 'should allow to provide batch instance', () => {
const batch = document.batch();
const file = createNativeFileMock();
const spy = sinon.spy( batch, 'insert' );
command.execute( { file } );
setModelData( document, '<paragraph>foo[]</paragraph>' );
command._doExecute( { batch, file } );
const id = fileRepository.getLoader( file ).id;
expect( getModelData( document ) ).to.equal( `[<image uploadId="${ id }"></image>]<paragraph>foo</paragraph>` );
sinon.assert.calledOnce( spy );
expect( getModelData( doc ) ).to.equal( '<other>[]</other>' );
} );
} );
} );

@@ -13,8 +13,13 @@ /**

import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer';
import FileRepository from '../src/filerepository';
import { AdapterMock, createNativeFileMock, NativeFileReaderMock } from './_utils/mocks';
import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
import { eventNameToConsumableType } from '@ckeditor/ckeditor5-engine/src/conversion/model-to-view-converters';
import Range from '@ckeditor/ckeditor5-engine/src/model/range';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';

@@ -24,8 +29,9 @@ import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';

describe( 'ImageUploadEngine', () => {
// eslint-disable-next-line max-len
const base64Sample = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
let editor, document, fileRepository, viewDocument, nativeReaderMock, loader, adapterMock;
let editor, doc, fileRepository, viewDocument, nativeReaderMock, loader, adapterMock;
testUtils.createSinonSandbox();
beforeEach( () => {
testUtils.sinon.stub( window, 'FileReader', () => {
testUtils.sinon.stub( window, 'FileReader' ).callsFake( () => {
nativeReaderMock = new NativeFileReaderMock();

@@ -36,22 +42,23 @@

return ClassicTestEditor.create( {
plugins: [ ImageEngine, ImageUploadEngine, Paragraph ]
} )
.then( newEditor => {
editor = newEditor;
document = editor.document;
viewDocument = editor.editing.view;
return ClassicTestEditor
.create( {
plugins: [ ImageEngine, ImageUploadEngine, Paragraph, UndoEngine ]
} )
.then( newEditor => {
editor = newEditor;
doc = editor.document;
viewDocument = editor.editing.view;
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = newLoader => {
loader = newLoader;
adapterMock = new AdapterMock( loader );
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = newLoader => {
loader = newLoader;
adapterMock = new AdapterMock( loader );
return adapterMock;
};
} );
return adapterMock;
};
} );
} );
it( 'should register proper schema rules', () => {
expect( document.schema.check( { name: 'image', attributes: [ 'uploadId' ], inside: '$root' } ) ).to.be.true;
expect( doc.schema.check( { name: 'image', attributes: [ 'uploadId' ], inside: '$root' } ) ).to.be.true;
} );

@@ -67,6 +74,9 @@

const dataTransfer = new DataTransfer( { files: [ fileMock ] } );
setModelData( document, '<paragraph>foo bar baz[]</paragraph>' );
setModelData( doc, '<paragraph>[]foo</paragraph>' );
viewDocument.fire( 'clipboardInput', { dataTransfer } );
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
sinon.assert.calledOnce( spy );

@@ -76,7 +86,52 @@ sinon.assert.calledWith( spy, 'imageUpload' );

const id = fileRepository.getLoader( fileMock ).id;
expect( getModelData( document ) ).to.equal(
`[<image uploadId="${ id }" uploadStatus="reading"></image>]<paragraph>foo bar baz</paragraph>`
expect( getModelData( doc ) ).to.equal(
`<paragraph>foo</paragraph>[<image uploadId="${ id }" uploadStatus="reading"></image>]`
);
} );
it( 'should execute imageUpload command with an optimized position when image is pasted', () => {
const spy = sinon.spy( editor, 'execute' );
const fileMock = createNativeFileMock();
const dataTransfer = new DataTransfer( { files: [ fileMock ] } );
setModelData( doc, '<paragraph>[]foo</paragraph>' );
const paragraph = doc.getRoot().getChild( 0 );
const targetRange = Range.createFromParentsAndOffsets( paragraph, 1, paragraph, 1 ); // f[]oo
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
sinon.assert.calledOnce( spy );
sinon.assert.calledWith( spy, 'imageUpload' );
const id = fileRepository.getLoader( fileMock ).id;
expect( getModelData( doc ) ).to.equal(
`[<image uploadId="${ id }" uploadStatus="reading"></image>]<paragraph>foo</paragraph>`
);
} );
it( 'should execute imageUpload command when multiple files image are pasted', () => {
const spy = sinon.spy( editor, 'execute' );
const files = [ createNativeFileMock(), createNativeFileMock() ];
const dataTransfer = new DataTransfer( { files } );
setModelData( doc, '<paragraph>[]foo</paragraph>' );
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
sinon.assert.calledTwice( spy );
sinon.assert.calledWith( spy, 'imageUpload' );
const id1 = fileRepository.getLoader( files[ 0 ] ).id;
const id2 = fileRepository.getLoader( files[ 1 ] ).id;
expect( getModelData( doc ) ).to.equal(
'<paragraph>foo</paragraph>' +
`<image uploadId="${ id1 }" uploadStatus="reading"></image>` +
`[<image uploadId="${ id2 }" uploadStatus="reading"></image>]`
);
} );
it( 'should not execute imageUpload command when file is not an image', () => {

@@ -90,6 +145,10 @@ const spy = sinon.spy( editor, 'execute' );

const dataTransfer = new DataTransfer( { files: [ fileMock ] } );
setModelData( document, '<paragraph>foo bar baz[]</paragraph>' );
viewDocument.fire( 'clipboardInput', { dataTransfer } );
setModelData( doc, '<paragraph>foo[]</paragraph>' );
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
sinon.assert.notCalled( spy );

@@ -103,16 +162,16 @@ } );

setModelData( document, '<image uploadId="1234"></image>' );
setModelData( doc, '<image uploadId="1234"></image>' );
expect( getViewData( viewDocument ) ).to.equal(
'[]<figure class="image ck-widget" contenteditable="false">' +
'[<figure class="image ck-widget" contenteditable="false">' +
'<img></img>' +
'</figure>' );
'</figure>]' );
} );
it( 'should use read data once it is present', ( done ) => {
it( 'should use read data once it is present', done => {
const file = createNativeFileMock();
setModelData( document, '<paragraph>{}foo bar</paragraph>' );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );
document.once( 'changesDone', () => {
doc.once( 'changesDone', () => {
expect( getViewData( viewDocument ) ).to.equal(

@@ -132,9 +191,9 @@ '[<figure class="image ck-widget" contenteditable="false">' +

it( 'should replace read data with server response once it is present', ( done ) => {
it( 'should replace read data with server response once it is present', done => {
const file = createNativeFileMock();
setModelData( document, '<paragraph>{}foo bar</paragraph>' );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );
document.once( 'changesDone', () => {
document.once( 'changesDone', () => {
doc.once( 'changesDone', () => {
doc.once( 'changesDone', () => {
expect( getViewData( viewDocument ) ).to.equal(

@@ -148,3 +207,3 @@ '[<figure class="image ck-widget" contenteditable="false"><img src="image.png"></img></figure>]<p>foo bar</p>'

adapterMock.mockSuccess( { original: 'image.png' } );
adapterMock.mockSuccess( { default: 'image.png' } );
} );

@@ -155,3 +214,3 @@

it( 'should fire notification event in case of error', ( done ) => {
it( 'should fire notification event in case of error', done => {
const notification = editor.plugins.get( Notification );

@@ -162,2 +221,3 @@ const file = createNativeFileMock();

expect( data.message ).to.equal( 'Reading error.' );
expect( data.title ).to.equal( 'Upload failed' );
evt.stop();

@@ -168,3 +228,3 @@

setModelData( document, '<paragraph>{}foo bar</paragraph>' );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );

@@ -175,3 +235,3 @@

it( 'should not fire notification on abort', ( done ) => {
it( 'should not fire notification on abort', done => {
const notification = editor.plugins.get( Notification );

@@ -186,3 +246,3 @@ const file = createNativeFileMock();

setModelData( document, '<paragraph>{}foo bar</paragraph>' );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );

@@ -198,8 +258,130 @@ nativeReaderMock.abort();

it( 'should do nothing if image does not have uploadId', () => {
setModelData( document, '<image src="image.png"></image>' );
setModelData( doc, '<image src="image.png"></image>' );
expect( getViewData( viewDocument ) ).to.equal(
'[]<figure class="image ck-widget" contenteditable="false"><img src="image.png"></img></figure>'
'[<figure class="image ck-widget" contenteditable="false"><img src="image.png"></img></figure>]'
);
} );
it( 'should remove image in case of upload error', done => {
const file = createNativeFileMock();
const spy = testUtils.sinon.spy();
const notification = editor.plugins.get( Notification );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
notification.on( 'show:warning', evt => {
spy();
evt.stop();
}, { priority: 'high' } );
editor.execute( 'imageUpload', { file } );
doc.once( 'changesDone', () => {
doc.once( 'changesDone', () => {
expect( getModelData( doc ) ).to.equal( '<paragraph>[]foo bar</paragraph>' );
sinon.assert.calledOnce( spy );
done();
} );
} );
nativeReaderMock.mockError( 'Upload error.' );
} );
it( 'should abort upload if image is removed', () => {
const file = createNativeFileMock();
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );
const abortSpy = testUtils.sinon.spy( loader, 'abort' );
expect( loader.status ).to.equal( 'reading' );
nativeReaderMock.mockSuccess( base64Sample );
const image = doc.getRoot().getChild( 0 );
doc.enqueueChanges( () => {
const batch = doc.batch();
batch.remove( image );
} );
expect( loader.status ).to.equal( 'aborted' );
sinon.assert.calledOnce( abortSpy );
} );
it( 'image should be permanently removed if it is removed by user during upload', done => {
const file = createNativeFileMock();
const notification = editor.plugins.get( Notification );
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
// Prevent popping up alert window.
notification.on( 'show:warning', evt => {
evt.stop();
}, { priority: 'high' } );
editor.execute( 'imageUpload', { file } );
doc.once( 'changesDone', () => {
// This is called after "manual" remove.
doc.once( 'changesDone', () => {
// This is called after attributes are removed.
let undone = false;
doc.once( 'changesDone', () => {
if ( !undone ) {
undone = true;
// This is called after abort remove.
expect( getModelData( doc ) ).to.equal( '<paragraph>[]foo bar</paragraph>' );
editor.execute( 'undo' );
// Expect that the image has not been brought back.
expect( getModelData( doc ) ).to.equal( '<paragraph>[]foo bar</paragraph>' );
done();
}
} );
} );
} );
const image = doc.getRoot().getChild( 0 );
doc.enqueueChanges( () => {
const batch = doc.batch();
batch.remove( image );
} );
} );
it( 'should create responsive image if server return multiple images', done => {
const file = createNativeFileMock();
setModelData( doc, '<paragraph>{}foo bar</paragraph>' );
editor.execute( 'imageUpload', { file } );
doc.once( 'changesDone', () => {
doc.once( 'changesDone', () => {
expect( getViewData( viewDocument ) ).to.equal(
'[<figure class="image ck-widget" contenteditable="false">' +
'<img sizes="100vw" src="image.png" srcset="image-500.png 500w, image-800.png 800w"></img>' +
'</figure>]<p>foo bar</p>'
);
expect( loader.status ).to.equal( 'idle' );
done();
} );
adapterMock.mockSuccess( { default: 'image.png', 500: 'image-500.png', 800: 'image-800.png' } );
} );
nativeReaderMock.mockSuccess( base64Sample );
} );
it( 'should prevent from browser redirecting when an image is dropped on another image', () => {
const spy = testUtils.sinon.spy();
editor.editing.view.fire( 'dragover', {
preventDefault: spy
} );
expect( spy.calledOnce ).to.equal( true );
} );
} );

@@ -22,2 +22,3 @@ /**

describe( 'ImageUploadProgress', () => {
// eslint-disable-next-line max-len
const base64Sample = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';

@@ -28,3 +29,3 @@ let editor, document, fileRepository, viewDocument, nativeReaderMock, loader, adapterMock;

beforeEach( () => {
testUtils.sinon.stub( window, 'FileReader', () => {
testUtils.sinon.stub( window, 'FileReader' ).callsFake( () => {
nativeReaderMock = new NativeFileReaderMock();

@@ -35,18 +36,19 @@

return ClassicTestEditor.create( {
plugins: [ ImageEngine, Paragraph, ImageUploadProgress ]
} )
.then( newEditor => {
editor = newEditor;
document = editor.document;
viewDocument = editor.editing.view;
return ClassicTestEditor
.create( {
plugins: [ ImageEngine, Paragraph, ImageUploadProgress ]
} )
.then( newEditor => {
editor = newEditor;
document = editor.document;
viewDocument = editor.editing.view;
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = newLoader => {
loader = newLoader;
adapterMock = new AdapterMock( loader );
fileRepository = editor.plugins.get( FileRepository );
fileRepository.createAdapter = newLoader => {
loader = newLoader;
adapterMock = new AdapterMock( loader );
return adapterMock;
};
} );
return adapterMock;
};
} );
} );

@@ -59,3 +61,3 @@

it( 'should convert image\'s "reading" uploadStatus attribute', () => {
setModelData( document, '<paragraph>foo[]</paragraph>' );
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -70,4 +72,4 @@

it( 'should convert image\'s "uploading" uploadStatus attribute', ( done ) => {
setModelData( document, '<paragraph>foo[]</paragraph>' );
it( 'should convert image\'s "uploading" uploadStatus attribute', done => {
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -89,4 +91,4 @@

it( 'should update progressbar width on progress', ( done ) => {
setModelData( document, '<paragraph>foo[]</paragraph>' );
it( 'should update progressbar width on progress', done => {
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -110,4 +112,4 @@

it( 'should convert image\'s "complete" uploadStatus attribute', ( done ) => {
setModelData( document, '<paragraph>foo[]</paragraph>' );
it( 'should convert image\'s "complete" uploadStatus attribute', done => {
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -126,3 +128,3 @@

adapterMock.mockSuccess( { original: 'image.png' } );
adapterMock.mockSuccess( { default: 'image.png' } );
} );

@@ -137,3 +139,3 @@

setModelData( document, '<paragraph>foo[]</paragraph>' );
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -153,3 +155,3 @@

setModelData( document, '<paragraph>foo[]</paragraph>' );
setModelData( document, '<paragraph>[]foo</paragraph>' );
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

@@ -162,9 +164,27 @@

it( 'should not process attribute change if there is wrong uploadId', () => {
setModelData( document, '<image uploadId="123" uploadStatus="reading"></image><paragraph>foo{}</paragraph>' );
it( 'should not show progress bar if there is no loader with given uploadId', () => {
setModelData( document, '<image uploadId="123" uploadStatus="reading"></image>' );
const image = document.getRoot().getChild( 0 );
document.enqueueChanges( () => {
document.batch().setAttribute( image, 'uploadStatus', 'uploading' );
} );
expect( getViewData( viewDocument ) ).to.equal(
'<figure class="image ck-widget" contenteditable="false"><img></img></figure><p>foo{}</p>'
'[<figure class="image ck-widget ck-appear ck-infinite-progress" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'</figure>]'
);
document.enqueueChanges( () => {
document.batch().setAttribute( image, 'uploadStatus', 'complete' );
} );
expect( getViewData( viewDocument ) ).to.equal(
'[<figure class="image ck-widget" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'</figure>]'
);
} );
} );

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';

@@ -28,24 +28,25 @@ import Typing from '@ckeditor/ckeditor5-typing/src/typing';

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [
Enter, Typing, Paragraph, Heading, Undo, Bold, Italic, Heading, List, Image, ImageToolbar, Clipboard,
ImageCaption, ImageStyle, ImageUpload
],
toolbar: [ 'headings', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'insertImage' ],
image: {
toolbar: [ 'imageStyleFull', 'imageStyleSide', '|' , 'imageTextAlternative' ]
}
} )
.then( editor => {
// Register fake adapter.
editor.plugins.get( 'upload/filerepository' ).createAdapter = loader => {
const adapterMock = new AdapterMock( loader );
createProgressButton( loader, adapterMock );
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [
Enter, Typing, Paragraph, Heading, Undo, Bold, Italic, Heading, List, Image, ImageToolbar, Clipboard,
ImageCaption, ImageStyle, ImageUpload
],
toolbar: [ 'headings', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'insertImage' ],
image: {
toolbar: [ 'imageStyleFull', 'imageStyleSide', '|', 'imageTextAlternative' ]
}
} )
.then( editor => {
// Register fake adapter.
editor.plugins.get( 'FileRepository' ).createAdapter = loader => {
const adapterMock = new AdapterMock( loader );
createProgressButton( loader, adapterMock );
return adapterMock;
};
} )
.catch( err => {
console.error( err.stack );
} );
return adapterMock;
};
} )
.catch( err => {
console.error( err.stack );
} );

@@ -57,6 +58,12 @@ function createProgressButton( loader, adapterMock ) {

progressInfo.innerHTML = `File: ${ fileName }. Progress: 0%.`;
const button = document.createElement( 'button' );
button.innerHTML = 'Upload progress';
const progressButton = document.createElement( 'button' );
const errorButton = document.createElement( 'button' );
const abortButton = document.createElement( 'button' );
progressButton.innerHTML = 'Upload progress';
errorButton.innerHTML = 'Simulate error';
abortButton.innerHTML = 'Simulate aborting';
container.appendChild( button );
container.appendChild( progressButton );
container.appendChild( errorButton );
container.appendChild( abortButton );
container.appendChild( progressInfo );

@@ -68,3 +75,3 @@

const total = 500;
button.addEventListener( 'click', () => {
progressButton.addEventListener( 'click', () => {
progress += 100;

@@ -74,4 +81,4 @@ adapterMock.mockProgress( progress, total );

if ( progress == total ) {
button.setAttribute( 'disabled', 'true' );
adapterMock.mockSuccess( { original: './sample.jpg' } );
disableButtons();
adapterMock.mockSuccess( { default: './sample.jpg' } );
}

@@ -81,3 +88,19 @@

} );
errorButton.addEventListener( 'click', () => {
adapterMock.mockError( 'Upload error!' );
disableButtons();
} );
abortButton.addEventListener( 'click', () => {
loader.abort();
disableButtons();
} );
function disableButtons() {
progressButton.setAttribute( 'disabled', 'true' );
errorButton.setAttribute( 'disabled', 'true' );
abortButton.setAttribute( 'disabled', 'true' );
}
}

@@ -8,4 +8,9 @@ ## Image upload

On the occasionn – when you drop an image on another image in the editor,
your browser [**should not** redirect to the image](https://github.com/ckeditor/ckeditor5-upload/issues/32).
Repeat all the steps with:
* dropping multiple images,
* using toolbar button to add one and multiple images.
* using toolbar button to add one and multiple images,
* using `Simulate error` button to stop upload, show error and remove image,
* using `Simulate aborting` button to stop upload and remove image.

@@ -8,3 +8,3 @@ /**

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import FileDialogButtonView from '../../src/ui/filedialogbuttonview';

@@ -19,8 +19,9 @@

return ClassicEditor.create( editorElement )
.then( newEditor => {
editor = newEditor;
return ClassicEditor
.create( editorElement )
.then( newEditor => {
editor = newEditor;
view = new FileDialogButtonView( editor.locale );
} );
view = new FileDialogButtonView( editor.locale );
} );
} );

@@ -33,5 +34,5 @@

it( 'should remove input view from body after destroy', () => {
return view.destroy().then( () => {
expect( view.fileInputView.element.parentNode ).to.be.null;
} );
view.destroy();
expect( view.fileInputView.element.parentNode ).to.be.null;
} );

@@ -58,3 +59,3 @@

it( 'should delegate input view done event', ( done ) => {
it( 'should delegate input view done event', done => {
const files = [];

@@ -61,0 +62,0 @@

@@ -6,6 +6,8 @@ /**

import { isImageType } from '../src/utils';
import { isImageType, findOptimalInsertionPosition } from '../src/utils';
import Document from '@ckeditor/ckeditor5-engine/src/model/document';
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
describe( 'utils', () => {
describe( 'isImageType', () => {
describe( 'upload utils', () => {
describe( 'isImageType()', () => {
it( 'should return true for png mime type', () => {

@@ -31,3 +33,81 @@ expect( isImageType( { type: 'image/png' } ) ).to.be.true;

} );
} );
} );
describe( 'findOptimalInsertionPosition()', () => {
let doc;
beforeEach( () => {
doc = new Document();
doc.createRoot();
doc.schema.registerItem( 'paragraph', '$block' );
doc.schema.registerItem( 'image' );
doc.schema.registerItem( 'span' );
doc.schema.allow( { name: 'image', inside: '$root' } );
doc.schema.objects.add( 'image' );
doc.schema.allow( { name: 'span', inside: 'paragraph' } );
doc.schema.allow( { name: '$text', inside: 'span' } );
} );
it( 'returns position after selected element', () => {
setData( doc, '<paragraph>x</paragraph>[<image></image>]<paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 2 ] );
} );
it( 'returns position inside empty block', () => {
setData( doc, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 1, 0 ] );
} );
it( 'returns position before block if at the beginning of that block', () => {
setData( doc, '<paragraph>x</paragraph><paragraph>[]foo</paragraph><paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 1 ] );
} );
it( 'returns position before block if in the middle of that block', () => {
setData( doc, '<paragraph>x</paragraph><paragraph>f[]oo</paragraph><paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 1 ] );
} );
it( 'returns position after block if at the end of that block', () => {
setData( doc, '<paragraph>x</paragraph><paragraph>foo[]</paragraph><paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 2 ] );
} );
// Checking if isTouching() was used.
it( 'returns position after block if at the end of that block (deeply nested)', () => {
setData( doc, '<paragraph>x</paragraph><paragraph>foo<span>bar[]</span></paragraph><paragraph>y</paragraph>' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 2 ] );
} );
it( 'returns selection focus if not in a block', () => {
doc.schema.allow( { name: '$text', inside: '$root' } );
setData( doc, 'foo[]bar' );
const pos = findOptimalInsertionPosition( doc.selection );
expect( pos.path ).to.deep.equal( [ 3 ] );
} );
} );
} );

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc