Socket
Socket
Sign inDemoInstall

@ckeditor/ckeditor5-upload

Package Overview
Dependencies
Maintainers
1
Versions
617
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 35.2.1 to 35.3.0

22

package.json
{
"name": "@ckeditor/ckeditor5-upload",
"version": "35.2.1",
"version": "35.3.0",
"description": "Upload feature for CKEditor 5.",

@@ -14,6 +14,11 @@ "keywords": [

"dependencies": {
"@ckeditor/ckeditor5-core": "^35.2.1",
"@ckeditor/ckeditor5-utils": "^35.2.1",
"@ckeditor/ckeditor5-ui": "^35.2.1"
"@ckeditor/ckeditor5-core": "^35.3.0",
"@ckeditor/ckeditor5-utils": "^35.3.0",
"@ckeditor/ckeditor5-ui": "^35.3.0"
},
"devDependencies": {
"typescript": "^4.8.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
},
"engines": {

@@ -34,7 +39,12 @@ "node": ">=14.0.0",

"lang",
"src",
"src/**/*.js",
"src/**/*.d.ts",
"theme",
"ckeditor5-metadata.json",
"CHANGELOG.md"
]
],
"scripts": {
"build": "tsc -p ./tsconfig.release.json",
"postversion": "npm run build"
}
}

@@ -5,12 +5,8 @@ /**

*/
/**
* @module upload/adapters/base64uploadadapter
*/
/* globals window */
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import FileRepository from '../filerepository';
/**

@@ -29,24 +25,21 @@ * A plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64)

export default class Base64UploadAdapter extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ FileRepository ];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'Base64UploadAdapter';
}
/**
* @inheritDoc
*/
init() {
this.editor.plugins.get( FileRepository ).createUploadAdapter = loader => new Adapter( loader );
}
/**
* @inheritDoc
*/
static get requires() {
return [FileRepository];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'Base64UploadAdapter';
}
/**
* @inheritDoc
*/
init() {
this.editor.plugins.get(FileRepository).createUploadAdapter = loader => new Adapter(loader);
}
}
/**

@@ -59,53 +52,47 @@ * The upload adapter that converts images inserted into the editor into Base64 strings.

class Adapter {
/**
* Creates a new adapter instance.
*
* @param {module:upload/filerepository~FileLoader} loader
*/
constructor( loader ) {
/**
* `FileLoader` instance to use during the upload.
*
* @member {module:upload/filerepository~FileLoader} #loader
*/
this.loader = loader;
}
/**
* Starts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#upload
* @returns {Promise}
*/
upload() {
return new Promise( ( resolve, reject ) => {
const reader = this.reader = new window.FileReader();
reader.addEventListener( 'load', () => {
resolve( { default: reader.result } );
} );
reader.addEventListener( 'error', err => {
reject( err );
} );
reader.addEventListener( 'abort', () => {
reject();
} );
this.loader.file.then( file => {
reader.readAsDataURL( file );
} );
} );
}
/**
* Aborts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#abort
* @returns {Promise}
*/
abort() {
this.reader.abort();
}
/**
* Creates a new adapter instance.
*
* @param {module:upload/filerepository~FileLoader} loader
*/
constructor(loader) {
/**
* `FileLoader` instance to use during the upload.
*
* @member {module:upload/filerepository~FileLoader} #loader
*/
this.loader = loader;
}
/**
* Starts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#upload
* @returns {Promise}
*/
upload() {
return new Promise((resolve, reject) => {
const reader = this.reader = new window.FileReader();
reader.addEventListener('load', () => {
resolve({ default: reader.result });
});
reader.addEventListener('error', err => {
reject(err);
});
reader.addEventListener('abort', () => {
reject();
});
this.loader.file.then(file => {
reader.readAsDataURL(file);
});
});
}
/**
* Aborts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#abort
* @returns {Promise}
*/
abort() {
this.reader.abort();
}
}

