@ckeditor/ckeditor5-ckbox 36.0.0 → 37.0.0-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,19 +2,16 @@
2
2
  * @license Copyright (c) 2003-2023, 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
  /* globals AbortController, FormData, URL, Image, XMLHttpRequest, window */
7
-
8
6
  /**
9
7
  * @module ckbox/ckboxuploadadapter
10
8
  */
11
-
12
9
  import { Plugin } from 'ckeditor5/src/core';
13
10
  import { FileRepository } from 'ckeditor5/src/upload';
14
11
  import { logError } from 'ckeditor5/src/utils';
15
12
  import CKBoxEditing from './ckboxediting';
16
13
  import { getImageUrls } from './utils';
17
-
14
+ import './ckboxconfig';
18
15
  /**
19
16
  * A plugin that enables file uploads in CKEditor 5 using the CKBox server–side connector.
20
17
  * See the {@glink features/images/image-upload/ckbox CKBox file manager integration} guide to learn how to configure
@@ -23,373 +20,259 @@ import { getImageUrls } from './utils';
23
20
  *
24
21
  * Check out the {@glink features/images/image-upload/image-upload Image upload overview} guide to learn about
25
22
  * other ways to upload images into CKEditor 5.
26
- *
27
- * @extends module:core/plugin~Plugin
28
23
  */
29
24
  export default class CKBoxUploadAdapter extends Plugin {
30
- /**
31
- * @inheritDoc
32
- */
33
- static get requires() {
34
- return [ 'ImageUploadEditing', 'ImageUploadProgress', FileRepository, CKBoxEditing ];
35
- }
36
-
37
- /**
38
- * @inheritDoc
39
- */
40
- static get pluginName() {
41
- return 'CKBoxUploadAdapter';
42
- }
43
-
44
- /**
45
- * @inheritDoc
46
- */
47
- async afterInit() {
48
- const editor = this.editor;
49
-
50
- const hasConfiguration = !!editor.config.get( 'ckbox' );
51
- const isLibraryLoaded = !!window.CKBox;
52
-
53
- // Editor supports only one upload adapter. Register the CKBox upload adapter (and potentially overwrite other one) only when the
54
- // integrator intentionally wants to use the CKBox plugin, i.e. when the `config.ckbox` exists or the CKBox JavaScript library is
55
- // loaded.
56
- if ( !hasConfiguration && !isLibraryLoaded ) {
57
- return;
58
- }
59
-
60
- const fileRepository = editor.plugins.get( FileRepository );
61
- const ckboxEditing = editor.plugins.get( CKBoxEditing );
62
-
63
- fileRepository.createUploadAdapter = loader => {
64
- return new Adapter( loader, ckboxEditing.getToken(), editor );
65
- };
66
-
67
- const shouldInsertDataId = !editor.config.get( 'ckbox.ignoreDataId' );
68
- const imageUploadEditing = editor.plugins.get( 'ImageUploadEditing' );
69
-
70
- // Mark uploaded assets with the `ckboxImageId` attribute. Its value represents an ID in CKBox.
71
- if ( shouldInsertDataId ) {
72
- imageUploadEditing.on( 'uploadComplete', ( evt, { imageElement, data } ) => {
73
- editor.model.change( writer => {
74
- writer.setAttribute( 'ckboxImageId', data.ckboxImageId, imageElement );
75
- } );
76
- } );
77
- }
78
- }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ static get requires() {
29
+ return ['ImageUploadEditing', 'ImageUploadProgress', FileRepository, CKBoxEditing];
30
+ }
31
+ /**
32
+ * @inheritDoc
33
+ */
34
+ static get pluginName() {
35
+ return 'CKBoxUploadAdapter';
36
+ }
37
+ /**
38
+ * @inheritDoc
39
+ */
40
+ async afterInit() {
41
+ const editor = this.editor;
42
+ const hasConfiguration = !!editor.config.get('ckbox');
43
+ const isLibraryLoaded = !!window.CKBox;
44
+ // Editor supports only one upload adapter. Register the CKBox upload adapter (and potentially overwrite other one) only when the
45
+ // integrator intentionally wants to use the CKBox plugin, i.e. when the `config.ckbox` exists or the CKBox JavaScript library is
46
+ // loaded.
47
+ if (!hasConfiguration && !isLibraryLoaded) {
48
+ return;
49
+ }
50
+ const fileRepository = editor.plugins.get(FileRepository);
51
+ const ckboxEditing = editor.plugins.get(CKBoxEditing);
52
+ fileRepository.createUploadAdapter = loader => {
53
+ return new Adapter(loader, ckboxEditing.getToken(), editor);
54
+ };
55
+ const shouldInsertDataId = !editor.config.get('ckbox.ignoreDataId');
56
+ const imageUploadEditing = editor.plugins.get('ImageUploadEditing');
57
+ // Mark uploaded assets with the `ckboxImageId` attribute. Its value represents an ID in CKBox.
58
+ if (shouldInsertDataId) {
59
+ imageUploadEditing.on('uploadComplete', (evt, { imageElement, data }) => {
60
+ editor.model.change(writer => {
61
+ writer.setAttribute('ckboxImageId', data.ckboxImageId, imageElement);
62
+ });
63
+ });
64
+ }
65
+ }
79
66
  }
