@ckeditor/ckeditor5-ckbox 40.1.0 → 40.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. package/build/ckbox.js +2 -2
  2. package/build/translations/ar.js +1 -1
  3. package/build/translations/az.js +1 -1
  4. package/build/translations/bg.js +1 -1
  5. package/build/translations/bn.js +1 -1
  6. package/build/translations/ca.js +1 -1
  7. package/build/translations/cs.js +1 -1
  8. package/build/translations/da.js +1 -1
  9. package/build/translations/de.js +1 -1
  10. package/build/translations/el.js +1 -1
  11. package/build/translations/en-au.js +1 -1
  12. package/build/translations/es-co.js +1 -1
  13. package/build/translations/es.js +1 -1
  14. package/build/translations/et.js +1 -1
  15. package/build/translations/fa.js +1 -1
  16. package/build/translations/fi.js +1 -1
  17. package/build/translations/fr.js +1 -1
  18. package/build/translations/gl.js +1 -1
  19. package/build/translations/he.js +1 -1
  20. package/build/translations/hi.js +1 -1
  21. package/build/translations/hr.js +1 -1
  22. package/build/translations/hu.js +1 -1
  23. package/build/translations/id.js +1 -1
  24. package/build/translations/it.js +1 -1
  25. package/build/translations/ja.js +1 -1
  26. package/build/translations/ko.js +1 -1
  27. package/build/translations/lt.js +1 -1
  28. package/build/translations/lv.js +1 -1
  29. package/build/translations/ms.js +1 -1
  30. package/build/translations/nl.js +1 -1
  31. package/build/translations/no.js +1 -1
  32. package/build/translations/pl.js +1 -1
  33. package/build/translations/pt-br.js +1 -1
  34. package/build/translations/pt.js +1 -1
  35. package/build/translations/ro.js +1 -1
  36. package/build/translations/ru.js +1 -1
  37. package/build/translations/sk.js +1 -1
  38. package/build/translations/sq.js +1 -1
  39. package/build/translations/sr-latn.js +1 -1
  40. package/build/translations/sr.js +1 -1
  41. package/build/translations/sv.js +1 -1
  42. package/build/translations/th.js +1 -1
  43. package/build/translations/tr.js +1 -1
  44. package/build/translations/ug.js +1 -1
  45. package/build/translations/uk.js +1 -1
  46. package/build/translations/ur.js +1 -1
  47. package/build/translations/uz.js +1 -1
  48. package/build/translations/vi.js +1 -1
  49. package/build/translations/zh-cn.js +1 -1
  50. package/build/translations/zh.js +1 -1
  51. package/ckeditor5-metadata.json +17 -0
  52. package/lang/contexts.json +6 -2
  53. package/lang/translations/ar.po +18 -2
  54. package/lang/translations/az.po +18 -2
  55. package/lang/translations/bg.po +18 -2
  56. package/lang/translations/bn.po +18 -2
  57. package/lang/translations/ca.po +18 -2
  58. package/lang/translations/cs.po +18 -2
  59. package/lang/translations/da.po +18 -2
  60. package/lang/translations/de.po +18 -2
  61. package/lang/translations/el.po +18 -2
  62. package/lang/translations/en-au.po +18 -2
  63. package/lang/translations/en.po +18 -2
  64. package/lang/translations/es-co.po +18 -2
  65. package/lang/translations/es.po +18 -2
  66. package/lang/translations/et.po +18 -2
  67. package/lang/translations/fa.po +18 -2
  68. package/lang/translations/fi.po +18 -2
  69. package/lang/translations/fr.po +18 -2
  70. package/lang/translations/gl.po +18 -2
  71. package/lang/translations/he.po +18 -2
  72. package/lang/translations/hi.po +18 -2
  73. package/lang/translations/hr.po +18 -2
  74. package/lang/translations/hu.po +18 -2
  75. package/lang/translations/id.po +18 -2
  76. package/lang/translations/it.po +18 -2
  77. package/lang/translations/ja.po +18 -2
  78. package/lang/translations/ko.po +18 -2
  79. package/lang/translations/lt.po +18 -2
  80. package/lang/translations/lv.po +18 -2
  81. package/lang/translations/ms.po +18 -2
  82. package/lang/translations/nl.po +18 -2
  83. package/lang/translations/no.po +18 -2
  84. package/lang/translations/pl.po +18 -2
  85. package/lang/translations/pt-br.po +18 -2
  86. package/lang/translations/pt.po +18 -2
  87. package/lang/translations/ro.po +18 -2
  88. package/lang/translations/ru.po +18 -2
  89. package/lang/translations/sk.po +18 -2
  90. package/lang/translations/sq.po +18 -2
  91. package/lang/translations/sr-latn.po +18 -2
  92. package/lang/translations/sr.po +18 -2
  93. package/lang/translations/sv.po +18 -2
  94. package/lang/translations/th.po +18 -2
  95. package/lang/translations/tr.po +18 -2
  96. package/lang/translations/ug.po +18 -2
  97. package/lang/translations/uk.po +18 -2
  98. package/lang/translations/ur.po +18 -2
  99. package/lang/translations/uz.po +18 -2
  100. package/lang/translations/vi.po +18 -2
  101. package/lang/translations/zh-cn.po +18 -2
  102. package/lang/translations/zh.po +18 -2
  103. package/package.json +4 -3
  104. package/src/augmentation.d.ts +11 -1
  105. package/src/ckboxcommand.d.ts +8 -6
  106. package/src/ckboxcommand.js +5 -1
  107. package/src/ckboxconfig.d.ts +26 -0
  108. package/src/ckboxediting.d.ts +5 -12
  109. package/src/ckboxediting.js +7 -55
  110. package/src/ckboximageedit/ckboximageeditcommand.d.ts +97 -0
  111. package/src/ckboximageedit/ckboximageeditcommand.js +298 -0
  112. package/src/ckboximageedit/ckboximageeditediting.d.ts +28 -0
  113. package/src/ckboximageedit/ckboximageeditediting.js +36 -0
  114. package/src/ckboximageedit/ckboximageeditui.d.ts +24 -0
  115. package/src/ckboximageedit/ckboximageeditui.js +48 -0
  116. package/src/ckboximageedit/utils.d.ts +10 -0
  117. package/src/ckboximageedit/utils.js +48 -0
  118. package/src/ckboximageedit.d.ts +24 -0
  119. package/src/ckboximageedit.js +28 -0
  120. package/src/ckboxui.js +28 -1
  121. package/src/ckboxuploadadapter.d.ts +0 -5
  122. package/src/ckboxuploadadapter.js +15 -160
  123. package/src/ckboxutils.d.ts +50 -0
  124. package/src/ckboxutils.js +183 -0
  125. package/src/index.d.ts +4 -0
  126. package/src/index.js +3 -0
  127. package/src/utils.d.ts +31 -0
  128. package/src/utils.js +93 -0
  129. package/theme/ckboximageedit.css +53 -0
  130. package/theme/icons/ckbox-image-edit.svg +1 -0