@@ -5,13 +5,9 @@ /**

*/
/**
* @module upload/adapters/simpleuploadadapter
*/
/* globals XMLHttpRequest, FormData */
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import FileRepository from '../filerepository';
import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
/**

@@ -43,45 +39,38 @@ * The Simple upload adapter allows uploading images to an application running on your server using

export default class SimpleUploadAdapter extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ FileRepository ];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'SimpleUploadAdapter';
}
/**
* @inheritDoc
*/
init() {
const options = this.editor.config.get( 'simpleUpload' );
if ( !options ) {
return;
}
if ( !options.uploadUrl ) {
/**
* The {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `config.simpleUpload.uploadUrl`}
* configuration required by the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter `SimpleUploadAdapter`}
* is missing. Make sure the correct URL is specified for the image upload to work properly.
*
* @error simple-upload-adapter-missing-uploadurl
*/
logWarning( 'simple-upload-adapter-missing-uploadurl' );
return;
}
this.editor.plugins.get( FileRepository ).createUploadAdapter = loader => {
return new Adapter( loader, options );
};
}
/**
* @inheritDoc
*/
static get requires() {
return [FileRepository];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'SimpleUploadAdapter';
}
/**
* @inheritDoc
*/
init() {
const options = this.editor.config.get('simpleUpload');
if (!options) {
return;
}
if (!options.uploadUrl) {
/**
* The {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `config.simpleUpload.uploadUrl`}
* configuration required by the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter `SimpleUploadAdapter`}
* is missing. Make sure the correct URL is specified for the image upload to work properly.
*
* @error simple-upload-adapter-missing-uploadurl
*/
logWarning('simple-upload-adapter-missing-uploadurl');
return;
}
this.editor.plugins.get(FileRepository).createUploadAdapter = loader => {
return new Adapter(loader, options);
};
}
}
/**

@@ -94,226 +83,118 @@ * Upload adapter.

class Adapter {
/**
* Creates a new adapter instance.
*
* @param {module:upload/filerepository~FileLoader} loader
* @param {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} options
*/
constructor( loader, options ) {
/**
* FileLoader instance to use during the upload.
*
* @member {module:upload/filerepository~FileLoader} #loader
*/
this.loader = loader;
/**
* The configuration of the adapter.
*
* @member {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} #options
*/
this.options = options;
}
/**
* Starts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#upload
* @returns {Promise}
*/
upload() {
return this.loader.file
.then( file => new Promise( ( resolve, reject ) => {
this._initRequest();
this._initListeners( resolve, reject, file );
this._sendRequest( file );
} ) );
}
/**
* Aborts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#abort
* @returns {Promise}
*/
abort() {
if ( this.xhr ) {
this.xhr.abort();
}
}
/**
* Initializes the `XMLHttpRequest` object using the URL specified as
* {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `simpleUpload.uploadUrl`} in the editor's
* configuration.
*
* @private
*/
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open( 'POST', this.options.uploadUrl, true );
xhr.responseType = 'json';
}
/**
* Initializes XMLHttpRequest listeners
*
* @private
* @param {Function} resolve Callback function to be called when the request is successful.
* @param {Function} reject Callback function to be called when the request cannot be completed.
* @param {File} file Native File object.
*/
_initListeners( resolve, reject, file ) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${ file.name }.`;
xhr.addEventListener( 'error', () => reject( genericErrorText ) );
xhr.addEventListener( 'abort', () => reject() );
xhr.addEventListener( 'load', () => {
const response = xhr.response;
if ( !response || response.error ) {
return reject( response && response.error && response.error.message ? response.error.message : genericErrorText );
}
const urls = response.url ? { default: response.url } : response.urls;
// Resolve with the normalized `urls` property and pass the rest of the response
// to allow customizing the behavior of features relying on the upload adapters.
resolve( {
...response,
urls
} );
} );
// Upload progress when it is supported.
/* istanbul ignore else */
if ( xhr.upload ) {
xhr.upload.addEventListener( 'progress', evt => {
if ( evt.lengthComputable ) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
} );
}
}
/**
* Prepares the data and sends the request.
*
* @private
* @param {File} file File instance to be uploaded.
*/
_sendRequest( file ) {
// Set headers if specified.
const headers = this.options.headers || {};
// Use the withCredentials flag if specified.
const withCredentials = this.options.withCredentials || false;
for ( const headerName of Object.keys( headers ) ) {
this.xhr.setRequestHeader( headerName, headers[ headerName ] );
}
this.xhr.withCredentials = withCredentials;
// Prepare the form data.
const data = new FormData();
data.append( 'upload', file );
// Send the request.
this.xhr.send( data );
}
/**
* Creates a new adapter instance.
*
* @param {module:upload/filerepository~FileLoader} loader
* @param {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} options
*/
constructor(loader, options) {
/**
* FileLoader instance to use during the upload.
*
* @member {module:upload/filerepository~FileLoader} #loader
*/
this.loader = loader;
/**
* The configuration of the adapter.
*
* @member {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} #options
*/
this.options = options;
}
/**
* Starts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#upload
* @returns {Promise}
*/
upload() {
return this.loader.file
.then(file => new Promise((resolve, reject) => {
this._initRequest();
this._initListeners(resolve, reject, file);
this._sendRequest(file);
}));
}
/**
* Aborts the upload process.
*
* @see module:upload/filerepository~UploadAdapter#abort
* @returns {Promise}
*/
abort() {
if (this.xhr) {
this.xhr.abort();
}
}
/**
* Initializes the `XMLHttpRequest` object using the URL specified as
* {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `simpleUpload.uploadUrl`} in the editor's
* configuration.
*
* @private
*/
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open('POST', this.options.uploadUrl, true);
xhr.responseType = 'json';
}
/**
* Initializes XMLHttpRequest listeners
*
* @private
* @param {Function} resolve Callback function to be called when the request is successful.
* @param {Function} reject Callback function to be called when the request cannot be completed.
* @param {File} file Native File object.
*/
_initListeners(resolve, reject, file) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${file.name}.`;
xhr.addEventListener('error', () => reject(genericErrorText));
xhr.addEventListener('abort', () => reject());
xhr.addEventListener('load', () => {
const response = xhr.response;
if (!response || response.error) {
return reject(response && response.error && response.error.message ? response.error.message : genericErrorText);
}
const urls = response.url ? { default: response.url } : response.urls;
// Resolve with the normalized `urls` property and pass the rest of the response
// to allow customizing the behavior of features relying on the upload adapters.
resolve({
...response,
urls
});
});
// Upload progress when it is supported.
/* istanbul ignore else */
if (xhr.upload) {
xhr.upload.addEventListener('progress', evt => {
if (evt.lengthComputable) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
});
}
}
/**
* Prepares the data and sends the request.
*
* @private
* @param {File} file File instance to be uploaded.
*/
_sendRequest(file) {
// Set headers if specified.
const headers = this.options.headers || {};
// Use the withCredentials flag if specified.
const withCredentials = this.options.withCredentials || false;
for (const headerName of Object.keys(headers)) {
this.xhr.setRequestHeader(headerName, headers[headerName]);
}
this.xhr.withCredentials = withCredentials;
// Prepare the form data.
const data = new FormData();
data.append('upload', file);
// Send the request.
this.xhr.send(data);
}
}
/**
* The configuration of the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter simple upload adapter}.
*
* ClassicEditor
* .create( editorElement, {
* simpleUpload: {
* // The URL the images are uploaded to.
* uploadUrl: 'http://example.com',
*
* // Headers sent along with the XMLHttpRequest to the upload server.
* headers: {
* ...
* }
* }
* } );
* .then( ... )
* .catch( ... );
*
* See the {@glink features/images/image-upload/simple-upload-adapter "Simple upload adapter"} guide to learn more.
*
* See {@link module:core/editor/editorconfig~EditorConfig all editor configuration options}.
*
* @interface SimpleUploadConfig
*/
/**
* The configuration of the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter simple upload adapter}.
*
* Read more in {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig}.
*
* @member {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} module:core/editor/editorconfig~EditorConfig#simpleUpload
*/
/**
* The path (URL) to the server (application) which handles the file upload. When specified, enables the automatic
* upload of resources (images) inserted into the editor content.
*
* Learn more about the server application requirements in the
* {@glink features/images/image-upload/simple-upload-adapter#server-side-configuration "Server-side configuration"} section
* of the feature guide.
*
* @member {String} module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl
*/
/**
* An object that defines additional [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) sent with
* the request to the server during the upload. This is the right place to implement security mechanisms like
* authentication and [CSRF](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) protection.
*
* ClassicEditor
* .create( editorElement, {
* simpleUpload: {
* headers: {
* 'X-CSRF-TOKEN': 'CSRF-Token',
* Authorization: 'Bearer <JSON Web Token>'
* }
* }
* } );
* .then( ... )
* .catch( ... );
*
* Learn more about the server application requirements in the
* {@glink features/images/image-upload/simple-upload-adapter#server-side-configuration "Server-side configuration"} section
* of the feature guide.
*
* @member {Object.<String, String>} module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#headers
*/
/**
* This flag enables the
* [`withCredentials`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
* property of the request sent to the server during the upload. It affects cross-site requests only and, for instance,
* allows credentials such as cookies to be sent along with the request.
*
* ClassicEditor
* .create( editorElement, {
* simpleUpload: {
* withCredentials: true
* }
* } );
* .then( ... )
* .catch( ... );
*
* Learn more about the server application requirements in the
* {@glink features/images/image-upload/simple-upload-adapter#server-side-configuration "Server-side configuration"} section
* of the feature guide.
*
* @member {Boolean} [module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#withCredentials=false]
*/

