8
0
Szymon Kupś 8 лет назад
Родитель
Сommit
cf1d0391d7

+ 1 - 0
packages/ckeditor5-upload/package.json

@@ -4,6 +4,7 @@
   "description": "Upload Feature for CKEditor 5.",
   "keywords": [],
   "dependencies": {
+	  "@ckeditor/ckeditor5-core": "^0.7.0",
 	  "@ckeditor/ckeditor5-utils": "^0.8.0"
   },
   "devDependencies": {

+ 2 - 7
packages/ckeditor5-upload/src/filereader.js

@@ -44,7 +44,7 @@ export default class FileReader {
 	/**
 	 * Returns error that occurred during file reading.
 	 *
-	 * @returns {DOMError}
+	 * @returns {Error}
 	 */
 	get error() {
 		return this._reader.error;
@@ -53,14 +53,9 @@ export default class FileReader {
 	/**
 	 * Reads provided file.
 	 *
-	 * @param {File} file
+	 * @param file Native File object.
 	 * @returns {Promise} Returns a promise that will resolve with file's contents. Promise can be rejected in case of
 	 * error or when reading process is aborted.
-	 *
-	 *	const reader = new FileReader();
-	 *
-	 * reader.read( file )
-	 *
 	 */
 	read( file ) {
 		const reader = this._reader;

+ 390 - 0
packages/ckeditor5-upload/src/filerepository.js

@@ -0,0 +1,390 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module upload/filerepository
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin.js';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin.js';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection.js';
+import mix from '@ckeditor/ckeditor5-utils/src/mix.js';
+
+import FileReader from './filereader.js';
+
+import uid from '@ckeditor/ckeditor5-utils/src/uid.js';
+
+/**
+ * FileRepository plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class FileRepository extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'fileRepository';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		/**
+		 * Collection of loaders associated with this repository.
+		 *
+		 * @member {module:utils/collection~Collection} #loaders
+		 */
+		this.loaders = new Collection();
+
+		/**
+		 * Function that should be defined before using FileRepository. It should return adapter that will be
+		 * used to upload files.
+		 *
+		 * @member {function} #createAdapter
+		 */
+
+		/**
+		 * Number of bytes uploaded.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Number} #uploaded
+		 */
+		this.set( 'uploaded', 0 );
+
+		/**
+		 * Number of total bytes to upload.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Number} #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 loader associated with specified file.
+	 * To get loader by id use `fileRepository.loaders.get( id )`.
+	 *
+	 * @param file Native File object.
+	 * @returns {module:upload/filerepository~FileLoader|null}
+	 */
+	getLoader( file ) {
+		for ( const loader of this.loaders ) {
+			if ( loader.file == file ) {
+				return loader;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Creates loader for specified file.
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-no-adapter` when
+	 * {@link #createAdapter} method is not defined for this FileRepository.
+	 *
+	 * @param file Native File object.
+	 * @returns {module:upload/filerepository~FileLoader}
+	 */
+	createLoader( file ) {
+		if ( !this.createAdapter ) {
+			throw new CKEditorError( 'filerepository-no-adapter: No createAdapter method found.' );
+		}
+
+		const loader = new FileLoader( file );
+		loader._adapter = this.createAdapter( loader );
+
+		this.loaders.add( loader );
+
+		loader.on( 'change:uploaded', () => {
+			let agregatedUploaded = 0;
+
+			for ( const loader of this.loaders ) {
+				agregatedUploaded += loader.uploaded;
+			}
+
+			this.uploaded = agregatedUploaded;
+		} );
+
+		loader.on( 'change:uploadTotal', () => {
+			let agregatedTotal = 0;
+
+			for ( const loader of this.loaders ) {
+				if ( loader.uploadTotal ) {
+					agregatedTotal += loader.uploadTotal;
+				}
+			}
+
+			this.uploadTotal = agregatedTotal;
+		} );
+
+		return loader;
+	}
+
+	destroyLoader( fileOrIdOrLoader ) {
+		const loader = fileOrIdOrLoader instanceof FileLoader ? fileOrIdOrLoader : this.getLoader( fileOrIdOrLoader );
+
+		loader._destroy();
+
+		this.loaders.remove( loader );
+	}
+}
+
+mix( FileRepository, ObservableMixin );
+
+/**
+ * File loader class.
+ * It is used to control the process of file reading and uploading using specified adapter.
+ */
+class FileLoader {
+	/**
+	 * Creates instance of FileLoader.
+	 *
+	 * @param file
+	 * @param {module:upload/filerepository~Adapter} adapter
+	 */
+	constructor( file, adapter ) {
+		/**
+		 * Unique id of FileLoader instance.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.id = uid();
+
+		/**
+		 * File instance associated with this FileLoader.
+		 *
+		 * @readonly
+		 * @member
+		 */
+		this.file = file;
+
+		/**
+		 * Adapter instance associated with this FileLoader.
+		 *
+		 * @readonly
+		 * @member {module:upload/filerepository~Adapter}
+		 */
+		this._adapter = adapter;
+
+		/**
+		 * 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.set( 'uploadedPercent', 0 );
+
+		/**
+		 * Response of the upload.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Object|null} #uploadedPercent
+		 */
+		this.set( 'uploadReponse', null );
+	}
+
+	/**
+	 * 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( e => {
+	 *			if ( e === 'aborted' ) {
+	 *				console.log( 'Reading aborted.' );
+	 *			} else {
+	 *				console.log( 'Reading error.', e );
+	 *			}
+	 *		} );
+	 *
+	 * @returns {Promise} 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' ) {
+			throw new CKEditorError( 'filerepository-read-wrong-status: You cannot call read if the status is different than idle.' );
+		}
+
+		this.status = 'reading';
+		this._reader = new FileReader();
+
+		return this._reader.read( this.file )
+			.then( data => {
+				if ( this.status === 'aborted' ) {
+					throw 'aborted';
+				}
+				this.status = 'idle';
+
+				return data;
+			} )
+			.catch( err => {
+				if ( this.status != 'aborted' ) {
+					this.status = 'error';
+				}
+				throw err;
+			} );
+	}
+
+	/**
+	 * Reads file using provided {@link module:upload/filereader~Adapter}.
+	 * 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} 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' ) {
+			throw new CKEditorError( 'filerepository-upload-wrong-status: You cannot call upload if the status is different than idle.' );
+		}
+
+		this.status = 'uploading';
+
+		return this._adapter.upload()
+			.then( data => {
+				if ( this.status === 'aborted' ) {
+					throw 'aborted';
+				}
+				this.uploadReponse = data;
+				this.status = 'idle';
+
+				return data;
+			} )
+			.catch( err => {
+				if ( this.status != 'aborted' ) {
+					this.status = 'error';
+				}
+				throw err;
+			} );
+	}
+
+	/**
+	 * Aborts loading process.
+	 */
+	abort() {
+		if ( this.status == 'loading' ) {
+			this._reader.abort();
+		}
+
+		if ( this.status == 'uploading' && this._adapter.abort ) {
+			this._adapter.abort();
+		}
+
+		this.status = 'aborted';
+		this._destroy();
+	}
+
+	/**
+	 * Performs cleanup.
+	 *
+	 * @private
+	 */
+	_destroy() {
+		this._reader = undefined;
+		this._adapter = undefined;
+		this.data = undefined;
+		this.uploadReponse = undefined;
+		this.file = undefined;
+	}
+}
+
+mix( FileLoader, ObservableMixin );
+
+/**
+ * Adapter abstract class used by FileRepository to handle file upload.
+ *
+ * @abstract
+ * @class Adapter
+ */
+
+/**
+ * Executes the upload process.
+ *
+ * @function
+ * @name module:upload/filerepository~Adapter#upload
+ * @returns {Promise}
+ */
+
+/**
+ * Aborts the upload process.
+ *
+ * @function
+ * @name module:upload/filerepository~Adapter#abort
+ */