@ckeditor/ckeditor5-upload
Advanced tools
Comparing version 35.2.1 to 35.3.0
{ | ||
"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(); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
103809
3
1091