80
-
81
67
  /**
82
68
  * Upload adapter for CKBox.
83
- *
84
- * @private
85
- * @implements module:upload/filerepository~UploadAdapter
86
69
  */
87
70
  class Adapter {
88
- /**
89
- * Creates a new adapter instance.
90
- *
91
- * @param {module:upload/filerepository~FileLoader} loader
92
- * @param {module:cloud-services/token~Token} token
93
- * @param {module:core/editor/editor~Editor} editor
94
- */
95
- constructor( loader, token, editor ) {
96
- /**
97
- * FileLoader instance to use during the upload.
98
- *
99
- * @member {module:upload/filerepository~FileLoader} #loader
100
- */
101
- this.loader = loader;
102
-
103
- /**
104
- * CKEditor Cloud Services access token.
105
- *
106
- * @member {module:cloud-services/token~Token} #token
107
- */
108
- this.token = token;
109
-
110
- /**
111
- * The editor instance.
112
- *
113
- * @member {module:core/editor/editor~Editor} #editor
114
- */
115
- this.editor = editor;
116
-
117
- /**
118
- * The abort controller for aborting asynchronous processes.
119
- *
120
- * @member {AbortController} #controller
121
- */
122
- this.controller = new AbortController();
123
-
124
- /**
125
- * The base URL where all requests should be sent.
126
- *
127
- * @member {String} #serviceOrigin
128
- */
129
- this.serviceOrigin = editor.config.get( 'ckbox.serviceOrigin' );
130
-
131
- /**
132
- * The base URL from where all assets are served.
133
- *
134
- * @member {String} #assetsOrigin
135
- */
136
- this.assetsOrigin = editor.config.get( 'ckbox.assetsOrigin' );
137
- }
138
-
139
- /**
140
- * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
141
- *
142
- * If the API returns limited results, the method will collect all items.
143
- *
144
- * @param {Number} [offset=0]
145
- * @returns {Promise.<Array>}
146
- */
147
- async getAvailableCategories( offset = 0 ) {
148
- const ITEMS_PER_REQUEST = 50;
149
- const categoryUrl = new URL( 'categories', this.serviceOrigin );
150
-
151
- categoryUrl.searchParams.set( 'limit', ITEMS_PER_REQUEST.toString() );
152
- categoryUrl.searchParams.set( 'offset', offset.toString() );
153
-
154
- return this._sendHttpRequest( { url: categoryUrl } )
155
- .then( async data => {
156
- const remainingItems = data.totalCount - ( offset + ITEMS_PER_REQUEST );
157
-
158
- if ( remainingItems > 0 ) {
159
- const offsetItems = await this.getAvailableCategories( offset + ITEMS_PER_REQUEST );
160
-
161
- return [
162
- ...data.items,
163
- ...offsetItems
164
- ];
165
- }
166
-
167
- return data.items;
168
- } )
169
- .catch( () => {
170
- this.controller.signal.throwIfAborted();
171
-
172
- /**
173
- * Fetching a list of available categories with which an uploaded file can be associated failed.
174
- *
175
- * @error ckbox-fetch-category-http-error
176
- */
177
- logError( 'ckbox-fetch-category-http-error' );
178
- } );
179
- }
180
-
181
- /**
182
- * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
183
- *
184
- * @param {File} file
185
- * @return {Promise.<String|null>}
186
- */
187
- async getCategoryIdForFile( file ) {
188
- const extension = getFileExtension( file.name );
189
- const allCategories = await this.getAvailableCategories();
190
-
191
- // Couldn't fetch all categories. Perhaps the authorization token is invalid.
192
- if ( !allCategories ) {
193
- return null;
194
- }
195
-
196
- // The plugin allows defining to which category the uploaded file should be assigned.
197
- const defaultCategories = this.editor.config.get( 'ckbox.defaultUploadCategories' );
198
-
199
- // If a user specifies the plugin configuration, find the first category that accepts the uploaded file.
200
- if ( defaultCategories ) {
201
- const userCategory = Object.keys( defaultCategories ).find( category => {
202
- return defaultCategories[ category ].includes( extension );
203
- } );
204
-
205
- // If found, return its ID if the category exists on the server side.
206
- if ( userCategory ) {
207
- const serverCategory = allCategories.find( category => category.id === userCategory || category.name === userCategory );
208
-
209
- if ( !serverCategory ) {
210
- return null;
211
- }
212
-
213
- return serverCategory.id;
214
- }
215
- }
216
-
217
- // Otherwise, find the first category that accepts the uploaded file and returns its ID.
218
- const category = allCategories.find( category => category.extensions.includes( extension ) );
219
-
220
- if ( !category ) {
221
- return null;
222
- }
223
-
224
- return category.id;
225
- }
226
-
227
- /**
228
- * Starts the upload process.
229
- *
230
- * @see module:upload/filerepository~UploadAdapter#upload
231
- * @returns {Promise.<Object>}
232
- */
233
- async upload() {
234
- const t = this.editor.t;
235
- const cannotFindCategoryError = t( 'Cannot determine a category for the uploaded file.' );
236
- const file = await this.loader.file;
237
- const category = await this.getCategoryIdForFile( file );
238
-
239
- if ( !category ) {
240
- return Promise.reject( cannotFindCategoryError );
241
- }
242
-
243
- const uploadUrl = new URL( 'assets', this.serviceOrigin );
244
- const formData = new FormData();
245
-
246
- formData.append( 'categoryId', category );
247
- formData.append( 'file', file );
248
-
249
- const requestConfig = {
250
- method: 'POST',
251
- url: uploadUrl,
252
- data: formData,
253
- onUploadProgress: evt => {
254
- /* istanbul ignore else */
255
- if ( evt.lengthComputable ) {
256
- this.loader.uploadTotal = evt.total;
257
- this.loader.uploaded = evt.loaded;
258
- }
259
- }
260
- };
261
-
262
- return this._sendHttpRequest( requestConfig )
263
- .then( async data => {
264
- const width = await this._getImageWidth();
265
- const extension = getFileExtension( file.name );
266
- const imageUrls = getImageUrls( {
267
- token: this.token,
268
- id: data.id,
269
- origin: this.assetsOrigin,
270
- width,
271
- extension
272
- } );
273
-
274
- return {
275
- ckboxImageId: data.id,
276
- default: imageUrls.imageFallbackUrl,
277
- sources: imageUrls.imageSources
278
- };
279
- } )
280
- .catch( () => {
281
- const genericError = t( 'Cannot upload file:' ) + ` ${ file.name }.`;
282
-
283
- return Promise.reject( genericError );
284
- } );
285
- }
286
-
287
- /**
288
- * Aborts the upload process.
289
- *
290
- * @see module:upload/filerepository~UploadAdapter#abort
291
- */
292
- abort() {
293
- this.controller.abort();
294
- }
295
-
296
- /**
297
- * Sends the HTTP request.
298
- *
299
- * @protected
300
- * @param {URL} config.url the URL where the request will be sent.
301
- * @param {'GET'|'POST'} [config.method='GET'] The HTTP method.
302
- * @param {FormData|null} [config.data] Additional data to send.
303
- * @param {Function} [config.onUploadProgress] A callback informing about the upload progress.
304
- * @returns {Promise}
305
- */
306
- _sendHttpRequest( config ) {
307
- const { url, data, onUploadProgress } = config;
308
- const method = config.method || 'GET';
309
- const signal = this.controller.signal;
310
-
311
- const xhr = new XMLHttpRequest();
312
- xhr.open( method, url.toString(), true );
313
- xhr.setRequestHeader( 'Authorization', this.token.value );
314
- xhr.setRequestHeader( 'CKBox-Version', 'CKEditor 5' );
315
- xhr.responseType = 'json';
316
-
317
- // The callback is attached to the `signal#abort` event.
318
- const abortCallback = () => {
319
- xhr.abort();
320
- };
321
-
322
- return new Promise( ( resolve, reject ) => {
323
- signal.addEventListener( 'abort', abortCallback );
324
-
325
- xhr.addEventListener( 'loadstart', () => {
326
- signal.addEventListener( 'abort', abortCallback );
327
- } );
328
-
329
- xhr.addEventListener( 'loadend', () => {
330
- signal.removeEventListener( 'abort', abortCallback );
331
- } );
332
-
333
- xhr.addEventListener( 'error', () => {
334
- reject();
335
- } );
336
-
337
- xhr.addEventListener( 'abort', () => {
338
- reject();
339
- } );
340
-
341
- xhr.addEventListener( 'load', async () => {
342
- const response = xhr.response;
343
-
344
- if ( !response || response.statusCode >= 400 ) {
345
- return reject( response && response.message );
346
- }
347
-
348
- return resolve( response );
349
- } );
350
-
351
- /* istanbul ignore else */
352
- if ( onUploadProgress ) {
353
- xhr.upload.addEventListener( 'progress', evt => {
354
- onUploadProgress( evt );
355
- } );
356
- }
357
-
358
- // Send the request.
359
- xhr.send( data );
360
- } );
361
- }
362
-
363
- /**
364
- * Resolves a promise with a number representing the width of a given image file.
365
- *
366
- * @protected
367
- * @returns {Promise.<Number>}
368
- */
369
- _getImageWidth() {
370
- return new Promise( resolve => {
371
- const image = new Image();
372
-
373
- image.onload = () => {
374
- // Let the browser know that it should not keep the reference any longer to avoid memory leeks.
375
- URL.revokeObjectURL( image.src );
376
-
377
- resolve( image.width );
378
- };
379
-
380
- image.src = this.loader.data;
381
- } );
382
- }
71
+ /**
72
+ * Creates a new adapter instance.
73
+ */
74
+ constructor(loader, token, editor) {
75
+ this.loader = loader;
76
+ this.token = token;
77
+ this.editor = editor;
78
+ this.controller = new AbortController();
79
+ this.serviceOrigin = editor.config.get('ckbox.serviceOrigin');
80
+ this.assetsOrigin = editor.config.get('ckbox.assetsOrigin');
81
+ }
82
+ /**
83
+ * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
84
+ *
85
+ * If the API returns limited results, the method will collect all items.
86
+ */
87
+ async getAvailableCategories(offset = 0) {
88
+ const ITEMS_PER_REQUEST = 50;
89
+ const categoryUrl = new URL('categories', this.serviceOrigin);
90
+ categoryUrl.searchParams.set('limit', ITEMS_PER_REQUEST.toString());
91
+ categoryUrl.searchParams.set('offset', offset.toString());
92
+ return this._sendHttpRequest({ url: categoryUrl })
93
+ .then(async (data) => {
94
+ const remainingItems = data.totalCount - (offset + ITEMS_PER_REQUEST);
95
+ if (remainingItems > 0) {
96
+ const offsetItems = await this.getAvailableCategories(offset + ITEMS_PER_REQUEST);
97
+ return [
98
+ ...data.items,
99
+ ...offsetItems
100
+ ];
101
+ }
102
+ return data.items;
103
+ })
104
+ .catch(() => {
105
+ this.controller.signal.throwIfAborted();
106
+ /**
107
+ * Fetching a list of available categories with which an uploaded file can be associated failed.
108
+ *
109
+ * @error ckbox-fetch-category-http-error
110
+ */
111
+ logError('ckbox-fetch-category-http-error');
112
+ });
113
+ }
114
+ /**
115
+ * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
116
+ */
117
+ async getCategoryIdForFile(file) {
118
+ const extension = getFileExtension(file.name);
119
+ const allCategories = await this.getAvailableCategories();
120
+ // Couldn't fetch all categories. Perhaps the authorization token is invalid.
121
+ if (!allCategories) {
122
+ return null;
123
+ }
124
+ // The plugin allows defining to which category the uploaded file should be assigned.
125
+ const defaultCategories = this.editor.config.get('ckbox.defaultUploadCategories');
126
+ // If a user specifies the plugin configuration, find the first category that accepts the uploaded file.
127
+ if (defaultCategories) {
128
+ const userCategory = Object.keys(defaultCategories).find(category => {
129
+ return defaultCategories[category].includes(extension);
130
+ });
131
+ // If found, return its ID if the category exists on the server side.
132
+ if (userCategory) {
133
+ const serverCategory = allCategories.find(category => category.id === userCategory || category.name === userCategory);
134
+ if (!serverCategory) {
135
+ return null;
136
+ }
137
+ return serverCategory.id;
138
+ }
139
+ }
140
+ // Otherwise, find the first category that accepts the uploaded file and returns its ID.
141
+ const category = allCategories.find(category => category.extensions.includes(extension));
142
+ if (!category) {
143
+ return null;
144
+ }
145
+ return category.id;
146
+ }
147
+ /**
148
+ * Starts the upload process.
149
+ *
150
+ * @see module:upload/filerepository~UploadAdapter#upload
151
+ */
152
+ async upload() {
153
+ const t = this.editor.t;
154
+ const cannotFindCategoryError = t('Cannot determine a category for the uploaded file.');
155
+ const file = (await this.loader.file);
156
+ const category = await this.getCategoryIdForFile(file);
157
+ if (!category) {
158
+ return Promise.reject(cannotFindCategoryError);
159
+ }
160
+ const uploadUrl = new URL('assets', this.serviceOrigin);
161
+ const formData = new FormData();
162
+ formData.append('categoryId', category);
163
+ formData.append('file', file);
164
+ const requestConfig = {
165
+ method: 'POST',
166
+ url: uploadUrl,
167
+ data: formData,
168
+ onUploadProgress: (evt) => {
169
+ /* istanbul ignore else */
170
+ if (evt.lengthComputable) {
171
+ this.loader.uploadTotal = evt.total;
172
+ this.loader.uploaded = evt.loaded;
173
+ }
174
+ }
175
+ };
176
+ return this._sendHttpRequest(requestConfig)
177
+ .then(async (data) => {
178
+ const width = await this._getImageWidth();
179
+ const extension = getFileExtension(file.name);
180
+ const imageUrls = getImageUrls({
181
+ token: this.token,
182
+ id: data.id,
183
+ origin: this.assetsOrigin,
184
+ width,
185
+ extension
186
+ });
187
+ return {
188
+ ckboxImageId: data.id,
189
+ default: imageUrls.imageFallbackUrl,
190
+ sources: imageUrls.imageSources
191
+ };
192
+ })
193
+ .catch(() => {
194
+ const genericError = t('Cannot upload file:') + ` ${file.name}.`;
195
+ return Promise.reject(genericError);
196
+ });
197
+ }
198
+ /**
199
+ * Aborts the upload process.
200
+ *
201
+ * @see module:upload/filerepository~UploadAdapter#abort
202
+ */
203
+ abort() {
204
+ this.controller.abort();
205
+ }
206
+ /**
207
+ * Sends the HTTP request.
208
+ *
209
+ * @param config.url the URL where the request will be sent.
210
+ * @param config.method The HTTP method.
211
+ * @param config.data Additional data to send.
212
+ * @param config.onUploadProgress A callback informing about the upload progress.
213
+ */
214
+ _sendHttpRequest({ url, method = 'GET', data, onUploadProgress }) {
215
+ const signal = this.controller.signal;
216
+ const xhr = new XMLHttpRequest();
217
+ xhr.open(method, url.toString(), true);
218
+ xhr.setRequestHeader('Authorization', this.token.value);
219
+ xhr.setRequestHeader('CKBox-Version', 'CKEditor 5');
220
+ xhr.responseType = 'json';
221
+ // The callback is attached to the `signal#abort` event.
222
+ const abortCallback = () => {
223
+ xhr.abort();
224
+ };
225
+ return new Promise((resolve, reject) => {
226
+ signal.addEventListener('abort', abortCallback);
227
+ xhr.addEventListener('loadstart', () => {
228
+ signal.addEventListener('abort', abortCallback);
229
+ });
230
+ xhr.addEventListener('loadend', () => {
231
+ signal.removeEventListener('abort', abortCallback);
232
+ });
233
+ xhr.addEventListener('error', () => {
234
+ reject();
235
+ });
236
+ xhr.addEventListener('abort', () => {
237
+ reject();
238
+ });
239
+ xhr.addEventListener('load', async () => {
240
+ const response = xhr.response;
241
+ if (!response || response.statusCode >= 400) {
242
+ return reject(response && response.message);
243
+ }
244
+ return resolve(response);
245
+ });
246
+ /* istanbul ignore else */
247
+ if (onUploadProgress) {
248
+ xhr.upload.addEventListener('progress', evt => {
249
+ onUploadProgress(evt);
250
+ });
251
+ }
252
+ // Send the request.
253
+ xhr.send(data);
254
+ });
255
+ }
256
+ /**
257
+ * Resolves a promise with a number representing the width of a given image file.
258
+ */
259
+ _getImageWidth() {
260
+ return new Promise(resolve => {
261
+ const image = new Image();
262
+ image.onload = () => {
263
+ // Let the browser know that it should not keep the reference any longer to avoid memory leeks.
264
+ URL.revokeObjectURL(image.src);
265
+ resolve(image.width);
266
+ };
267
+ image.src = this.loader.data;
268
+ });
269
+ }
383
270
  }
384
-
385
- // Returns an extension from the given value.
386
- //
387
- // @private
388
- // @param {String} value
389
- // @returns {String}
390
- function getFileExtension( value ) {
391
- const extensionRegExp = /\.(?<ext>[^.]+)$/;
392
- const match = value.match( extensionRegExp );
393
-
394
- return match.groups.ext;
271
+ /**
272
+ * Returns an extension from the given value.
273
+ */
274
+ function getFileExtension(value) {
275
+ const extensionRegExp = /\.(?<ext>[^.]+)$/;
276
+ const match = value.match(extensionRegExp);
277
+ return match.groups.ext;
395
278
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ckbox
7
+ */
8
+ export { default as CKBox } from './ckbox';
9
+ export { default as CKBoxEditing } from './ckboxediting';
10
+ export { default as CKBoxUI } from './ckboxui';