@@ -5,105 +5,85 @@ /**

*/
/**
* @module upload/filereader
*/
/* globals window */
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { Observable } from '@ckeditor/ckeditor5-utils/src/observablemixin';
/**
* Wrapper over the native `FileReader`.
*/
export default class FileReader {
/**
* Creates an instance of the FileReader.
*/
constructor() {
const reader = new window.FileReader();
/**
* Instance of native FileReader.
*
* @private
* @member {FileReader} #_reader
*/
this._reader = reader;
this._data = undefined;
/**
* Number of bytes loaded.
*
* @readonly
* @observable
* @member {Number} #loaded
*/
this.set( 'loaded', 0 );
reader.onprogress = evt => {
this.loaded = evt.loaded;
};
}
/**
* Returns error that occurred during file reading.
*
* @returns {Error}
*/
get error() {
return this._reader.error;
}
/**
* Holds the data of an already loaded file. The file must be first loaded
* by using {@link module:upload/filereader~FileReader#read `read()`}.
*
* @type {File|undefined}
*/
get data() {
return this._data;
}
/**
* Reads the provided file.
*
* @param {File} file Native File object.
* @returns {Promise.<String>} Returns a promise that will be resolved with file's content.
* The promise will be rejected in case of an error or when the reading process is aborted.
*/
read( file ) {
const reader = this._reader;
this.total = file.size;
return new Promise( ( resolve, reject ) => {
reader.onload = () => {
const result = reader.result;
this._data = result;
resolve( result );
};
reader.onerror = () => {
reject( 'error' );
};
reader.onabort = () => {
reject( 'aborted' );
};
this._reader.readAsDataURL( file );
} );
}
/**
* Aborts file reader.
*/
abort() {
this._reader.abort();
}
export default class FileReader extends Observable {
/**
* Creates an instance of the FileReader.
*/
constructor() {
super();
const reader = new window.FileReader();
/**
* Instance of native FileReader.
*
* @private
* @member {FileReader} #_reader
*/
this._reader = reader;
this._data = undefined;
/**
* Number of bytes loaded.
*
* @readonly
* @observable
* @member {Number} #loaded
*/
this.set('loaded', 0);
reader.onprogress = evt => {
this.loaded = evt.loaded;
};
}
/**
* Returns error that occurred during file reading.
*
* @returns {Error}
*/
get error() {
return this._reader.error;
}
/**
* Holds the data of an already loaded file. The file must be first loaded
* by using {@link module:upload/filereader~FileReader#read `read()`}.
*
* @type {File|undefined}
*/
get data() {
return this._data;
}
/**
* Reads the provided file.
*
* @param {File} file Native File object.
* @returns {Promise.<String>} Returns a promise that will be resolved with file's content.
* The promise will be rejected in case of an error or when the reading process is aborted.
*/
read(file) {
const reader = this._reader;
this.total = file.size;
return new Promise((resolve, reject) => {
reader.onload = () => {
const result = reader.result;
this._data = result;
resolve(result);
};
reader.onerror = () => {
reject('error');
};
reader.onabort = () => {
reject('aborted');
};
this._reader.readAsDataURL(file);
});
}
/**
* Aborts file reader.
*/
abort() {
this._reader.abort();
}
}
mix( FileReader, ObservableMixin );

@@ -5,19 +5,12 @@ /**

*/
/**
* @module upload/filerepository
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import { Observable } from '@ckeditor/ckeditor5-utils/src/observablemixin';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import FileReader from './filereader.js';
import FileReader from './filereader';
import uid from '@ckeditor/ckeditor5-utils/src/uid';
/**

@@ -38,231 +31,199 @@ * File repository plugin. A central point for managing file upload.

export default class FileRepository extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'FileRepository';
}
/**
* @inheritDoc
*/
static get requires() {
return [ PendingActions ];
}
/**
* @inheritDoc
*/
init() {
/**
* Collection of loaders associated with this repository.
*
* @member {module:utils/collection~Collection} #loaders
*/
this.loaders = new Collection();
// Keeps upload in a sync with pending actions.
this.loaders.on( 'add', () => this._updatePendingAction() );
this.loaders.on( 'remove', () => this._updatePendingAction() );
/**
* Loaders mappings used to retrieve loaders references.
*
* @private
* @member {Map<File|Promise, FileLoader>} #_loadersMap
*/
this._loadersMap = new Map();
/**
* Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
* while upload is in progress. When there is no upload then value is `null`.
*
* @private
* @member {Object} #_pendingAction
*/
this._pendingAction = null;
/**
* A factory function which should be defined before using `FileRepository`.
*
* It should return a new instance of {@link module:upload/filerepository~UploadAdapter} that will be used to upload files.
* {@link module:upload/filerepository~FileLoader} instance associated with the adapter
* will be passed to that function.
*
* For more information and example see {@link module:upload/filerepository~UploadAdapter}.
*
* @member {Function} #createUploadAdapter
*/
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set( 'uploaded', 0 );
/**
* Number of total bytes to upload.
*
* It might be different than the file size because of headers and additional data.
* It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
* the progress.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set( 'uploadTotal', null );
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
return total ? ( uploaded / total * 100 ) : 0;
} );
}
/**
* Returns the loader associated with specified file or promise.
*
* To get loader by id use `fileRepository.loaders.get( id )`.
*
* @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
* @returns {module:upload/filerepository~FileLoader|null}
*/
getLoader( fileOrPromise ) {
return this._loadersMap.get( fileOrPromise ) || null;
}
/**
* Creates a loader instance for the given file.
*
* Requires {@link #createUploadAdapter} factory to be defined.
*
* @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
* @returns {module:upload/filerepository~FileLoader|null}
*/
createLoader( fileOrPromise ) {
if ( !this.createUploadAdapter ) {
/**
* You need to enable an upload adapter in order to be able to upload files.
*
* This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
* without {@link #createUploadAdapter defining an upload adapter}.
*
* **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
* CKEditor 5 Builds}**
* it means that you did not configure any of the upload adapters available by default in those builds.
*
* See the {@glink features/images/image-upload/image-upload comprehensive "Image upload overview"} to learn which upload
* adapters are available in the builds and how to configure them.
*
* **If you see this warning when using a custom build** there is a chance that you enabled
* a feature like {@link module:image/imageupload~ImageUpload},
* or {@link module:image/imageupload/imageuploadui~ImageUploadUI} but you did not enable any upload adapter.
* You can choose one of the existing upload adapters listed in the
* {@glink features/images/image-upload/image-upload "Image upload overview"}.
*
* You can also implement your {@glink framework/guides/deep-dive/upload-adapter own image upload adapter}.
*
* @error filerepository-no-upload-adapter
*/
logWarning( 'filerepository-no-upload-adapter' );
return null;
}
const loader = new FileLoader( Promise.resolve( fileOrPromise ), this.createUploadAdapter );
this.loaders.add( loader );
this._loadersMap.set( fileOrPromise, loader );
// Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
if ( fileOrPromise instanceof Promise ) {
loader.file
.then( file => {
this._loadersMap.set( file, loader );
} )
// Every then() must have a catch().
// File loader state (and rejections) are handled in read() and upload().
// Also, see the "does not swallow the file promise rejection" test.
.catch( () => {} );
}
loader.on( 'change:uploaded', () => {
let aggregatedUploaded = 0;
for ( const loader of this.loaders ) {
aggregatedUploaded += loader.uploaded;
}
this.uploaded = aggregatedUploaded;
} );
loader.on( 'change:uploadTotal', () => {
let aggregatedTotal = 0;
for ( const loader of this.loaders ) {
if ( loader.uploadTotal ) {
aggregatedTotal += loader.uploadTotal;
}
}
this.uploadTotal = aggregatedTotal;
} );
return loader;
}
/**
* Destroys the given loader.
*
* @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
* with that loader or loader itself.
*/
destroyLoader( fileOrPromiseOrLoader ) {
const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader( fileOrPromiseOrLoader );
loader._destroy();
this.loaders.remove( loader );
this._loadersMap.forEach( ( value, key ) => {
if ( value === loader ) {
this._loadersMap.delete( key );
}
} );
}
/**
* Registers or deregisters pending action bound with upload progress.
*
* @private
*/
_updatePendingAction() {
const pendingActions = this.editor.plugins.get( PendingActions );
if ( this.loaders.length ) {
if ( !this._pendingAction ) {
const t = this.editor.t;
const getMessage = value => `${ t( 'Upload in progress' ) } ${ parseInt( value ) }%.`;
this._pendingAction = pendingActions.add( getMessage( this.uploadedPercent ) );
this._pendingAction.bind( 'message' ).to( this, 'uploadedPercent', getMessage );
}
} else {
pendingActions.remove( this._pendingAction );
this._pendingAction = null;
}
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'FileRepository';
}
/**
* @inheritDoc
*/
static get requires() {
return [PendingActions];
}
/**
* @inheritDoc
*/
init() {
/**
* Collection of loaders associated with this repository.
*
* @member {module:utils/collection~Collection} #loaders
*/
this.loaders = new Collection();
// Keeps upload in a sync with pending actions.
this.loaders.on('change', () => this._updatePendingAction());
/**
* Loaders mappings used to retrieve loaders references.
*
* @private
* @member {Map<File|Promise, FileLoader>} #_loadersMap
*/
this._loadersMap = new Map();
/**
* Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
* while upload is in progress. When there is no upload then value is `null`.
*
* @private
* @member {Object} #_pendingAction
*/
this._pendingAction = null;
/**
* A factory function which should be defined before using `FileRepository`.
*
* It should return a new instance of {@link module:upload/filerepository~UploadAdapter} that will be used to upload files.
* {@link module:upload/filerepository~FileLoader} instance associated with the adapter
* will be passed to that function.
*
* For more information and example see {@link module:upload/filerepository~UploadAdapter}.
*
* @member {Function} #createUploadAdapter
*/
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set('uploaded', 0);
/**
* Number of total bytes to upload.
*
* It might be different than the file size because of headers and additional data.
* It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
* the progress.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set('uploadTotal', null);
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind('uploadedPercent').to(this, 'uploaded', this, 'uploadTotal', (uploaded, total) => {
return total ? (uploaded / total * 100) : 0;
});
}
/**
* Returns the loader associated with specified file or promise.
*
* To get loader by id use `fileRepository.loaders.get( id )`.
*
* @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
* @returns {module:upload/filerepository~FileLoader|null}
*/
getLoader(fileOrPromise) {
return this._loadersMap.get(fileOrPromise) || null;
}
/**
* Creates a loader instance for the given file.
*
* Requires {@link #createUploadAdapter} factory to be defined.
*
* @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
* @returns {module:upload/filerepository~FileLoader|null}
*/
createLoader(fileOrPromise) {
if (!this.createUploadAdapter) {
/**
* You need to enable an upload adapter in order to be able to upload files.
*
* This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
* without {@link #createUploadAdapter defining an upload adapter}.
*
* **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
* CKEditor 5 Builds}**
* it means that you did not configure any of the upload adapters available by default in those builds.
*
* See the {@glink features/images/image-upload/image-upload comprehensive "Image upload overview"} to learn which upload
* adapters are available in the builds and how to configure them.
*
* **If you see this warning when using a custom build** there is a chance that you enabled
* a feature like {@link module:image/imageupload~ImageUpload},
* or {@link module:image/imageupload/imageuploadui~ImageUploadUI} but you did not enable any upload adapter.
* You can choose one of the existing upload adapters listed in the
* {@glink features/images/image-upload/image-upload "Image upload overview"}.
*
* You can also implement your {@glink framework/guides/deep-dive/upload-adapter own image upload adapter}.
*
* @error filerepository-no-upload-adapter
*/
logWarning('filerepository-no-upload-adapter');
return null;
}
const loader = new FileLoader(Promise.resolve(fileOrPromise), this.createUploadAdapter);
this.loaders.add(loader);
this._loadersMap.set(fileOrPromise, loader);
// Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
if (fileOrPromise instanceof Promise) {
loader.file
.then(file => {
this._loadersMap.set(file, loader);
})
// Every then() must have a catch().
// File loader state (and rejections) are handled in read() and upload().
// Also, see the "does not swallow the file promise rejection" test.
.catch(() => { });
}
loader.on('change:uploaded', () => {
let aggregatedUploaded = 0;
for (const loader of this.loaders) {
aggregatedUploaded += loader.uploaded;
}
this.uploaded = aggregatedUploaded;
});
loader.on('change:uploadTotal', () => {
let aggregatedTotal = 0;
for (const loader of this.loaders) {
if (loader.uploadTotal) {
aggregatedTotal += loader.uploadTotal;
}
}
this.uploadTotal = aggregatedTotal;
});
return loader;
}
/**
* Destroys the given loader.
*
* @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
* with that loader or loader itself.
*/
destroyLoader(fileOrPromiseOrLoader) {
const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader(fileOrPromiseOrLoader);
loader._destroy();
this.loaders.remove(loader);
this._loadersMap.forEach((value, key) => {
if (value === loader) {
this._loadersMap.delete(key);
}
});
}
/**
* Registers or deregisters pending action bound with upload progress.
*
* @private
*/
_updatePendingAction() {
const pendingActions = this.editor.plugins.get(PendingActions);
if (this.loaders.length) {
if (!this._pendingAction) {
const t = this.editor.t;
const getMessage = (value) => `${t('Upload in progress')} ${parseInt(value)}%.`;
this._pendingAction = pendingActions.add(getMessage(this.uploadedPercent));
this._pendingAction.bind('message').to(this, 'uploadedPercent', getMessage);
}
}
else {
pendingActions.remove(this._pendingAction);
this._pendingAction = null;
}
}
}
mix( FileRepository, ObservableMixin );
/**

@@ -273,388 +234,284 @@ * File loader class.

*/
class FileLoader {
/**
* Creates a new instance of `FileLoader`.
*
* @param {Promise.<File>} filePromise A promise which resolves to a file instance.
* @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
*/
constructor( filePromise, uploadAdapterCreator ) {
/**
* Unique id of FileLoader instance.
*
* @readonly
* @member {Number}
*/
this.id = uid();
/**
* Additional wrapper over the initial file promise passed to this loader.
*
* @protected
* @member {module:upload/filerepository~FilePromiseWrapper}
*/
this._filePromiseWrapper = this._createFilePromiseWrapper( filePromise );
/**
* Adapter instance associated with this file loader.
*
* @private
* @member {module:upload/filerepository~UploadAdapter}
*/
this._adapter = uploadAdapterCreator( this );
/**
* FileReader used by FileLoader.
*
* @protected
* @member {module:upload/filereader~FileReader}
*/
this._reader = new FileReader();
/**
* Current status of FileLoader. It can be one of the following:
*
* * 'idle',
* * 'reading',
* * 'uploading',
* * 'aborted',
* * 'error'.
*
* When reading status can change in a following way:
*
* `idle` -> `reading` -> `idle`
* `idle` -> `reading -> `aborted`
* `idle` -> `reading -> `error`
*
* When uploading status can change in a following way:
*
* `idle` -> `uploading` -> `idle`
* `idle` -> `uploading` -> `aborted`
* `idle` -> `uploading` -> `error`
*
* @readonly
* @observable
* @member {String} #status
*/
this.set( 'status', 'idle' );
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set( 'uploaded', 0 );
/**
* Number of total bytes to upload.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set( 'uploadTotal', null );
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
return total ? ( uploaded / total * 100 ) : 0;
} );
/**
* Response of the upload.
*
* @readonly
* @observable
* @member {Object|null} #uploadResponse
*/
this.set( 'uploadResponse', null );
}
/**
* A `Promise` which resolves to a `File` instance associated with this file loader.
*
* @type {Promise.<File|null>}
*/
get file() {
if ( !this._filePromiseWrapper ) {
// Loader was destroyed, return promise which resolves to null.
return Promise.resolve( null );
} else {
// The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
//
// * The `loader.file.then( ... )` is called by external code (returned promise is pending).
// * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
// * Promise returned by the first `loader.file.then( ... )` call is resolved.
//
// Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
// is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
return this._filePromiseWrapper.promise.then( file => this._filePromiseWrapper ? file : null );
}
}
/**
* Returns the file data. To read its data, you need for first load the file
* by using the {@link module:upload/filerepository~FileLoader#read `read()`} method.
*
* @type {File|undefined}
*/
get data() {
return this._reader.data;
}
/**
* Reads file using {@link module:upload/filereader~FileReader}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
* is different than `idle`.
*
* Example usage:
*
* fileLoader.read()
* .then( data => { ... } )
* .catch( err => {
* if ( err === 'aborted' ) {
* console.log( 'Reading aborted.' );
* } else {
* console.log( 'Reading error.', err );
* }
* } );
*
* @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
read() {
if ( this.status != 'idle' ) {
/**
* You cannot call read if the status is different than idle.
*
* @error filerepository-read-wrong-status
*/
throw new CKEditorError( 'filerepository-read-wrong-status', this );
}
this.status = 'reading';
return this.file
.then( file => this._reader.read( file ) )
.then( data => {
// Edge case: reader was aborted after file was read - double check for proper status.
// It can happen when image was deleted during its upload.
if ( this.status !== 'reading' ) {
throw this.status;
}
this.status = 'idle';
return data;
} )
.catch( err => {
if ( err === 'aborted' ) {
this.status = 'aborted';
throw 'aborted';
}
this.status = 'error';
throw this._reader.error ? this._reader.error : err;
} );
}
/**
* Reads file using the provided {@link module:upload/filerepository~UploadAdapter}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
* is different than `idle`.
* Example usage:
*
* fileLoader.upload()
* .then( data => { ... } )
* .catch( e => {
* if ( e === 'aborted' ) {
* console.log( 'Uploading aborted.' );
* } else {
* console.log( 'Uploading error.', e );
* }
* } );
*
* @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
upload() {
if ( this.status != 'idle' ) {
/**
* You cannot call upload if the status is different than idle.
*
* @error filerepository-upload-wrong-status
*/
throw new CKEditorError( 'filerepository-upload-wrong-status', this );
}
this.status = 'uploading';
return this.file
.then( () => this._adapter.upload() )
.then( data => {
this.uploadResponse = data;
this.status = 'idle';
return data;
} )
.catch( err => {
if ( this.status === 'aborted' ) {
throw 'aborted';
}
this.status = 'error';
throw err;
} );
}
/**
* Aborts loading process.
*/
abort() {
const status = this.status;
this.status = 'aborted';
if ( !this._filePromiseWrapper.isFulfilled ) {
// Edge case: file loader is aborted before read() is called
// so it might happen that no one handled the rejection of this promise.
// See https://github.com/ckeditor/ckeditor5-upload/pull/100
this._filePromiseWrapper.promise.catch( () => {} );
this._filePromiseWrapper.rejecter( 'aborted' );
} else if ( status == 'reading' ) {
this._reader.abort();
} else if ( status == 'uploading' && this._adapter.abort ) {
this._adapter.abort();
}
this._destroy();
}
/**
* Performs cleanup.
*
* @private
*/
_destroy() {
this._filePromiseWrapper = undefined;
this._reader = undefined;
this._adapter = undefined;
this.uploadResponse = undefined;
}
/**
* Wraps a given file promise into another promise giving additional
* control (resolving, rejecting, checking if fulfilled) over it.
*
* @private
* @param filePromise The initial file promise to be wrapped.
* @returns {module:upload/filerepository~FilePromiseWrapper}
*/
_createFilePromiseWrapper( filePromise ) {
const wrapper = {};
wrapper.promise = new Promise( ( resolve, reject ) => {
wrapper.rejecter = reject;
wrapper.isFulfilled = false;
filePromise
.then( file => {
wrapper.isFulfilled = true;
resolve( file );
} )
.catch( err => {
wrapper.isFulfilled = true;
reject( err );
} );
} );
return wrapper;
}
class FileLoader extends Observable {
/**
* Creates a new instance of `FileLoader`.
*
* @param {Promise.<File>} filePromise A promise which resolves to a file instance.
* @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
*/
constructor(filePromise, uploadAdapterCreator) {
super();
/**
* Unique id of FileLoader instance.
*
* @readonly
* @member {Number}
*/
this.id = uid();
/**
* Additional wrapper over the initial file promise passed to this loader.
*
* @protected
* @member {module:upload/filerepository~FilePromiseWrapper}
*/
this._filePromiseWrapper = this._createFilePromiseWrapper(filePromise);
/**
* Adapter instance associated with this file loader.
*
* @private
* @member {module:upload/filerepository~UploadAdapter}
*/
this._adapter = uploadAdapterCreator(this);
/**
* FileReader used by FileLoader.
*
* @protected
* @member {module:upload/filereader~FileReader}
*/
this._reader = new FileReader();
/**
* Current status of FileLoader. It can be one of the following:
*
* * 'idle',
* * 'reading',
* * 'uploading',
* * 'aborted',
* * 'error'.
*
* When reading status can change in a following way:
*
* `idle` -> `reading` -> `idle`
* `idle` -> `reading -> `aborted`
* `idle` -> `reading -> `error`
*
* When uploading status can change in a following way:
*
* `idle` -> `uploading` -> `idle`
* `idle` -> `uploading` -> `aborted`
* `idle` -> `uploading` -> `error`
*
* @readonly
* @observable
* @member {String} #status
*/
this.set('status', 'idle');
/**
* Number of bytes uploaded.
*
* @readonly
* @observable
* @member {Number} #uploaded
*/
this.set('uploaded', 0);
/**
* Number of total bytes to upload.
*
* @readonly
* @observable
* @member {Number|null} #uploadTotal
*/
this.set('uploadTotal', null);
/**
* Upload progress in percents.
*
* @readonly
* @observable
* @member {Number} #uploadedPercent
*/
this.bind('uploadedPercent').to(this, 'uploaded', this, 'uploadTotal', (uploaded, total) => {
return total ? (uploaded / total * 100) : 0;
});
/**
* Response of the upload.
*
* @readonly
* @observable
* @member {Object|null} #uploadResponse
*/
this.set('uploadResponse', null);
}
/**
* A `Promise` which resolves to a `File` instance associated with this file loader.
*
* @type {Promise.<File|null>}
*/
get file() {
if (!this._filePromiseWrapper) {
// Loader was destroyed, return promise which resolves to null.
return Promise.resolve(null);
}
else {
// The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
//
// * The `loader.file.then( ... )` is called by external code (returned promise is pending).
// * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
// * Promise returned by the first `loader.file.then( ... )` call is resolved.
//
// Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
// is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
return this._filePromiseWrapper.promise.then(file => this._filePromiseWrapper ? file : null);
}
}
/**
* Returns the file data. To read its data, you need for first load the file
* by using the {@link module:upload/filerepository~FileLoader#read `read()`} method.
*
* @type {File|undefined}
*/
get data() {
return this._reader.data;
}
/**
* Reads file using {@link module:upload/filereader~FileReader}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
* is different than `idle`.
*
* Example usage:
*
* fileLoader.read()
* .then( data => { ... } )
* .catch( err => {
* if ( err === 'aborted' ) {
* console.log( 'Reading aborted.' );
* } else {
* console.log( 'Reading error.', err );
* }
* } );
*
* @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
read() {
if (this.status != 'idle') {
/**
* You cannot call read if the status is different than idle.
*
* @error filerepository-read-wrong-status
*/
throw new CKEditorError('filerepository-read-wrong-status', this);
}
this.status = 'reading';
return this.file
.then(file => this._reader.read(file))
.then(data => {
// Edge case: reader was aborted after file was read - double check for proper status.
// It can happen when image was deleted during its upload.
if (this.status !== 'reading') {
throw this.status;
}
this.status = 'idle';
return data;
})
.catch(err => {
if (err === 'aborted') {
this.status = 'aborted';
throw 'aborted';
}
this.status = 'error';
throw this._reader.error ? this._reader.error : err;
});
}
/**
* Reads file using the provided {@link module:upload/filerepository~UploadAdapter}.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
* is different than `idle`.
* Example usage:
*
* fileLoader.upload()
* .then( data => { ... } )
* .catch( e => {
* if ( e === 'aborted' ) {
* console.log( 'Uploading aborted.' );
* } else {
* console.log( 'Uploading error.', e );
* }
* } );
*
* @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
* occurs or if read process is aborted.
*/
upload() {
if (this.status != 'idle') {
/**
* You cannot call upload if the status is different than idle.
*
* @error filerepository-upload-wrong-status
*/
throw new CKEditorError('filerepository-upload-wrong-status', this);
}
this.status = 'uploading';
return this.file
.then(() => this._adapter.upload())
.then(data => {
this.uploadResponse = data;
this.status = 'idle';
return data;
})
.catch(err => {
if (this.status === 'aborted') {
throw 'aborted';
}
this.status = 'error';
throw err;
});
}
/**
* Aborts loading process.
*/
abort() {
const status = this.status;
this.status = 'aborted';
if (!this._filePromiseWrapper.isFulfilled) {
// Edge case: file loader is aborted before read() is called
// so it might happen that no one handled the rejection of this promise.
// See https://github.com/ckeditor/ckeditor5-upload/pull/100
this._filePromiseWrapper.promise.catch(() => { });
this._filePromiseWrapper.rejecter('aborted');
}
else if (status == 'reading') {
this._reader.abort();
}
else if (status == 'uploading' && this._adapter.abort) {
this._adapter.abort();
}
this._destroy();
}
/**
* Performs cleanup.
*
* @internal
*/
_destroy() {
this._filePromiseWrapper = undefined;
this._reader = undefined;
this._adapter = undefined;
this.uploadResponse = undefined;
}
/**
* Wraps a given file promise into another promise giving additional
* control (resolving, rejecting, checking if fulfilled) over it.
*
* @private
* @param filePromise The initial file promise to be wrapped.
* @returns {module:upload/filerepository~FilePromiseWrapper}
*/
_createFilePromiseWrapper(filePromise) {
const wrapper = {};
wrapper.promise = new Promise((resolve, reject) => {
wrapper.rejecter = reject;
wrapper.isFulfilled = false;
filePromise
.then(file => {
wrapper.isFulfilled = true;
resolve(file);
})
.catch(err => {
wrapper.isFulfilled = true;
reject(err);
});
});
return wrapper;
}
}
mix( FileLoader, ObservableMixin );
/**
* Upload adapter interface used by the {@link module:upload/filerepository~FileRepository file repository}
* to handle file upload. An upload adapter is a bridge between the editor and server that handles file uploads.
* It should contain a logic necessary to initiate an upload process and monitor its progress.
*
* Learn how to develop your own upload adapter for CKEditor 5 in the
* {@glink framework/guides/deep-dive/upload-adapter "Custom upload adapter" guide}.
*
* @interface UploadAdapter
*/
/**
* Executes the upload process.
* This method should return a promise that will resolve when data will be uploaded to server. Promise should be
* resolved with an object containing information about uploaded file:
*
* {
* 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',
* '1052': 'http://server/default-size.image.png'
* }
*
* You can also pass additional properties from the server. In this case you need to wrap URLs
* in the `urls` object and pass additional properties along the `urls` property.
*
* {
* myCustomProperty: 'foo',
* urls: {
* 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',
* '1052': 'http://server/default-size.image.png'
* }
* }
*
* NOTE: When returning multiple images, the widest returned one should equal the default one. It is essential to
* correctly set `width` attribute of the image. See this discussion:
* https://github.com/ckeditor/ckeditor5-easy-image/issues/4 for more information.
*
* Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
* {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
*
* @method module:upload/filerepository~UploadAdapter#upload
* @returns {Promise.<Object>} Promise that should be resolved when data is uploaded.
*/
/**
* Aborts the upload process.
* After aborting it should reject promise returned from {@link #upload upload()}.
*
* Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
* {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
*
* @method module:upload/filerepository~UploadAdapter#abort
*/
/**
* Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
* to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
*
* @protected
* @typedef {Object} module:upload/filerepository~FilePromiseWrapper
* @property {Promise.<File>} promise Wrapper promise which can be chained for further processing.
* @property {Function} rejecter Rejects the promise when called.
* @property {Boolean} isFulfilled Whether original promise is already fulfilled.
*/

