@ckeditor/ckeditor5-upload 35.2.1 → 35.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,23 +2,16 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module upload/filerepository
8
7
  */
9
-
10
8
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11
-
12
9
  import PendingActions from '@ckeditor/ckeditor5-core/src/pendingactions';
13
10
  import CKEditorError, { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
14
- import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
11
+ import { Observable } from '@ckeditor/ckeditor5-utils/src/observablemixin';
15
12
  import Collection from '@ckeditor/ckeditor5-utils/src/collection';
16
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
17
-
18
- import FileReader from './filereader.js';
19
-
13
+ import FileReader from './filereader';
20
14
  import uid from '@ckeditor/ckeditor5-utils/src/uid';
21
-
22
15
  /**
23
16
  * File repository plugin. A central point for managing file upload.
24
17
  *
@@ -34,624 +27,488 @@ import uid from '@ckeditor/ckeditor5-utils/src/uid';
34
27
  * @extends module:core/plugin~Plugin
35
28
  */
36
29
  export default class FileRepository extends Plugin {
37
- /**
38
- * @inheritDoc
39
- */
40
- static get pluginName() {
41
- return 'FileRepository';
42
- }
43
-
44
- /**
45
- * @inheritDoc
46
- */
47
- static get requires() {
48
- return [ PendingActions ];
49
- }
50
-
51
- /**
52
- * @inheritDoc
53
- */
54
- init() {
55
- /**
56
- * Collection of loaders associated with this repository.
57
- *
58
- * @member {module:utils/collection~Collection} #loaders
59
- */
60
- this.loaders = new Collection();
61
-
62
- // Keeps upload in a sync with pending actions.
63
- this.loaders.on( 'add', () => this._updatePendingAction() );
64
- this.loaders.on( 'remove', () => this._updatePendingAction() );
65
-
66
- /**
67
- * Loaders mappings used to retrieve loaders references.
68
- *
69
- * @private
70
- * @member {Map<File|Promise, FileLoader>} #_loadersMap
71
- */
72
- this._loadersMap = new Map();
73
-
74
- /**
75
- * Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
76
- * while upload is in progress. When there is no upload then value is `null`.
77
- *
78
- * @private
79
- * @member {Object} #_pendingAction
80
- */
81
- this._pendingAction = null;
82
-
83
- /**
84
- * A factory function which should be defined before using `FileRepository`.
85
- *
86
- * It should return a new instance of {@link module:upload/filerepository~UploadAdapter} that will be used to upload files.
87
- * {@link module:upload/filerepository~FileLoader} instance associated with the adapter
88
- * will be passed to that function.
89
- *
90
- * For more information and example see {@link module:upload/filerepository~UploadAdapter}.
91
- *
92
- * @member {Function} #createUploadAdapter
93
- */
94
-
95
- /**
96
- * Number of bytes uploaded.
97
- *
98
- * @readonly
99
- * @observable
100
- * @member {Number} #uploaded
101
- */
102
- this.set( 'uploaded', 0 );
103
-
104
- /**
105
- * Number of total bytes to upload.
106
- *
107
- * It might be different than the file size because of headers and additional data.
108
- * It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
109
- * the progress.
110
- *
111
- * @readonly
112
- * @observable
113
- * @member {Number|null} #uploadTotal
114
- */
115
- this.set( 'uploadTotal', null );
116
-
117
- /**
118
- * Upload progress in percents.
119
- *
120
- * @readonly
121
- * @observable
122
- * @member {Number} #uploadedPercent
123
- */
124
- this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
125
- return total ? ( uploaded / total * 100 ) : 0;
126
- } );
127
- }
128
-
129
- /**
130
- * Returns the loader associated with specified file or promise.
131
- *
132
- * To get loader by id use `fileRepository.loaders.get( id )`.
133
- *
134
- * @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
135
- * @returns {module:upload/filerepository~FileLoader|null}
136
- */
137
- getLoader( fileOrPromise ) {
138
- return this._loadersMap.get( fileOrPromise ) || null;
139
- }
140
-
141
- /**
142
- * Creates a loader instance for the given file.
143
- *
144
- * Requires {@link #createUploadAdapter} factory to be defined.
145
- *
146
- * @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
147
- * @returns {module:upload/filerepository~FileLoader|null}
148
- */
149
- createLoader( fileOrPromise ) {
150
- if ( !this.createUploadAdapter ) {
151
- /**
152
- * You need to enable an upload adapter in order to be able to upload files.
153
- *
154
- * This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
155
- * without {@link #createUploadAdapter defining an upload adapter}.
156
- *
157
- * **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
158
- * CKEditor 5 Builds}**
159
- * it means that you did not configure any of the upload adapters available by default in those builds.
160
- *
161
- * See the {@glink features/images/image-upload/image-upload comprehensive "Image upload overview"} to learn which upload
162
- * adapters are available in the builds and how to configure them.
163
- *
164
- * **If you see this warning when using a custom build** there is a chance that you enabled
165
- * a feature like {@link module:image/imageupload~ImageUpload},
166
- * or {@link module:image/imageupload/imageuploadui~ImageUploadUI} but you did not enable any upload adapter.
167
- * You can choose one of the existing upload adapters listed in the
168
- * {@glink features/images/image-upload/image-upload "Image upload overview"}.
169
- *
170
- * You can also implement your {@glink framework/guides/deep-dive/upload-adapter own image upload adapter}.
171
- *
172
- * @error filerepository-no-upload-adapter
173
- */
174
- logWarning( 'filerepository-no-upload-adapter' );
175
-
176
- return null;
177
- }
178
-
179
- const loader = new FileLoader( Promise.resolve( fileOrPromise ), this.createUploadAdapter );
180
-
181
- this.loaders.add( loader );
182
- this._loadersMap.set( fileOrPromise, loader );
183
-
184
- // Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
185
- if ( fileOrPromise instanceof Promise ) {
186
- loader.file
187
- .then( file => {
188
- this._loadersMap.set( file, loader );
189
- } )
190
- // Every then() must have a catch().
191
- // File loader state (and rejections) are handled in read() and upload().
192
- // Also, see the "does not swallow the file promise rejection" test.
193
- .catch( () => {} );
194
- }
195
-
196
- loader.on( 'change:uploaded', () => {
197
- let aggregatedUploaded = 0;
198
-
199
- for ( const loader of this.loaders ) {
200
- aggregatedUploaded += loader.uploaded;
201
- }
202
-
203
- this.uploaded = aggregatedUploaded;
204
- } );
205
-
206
- loader.on( 'change:uploadTotal', () => {
207
- let aggregatedTotal = 0;
208
-
209
- for ( const loader of this.loaders ) {
210
- if ( loader.uploadTotal ) {
211
- aggregatedTotal += loader.uploadTotal;
212
- }
213
- }
214
-
215
- this.uploadTotal = aggregatedTotal;
216
- } );
217
-
218
- return loader;
219
- }
220
-
221
- /**
222
- * Destroys the given loader.
223
- *
224
- * @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
225
- * with that loader or loader itself.
226
- */
227
- destroyLoader( fileOrPromiseOrLoader ) {
228
- const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader( fileOrPromiseOrLoader );
229
-
230
- loader._destroy();
231
-
232
- this.loaders.remove( loader );
233
-
234
- this._loadersMap.forEach( ( value, key ) => {
235
- if ( value === loader ) {
236
- this._loadersMap.delete( key );
237
- }
238
- } );
239
- }
240
-
241
- /**
242
- * Registers or deregisters pending action bound with upload progress.
243
- *
244
- * @private
245
- */
246
- _updatePendingAction() {
247
- const pendingActions = this.editor.plugins.get( PendingActions );
248
-
249
- if ( this.loaders.length ) {
250
- if ( !this._pendingAction ) {
251
- const t = this.editor.t;
252
- const getMessage = value => `${ t( 'Upload in progress' ) } ${ parseInt( value ) }%.`;
253
-
254
- this._pendingAction = pendingActions.add( getMessage( this.uploadedPercent ) );
255
- this._pendingAction.bind( 'message' ).to( this, 'uploadedPercent', getMessage );
256
- }
257
- } else {
258
- pendingActions.remove( this._pendingAction );
259
- this._pendingAction = null;
260
- }
261
- }
30
+ /**
31
+ * @inheritDoc
32
+ */
33
+ static get pluginName() {
34
+ return 'FileRepository';
35
+ }
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ static get requires() {
40
+ return [PendingActions];
41
+ }
42
+ /**
43
+ * @inheritDoc
44
+ */
45
+ init() {
46
+ /**
47
+ * Collection of loaders associated with this repository.
48
+ *
49
+ * @member {module:utils/collection~Collection} #loaders
50
+ */
51
+ this.loaders = new Collection();
52
+ // Keeps upload in a sync with pending actions.
53
+ this.loaders.on('change', () => this._updatePendingAction());
54
+ /**
55
+ * Loaders mappings used to retrieve loaders references.
56
+ *
57
+ * @private
58
+ * @member {Map<File|Promise, FileLoader>} #_loadersMap
59
+ */
60
+ this._loadersMap = new Map();
61
+ /**
62
+ * Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
63
+ * while upload is in progress. When there is no upload then value is `null`.
64
+ *
65
+ * @private
66
+ * @member {Object} #_pendingAction
67
+ */
68
+ this._pendingAction = null;
69
+ /**
70
+ * A factory function which should be defined before using `FileRepository`.
71
+ *
72
+ * It should return a new instance of {@link module:upload/filerepository~UploadAdapter} that will be used to upload files.
73
+ * {@link module:upload/filerepository~FileLoader} instance associated with the adapter
74
+ * will be passed to that function.
75
+ *
76
+ * For more information and example see {@link module:upload/filerepository~UploadAdapter}.
77
+ *
78
+ * @member {Function} #createUploadAdapter
79
+ */
80
+ /**
81
+ * Number of bytes uploaded.
82
+ *
83
+ * @readonly
84
+ * @observable
85
+ * @member {Number} #uploaded
86
+ */
87
+ this.set('uploaded', 0);
88
+ /**
89
+ * Number of total bytes to upload.
90
+ *
91
+ * It might be different than the file size because of headers and additional data.
92
+ * It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
93
+ * the progress.
94
+ *
95
+ * @readonly
96
+ * @observable
97
+ * @member {Number|null} #uploadTotal
98
+ */
99
+ this.set('uploadTotal', null);
100
+ /**
101
+ * Upload progress in percents.
102
+ *
103
+ * @readonly
104
+ * @observable
105
+ * @member {Number} #uploadedPercent
106
+ */
107
+ this.bind('uploadedPercent').to(this, 'uploaded', this, 'uploadTotal', (uploaded, total) => {
108
+ return total ? (uploaded / total * 100) : 0;
109
+ });
110
+ }
111
+ /**
112
+ * Returns the loader associated with specified file or promise.
113
+ *
114
+ * To get loader by id use `fileRepository.loaders.get( id )`.
115
+ *
116
+ * @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
117
+ * @returns {module:upload/filerepository~FileLoader|null}
118
+ */
119
+ getLoader(fileOrPromise) {
120
+ return this._loadersMap.get(fileOrPromise) || null;
121
+ }
122
+ /**
123
+ * Creates a loader instance for the given file.
124
+ *
125
+ * Requires {@link #createUploadAdapter} factory to be defined.
126
+ *
127
+ * @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
128
+ * @returns {module:upload/filerepository~FileLoader|null}
129
+ */
130
+ createLoader(fileOrPromise) {
131
+ if (!this.createUploadAdapter) {
132
+ /**
133
+ * You need to enable an upload adapter in order to be able to upload files.
134
+ *
135
+ * This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
136
+ * without {@link #createUploadAdapter defining an upload adapter}.
137
+ *
138
+ * **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
139
+ * CKEditor 5 Builds}**
140
+ * it means that you did not configure any of the upload adapters available by default in those builds.
141
+ *
142
+ * See the {@glink features/images/image-upload/image-upload comprehensive "Image upload overview"} to learn which upload
143
+ * adapters are available in the builds and how to configure them.
144
+ *
145
+ * **If you see this warning when using a custom build** there is a chance that you enabled
146
+ * a feature like {@link module:image/imageupload~ImageUpload},
147
+ * or {@link module:image/imageupload/imageuploadui~ImageUploadUI} but you did not enable any upload adapter.
148
+ * You can choose one of the existing upload adapters listed in the
149
+ * {@glink features/images/image-upload/image-upload "Image upload overview"}.
150
+ *
151
+ * You can also implement your {@glink framework/guides/deep-dive/upload-adapter own image upload adapter}.
152
+ *
153
+ * @error filerepository-no-upload-adapter
154
+ */
155
+ logWarning('filerepository-no-upload-adapter');
156
+ return null;
157
+ }
158
+ const loader = new FileLoader(Promise.resolve(fileOrPromise), this.createUploadAdapter);
159
+ this.loaders.add(loader);
160
+ this._loadersMap.set(fileOrPromise, loader);
161
+ // Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
162
+ if (fileOrPromise instanceof Promise) {
163
+ loader.file
164
+ .then(file => {
165
+ this._loadersMap.set(file, loader);
166
+ })
167
+ // Every then() must have a catch().
168
+ // File loader state (and rejections) are handled in read() and upload().
169
+ // Also, see the "does not swallow the file promise rejection" test.
170
+ .catch(() => { });
171
+ }
172
+ loader.on('change:uploaded', () => {
173
+ let aggregatedUploaded = 0;
174
+ for (const loader of this.loaders) {
175
+ aggregatedUploaded += loader.uploaded;
176
+ }
177
+ this.uploaded = aggregatedUploaded;
178
+ });
179
+ loader.on('change:uploadTotal', () => {
180
+ let aggregatedTotal = 0;
181
+ for (const loader of this.loaders) {
182
+ if (loader.uploadTotal) {
183
+ aggregatedTotal += loader.uploadTotal;
184
+ }
185
+ }
186
+ this.uploadTotal = aggregatedTotal;
187
+ });
188
+ return loader;
189
+ }
190
+ /**
191
+ * Destroys the given loader.
192
+ *
193
+ * @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
194
+ * with that loader or loader itself.
195
+ */
196
+ destroyLoader(fileOrPromiseOrLoader) {
197
+ const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader(fileOrPromiseOrLoader);
198
+ loader._destroy();
199
+ this.loaders.remove(loader);
200
+ this._loadersMap.forEach((value, key) => {
201
+ if (value === loader) {
202
+ this._loadersMap.delete(key);
203
+ }
204
+ });
205
+ }
206
+ /**
207
+ * Registers or deregisters pending action bound with upload progress.
208
+ *
209
+ * @private
210
+ */
211
+ _updatePendingAction() {
212
+ const pendingActions = this.editor.plugins.get(PendingActions);
213
+ if (this.loaders.length) {
214
+ if (!this._pendingAction) {
215
+ const t = this.editor.t;
216
+ const getMessage = (value) => `${t('Upload in progress')} ${parseInt(value)}%.`;
217
+ this._pendingAction = pendingActions.add(getMessage(this.uploadedPercent));
218
+ this._pendingAction.bind('message').to(this, 'uploadedPercent', getMessage);
219
+ }
220
+ }
221
+ else {
222
+ pendingActions.remove(this._pendingAction);
223
+ this._pendingAction = null;
224
+ }
225
+ }
262
226
  }
