@ckeditor/ckeditor5-ckbox 40.1.0 → 40.2.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.
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;