@@ -5,7 +5,5 @@ /**

*/
/**
* @module upload
*/
export { default as FileRepository } from './filerepository';

@@ -12,0 +10,0 @@ export { default as FileDialogButtonView } from './ui/filedialogbuttonview';

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

*/
/**
* @module upload/ui/filedialogbuttonview
*/
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import View from '@ckeditor/ckeditor5-ui/src/view';
/**

@@ -42,80 +39,71 @@ * The file dialog button view.

export default class FileDialogButtonView extends View {
/**
* @inheritDoc
*/
constructor( locale ) {
super( locale );
/**
* The button view of the component.
*
* @member {module:ui/button/buttonview~ButtonView}
*/
this.buttonView = new ButtonView( locale );
/**
* A hidden `<input>` view used to execute file dialog.
*
* @protected
* @member {module:upload/ui/filedialogbuttonview~FileInputView}
*/
this._fileInputView = new FileInputView( locale );
/**
* Accepted file types. Can be provided in form of file extensions, media type or one of:
* * `audio/*`,
* * `video/*`,
* * `image/*`.
*
* @observable
* @member {String} #acceptedType
*/
this._fileInputView.bind( 'acceptedType' ).to( this );
/**
* Indicates if multiple files can be selected. Defaults to `true`.
*
* @observable
* @member {Boolean} #allowMultipleFiles
*/
this._fileInputView.bind( 'allowMultipleFiles' ).to( this );
/**
* Fired when file dialog is closed with file selected.
*
* view.on( 'done', ( evt, files ) => {
* for ( const file of files ) {
* console.log( 'Selected file', file );
* }
* }
*
* @event done
* @param {Array.<File>} files Array of selected files.
*/
this._fileInputView.delegate( 'done' ).to( this );
this.setTemplate( {
tag: 'span',
attributes: {
class: 'ck-file-dialog-button'
},
children: [
this.buttonView,
this._fileInputView
]
} );
this.buttonView.on( 'execute', () => {
this._fileInputView.open();
} );
}
/**
* Focuses the {@link #buttonView}.
*/
focus() {
this.buttonView.focus();
}
/**
* @inheritDoc
*/
constructor(locale) {
super(locale);
/**
* The button view of the component.
*
* @member {module:ui/button/buttonview~ButtonView}
*/
this.buttonView = new ButtonView(locale);
/**
* A hidden `<input>` view used to execute file dialog.
*
* @protected
* @member {module:upload/ui/filedialogbuttonview~FileInputView}
*/
this._fileInputView = new FileInputView(locale);
/**
* Accepted file types. Can be provided in form of file extensions, media type or one of:
* * `audio/*`,
* * `video/*`,
* * `image/*`.
*
* @observable
* @member {String} #acceptedType
*/
this._fileInputView.bind('acceptedType').to(this);
/**
* Indicates if multiple files can be selected. Defaults to `true`.
*
* @observable
* @member {Boolean} #allowMultipleFiles
*/
this._fileInputView.bind('allowMultipleFiles').to(this);
/**
* Fired when file dialog is closed with file selected.
*
* view.on( 'done', ( evt, files ) => {
* for ( const file of files ) {
* console.log( 'Selected file', file );
* }
* }
*
* @event done
* @param {Array.<File>} files Array of selected files.
*/
this._fileInputView.delegate('done').to(this);
this.setTemplate({
tag: 'span',
attributes: {
class: 'ck-file-dialog-button'
},
children: [
this.buttonView,
this._fileInputView
]
});
this.buttonView.on('execute', () => {
this._fileInputView.open();
});
}
/**
* Focuses the {@link #buttonView}.
*/
focus() {
this.buttonView.focus();
}
}
/**

@@ -128,61 +116,53 @@ * The hidden file input view class.

class FileInputView extends View {
/**
* @inheritDoc
*/
constructor( locale ) {
super( locale );
/**
* Accepted file types. Can be provided in form of file extensions, media type or one of:
* * `audio/*`,
* * `video/*`,
* * `image/*`.
*
* @observable
* @member {String} #acceptedType
*/
this.set( 'acceptedType' );
/**
* Indicates if multiple files can be selected. Defaults to `false`.
*
* @observable
* @member {Boolean} #allowMultipleFiles
*/
this.set( 'allowMultipleFiles', false );
const bind = this.bindTemplate;
this.setTemplate( {
tag: 'input',
attributes: {
class: [
'ck-hidden'
],
type: 'file',
tabindex: '-1',
accept: bind.to( 'acceptedType' ),
multiple: bind.to( 'allowMultipleFiles' )
},
on: {
// Removing from code coverage since we cannot programmatically set input element files.
change: bind.to( /* istanbul ignore next */ () => {
if ( this.element && this.element.files && this.element.files.length ) {
this.fire( 'done', this.element.files );
}
this.element.value = '';
} )
}
} );
}
/**
* Opens file dialog.
*/
open() {
this.element.click();
}
/**
* @inheritDoc
*/
constructor(locale) {
super(locale);
/**
* Accepted file types. Can be provided in form of file extensions, media type or one of:
* * `audio/*`,
* * `video/*`,
* * `image/*`.
*
* @observable
* @member {String} #acceptedType
*/
this.set('acceptedType', undefined);
/**
* Indicates if multiple files can be selected. Defaults to `false`.
*
* @observable
* @member {Boolean} #allowMultipleFiles
*/
this.set('allowMultipleFiles', false);
const bind = this.bindTemplate;
this.setTemplate({
tag: 'input',
attributes: {
class: [
'ck-hidden'
],
type: 'file',
tabindex: '-1',
accept: bind.to('acceptedType'),
multiple: bind.to('allowMultipleFiles')
},
on: {
// Removing from code coverage since we cannot programmatically set input element files.
change: bind.to(/* istanbul ignore next */ () => {
if (this.element && this.element.files && this.element.files.length) {
this.fire('done', this.element.files);
}
this.element.value = '';
})
}
});
}
/**
* Opens file dialog.
*/
open() {
this.element.click();
}
}
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