263
-
264
- mix( FileRepository, ObservableMixin );
265
-
266
227
  /**
267
228
  * File loader class.
268
229
  *
269
230
  * It is used to control the process of reading the file and uploading it using the specified upload adapter.
270
231
  */
271
- class FileLoader {
272
- /**
273
- * Creates a new instance of `FileLoader`.
274
- *
275
- * @param {Promise.<File>} filePromise A promise which resolves to a file instance.
276
- * @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
277
- */
278
- constructor( filePromise, uploadAdapterCreator ) {
279
- /**
280
- * Unique id of FileLoader instance.
281
- *
282
- * @readonly
283
- * @member {Number}
284
- */
285
- this.id = uid();
286
-
287
- /**
288
- * Additional wrapper over the initial file promise passed to this loader.
289
- *
290
- * @protected
291
- * @member {module:upload/filerepository~FilePromiseWrapper}
292
- */
293
- this._filePromiseWrapper = this._createFilePromiseWrapper( filePromise );
294
-
295
- /**
296
- * Adapter instance associated with this file loader.
297
- *
298
- * @private
299
- * @member {module:upload/filerepository~UploadAdapter}
300
- */
301
- this._adapter = uploadAdapterCreator( this );
302
-
303
- /**
304
- * FileReader used by FileLoader.
305
- *
306
- * @protected
307
- * @member {module:upload/filereader~FileReader}
308
- */
309
- this._reader = new FileReader();
310
-
311
- /**
312
- * Current status of FileLoader. It can be one of the following:
313
- *
314
- * * 'idle',
315
- * * 'reading',
316
- * * 'uploading',
317
- * * 'aborted',
318
- * * 'error'.
319
- *
320
- * When reading status can change in a following way:
321
- *
322
- * `idle` -> `reading` -> `idle`
323
- * `idle` -> `reading -> `aborted`
324
- * `idle` -> `reading -> `error`
325
- *
326
- * When uploading status can change in a following way:
327
- *
328
- * `idle` -> `uploading` -> `idle`
329
- * `idle` -> `uploading` -> `aborted`
330
- * `idle` -> `uploading` -> `error`
331
- *
332
- * @readonly
333
- * @observable
334
- * @member {String} #status
335
- */
336
- this.set( 'status', 'idle' );
337
-
338
- /**
339
- * Number of bytes uploaded.
340
- *
341
- * @readonly
342
- * @observable
343
- * @member {Number} #uploaded
344
- */
345
- this.set( 'uploaded', 0 );
346
-
347
- /**
348
- * Number of total bytes to upload.
349
- *
350
- * @readonly
351
- * @observable
352
- * @member {Number|null} #uploadTotal
353
- */
354
- this.set( 'uploadTotal', null );
355
-
356
- /**
357
- * Upload progress in percents.
358
- *
359
- * @readonly
360
- * @observable
361
- * @member {Number} #uploadedPercent
362
- */
363
- this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
364
- return total ? ( uploaded / total * 100 ) : 0;
365
- } );
366
-
367
- /**
368
- * Response of the upload.
369
- *
370
- * @readonly
371
- * @observable
372
- * @member {Object|null} #uploadResponse
373
- */
374
- this.set( 'uploadResponse', null );
375
- }
376
-
377
- /**
378
- * A `Promise` which resolves to a `File` instance associated with this file loader.
379
- *
380
- * @type {Promise.<File|null>}
381
- */
382
- get file() {
383
- if ( !this._filePromiseWrapper ) {
384
- // Loader was destroyed, return promise which resolves to null.
385
- return Promise.resolve( null );
386
- } else {
387
- // The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
388
- //
389
- // * The `loader.file.then( ... )` is called by external code (returned promise is pending).
390
- // * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
391
- // * Promise returned by the first `loader.file.then( ... )` call is resolved.
392
- //
393
- // Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
394
- // is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
395
- return this._filePromiseWrapper.promise.then( file => this._filePromiseWrapper ? file : null );
396
- }
397
- }
398
-
399
- /**
400
- * Returns the file data. To read its data, you need for first load the file
401
- * by using the {@link module:upload/filerepository~FileLoader#read `read()`} method.
402
- *
403
- * @type {File|undefined}
404
- */
405
- get data() {
406
- return this._reader.data;
407
- }
408
-
409
- /**
410
- * Reads file using {@link module:upload/filereader~FileReader}.
411
- *
412
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
413
- * is different than `idle`.
414
- *
415
- * Example usage:
416
- *
417
- * fileLoader.read()
418
- * .then( data => { ... } )
419
- * .catch( err => {
420
- * if ( err === 'aborted' ) {
421
- * console.log( 'Reading aborted.' );
422
- * } else {
423
- * console.log( 'Reading error.', err );
424
- * }
425
- * } );
426
- *
427
- * @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
428
- * occurs or if read process is aborted.
429
- */
430
- read() {
431
- if ( this.status != 'idle' ) {
432
- /**
433
- * You cannot call read if the status is different than idle.
434
- *
435
- * @error filerepository-read-wrong-status
436
- */
437
- throw new CKEditorError( 'filerepository-read-wrong-status', this );
438
- }
439
-
440
- this.status = 'reading';
441
-
442
- return this.file
443
- .then( file => this._reader.read( file ) )
444
- .then( data => {
445
- // Edge case: reader was aborted after file was read - double check for proper status.
446
- // It can happen when image was deleted during its upload.
447
- if ( this.status !== 'reading' ) {
448
- throw this.status;
449
- }
450
-
451
- this.status = 'idle';
452
-
453
- return data;
454
- } )
455
- .catch( err => {
456
- if ( err === 'aborted' ) {
457
- this.status = 'aborted';
458
- throw 'aborted';
459
- }
460
-
461
- this.status = 'error';
462
- throw this._reader.error ? this._reader.error : err;
463
- } );
464
- }
465
-
466
- /**
467
- * Reads file using the provided {@link module:upload/filerepository~UploadAdapter}.
468
- *
469
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
470
- * is different than `idle`.
471
- * Example usage:
472
- *
473
- * fileLoader.upload()
474
- * .then( data => { ... } )
475
- * .catch( e => {
476
- * if ( e === 'aborted' ) {
477
- * console.log( 'Uploading aborted.' );
478
- * } else {
479
- * console.log( 'Uploading error.', e );
480
- * }
481
- * } );
482
- *
483
- * @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
484
- * occurs or if read process is aborted.
485
- */
486
- upload() {
487
- if ( this.status != 'idle' ) {
488
- /**
489
- * You cannot call upload if the status is different than idle.
490
- *
491
- * @error filerepository-upload-wrong-status
492
- */
493
- throw new CKEditorError( 'filerepository-upload-wrong-status', this );
494
- }
495
-
496
- this.status = 'uploading';
497
-
498
- return this.file
499
- .then( () => this._adapter.upload() )
500
- .then( data => {
501
- this.uploadResponse = data;
502
- this.status = 'idle';
503
-
504
- return data;
505
- } )
506
- .catch( err => {
507
- if ( this.status === 'aborted' ) {
508
- throw 'aborted';
509
- }
510
-
511
- this.status = 'error';
512
- throw err;
513
- } );
514
- }
515
-
516
- /**
517
- * Aborts loading process.
518
- */
519
- abort() {
520
- const status = this.status;
521
- this.status = 'aborted';
522
-
523
- if ( !this._filePromiseWrapper.isFulfilled ) {
524
- // Edge case: file loader is aborted before read() is called
525
- // so it might happen that no one handled the rejection of this promise.
526
- // See https://github.com/ckeditor/ckeditor5-upload/pull/100
527
- this._filePromiseWrapper.promise.catch( () => {} );
528
-
529
- this._filePromiseWrapper.rejecter( 'aborted' );
530
- } else if ( status == 'reading' ) {
531
- this._reader.abort();
532
- } else if ( status == 'uploading' && this._adapter.abort ) {
533
- this._adapter.abort();
534
- }
535
-
536
- this._destroy();
537
- }
538
-
539
- /**
540
- * Performs cleanup.
541
- *
542
- * @private
543
- */
544
- _destroy() {
545
- this._filePromiseWrapper = undefined;
546
- this._reader = undefined;
547
- this._adapter = undefined;
548
- this.uploadResponse = undefined;
549
- }
550
-
551
- /**
552
- * Wraps a given file promise into another promise giving additional
553
- * control (resolving, rejecting, checking if fulfilled) over it.
554
- *
555
- * @private
556
- * @param filePromise The initial file promise to be wrapped.
557
- * @returns {module:upload/filerepository~FilePromiseWrapper}
558
- */
559
- _createFilePromiseWrapper( filePromise ) {
560
- const wrapper = {};
561
-
562
- wrapper.promise = new Promise( ( resolve, reject ) => {
563
- wrapper.rejecter = reject;
564
- wrapper.isFulfilled = false;
565
-
566
- filePromise
567
- .then( file => {
568
- wrapper.isFulfilled = true;
569
- resolve( file );
570
- } )
571
- .catch( err => {
572
- wrapper.isFulfilled = true;
573
- reject( err );
574
- } );
575
- } );
576
-
577
- return wrapper;
578
- }
232
+ class FileLoader extends Observable {
233
+ /**
234
+ * Creates a new instance of `FileLoader`.
235
+ *
236
+ * @param {Promise.<File>} filePromise A promise which resolves to a file instance.
237
+ * @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
238
+ */
239
+ constructor(filePromise, uploadAdapterCreator) {
240
+ super();
241
+ /**
242
+ * Unique id of FileLoader instance.
243
+ *
244
+ * @readonly
245
+ * @member {Number}
246
+ */
247
+ this.id = uid();
248
+ /**
249
+ * Additional wrapper over the initial file promise passed to this loader.
250
+ *
251
+ * @protected
252
+ * @member {module:upload/filerepository~FilePromiseWrapper}
253
+ */
254
+ this._filePromiseWrapper = this._createFilePromiseWrapper(filePromise);
255
+ /**
256
+ * Adapter instance associated with this file loader.
257
+ *
258
+ * @private
259
+ * @member {module:upload/filerepository~UploadAdapter}
260
+ */
261
+ this._adapter = uploadAdapterCreator(this);
262
+ /**
263
+ * FileReader used by FileLoader.
264
+ *
265
+ * @protected
266
+ * @member {module:upload/filereader~FileReader}
267
+ */
268
+ this._reader = new FileReader();
269
+ /**
270
+ * Current status of FileLoader. It can be one of the following:
271
+ *
272
+ * * 'idle',
273
+ * * 'reading',
274
+ * * 'uploading',
275
+ * * 'aborted',
276
+ * * 'error'.
277
+ *
278
+ * When reading status can change in a following way:
279
+ *
280
+ * `idle` -> `reading` -> `idle`
281
+ * `idle` -> `reading -> `aborted`
282
+ * `idle` -> `reading -> `error`
283
+ *
284
+ * When uploading status can change in a following way:
285
+ *
286
+ * `idle` -> `uploading` -> `idle`
287
+ * `idle` -> `uploading` -> `aborted`
288
+ * `idle` -> `uploading` -> `error`
289
+ *
290
+ * @readonly
291
+ * @observable
292
+ * @member {String} #status
293
+ */
294
+ this.set('status', 'idle');
295
+ /**
296
+ * Number of bytes uploaded.
297
+ *
298
+ * @readonly
299
+ * @observable
300
+ * @member {Number} #uploaded
301
+ */
302
+ this.set('uploaded', 0);
303
+ /**
304
+ * Number of total bytes to upload.
305
+ *
306
+ * @readonly
307
+ * @observable
308
+ * @member {Number|null} #uploadTotal
309
+ */
310
+ this.set('uploadTotal', null);
311
+ /**
312
+ * Upload progress in percents.
313
+ *
314
+ * @readonly
315
+ * @observable
316
+ * @member {Number} #uploadedPercent
317
+ */
318
+ this.bind('uploadedPercent').to(this, 'uploaded', this, 'uploadTotal', (uploaded, total) => {
319
+ return total ? (uploaded / total * 100) : 0;
320
+ });
321
+ /**
322
+ * Response of the upload.
323
+ *
324
+ * @readonly
325
+ * @observable
326
+ * @member {Object|null} #uploadResponse
327
+ */
328
+ this.set('uploadResponse', null);
329
+ }
330
+ /**
331
+ * A `Promise` which resolves to a `File` instance associated with this file loader.
332
+ *
333
+ * @type {Promise.<File|null>}
334
+ */
335
+ get file() {
336
+ if (!this._filePromiseWrapper) {
337
+ // Loader was destroyed, return promise which resolves to null.
338
+ return Promise.resolve(null);
339
+ }
340
+ else {
341
+ // The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
342
+ //
343
+ // * The `loader.file.then( ... )` is called by external code (returned promise is pending).
344
+ // * Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
345
+ // * Promise returned by the first `loader.file.then( ... )` call is resolved.
346
+ //
347
+ // Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
348
+ // is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
349
+ return this._filePromiseWrapper.promise.then(file => this._filePromiseWrapper ? file : null);
350
+ }
351
+ }
352
+ /**
353
+ * Returns the file data. To read its data, you need for first load the file
354
+ * by using the {@link module:upload/filerepository~FileLoader#read `read()`} method.
355
+ *
356
+ * @type {File|undefined}
357
+ */
358
+ get data() {
359
+ return this._reader.data;
360
+ }
361
+ /**
362
+ * Reads file using {@link module:upload/filereader~FileReader}.
363
+ *
364
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
365
+ * is different than `idle`.
366
+ *
367
+ * Example usage:
368
+ *
369
+ * fileLoader.read()
370
+ * .then( data => { ... } )
371
+ * .catch( err => {
372
+ * if ( err === 'aborted' ) {
373
+ * console.log( 'Reading aborted.' );
374
+ * } else {
375
+ * console.log( 'Reading error.', err );
376
+ * }
377
+ * } );
378
+ *
379
+ * @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
380
+ * occurs or if read process is aborted.
381
+ */
382
+ read() {
383
+ if (this.status != 'idle') {
384
+ /**
385
+ * You cannot call read if the status is different than idle.
386
+ *
387
+ * @error filerepository-read-wrong-status
388
+ */
389
+ throw new CKEditorError('filerepository-read-wrong-status', this);
390
+ }
391
+ this.status = 'reading';
392
+ return this.file
393
+ .then(file => this._reader.read(file))
394
+ .then(data => {
395
+ // Edge case: reader was aborted after file was read - double check for proper status.
396
+ // It can happen when image was deleted during its upload.
397
+ if (this.status !== 'reading') {
398
+ throw this.status;
399
+ }
400
+ this.status = 'idle';
401
+ return data;
402
+ })
403
+ .catch(err => {
404
+ if (err === 'aborted') {
405
+ this.status = 'aborted';
406
+ throw 'aborted';
407
+ }
408
+ this.status = 'error';
409
+ throw this._reader.error ? this._reader.error : err;
410
+ });
411
+ }
412
+ /**
413
+ * Reads file using the provided {@link module:upload/filerepository~UploadAdapter}.
414
+ *
415
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
416
+ * is different than `idle`.
417
+ * Example usage:
418
+ *
419
+ * fileLoader.upload()
420
+ * .then( data => { ... } )
421
+ * .catch( e => {
422
+ * if ( e === 'aborted' ) {
423
+ * console.log( 'Uploading aborted.' );
424
+ * } else {
425
+ * console.log( 'Uploading error.', e );
426
+ * }
427
+ * } );
428
+ *
429
+ * @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
430
+ * occurs or if read process is aborted.
431
+ */
432
+ upload() {
433
+ if (this.status != 'idle') {
434
+ /**
435
+ * You cannot call upload if the status is different than idle.
436
+ *
437
+ * @error filerepository-upload-wrong-status
438
+ */
439
+ throw new CKEditorError('filerepository-upload-wrong-status', this);
440
+ }
441
+ this.status = 'uploading';
442
+ return this.file
443
+ .then(() => this._adapter.upload())
444
+ .then(data => {
445
+ this.uploadResponse = data;
446
+ this.status = 'idle';
447
+ return data;
448
+ })
449
+ .catch(err => {
450
+ if (this.status === 'aborted') {
451
+ throw 'aborted';
452
+ }
453
+ this.status = 'error';
454
+ throw err;
455
+ });
456
+ }
457
+ /**
458
+ * Aborts loading process.
459
+ */
460
+ abort() {
461
+ const status = this.status;
462
+ this.status = 'aborted';
463
+ if (!this._filePromiseWrapper.isFulfilled) {
464
+ // Edge case: file loader is aborted before read() is called
465
+ // so it might happen that no one handled the rejection of this promise.
466
+ // See https://github.com/ckeditor/ckeditor5-upload/pull/100
467
+ this._filePromiseWrapper.promise.catch(() => { });
468
+ this._filePromiseWrapper.rejecter('aborted');
469
+ }
470
+ else if (status == 'reading') {
471
+ this._reader.abort();
472
+ }
473
+ else if (status == 'uploading' && this._adapter.abort) {
474
+ this._adapter.abort();
475
+ }
476
+ this._destroy();
477
+ }
478
+ /**
479
+ * Performs cleanup.
480
+ *
481
+ * @internal
482
+ */
483
+ _destroy() {
484
+ this._filePromiseWrapper = undefined;
485
+ this._reader = undefined;
486
+ this._adapter = undefined;
487
+ this.uploadResponse = undefined;
488
+ }
489
+ /**
490
+ * Wraps a given file promise into another promise giving additional
491
+ * control (resolving, rejecting, checking if fulfilled) over it.
492
+ *
493
+ * @private
494
+ * @param filePromise The initial file promise to be wrapped.
495
+ * @returns {module:upload/filerepository~FilePromiseWrapper}
496
+ */
497
+ _createFilePromiseWrapper(filePromise) {
498
+ const wrapper = {};
499
+ wrapper.promise = new Promise((resolve, reject) => {
500
+ wrapper.rejecter = reject;
501
+ wrapper.isFulfilled = false;
502
+ filePromise
503
+ .then(file => {
504
+ wrapper.isFulfilled = true;
505
+ resolve(file);
506
+ })
507
+ .catch(err => {
508
+ wrapper.isFulfilled = true;
509
+ reject(err);
510
+ });
511
+ });
512
+ return wrapper;
513
+ }
579
514
  }
