@ckeditor/ckeditor5-upload 35.2.0 → 35.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- */