@@ -0,0 +1,48 @@
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/ckboximageedit/utils
7
+ */
8
+ import { global } from 'ckeditor5/src/utils';
9
+ /**
10
+ * @internal
11
+ */
12
+ export function createEditabilityChecker(allowExternalImagesEditing) {
13
+ const checkUrl = createUrlChecker(allowExternalImagesEditing);
14
+ return element => {
15
+ const isImageElement = element.is('element', 'imageInline') ||
16
+ element.is('element', 'imageBlock');
17
+ if (!isImageElement) {
18
+ return false;
19
+ }
20
+ if (element.hasAttribute('ckboxImageId')) {
21
+ return true;
22
+ }
23
+ if (element.hasAttribute('src')) {
24
+ return checkUrl(element.getAttribute('src'));
25
+ }
26
+ return false;
27
+ };
28
+ }
29
+ function createUrlChecker(allowExternalImagesEditing) {
30
+ if (Array.isArray(allowExternalImagesEditing)) {
31
+ const urlMatchers = allowExternalImagesEditing.map(createUrlChecker);
32
+ return src => urlMatchers.some(matcher => matcher(src));
33
+ }
34
+ if (allowExternalImagesEditing == 'origin') {
35
+ const origin = global.window.location.origin;
36
+ return src => new URL(src, global.document.baseURI).origin == origin;
37
+ }
38
+ if (typeof allowExternalImagesEditing == 'function') {
39
+ return allowExternalImagesEditing;
40
+ }
41
+ if (allowExternalImagesEditing instanceof RegExp) {
42
+ return src => !!(src.match(allowExternalImagesEditing) ||
43
+ src.replace(/^https?:\/\//, '').match(allowExternalImagesEditing));
44
+ }
45
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
46
+ const shouldBeUndefned = allowExternalImagesEditing;
47
+ return () => false;
48
+ }
@@ -0,0 +1,24 @@
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/ckboximageedit
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core';
9
+ import CKBoxImageEditEditing from './ckboximageedit/ckboximageeditediting';
10
+ import CKBoxImageEditUI from './ckboximageedit/ckboximageeditui';
11
+ import '../theme/ckboximageedit.css';
12
+ /**
13
+ * The CKBox image edit feature.
14
+ */
15
+ export default class CKBoxImageEdit extends Plugin {
16
+ /**
17
+ * @inheritDoc
18
+ */
19
+ static get pluginName(): "CKBoxImageEdit";
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ static get requires(): readonly [typeof CKBoxImageEditEditing, typeof CKBoxImageEditUI];
24
+ }
@@ -0,0 +1,28 @@
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/ckboximageedit
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core';
9
+ import CKBoxImageEditEditing from './ckboximageedit/ckboximageeditediting';
10
+ import CKBoxImageEditUI from './ckboximageedit/ckboximageeditui';
11
+ import '../theme/ckboximageedit.css';
12
+ /**
13
+ * The CKBox image edit feature.
14
+ */
15
+ export default class CKBoxImageEdit extends Plugin {
16
+ /**
17
+ * @inheritDoc
18
+ */
19
+ static get pluginName() {
20
+ return 'CKBoxImageEdit';
21
+ }
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires() {
26
+ return [CKBoxImageEditEditing, CKBoxImageEditUI];
27
+ }
28
+ }
package/src/ckboxui.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @module ckbox/ckboxui
7
7
  */
8
- import { Plugin } from 'ckeditor5/src/core';
8
+ import { icons, Plugin } from 'ckeditor5/src/core';
9
9
  import { ButtonView } from 'ckeditor5/src/ui';
10
10
  import browseFilesIcon from '../theme/icons/browse-files.svg';
11
11
  /**
@@ -43,5 +43,32 @@ export default class CKBoxUI extends Plugin {
43
43
  });
44
44
  return button;
45
45
  });
46
+ if (editor.plugins.has('ImageInsertUI')) {
47
+ const imageInsertUI = editor.plugins.get('ImageInsertUI');
48
+ imageInsertUI.registerIntegration({
49
+ name: 'assetManager',
50
+ observable: command,
51
+ buttonViewCreator: () => {
52
+ const button = this.editor.ui.componentFactory.create('ckbox');
53
+ button.icon = icons.imageAssetManager;
54
+ button.bind('label').to(imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ?
55
+ t('Replace image with file manager') :
56
+ t('Insert image with file manager'));
57
+ return button;
58
+ },
59
+ formViewCreator: () => {
60
+ const button = this.editor.ui.componentFactory.create('ckbox');
61
+ button.icon = icons.imageAssetManager;
62
+ button.withText = true;
63
+ button.bind('label').to(imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ?
64
+ t('Replace with file manager') :
65
+ t('Insert with file manager'));
66
+ button.on('execute', () => {
67
+ imageInsertUI.dropdownView.isOpen = false;
68
+ });
69
+ return button;
70
+ }
71
+ });
72
+ }
46
73
  }
47
74
  }
@@ -31,8 +31,3 @@ export default class CKBoxUploadAdapter extends Plugin {
31
31
  */
32
32
  afterInit(): Promise<void>;
33
33
  }
34
- export interface AvailableCategory {
35
- id: string;
36
- name: string;
37
- extensions: Array<string>;
38
- }
@@ -2,15 +2,15 @@
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
- /* globals AbortController, FormData, URL, XMLHttpRequest, window */
5
+ /* globals AbortController, FormData, URL, window */
6
6
  /**
7
7
  * @module ckbox/ckboxuploadadapter
8
8
  */
9
9
  import { Plugin } from 'ckeditor5/src/core';
10
10
  import { FileRepository } from 'ckeditor5/src/upload';
11
- import { logError } from 'ckeditor5/src/utils';
12
11
  import CKBoxEditing from './ckboxediting';
13
- import { getImageUrls, getWorkspaceId } from './utils';
12
+ import { getImageUrls, sendHttpRequest } from './utils';
13
+ import CKBoxUtils from './ckboxutils';
14
14
  /**
15
15
  * A plugin that enables file uploads in CKEditor 5 using the CKBox server–side connector.
16
16
  * See the {@glink features/file-management/ckbox CKBox file manager integration} guide to learn how to configure
@@ -47,10 +47,8 @@ export default class CKBoxUploadAdapter extends Plugin {
47
47
  return;
48
48
  }
49
49
  const fileRepository = editor.plugins.get(FileRepository);
50
- const ckboxEditing = editor.plugins.get(CKBoxEditing);
51
- fileRepository.createUploadAdapter = loader => {
52
- return new Adapter(loader, ckboxEditing.getToken(), editor);
53
- };
50
+ const ckboxUtils = editor.plugins.get(CKBoxUtils);
51
+ fileRepository.createUploadAdapter = loader => new Adapter(loader, editor, ckboxUtils);
54
52
  const shouldInsertDataId = !editor.config.get('ckbox.ignoreDataId');
55
53
  const imageUploadEditing = editor.plugins.get('ImageUploadEditing');
56
54
  // Mark uploaded assets with the `ckboxImageId` attribute. Its value represents an ID in CKBox.
@@ -70,114 +68,27 @@ class Adapter {
70
68
  /**
71
69
  * Creates a new adapter instance.
72
70
  */
73
- constructor(loader, token, editor) {
71
+ constructor(loader, editor, ckboxUtils) {
74
72
  this.loader = loader;
75
- this.token = token;
73
+ this.token = ckboxUtils.getToken();
74
+ this.ckboxUtils = ckboxUtils;
76
75
  this.editor = editor;
77
76
  this.controller = new AbortController();
78
77
  this.serviceOrigin = editor.config.get('ckbox.serviceOrigin');
79
78
  }
80
- /**
81
- * The ID of workspace to use.
82
- */
83
- getWorkspaceId() {
84
- const t = this.editor.t;
85
- const cannotAccessDefaultWorkspaceError = t('Cannot access default workspace.');
86
- const defaultWorkspaceId = this.editor.config.get('ckbox.defaultUploadWorkspaceId');
87
- const workspaceId = getWorkspaceId(this.token, defaultWorkspaceId);
88
- if (workspaceId == null) {
89
- /**
90
- * The user is not authorized to access the workspace defined in the`ckbox.defaultUploadWorkspaceId` configuration.
91
- *
92
- * @error ckbox-access-default-workspace-error
93
- */
94
- logError('ckbox-access-default-workspace-error');
95
- throw cannotAccessDefaultWorkspaceError;
96
- }
97
- return workspaceId;
98
- }
99
- /**
100
- * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
101
- *
102
- * If the API returns limited results, the method will collect all items.
103
- */
104
- async getAvailableCategories(offset = 0) {
105
- const ITEMS_PER_REQUEST = 50;
106
- const categoryUrl = new URL('categories', this.serviceOrigin);
107
- categoryUrl.searchParams.set('limit', ITEMS_PER_REQUEST.toString());
108
- categoryUrl.searchParams.set('offset', offset.toString());
109
- categoryUrl.searchParams.set('workspaceId', this.getWorkspaceId());
110
- return this._sendHttpRequest({ url: categoryUrl })
111
- .then(async (data) => {
112
- const remainingItems = data.totalCount - (offset + ITEMS_PER_REQUEST);
113
- if (remainingItems > 0) {
114
- const offsetItems = await this.getAvailableCategories(offset + ITEMS_PER_REQUEST);
115
- return [
116
- ...data.items,
117
- ...offsetItems
118
- ];
119
- }
120
- return data.items;
121
- })
122
- .catch(() => {
123
- this.controller.signal.throwIfAborted();
124
- /**
125
- * Fetching a list of available categories with which an uploaded file can be associated failed.
126
- *
127
- * @error ckbox-fetch-category-http-error
128
- */
129
- logError('ckbox-fetch-category-http-error');
130
- });
131
- }
132
- /**
133
- * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
134
- */
135
- async getCategoryIdForFile(file) {
136
- const extension = getFileExtension(file.name);
137
- const allCategories = await this.getAvailableCategories();
138
- // Couldn't fetch all categories. Perhaps the authorization token is invalid.
139
- if (!allCategories) {
140
- return null;
141
- }
142
- // The plugin allows defining to which category the uploaded file should be assigned.
143
- const defaultCategories = this.editor.config.get('ckbox.defaultUploadCategories');
144
- // If a user specifies the plugin configuration, find the first category that accepts the uploaded file.
145
- if (defaultCategories) {
146
- const userCategory = Object.keys(defaultCategories).find(category => {
147
- return defaultCategories[category].find(e => e.toLowerCase() == extension);
148
- });
149
- // If found, return its ID if the category exists on the server side.
150
- if (userCategory) {
151
- const serverCategory = allCategories.find(category => category.id === userCategory || category.name === userCategory);
152
- if (!serverCategory) {
153
- return null;
154
- }
155
- return serverCategory.id;
156
- }
157
- }
158
- // Otherwise, find the first category that accepts the uploaded file and returns its ID.
159
- const category = allCategories.find(category => category.extensions.find(e => e.toLowerCase() == extension));
160
- if (!category) {
161
- return null;
162
- }
163
- return category.id;
164
- }
165
79
  /**
166
80
  * Starts the upload process.
167
81
  *
168
82
  * @see module:upload/filerepository~UploadAdapter#upload
169
83
  */
170
84
  async upload() {
85
+ const ckboxUtils = this.ckboxUtils;
171
86
  const t = this.editor.t;
172
- const cannotFindCategoryError = t('Cannot determine a category for the uploaded file.');
173
87
  const file = (await this.loader.file);
174
- const category = await this.getCategoryIdForFile(file);
175
- if (!category) {
176
- return Promise.reject(cannotFindCategoryError);
177
- }
88
+ const category = await ckboxUtils.getCategoryIdForFile(file, { signal: this.controller.signal });
178
89
  const uploadUrl = new URL('assets', this.serviceOrigin);
179
90
  const formData = new FormData();
180
- uploadUrl.searchParams.set('workspaceId', this.getWorkspaceId());
91
+ uploadUrl.searchParams.set('workspaceId', ckboxUtils.getWorkspaceId());
181
92
  formData.append('categoryId', category);
182
93
  formData.append('file', file);
183
94
  const requestConfig = {
@@ -190,9 +101,11 @@ class Adapter {
190
101
  this.loader.uploadTotal = evt.total;
191
102
  this.loader.uploaded = evt.loaded;
192
103
  }
193
- }
104
+ },
105
+ signal: this.controller.signal,
106
+ authorization: this.token.value
194
107
  };
195
- return this._sendHttpRequest(requestConfig)
108
+ return sendHttpRequest(requestConfig)
196
109
  .then(async (data) => {
197
110
  const imageUrls = getImageUrls(data.imageUrls);
198
111
  return {
@@ -214,62 +127,4 @@ class Adapter {
214
127
  abort() {
215
128
  this.controller.abort();
216
129
  }
217
- /**
218
- * Sends the HTTP request.
219
- *
220
- * @param config.url the URL where the request will be sent.
221
- * @param config.method The HTTP method.
222
- * @param config.data Additional data to send.
223
- * @param config.onUploadProgress A callback informing about the upload progress.
224
- */
225
- _sendHttpRequest({ url, method = 'GET', data, onUploadProgress }) {
226
- const signal = this.controller.signal;
227
- const xhr = new XMLHttpRequest();
228
- xhr.open(method, url.toString(), true);
229
- xhr.setRequestHeader('Authorization', this.token.value);
230
- xhr.setRequestHeader('CKBox-Version', 'CKEditor 5');
231
- xhr.responseType = 'json';
232
- // The callback is attached to the `signal#abort` event.
233
- const abortCallback = () => {
234
- xhr.abort();
235
- };
236
- return new Promise((resolve, reject) => {
237
- signal.addEventListener('abort', abortCallback);
238
- xhr.addEventListener('loadstart', () => {
239
- signal.addEventListener('abort', abortCallback);
240
- });
241
- xhr.addEventListener('loadend', () => {
242
- signal.removeEventListener('abort', abortCallback);
243
- });
244
- xhr.addEventListener('error', () => {
245
- reject();
246
- });
247
- xhr.addEventListener('abort', () => {
248
- reject();
249
- });
250
- xhr.addEventListener('load', async () => {
251
- const response = xhr.response;
252
- if (!response || response.statusCode >= 400) {
253
- return reject(response && response.message);
254
- }
255
- return resolve(response);
256
- });
257
- /* istanbul ignore else -- @preserve */
258
- if (onUploadProgress) {
259
- xhr.upload.addEventListener('progress', evt => {
260
- onUploadProgress(evt);
261
- });
262
- }
263
- // Send the request.
264
- xhr.send(data);
265
- });
266
- }
267
- }
268
- /**
269
- * Returns an extension from the given value.
270
- */
271
- function getFileExtension(value) {
272
- const extensionRegExp = /\.(?<ext>[^.]+)$/;
273
- const match = value.match(extensionRegExp);
274
- return match.groups.ext.toLowerCase();
275
130
  }
@@ -0,0 +1,50 @@
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/ckboxutils
7
+ */
8
+ import type { InitializedToken } from '@ckeditor/ckeditor5-cloud-services';
9
+ import { Plugin } from 'ckeditor5/src/core';
10
+ /**
11
+ * The CKBox utilities plugin.
12
+ */
13
+ export default class CKBoxUtils extends Plugin {
14
+ /**
15
+ * CKEditor Cloud Services access token.
16
+ */
17
+ private _token;
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ static get pluginName(): "CKBoxUtils";
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires(): readonly ["CloudServices"];
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ init(): Promise<void>;
30
+ /**
31
+ * Returns a token used by the CKBox plugin for communication with the CKBox service.
32
+ */
33
+ getToken(): InitializedToken;
34
+ /**
35
+ * The ID of workspace to use when uploading an image.
36
+ */
37
+ getWorkspaceId(): string;
38
+ /**
39
+ * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
40
+ */
41
+ getCategoryIdForFile(fileOrUrl: File | string, options: {
42
+ signal: AbortSignal;
43
+ }): Promise<string>;
44
+ /**
45
+ * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
46
+ *
47
+ * If the API returns limited results, the method will collect all items.
48
+ */
49
+ private _getAvailableCategories;
50
+ }
@@ -0,0 +1,183 @@
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
+ import { CKEditorError, logError } from 'ckeditor5/src/utils';
6
+ import { Plugin } from 'ckeditor5/src/core';
7
+ import { convertMimeTypeToExtension, getContentTypeOfUrl, getFileExtension, getWorkspaceId, sendHttpRequest } from './utils';
8
+ const DEFAULT_CKBOX_THEME_NAME = 'lark';
9
+ /**
10
+ * The CKBox utilities plugin.
11
+ */
12
+ export default class CKBoxUtils extends Plugin {
13
+ /**
14
+ * @inheritDoc
15
+ */
16
+ static get pluginName() {
17
+ return 'CKBoxUtils';
18
+ }
19
+ /**
20
+ * @inheritDoc
21
+ */
22
+ static get requires() {
23
+ return ['CloudServices'];
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ async init() {
29
+ const editor = this.editor;
30
+ const hasConfiguration = !!editor.config.get('ckbox');
31
+ const isLibraryLoaded = !!window.CKBox;
32
+ // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
33
+ // the CKBox JavaScript library is loaded.
34
+ if (!hasConfiguration && !isLibraryLoaded) {
35
+ return;
36
+ }
37
+ editor.config.define('ckbox', {
38
+ serviceOrigin: 'https://api.ckbox.io',
39
+ defaultUploadCategories: null,
40
+ ignoreDataId: false,
41
+ language: editor.locale.uiLanguage,
42
+ theme: DEFAULT_CKBOX_THEME_NAME,
43
+ tokenUrl: editor.config.get('cloudServices.tokenUrl')
44
+ });
45
+ const cloudServices = editor.plugins.get('CloudServices');
46
+ const cloudServicesTokenUrl = editor.config.get('cloudServices.tokenUrl');
47
+ const ckboxTokenUrl = editor.config.get('ckbox.tokenUrl');
48
+ if (!ckboxTokenUrl) {
49
+ /**
50
+ * The {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
51
+ * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
52
+ * configuration is required for the CKBox plugin.
53
+ *
54
+ * ```ts
55
+ * ClassicEditor.create( document.createElement( 'div' ), {
56
+ * ckbox: {
57
+ * tokenUrl: "YOUR_TOKEN_URL"
58
+ * // ...
59
+ * }
60
+ * // ...
61
+ * } );
62
+ * ```
63
+ *
64
+ * @error ckbox-plugin-missing-token-url
65
+ */
66
+ throw new CKEditorError('ckbox-plugin-missing-token-url', this);
67
+ }
68
+ if (ckboxTokenUrl == cloudServicesTokenUrl) {
69
+ this._token = cloudServices.token;
70
+ }
71
+ else {
72
+ this._token = await cloudServices.registerTokenUrl(ckboxTokenUrl);
73
+ }
74
+ }
75
+ /**
76
+ * Returns a token used by the CKBox plugin for communication with the CKBox service.
77
+ */
78
+ getToken() {
79
+ return this._token;
80
+ }
81
+ /**
82
+ * The ID of workspace to use when uploading an image.
83
+ */
84
+ getWorkspaceId() {
85
+ const t = this.editor.t;
86
+ const cannotAccessDefaultWorkspaceError = t('Cannot access default workspace.');
87
+ const defaultWorkspaceId = this.editor.config.get('ckbox.defaultUploadWorkspaceId');
88
+ const workspaceId = getWorkspaceId(this._token, defaultWorkspaceId);
89
+ if (workspaceId == null) {
90
+ /**
91
+ * The user is not authorized to access the workspace defined in the`ckbox.defaultUploadWorkspaceId` configuration.
92
+ *
93
+ * @error ckbox-access-default-workspace-error
94
+ */
95
+ logError('ckbox-access-default-workspace-error');
96
+ throw cannotAccessDefaultWorkspaceError;
97
+ }
98
+ return workspaceId;
99
+ }
100
+ /**
101
+ * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
102
+ */
103
+ async getCategoryIdForFile(fileOrUrl, options) {
104
+ const t = this.editor.t;
105
+ const cannotFindCategoryError = t('Cannot determine a category for the uploaded file.');
106
+ const defaultCategories = this.editor.config.get('ckbox.defaultUploadCategories');
107
+ const allCategoriesPromise = this._getAvailableCategories(options);
108
+ const extension = typeof fileOrUrl == 'string' ?
109
+ convertMimeTypeToExtension(await getContentTypeOfUrl(fileOrUrl, options)) :
110
+ getFileExtension(fileOrUrl);
111
+ const allCategories = await allCategoriesPromise;
112
+ // Couldn't fetch all categories. Perhaps the authorization token is invalid.
113
+ if (!allCategories) {
114
+ throw cannotFindCategoryError;
115
+ }
116
+ // If a user specifies the plugin configuration, find the first category that accepts the uploaded file.
117
+ if (defaultCategories) {
118
+ const userCategory = Object.keys(defaultCategories).find(category => {
119
+ return defaultCategories[category].find(e => e.toLowerCase() == extension);
120
+ });
121
+ // If found, return its ID if the category exists on the server side.
122
+ if (userCategory) {
123
+ const serverCategory = allCategories.find(category => category.id === userCategory || category.name === userCategory);
124
+ if (!serverCategory) {
125
+ throw cannotFindCategoryError;
126
+ }
127
+ return serverCategory.id;
128
+ }
129
+ }
130
+ // Otherwise, find the first category that accepts the uploaded file and returns its ID.
131
+ const category = allCategories.find(category => category.extensions.find(e => e.toLowerCase() == extension));
132
+ if (!category) {
133
+ throw cannotFindCategoryError;
134
+ }
135
+ return category.id;
136
+ }
137
+ /**
138
+ * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
139
+ *
140
+ * If the API returns limited results, the method will collect all items.
141
+ */
142
+ async _getAvailableCategories(options) {
143
+ const ITEMS_PER_REQUEST = 50;
144
+ const editor = this.editor;
145
+ const token = this._token;
146
+ const { signal } = options;
147
+ const serviceOrigin = editor.config.get('ckbox.serviceOrigin');
148
+ const workspaceId = this.getWorkspaceId();
149
+ try {
150
+ const result = [];
151
+ let offset = 0;
152
+ let remainingItems;
153
+ do {
154
+ const data = await fetchCategories(offset);
155
+ result.push(...data.items);
156
+ remainingItems = data.totalCount - (offset + ITEMS_PER_REQUEST);
157
+ offset += ITEMS_PER_REQUEST;
158
+ } while (remainingItems > 0);
159
+ return result;
160
+ }
161
+ catch {
162
+ signal.throwIfAborted();
163
+ /**
164
+ * Fetching a list of available categories with which an uploaded file can be associated failed.
165
+ *
166
+ * @error ckbox-fetch-category-http-error
167
+ */
168
+ logError('ckbox-fetch-category-http-error');
169
+ return undefined;
170
+ }
171
+ function fetchCategories(offset) {
172
+ const categoryUrl = new URL('categories', serviceOrigin);
173
+ categoryUrl.searchParams.set('limit', ITEMS_PER_REQUEST.toString());
174
+ categoryUrl.searchParams.set('offset', offset.toString());
175
+ categoryUrl.searchParams.set('workspaceId', workspaceId);
176
+ return sendHttpRequest({
177
+ url: categoryUrl,
178
+ signal,
179
+ authorization: token.value
180
+ });
181
+ }
182
+ }
183
+ }
package/src/index.d.ts CHANGED
@@ -8,6 +8,10 @@
8
8
  export { default as CKBox } from './ckbox';
9
9
  export { default as CKBoxEditing } from './ckboxediting';
10
10
  export { default as CKBoxUI } from './ckboxui';
11
+ export { default as CKBoxImageEditEditing } from './ckboximageedit/ckboximageeditediting';
12
+ export { default as CKBoxImageEditUI } from './ckboximageedit/ckboximageeditui';
13
+ export { default as CKBoxImageEdit } from './ckboximageedit';
11
14
  export type { default as CKBoxCommand } from './ckboxcommand';
15
+ export type { default as CKBoxImageEditCommand } from './ckboximageedit/ckboximageeditcommand';
12
16
  export type { CKBoxConfig } from './ckboxconfig';
13
17
  import './augmentation';
package/src/index.js CHANGED
@@ -8,4 +8,7 @@
8
8
  export { default as CKBox } from './ckbox';
9
9
  export { default as CKBoxEditing } from './ckboxediting';
10
10
  export { default as CKBoxUI } from './ckboxui';
11
+ export { default as CKBoxImageEditEditing } from './ckboximageedit/ckboximageeditediting';
12
+ export { default as CKBoxImageEditUI } from './ckboximageedit/ckboximageeditui';
13
+ export { default as CKBoxImageEdit } from './ckboximageedit';
11
14
  import './augmentation';
package/src/utils.d.ts CHANGED
@@ -30,3 +30,34 @@ export declare function getWorkspaceId(token: InitializedToken, defaultWorkspace
30
30
  * Generates an image data URL from its `blurhash` representation.
31
31
  */
32
32
  export declare function blurHashToDataUrl(hash?: string): string | undefined;
33
+ /**
34
+ * Sends the HTTP request.
35
+ *
36
+ * @internal
37
+ * @param config.url the URL where the request will be sent.
38
+ * @param config.method The HTTP method.
39
+ * @param config.data Additional data to send.
40
+ * @param config.onUploadProgress A callback informing about the upload progress.
41
+ */
42
+ export declare function sendHttpRequest({ url, method, data, onUploadProgress, signal, authorization }: {
43
+ url: URL;
44
+ signal: AbortSignal;
45
+ authorization: string;
46
+ method?: 'GET' | 'POST';
47
+ data?: FormData | null;
48
+ onUploadProgress?: (evt: ProgressEvent) => void;
49
+ }): Promise<any>;
50
+ /**
51
+ * Returns an extension a typical file in the specified `mimeType` format would have.
52
+ */
53
+ export declare function convertMimeTypeToExtension(mimeType: string): string;
54
+ /**
55
+ * Tries to fetch the given `url` and returns 'content-type' of the response.
56
+ */
57
+ export declare function getContentTypeOfUrl(url: string, options: {
58
+ signal: AbortSignal;
59
+ }): Promise<string>;
60
+ /**
61
+ * Returns an extension from the given value.
62
+ */
63
+ export declare function getFileExtension(file: File): string;