580
-
581
- mix( FileLoader, ObservableMixin );
582
-
583
- /**
584
- * Upload adapter interface used by the {@link module:upload/filerepository~FileRepository file repository}
585
- * to handle file upload. An upload adapter is a bridge between the editor and server that handles file uploads.
586
- * It should contain a logic necessary to initiate an upload process and monitor its progress.
587
- *
588
- * Learn how to develop your own upload adapter for CKEditor 5 in the
589
- * {@glink framework/guides/deep-dive/upload-adapter "Custom upload adapter" guide}.
590
- *
591
- * @interface UploadAdapter
592
- */
593
-
594
- /**
595
- * Executes the upload process.
596
- * This method should return a promise that will resolve when data will be uploaded to server. Promise should be
597
- * resolved with an object containing information about uploaded file:
598
- *
599
- * {
600
- * default: 'http://server/default-size.image.png'
601
- * }
602
- *
603
- * Additionally, other image sizes can be provided:
604
- *
605
- * {
606
- * default: 'http://server/default-size.image.png',
607
- * '160': 'http://server/size-160.image.png',
608
- * '500': 'http://server/size-500.image.png',
609
- * '1000': 'http://server/size-1000.image.png',
610
- * '1052': 'http://server/default-size.image.png'
611
- * }
612
- *
613
- * You can also pass additional properties from the server. In this case you need to wrap URLs
614
- * in the `urls` object and pass additional properties along the `urls` property.
615
- *
616
- * {
617
- * myCustomProperty: 'foo',
618
- * urls: {
619
- * default: 'http://server/default-size.image.png',
620
- * '160': 'http://server/size-160.image.png',
621
- * '500': 'http://server/size-500.image.png',
622
- * '1000': 'http://server/size-1000.image.png',
623
- * '1052': 'http://server/default-size.image.png'
624
- * }
625
- * }
626
- *
627
- * NOTE: When returning multiple images, the widest returned one should equal the default one. It is essential to
628
- * correctly set `width` attribute of the image. See this discussion:
629
- * https://github.com/ckeditor/ckeditor5-easy-image/issues/4 for more information.
630
- *
631
- * Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
632
- * {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
633
- *
634
- * @method module:upload/filerepository~UploadAdapter#upload
635
- * @returns {Promise.<Object>} Promise that should be resolved when data is uploaded.
636
- */
637
-
638
- /**
639
- * Aborts the upload process.
640
- * After aborting it should reject promise returned from {@link #upload upload()}.
641
- *
642
- * Take a look at {@link module:upload/filerepository~UploadAdapter example Adapter implementation} and
643
- * {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
644
- *
645
- * @method module:upload/filerepository~UploadAdapter#abort
646
- */
647
-
648
- /**
649
- * Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
650
- * to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
651
- *
652
- * @protected
653
- * @typedef {Object} module:upload/filerepository~FilePromiseWrapper
654
- * @property {Promise.<File>} promise Wrapper promise which can be chained for further processing.
655
- * @property {Function} rejecter Rejects the promise when called.
656
- * @property {Boolean} isFulfilled Whether original promise is already fulfilled.
657
- */