@ckeditor/ckeditor5-ckbox 48.8.1-alpha.4 → 49.0.0-alpha.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 (171) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/augmentation.d.ts +28 -28
  3. package/dist/ckbox.d.ts +32 -32
  4. package/dist/ckboxcommand.d.ts +120 -120
  5. package/dist/ckboxconfig.d.ts +417 -417
  6. package/dist/ckboxediting.d.ts +58 -58
  7. package/dist/ckboximageedit/ckboximageeditcommand.d.ts +100 -100
  8. package/dist/ckboximageedit/ckboximageeditediting.d.ts +28 -28
  9. package/dist/ckboximageedit/ckboximageeditui.d.ts +23 -23
  10. package/dist/ckboximageedit/utils.d.ts +8 -8
  11. package/dist/ckboximageedit.d.ts +22 -23
  12. package/dist/ckboxui.d.ts +51 -51
  13. package/dist/ckboxuploadadapter.d.ts +33 -33
  14. package/dist/ckboxutils.d.ts +54 -54
  15. package/dist/index-content.css +3 -0
  16. package/dist/index-content.css.map +1 -0
  17. package/dist/index-editor.css +32 -26
  18. package/dist/index-editor.css.map +1 -0
  19. package/dist/index.css +22 -18
  20. package/dist/index.css.map +1 -1
  21. package/dist/index.d.ts +23 -21
  22. package/dist/index.js +1674 -1763
  23. package/dist/index.js.map +1 -1
  24. package/dist/translations/af.js +1 -1
  25. package/dist/translations/af.umd.js +5 -3
  26. package/dist/translations/ar.js +1 -1
  27. package/dist/translations/ar.umd.js +5 -3
  28. package/dist/translations/ast.js +1 -1
  29. package/dist/translations/ast.umd.js +5 -3
  30. package/dist/translations/az.js +1 -1
  31. package/dist/translations/az.umd.js +5 -3
  32. package/dist/translations/be.js +1 -1
  33. package/dist/translations/be.umd.js +5 -3
  34. package/dist/translations/bg.js +1 -1
  35. package/dist/translations/bg.umd.js +5 -3
  36. package/dist/translations/bn.js +1 -1
  37. package/dist/translations/bn.umd.js +5 -3
  38. package/dist/translations/bs.js +1 -1
  39. package/dist/translations/bs.umd.js +5 -3
  40. package/dist/translations/ca.js +1 -1
  41. package/dist/translations/ca.umd.js +5 -3
  42. package/dist/translations/cs.js +1 -1
  43. package/dist/translations/cs.umd.js +5 -3
  44. package/dist/translations/da.js +1 -1
  45. package/dist/translations/da.umd.js +5 -3
  46. package/dist/translations/de-ch.js +1 -1
  47. package/dist/translations/de-ch.umd.js +5 -3
  48. package/dist/translations/de.js +1 -1
  49. package/dist/translations/de.umd.js +5 -3
  50. package/dist/translations/el.js +1 -1
  51. package/dist/translations/el.umd.js +5 -3
  52. package/dist/translations/en-au.js +1 -1
  53. package/dist/translations/en-au.umd.js +5 -3
  54. package/dist/translations/en-gb.js +1 -1
  55. package/dist/translations/en-gb.umd.js +5 -3
  56. package/dist/translations/en.js +1 -1
  57. package/dist/translations/en.umd.js +5 -3
  58. package/dist/translations/eo.js +1 -1
  59. package/dist/translations/eo.umd.js +5 -3
  60. package/dist/translations/es-co.js +1 -1
  61. package/dist/translations/es-co.umd.js +5 -3
  62. package/dist/translations/es.js +1 -1
  63. package/dist/translations/es.umd.js +5 -3
  64. package/dist/translations/et.js +1 -1
  65. package/dist/translations/et.umd.js +5 -3
  66. package/dist/translations/eu.js +1 -1
  67. package/dist/translations/eu.umd.js +5 -3
  68. package/dist/translations/fa.js +1 -1
  69. package/dist/translations/fa.umd.js +5 -3
  70. package/dist/translations/fi.js +1 -1
  71. package/dist/translations/fi.umd.js +5 -3
  72. package/dist/translations/fr.js +1 -1
  73. package/dist/translations/fr.umd.js +5 -3
  74. package/dist/translations/gl.js +1 -1
  75. package/dist/translations/gl.umd.js +5 -3
  76. package/dist/translations/gu.js +1 -1
  77. package/dist/translations/gu.umd.js +5 -3
  78. package/dist/translations/he.js +1 -1
  79. package/dist/translations/he.umd.js +5 -3
  80. package/dist/translations/hi.js +1 -1
  81. package/dist/translations/hi.umd.js +5 -3
  82. package/dist/translations/hr.js +1 -1
  83. package/dist/translations/hr.umd.js +5 -3
  84. package/dist/translations/hu.js +1 -1
  85. package/dist/translations/hu.umd.js +5 -3
  86. package/dist/translations/hy.js +1 -1
  87. package/dist/translations/hy.umd.js +5 -3
  88. package/dist/translations/id.js +1 -1
  89. package/dist/translations/id.umd.js +5 -3
  90. package/dist/translations/it.js +1 -1
  91. package/dist/translations/it.umd.js +5 -3
  92. package/dist/translations/ja.js +1 -1
  93. package/dist/translations/ja.umd.js +5 -3
  94. package/dist/translations/jv.js +1 -1
  95. package/dist/translations/jv.umd.js +5 -3
  96. package/dist/translations/kk.js +1 -1
  97. package/dist/translations/kk.umd.js +5 -3
  98. package/dist/translations/km.js +1 -1
  99. package/dist/translations/km.umd.js +5 -3
  100. package/dist/translations/kn.js +1 -1
  101. package/dist/translations/kn.umd.js +5 -3
  102. package/dist/translations/ko.js +1 -1
  103. package/dist/translations/ko.umd.js +5 -3
  104. package/dist/translations/ku.js +1 -1
  105. package/dist/translations/ku.umd.js +5 -3
  106. package/dist/translations/lt.js +1 -1
  107. package/dist/translations/lt.umd.js +5 -3
  108. package/dist/translations/lv.js +1 -1
  109. package/dist/translations/lv.umd.js +5 -3
  110. package/dist/translations/ms.js +1 -1
  111. package/dist/translations/ms.umd.js +5 -3
  112. package/dist/translations/nb.js +1 -1
  113. package/dist/translations/nb.umd.js +5 -3
  114. package/dist/translations/ne.js +1 -1
  115. package/dist/translations/ne.umd.js +5 -3
  116. package/dist/translations/nl.js +1 -1
  117. package/dist/translations/nl.umd.js +5 -3
  118. package/dist/translations/no.js +1 -1
  119. package/dist/translations/no.umd.js +5 -3
  120. package/dist/translations/oc.js +1 -1
  121. package/dist/translations/oc.umd.js +5 -3
  122. package/dist/translations/pl.js +1 -1
  123. package/dist/translations/pl.umd.js +5 -3
  124. package/dist/translations/pt-br.js +1 -1
  125. package/dist/translations/pt-br.umd.js +5 -3
  126. package/dist/translations/pt.js +1 -1
  127. package/dist/translations/pt.umd.js +5 -3
  128. package/dist/translations/ro.js +1 -1
  129. package/dist/translations/ro.umd.js +5 -3
  130. package/dist/translations/ru.js +1 -1
  131. package/dist/translations/ru.umd.js +5 -3
  132. package/dist/translations/si.js +1 -1
  133. package/dist/translations/si.umd.js +5 -3
  134. package/dist/translations/sk.js +1 -1
  135. package/dist/translations/sk.umd.js +5 -3
  136. package/dist/translations/sl.js +1 -1
  137. package/dist/translations/sl.umd.js +5 -3
  138. package/dist/translations/sq.js +1 -1
  139. package/dist/translations/sq.umd.js +5 -3
  140. package/dist/translations/sr-latn.js +1 -1
  141. package/dist/translations/sr-latn.umd.js +5 -3
  142. package/dist/translations/sr.js +1 -1
  143. package/dist/translations/sr.umd.js +5 -3
  144. package/dist/translations/sv.js +1 -1
  145. package/dist/translations/sv.umd.js +5 -3
  146. package/dist/translations/th.js +1 -1
  147. package/dist/translations/th.umd.js +5 -3
  148. package/dist/translations/ti.js +1 -1
  149. package/dist/translations/ti.umd.js +5 -3
  150. package/dist/translations/tk.js +1 -1
  151. package/dist/translations/tk.umd.js +5 -3
  152. package/dist/translations/tr.js +1 -1
  153. package/dist/translations/tr.umd.js +5 -3
  154. package/dist/translations/tt.js +1 -1
  155. package/dist/translations/tt.umd.js +5 -3
  156. package/dist/translations/ug.js +1 -1
  157. package/dist/translations/ug.umd.js +5 -3
  158. package/dist/translations/uk.js +1 -1
  159. package/dist/translations/uk.umd.js +5 -3
  160. package/dist/translations/ur.js +1 -1
  161. package/dist/translations/ur.umd.js +5 -3
  162. package/dist/translations/uz.js +1 -1
  163. package/dist/translations/uz.umd.js +5 -3
  164. package/dist/translations/vi.js +1 -1
  165. package/dist/translations/vi.umd.js +5 -3
  166. package/dist/translations/zh-cn.js +1 -1
  167. package/dist/translations/zh-cn.umd.js +5 -3
  168. package/dist/translations/zh.js +1 -1
  169. package/dist/translations/zh.umd.js +5 -3
  170. package/dist/utils.d.ts +58 -58
  171. package/package.json +11 -11
package/dist/index.js CHANGED
@@ -2,1843 +2,1754 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { Plugin, Command, PendingActions } from '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { ButtonView, MenuBarMenuListItemButtonView, Notification } from '@ckeditor/ckeditor5-ui/dist/index.js';
7
- import { IconBrowseFiles, IconImageAssetManager, IconCkboxImageEdit } from '@ckeditor/ckeditor5-icons/dist/index.js';
8
- import { ModelRange } from '@ckeditor/ckeditor5-engine/dist/index.js';
9
- import { createElement, toMap, CKEditorError, logError, global, delay, abortableDebounce, retry } from '@ckeditor/ckeditor5-utils/dist/index.js';
10
- import { LinkEditing } from '@ckeditor/ckeditor5-link/dist/index.js';
11
- import { decode } from 'blurhash';
12
- import { FileRepository } from '@ckeditor/ckeditor5-upload/dist/index.js';
13
- import { ImageUploadEditing, ImageUploadProgress, PictureEditing, ImageUtils, ImageEditing } from '@ckeditor/ckeditor5-image/dist/index.js';
14
- import { CloudServices } from '@ckeditor/ckeditor5-cloud-services/dist/index.js';
15
- import { isEqual } from 'es-toolkit/compat';
5
+ import { Command, PendingActions, Plugin } from "@ckeditor/ckeditor5-core";
6
+ import { ButtonView, MenuBarMenuListItemButtonView, Notification } from "@ckeditor/ckeditor5-ui";
7
+ import { IconBrowseFiles, IconCkboxImageEdit, IconImageAssetManager } from "@ckeditor/ckeditor5-icons";
8
+ import { ModelRange } from "@ckeditor/ckeditor5-engine";
9
+ import { CKEditorError, abortableDebounce, createElement, delay, global, isOffline, isTrustedTypesEnforced, logError, logWarning, retry, toMap } from "@ckeditor/ckeditor5-utils";
10
+ import { LinkEditing } from "@ckeditor/ckeditor5-link";
11
+ import { decode } from "blurhash";
12
+ import { FileRepository } from "@ckeditor/ckeditor5-upload";
13
+ import { ImageEditing, ImageUploadEditing, ImageUploadProgress, ImageUtils, PictureEditing } from "@ckeditor/ckeditor5-image";
14
+ import { CloudServices } from "@ckeditor/ckeditor5-cloud-services";
15
+ import { isEqual } from "es-toolkit/compat";
16
16
 
17
17
  /**
18
- * Introduces UI components for the `CKBox` plugin.
19
- *
20
- * The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}:
21
- *
22
- * * the `'ckbox'` toolbar button,
23
- * * the `'menuBar:ckbox'` menu bar component, which is by default added to the `'Insert'` menu.
24
- *
25
- * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component.
26
- */ class CKBoxUI extends Plugin {
27
- /**
28
- * @inheritDoc
29
- */ static get pluginName() {
30
- return 'CKBoxUI';
31
- }
32
- /**
33
- * @inheritDoc
34
- */ static get isOfficialPlugin() {
35
- return true;
36
- }
37
- /**
38
- * @inheritDoc
39
- */ afterInit() {
40
- const editor = this.editor;
41
- // Do not register the `ckbox` button if the command does not exist.
42
- // This might happen when CKBox library is not loaded on the page.
43
- if (!editor.commands.get('ckbox')) {
44
- return;
45
- }
46
- editor.ui.componentFactory.add('ckbox', ()=>this._createFileToolbarButton());
47
- editor.ui.componentFactory.add('menuBar:ckbox', ()=>this._createFileMenuBarButton());
48
- if (editor.plugins.has('ImageInsertUI')) {
49
- editor.plugins.get('ImageInsertUI').registerIntegration({
50
- name: 'assetManager',
51
- observable: ()=>editor.commands.get('ckbox'),
52
- buttonViewCreator: ()=>this._createImageToolbarButton(),
53
- formViewCreator: ()=>this._createImageDropdownButton(),
54
- menuBarButtonViewCreator: (isOnly)=>this._createImageMenuBarButton(isOnly ? 'insertOnly' : 'insertNested')
55
- });
56
- }
57
- }
58
- /**
59
- * Creates the base for various kinds of the button component provided by this feature.
60
- */ _createButton(ButtonClass) {
61
- const editor = this.editor;
62
- const locale = editor.locale;
63
- const view = new ButtonClass(locale);
64
- const command = editor.commands.get('ckbox');
65
- view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');
66
- view.on('execute', ()=>{
67
- editor.execute('ckbox');
68
- });
69
- return view;
70
- }
71
- /**
72
- * Creates a simple toolbar button for files management, with an icon and a tooltip.
73
- */ _createFileToolbarButton() {
74
- const t = this.editor.locale.t;
75
- const button = this._createButton(ButtonView);
76
- button.icon = IconBrowseFiles;
77
- button.label = t('Open file manager');
78
- button.tooltip = true;
79
- return button;
80
- }
81
- /**
82
- * Creates a simple toolbar button for images management, with an icon and a tooltip.
83
- */ _createImageToolbarButton() {
84
- const t = this.editor.locale.t;
85
- const imageInsertUI = this.editor.plugins.get('ImageInsertUI');
86
- const button = this._createButton(ButtonView);
87
- button.icon = IconImageAssetManager;
88
- button.bind('label').to(imageInsertUI, 'isImageSelected', (isImageSelected)=>isImageSelected ? t('Replace image with file manager') : t('Insert image with file manager'));
89
- button.tooltip = true;
90
- return button;
91
- }
92
- /**
93
- * Creates a button for images management for the dropdown view, with an icon, text and no tooltip.
94
- */ _createImageDropdownButton() {
95
- const t = this.editor.locale.t;
96
- const imageInsertUI = this.editor.plugins.get('ImageInsertUI');
97
- const button = this._createButton(ButtonView);
98
- button.icon = IconImageAssetManager;
99
- button.withText = true;
100
- button.bind('label').to(imageInsertUI, 'isImageSelected', (isImageSelected)=>isImageSelected ? t('Replace with file manager') : t('Insert with file manager'));
101
- button.on('execute', ()=>{
102
- imageInsertUI.dropdownView.isOpen = false;
103
- });
104
- return button;
105
- }
106
- /**
107
- * Creates a button for files management for the menu bar.
108
- */ _createFileMenuBarButton() {
109
- const t = this.editor.locale.t;
110
- const button = this._createButton(MenuBarMenuListItemButtonView);
111
- button.icon = IconBrowseFiles;
112
- button.withText = true;
113
- button.label = t('File');
114
- return button;
115
- }
116
- /**
117
- * Creates a button for images management for the menu bar.
118
- */ _createImageMenuBarButton(type) {
119
- // Use t() stored in a variable with a different name to reuse existing translations from another package.
120
- const translateVariableKey = this.editor.locale.t;
121
- const t = this.editor.locale.t;
122
- const button = this._createButton(MenuBarMenuListItemButtonView);
123
- button.icon = IconImageAssetManager;
124
- button.withText = true;
125
- switch(type){
126
- case 'insertOnly':
127
- button.label = translateVariableKey('Image');
128
- break;
129
- case 'insertNested':
130
- button.label = t('With file manager');
131
- break;
132
- }
133
- return button;
134
- }
135
- }
18
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
19
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
20
+ */
21
+ /**
22
+ * @module ckbox/ckboxui
23
+ */
24
+ /**
25
+ * Introduces UI components for the `CKBox` plugin.
26
+ *
27
+ * The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}:
28
+ *
29
+ * * the `'ckbox'` toolbar button,
30
+ * * the `'menuBar:ckbox'` menu bar component, which is by default added to the `'Insert'` menu.
31
+ *
32
+ * It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component.
33
+ */
34
+ var CKBoxUI = class extends Plugin {
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ static get pluginName() {
39
+ return "CKBoxUI";
40
+ }
41
+ /**
42
+ * @inheritDoc
43
+ */
44
+ static get isOfficialPlugin() {
45
+ return true;
46
+ }
47
+ /**
48
+ * @inheritDoc
49
+ */
50
+ afterInit() {
51
+ const editor = this.editor;
52
+ if (!editor.commands.get("ckbox")) return;
53
+ editor.ui.componentFactory.add("ckbox", () => this._createFileToolbarButton());
54
+ editor.ui.componentFactory.add("menuBar:ckbox", () => this._createFileMenuBarButton());
55
+ if (editor.plugins.has("ImageInsertUI")) editor.plugins.get("ImageInsertUI").registerIntegration({
56
+ name: "assetManager",
57
+ observable: () => editor.commands.get("ckbox"),
58
+ buttonViewCreator: () => this._createImageToolbarButton(),
59
+ formViewCreator: () => this._createImageDropdownButton(),
60
+ menuBarButtonViewCreator: (isOnly) => this._createImageMenuBarButton(isOnly ? "insertOnly" : "insertNested")
61
+ });
62
+ }
63
+ /**
64
+ * Creates the base for various kinds of the button component provided by this feature.
65
+ */
66
+ _createButton(ButtonClass) {
67
+ const editor = this.editor;
68
+ const locale = editor.locale;
69
+ const view = new ButtonClass(locale);
70
+ const command = editor.commands.get("ckbox");
71
+ view.bind("isOn", "isEnabled").to(command, "value", "isEnabled");
72
+ view.on("execute", () => {
73
+ editor.execute("ckbox");
74
+ });
75
+ return view;
76
+ }
77
+ /**
78
+ * Creates a simple toolbar button for files management, with an icon and a tooltip.
79
+ */
80
+ _createFileToolbarButton() {
81
+ const t = this.editor.locale.t;
82
+ const button = this._createButton(ButtonView);
83
+ button.icon = IconBrowseFiles;
84
+ button.label = t("Open file manager");
85
+ button.tooltip = true;
86
+ return button;
87
+ }
88
+ /**
89
+ * Creates a simple toolbar button for images management, with an icon and a tooltip.
90
+ */
91
+ _createImageToolbarButton() {
92
+ const t = this.editor.locale.t;
93
+ const imageInsertUI = this.editor.plugins.get("ImageInsertUI");
94
+ const button = this._createButton(ButtonView);
95
+ button.icon = IconImageAssetManager;
96
+ button.bind("label").to(imageInsertUI, "isImageSelected", (isImageSelected) => isImageSelected ? t("Replace image with file manager") : t("Insert image with file manager"));
97
+ button.tooltip = true;
98
+ return button;
99
+ }
100
+ /**
101
+ * Creates a button for images management for the dropdown view, with an icon, text and no tooltip.
102
+ */
103
+ _createImageDropdownButton() {
104
+ const t = this.editor.locale.t;
105
+ const imageInsertUI = this.editor.plugins.get("ImageInsertUI");
106
+ const button = this._createButton(ButtonView);
107
+ button.icon = IconImageAssetManager;
108
+ button.withText = true;
109
+ button.bind("label").to(imageInsertUI, "isImageSelected", (isImageSelected) => isImageSelected ? t("Replace with file manager") : t("Insert with file manager"));
110
+ button.on("execute", () => {
111
+ imageInsertUI.dropdownView.isOpen = false;
112
+ });
113
+ return button;
114
+ }
115
+ /**
116
+ * Creates a button for files management for the menu bar.
117
+ */
118
+ _createFileMenuBarButton() {
119
+ const t = this.editor.locale.t;
120
+ const button = this._createButton(MenuBarMenuListItemButtonView);
121
+ button.icon = IconBrowseFiles;
122
+ button.withText = true;
123
+ button.label = t("File");
124
+ return button;
125
+ }
126
+ /**
127
+ * Creates a button for images management for the menu bar.
128
+ */
129
+ _createImageMenuBarButton(type) {
130
+ const translateVariableKey = this.editor.locale.t;
131
+ const t = this.editor.locale.t;
132
+ const button = this._createButton(MenuBarMenuListItemButtonView);
133
+ button.icon = IconImageAssetManager;
134
+ button.withText = true;
135
+ switch (type) {
136
+ case "insertOnly":
137
+ button.label = translateVariableKey("Image");
138
+ break;
139
+ case "insertNested": button.label = t("With file manager");
140
+ }
141
+ return button;
142
+ }
143
+ };
136
144
 
137
145
  /**
138
- * Converts image source set provided by the CKBox into an object containing:
139
- * - responsive URLs for the "webp" image format,
140
- * - one fallback URL for browsers that do not support the "webp" format.
141
- *
142
- * @internal
143
- */ function getImageUrls(imageUrls) {
144
- const responsiveUrls = [];
145
- let maxWidth = 0;
146
- for(const key in imageUrls){
147
- const width = parseInt(key, 10);
148
- if (!isNaN(width)) {
149
- if (width > maxWidth) {
150
- maxWidth = width;
151
- }
152
- responsiveUrls.push(`${imageUrls[key]} ${key}w`);
153
- }
154
- }
155
- const imageSources = [
156
- {
157
- srcset: responsiveUrls.join(','),
158
- sizes: `(max-width: ${maxWidth}px) 100vw, ${maxWidth}px`,
159
- type: 'image/webp'
160
- }
161
- ];
162
- return {
163
- imageFallbackUrl: imageUrls.default,
164
- imageSources
165
- };
146
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
147
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
148
+ */
149
+ /**
150
+ * Converts image source set provided by the CKBox into an object containing:
151
+ * - responsive URLs for the "webp" image format,
152
+ * - one fallback URL for browsers that do not support the "webp" format.
153
+ *
154
+ * @internal
155
+ */
156
+ function getImageUrls(imageUrls) {
157
+ const responsiveUrls = [];
158
+ let maxWidth = 0;
159
+ for (const key in imageUrls) {
160
+ const width = parseInt(key, 10);
161
+ if (!isNaN(width)) {
162
+ if (width > maxWidth) maxWidth = width;
163
+ responsiveUrls.push(`${imageUrls[key]} ${key}w`);
164
+ }
165
+ }
166
+ const imageSources = [{
167
+ srcset: responsiveUrls.join(","),
168
+ sizes: `(max-width: ${maxWidth}px) 100vw, ${maxWidth}px`,
169
+ type: "image/webp"
170
+ }];
171
+ return {
172
+ imageFallbackUrl: imageUrls.default,
173
+ imageSources
174
+ };
166
175
  }
167
176
  /**
168
- * Returns a workspace id to use for communication with the CKBox service.
169
- *
170
- * @param defaultWorkspaceId The default workspace to use taken from editor config.
171
- * @internal
172
- */ function getWorkspaceId(token, defaultWorkspaceId) {
173
- const [, binaryTokenPayload] = token.value.split('.');
174
- const payload = JSON.parse(atob(binaryTokenPayload));
175
- const workspaces = payload.auth?.ckbox?.workspaces || [
176
- payload.aud
177
- ];
178
- if (!defaultWorkspaceId) {
179
- return workspaces[0];
180
- }
181
- if (payload.auth?.ckbox?.role == 'superadmin' || workspaces.includes(defaultWorkspaceId)) {
182
- return defaultWorkspaceId;
183
- }
184
- return null;
177
+ * Returns a workspace id to use for communication with the CKBox service.
178
+ *
179
+ * @param defaultWorkspaceId The default workspace to use taken from editor config.
180
+ * @internal
181
+ */
182
+ function getWorkspaceId(token, defaultWorkspaceId) {
183
+ const [, binaryTokenPayload] = token.value.split(".");
184
+ const payload = JSON.parse(atob(binaryTokenPayload));
185
+ const workspaces = payload.auth?.ckbox?.workspaces || [payload.aud];
186
+ if (!defaultWorkspaceId) return workspaces[0];
187
+ if (payload.auth?.ckbox?.role == "superadmin" || workspaces.includes(defaultWorkspaceId)) return defaultWorkspaceId;
188
+ return null;
185
189
  }
186
190
  /**
187
- * Default resolution for decoding blurhash values.
188
- * Relatively small values must be used in order to ensure acceptable performance.
189
- */ const BLUR_RESOLUTION = 32;
191
+ * Default resolution for decoding blurhash values.
192
+ * Relatively small values must be used in order to ensure acceptable performance.
193
+ */
194
+ const BLUR_RESOLUTION = 32;
190
195
  /**
191
- * Generates an image data URL from its `blurhash` representation.
192
- *
193
- * @internal
194
- */ function blurHashToDataUrl(hash) {
195
- if (!hash) {
196
- return;
197
- }
198
- try {
199
- const resolutionInPx = `${BLUR_RESOLUTION}px`;
200
- const canvas = document.createElement('canvas');
201
- canvas.setAttribute('width', resolutionInPx);
202
- canvas.setAttribute('height', resolutionInPx);
203
- const ctx = canvas.getContext('2d');
204
- /* istanbul ignore next -- @preserve */ if (!ctx) {
205
- return;
206
- }
207
- const imageData = ctx.createImageData(BLUR_RESOLUTION, BLUR_RESOLUTION);
208
- const decoded = decode(hash, BLUR_RESOLUTION, BLUR_RESOLUTION);
209
- imageData.data.set(decoded);
210
- ctx.putImageData(imageData, 0, 0);
211
- return canvas.toDataURL();
212
- } catch {
213
- return undefined;
214
- }
196
+ * Generates an image data URL from its `blurhash` representation.
197
+ *
198
+ * @internal
199
+ */
200
+ function blurHashToDataUrl(hash) {
201
+ if (!hash) return;
202
+ try {
203
+ const resolutionInPx = `${BLUR_RESOLUTION}px`;
204
+ const canvas = document.createElement("canvas");
205
+ canvas.setAttribute("width", resolutionInPx);
206
+ canvas.setAttribute("height", resolutionInPx);
207
+ const ctx = canvas.getContext("2d");
208
+ /* istanbul ignore next -- @preserve */
209
+ if (!ctx) return;
210
+ const imageData = ctx.createImageData(BLUR_RESOLUTION, BLUR_RESOLUTION);
211
+ const decoded = decode(hash, BLUR_RESOLUTION, BLUR_RESOLUTION);
212
+ imageData.data.set(decoded);
213
+ ctx.putImageData(imageData, 0, 0);
214
+ return canvas.toDataURL();
215
+ } catch {
216
+ return;
217
+ }
215
218
  }
216
219
  /**
217
- * Sends the HTTP request.
218
- *
219
- * @internal
220
- * @param options Configuration options
221
- * @param options.url The URL where the request will be sent.
222
- * @param options.signal The AbortSignal to abort the request when needed.
223
- * @param options.authorization The authorization token for the request.
224
- * @param options.method The HTTP method (default: 'GET').
225
- * @param options.data Additional data to send.
226
- * @param options.onUploadProgress A callback informing about the upload progress.
227
- */ function sendHttpRequest({ url, method = 'GET', data, onUploadProgress, signal, authorization }) {
228
- const xhr = new XMLHttpRequest();
229
- xhr.open(method, url.toString());
230
- xhr.setRequestHeader('Authorization', authorization);
231
- xhr.setRequestHeader('CKBox-Version', 'CKEditor 5');
232
- xhr.responseType = 'json';
233
- // The callback is attached to the `signal#abort` event.
234
- const abortCallback = ()=>{
235
- xhr.abort();
236
- };
237
- return new Promise((resolve, reject)=>{
238
- signal.throwIfAborted();
239
- signal.addEventListener('abort', abortCallback);
240
- xhr.addEventListener('loadstart', ()=>{
241
- signal.addEventListener('abort', abortCallback);
242
- });
243
- xhr.addEventListener('loadend', ()=>{
244
- signal.removeEventListener('abort', abortCallback);
245
- });
246
- xhr.addEventListener('error', ()=>{
247
- reject();
248
- });
249
- xhr.addEventListener('abort', ()=>{
250
- reject();
251
- });
252
- xhr.addEventListener('load', ()=>{
253
- const response = xhr.response;
254
- if (!response || response.statusCode >= 400) {
255
- return reject(response && response.message);
256
- }
257
- resolve(response);
258
- });
259
- /* istanbul ignore else -- @preserve */ if (onUploadProgress) {
260
- xhr.upload.addEventListener('progress', (evt)=>{
261
- onUploadProgress(evt);
262
- });
263
- }
264
- // Send the request.
265
- xhr.send(data);
266
- });
220
+ * Sends the HTTP request.
221
+ *
222
+ * @internal
223
+ * @param options Configuration options
224
+ * @param options.url The URL where the request will be sent.
225
+ * @param options.signal The AbortSignal to abort the request when needed.
226
+ * @param options.authorization The authorization token for the request.
227
+ * @param options.method The HTTP method (default: 'GET').
228
+ * @param options.data Additional data to send.
229
+ * @param options.onUploadProgress A callback informing about the upload progress.
230
+ */
231
+ function sendHttpRequest({ url, method = "GET", data, onUploadProgress, signal, authorization }) {
232
+ const xhr = new XMLHttpRequest();
233
+ xhr.open(method, url.toString());
234
+ xhr.setRequestHeader("Authorization", authorization);
235
+ xhr.setRequestHeader("CKBox-Version", "CKEditor 5");
236
+ xhr.responseType = "json";
237
+ const abortCallback = () => {
238
+ xhr.abort();
239
+ };
240
+ return new Promise((resolve, reject) => {
241
+ signal.throwIfAborted();
242
+ signal.addEventListener("abort", abortCallback);
243
+ xhr.addEventListener("loadstart", () => {
244
+ signal.addEventListener("abort", abortCallback);
245
+ });
246
+ xhr.addEventListener("loadend", () => {
247
+ signal.removeEventListener("abort", abortCallback);
248
+ });
249
+ xhr.addEventListener("error", () => {
250
+ reject();
251
+ });
252
+ xhr.addEventListener("abort", () => {
253
+ reject();
254
+ });
255
+ xhr.addEventListener("load", () => {
256
+ const response = xhr.response;
257
+ if (!response || response.statusCode >= 400) return reject(response && response.message);
258
+ resolve(response);
259
+ });
260
+ /* istanbul ignore else -- @preserve */
261
+ if (onUploadProgress) xhr.upload.addEventListener("progress", (evt) => {
262
+ onUploadProgress(evt);
263
+ });
264
+ xhr.send(data);
265
+ });
267
266
  }
268
267
  const MIME_TO_EXTENSION = {
269
- 'image/gif': 'gif',
270
- 'image/jpeg': 'jpg',
271
- 'image/png': 'png',
272
- 'image/webp': 'webp',
273
- 'image/bmp': 'bmp',
274
- 'image/tiff': 'tiff'
268
+ "image/gif": "gif",
269
+ "image/jpeg": "jpg",
270
+ "image/png": "png",
271
+ "image/webp": "webp",
272
+ "image/bmp": "bmp",
273
+ "image/tiff": "tiff"
275
274
  };
276
275
  /**
277
- * Returns an extension a typical file in the specified `mimeType` format would have.
278
- *
279
- * @internal
280
- */ function convertMimeTypeToExtension(mimeType) {
281
- return MIME_TO_EXTENSION[mimeType];
276
+ * Returns an extension a typical file in the specified `mimeType` format would have.
277
+ *
278
+ * @internal
279
+ */
280
+ function convertMimeTypeToExtension(mimeType) {
281
+ return MIME_TO_EXTENSION[mimeType];
282
282
  }
283
283
  /**
284
- * Tries to fetch the given `url` and returns 'content-type' of the response.
285
- *
286
- * @internal
287
- */ async function getContentTypeOfUrl(url, options) {
288
- try {
289
- const response = await fetch(url, {
290
- method: 'HEAD',
291
- cache: 'force-cache',
292
- ...options
293
- });
294
- if (!response.ok) {
295
- return '';
296
- }
297
- return response.headers.get('content-type') || '';
298
- } catch {
299
- return '';
300
- }
284
+ * Tries to fetch the given `url` and returns 'content-type' of the response.
285
+ *
286
+ * @internal
287
+ */
288
+ async function getContentTypeOfUrl(url, options) {
289
+ try {
290
+ const response = await fetch(url, {
291
+ method: "HEAD",
292
+ cache: "force-cache",
293
+ ...options
294
+ });
295
+ if (!response.ok) return "";
296
+ return response.headers.get("content-type") || "";
297
+ } catch {
298
+ return "";
299
+ }
301
300
  }
302
301
  /**
303
- * Returns an extension from the given value.
304
- *
305
- * @internal
306
- */ function getFileExtension(file) {
307
- const fileName = file.name;
308
- const extensionRegExp = /\.(?<ext>[^.]+)$/;
309
- const match = fileName.match(extensionRegExp);
310
- return match.groups.ext.toLowerCase();
302
+ * Returns an extension from the given value.
303
+ *
304
+ * @internal
305
+ */
306
+ function getFileExtension(file) {
307
+ return file.name.match(/\.(?<ext>[^.]+)$/).groups.ext.toLowerCase();
311
308
  }
312
309
 
313
- // Defines the waiting time (in milliseconds) for inserting the chosen asset into the model. The chosen asset is temporarily stored in the
314
- // `CKBoxCommand#_chosenAssets` and it is removed from there automatically after this time. See `CKBoxCommand#_chosenAssets` for more
315
- // details.
316
- const ASSET_INSERTION_WAIT_TIMEOUT = 1000;
317
310
  /**
318
- * The CKBox command. It is used by the {@link module:ckbox/ckboxediting~CKBoxEditing CKBox editing feature} to open the CKBox file manager.
319
- * The file manager allows inserting an image or a link to a file into the editor content.
320
- *
321
- * ```ts
322
- * editor.execute( 'ckbox' );
323
- * ```
324
- *
325
- * **Note:** This command uses other features to perform the following tasks:
326
- * - To insert images it uses the {@link module:image/image/insertimagecommand~InsertImageCommand 'insertImage'} command from the
327
- * {@link module:image/image~Image Image feature}.
328
- * - To insert links to other files it uses the {@link module:link/linkcommand~LinkCommand 'link'} command from the
329
- * {@link module:link/link~Link Link feature}.
330
- */ class CKBoxCommand extends Command {
331
- /**
332
- * A set of all chosen assets. They are stored temporarily and they are automatically removed 1 second after being chosen.
333
- * Chosen assets have to be "remembered" for a while to be able to map the given asset with the element inserted into the model.
334
- * This association map is then used to set the ID on the model element.
335
- *
336
- * All chosen assets are automatically removed after the timeout, because (theoretically) it may happen that they will never be
337
- * inserted into the model, even if the {@link module:link/linkcommand~LinkCommand `'link'`} command or the
338
- * {@link module:image/image/insertimagecommand~InsertImageCommand `'insertImage'`} command is enabled. Such a case may arise when
339
- * another plugin blocks the command execution. Then, in order not to keep the chosen (but not inserted) assets forever, we delete
340
- * them automatically to prevent memory leakage. The 1 second timeout is enough to insert the asset into the model and extract the
341
- * ID from the chosen asset.
342
- *
343
- * The assets are stored only if
344
- * the {@link module:ckbox/ckboxconfig~CKBoxConfig#ignoreDataId `config.ckbox.ignoreDataId`} option is set to `false` (by default).
345
- *
346
- * @internal
347
- */ _chosenAssets = new Set();
348
- /**
349
- * The DOM element that acts as a mounting point for the CKBox dialog.
350
- */ _wrapper = null;
351
- /**
352
- * @inheritDoc
353
- */ constructor(editor){
354
- super(editor);
355
- this._initListeners();
356
- }
357
- /**
358
- * @inheritDoc
359
- */ refresh() {
360
- this.value = this._getValue();
361
- this.isEnabled = this._checkEnabled();
362
- }
363
- /**
364
- * @inheritDoc
365
- */ execute() {
366
- this.fire('ckbox:open');
367
- }
368
- /**
369
- * Indicates if the CKBox dialog is already opened.
370
- *
371
- * @protected
372
- * @returns {Boolean}
373
- */ _getValue() {
374
- return this._wrapper !== null;
375
- }
376
- /**
377
- * Checks whether the command can be enabled in the current context.
378
- */ _checkEnabled() {
379
- const imageCommand = this.editor.commands.get('insertImage');
380
- const linkCommand = this.editor.commands.get('link');
381
- if (!imageCommand.isEnabled && !linkCommand.isEnabled) {
382
- return false;
383
- }
384
- return true;
385
- }
386
- /**
387
- * Creates the options object for the CKBox dialog.
388
- *
389
- * @returns The object with properties:
390
- * - theme The theme for CKBox dialog.
391
- * - language The language for CKBox dialog.
392
- * - tokenUrl The token endpoint URL.
393
- * - serviceOrigin The base URL of the API service.
394
- * - forceDemoLabel Whether to force "Powered by CKBox" link.
395
- * - assets.onChoose The callback function invoked after choosing the assets.
396
- * - dialog.onClose The callback function invoked after closing the CKBox dialog.
397
- * - dialog.width The dialog width in pixels.
398
- * - dialog.height The dialog height in pixels.
399
- * - categories.icons Allows setting custom icons for categories.
400
- * - view.openLastView Sets if the last view visited by the user will be reopened
401
- * on the next startup.
402
- * - view.startupFolderId Sets the ID of the folder that will be opened on startup.
403
- * - view.startupCategoryId Sets the ID of the category that will be opened on startup.
404
- * - view.hideMaximizeButton Sets whether to hide the ‘Maximize’ button.
405
- * - view.componentsHideTimeout Sets timeout after which upload components are hidden
406
- * after completed upload.
407
- * - view.dialogMinimizeTimeout Sets timeout after which upload dialog is minimized
408
- * after completed upload.
409
- */ _prepareOptions() {
410
- const editor = this.editor;
411
- const ckboxConfig = editor.config.get('ckbox');
412
- const dialog = ckboxConfig.dialog;
413
- const categories = ckboxConfig.categories;
414
- const view = ckboxConfig.view;
415
- const upload = ckboxConfig.upload;
416
- return {
417
- theme: ckboxConfig.theme,
418
- language: ckboxConfig.language,
419
- tokenUrl: ckboxConfig.tokenUrl,
420
- serviceOrigin: ckboxConfig.serviceOrigin,
421
- forceDemoLabel: ckboxConfig.forceDemoLabel,
422
- choosableFileExtensions: ckboxConfig.choosableFileExtensions,
423
- assets: {
424
- onChoose: (assets)=>this.fire('ckbox:choose', assets)
425
- },
426
- dialog: {
427
- onClose: ()=>this.fire('ckbox:close'),
428
- width: dialog && dialog.width,
429
- height: dialog && dialog.height
430
- },
431
- categories: categories && {
432
- icons: categories.icons
433
- },
434
- view: view && {
435
- openLastView: view.openLastView,
436
- startupFolderId: view.startupFolderId,
437
- startupCategoryId: view.startupCategoryId,
438
- hideMaximizeButton: view.hideMaximizeButton
439
- },
440
- upload: upload && {
441
- componentsHideTimeout: upload.componentsHideTimeout,
442
- dialogMinimizeTimeout: upload.dialogMinimizeTimeout
443
- }
444
- };
445
- }
446
- /**
447
- * Initializes various event listeners for the `ckbox:*` events, because all functionality of the `ckbox` command is event-based.
448
- */ _initListeners() {
449
- const editor = this.editor;
450
- const model = editor.model;
451
- const shouldInsertDataId = !editor.config.get('ckbox.ignoreDataId');
452
- const downloadableFilesConfig = editor.config.get('ckbox.downloadableFiles');
453
- // Refresh the command after firing the `ckbox:*` event.
454
- this.on('ckbox', ()=>{
455
- this.refresh();
456
- }, {
457
- priority: 'low'
458
- });
459
- // Handle opening of the CKBox dialog.
460
- this.on('ckbox:open', ()=>{
461
- if (!this.isEnabled || this.value) {
462
- return;
463
- }
464
- this._wrapper = createElement(document, 'div', {
465
- class: 'ck ckbox-wrapper'
466
- });
467
- document.body.appendChild(this._wrapper);
468
- window.CKBox.mount(this._wrapper, this._prepareOptions());
469
- });
470
- // Handle closing of the CKBox dialog.
471
- this.on('ckbox:close', ()=>{
472
- if (!this.value) {
473
- return;
474
- }
475
- this._wrapper.remove();
476
- this._wrapper = null;
477
- editor.editing.view.focus();
478
- });
479
- // Handle choosing the assets.
480
- this.on('ckbox:choose', (evt, assets)=>{
481
- if (!this.isEnabled) {
482
- return;
483
- }
484
- const imageCommand = editor.commands.get('insertImage');
485
- const linkCommand = editor.commands.get('link');
486
- const assetsToProcess = prepareAssets({
487
- assets,
488
- downloadableFilesConfig,
489
- isImageAllowed: imageCommand.isEnabled,
490
- isLinkAllowed: linkCommand.isEnabled
491
- });
492
- const assetsCount = assetsToProcess.length;
493
- if (assetsCount === 0) {
494
- return;
495
- }
496
- // All assets are inserted in one undo step.
497
- model.change((writer)=>{
498
- for (const asset of assetsToProcess){
499
- const isLastAsset = asset === assetsToProcess[assetsCount - 1];
500
- const isSingleAsset = assetsCount === 1;
501
- this._insertAsset(asset, isLastAsset, writer, isSingleAsset);
502
- // If asset ID must be set for the inserted model element, store the asset temporarily and remove it automatically
503
- // after the timeout.
504
- if (shouldInsertDataId) {
505
- setTimeout(()=>this._chosenAssets.delete(asset), ASSET_INSERTION_WAIT_TIMEOUT);
506
- this._chosenAssets.add(asset);
507
- }
508
- }
509
- });
510
- editor.editing.view.focus();
511
- });
512
- // Clean up after the editor is destroyed.
513
- this.listenTo(editor, 'destroy', ()=>{
514
- this.fire('ckbox:close');
515
- this._chosenAssets.clear();
516
- });
517
- }
518
- /**
519
- * Inserts the asset into the model.
520
- *
521
- * @param asset The asset to be inserted.
522
- * @param isLastAsset Indicates if the current asset is the last one from the chosen set.
523
- * @param writer An instance of the model writer.
524
- * @param isSingleAsset It's true when only one asset is processed.
525
- */ _insertAsset(asset, isLastAsset, writer, isSingleAsset) {
526
- const editor = this.editor;
527
- const model = editor.model;
528
- const selection = model.document.selection;
529
- // Remove the `linkHref` attribute to not affect the asset to be inserted.
530
- writer.removeSelectionAttribute('linkHref');
531
- if (asset.type === 'image') {
532
- this._insertImage(asset);
533
- } else {
534
- this._insertLink(asset, writer, isSingleAsset);
535
- }
536
- // Except for the last chosen asset, move the selection to the end of the current range to avoid overwriting other, already
537
- // inserted assets.
538
- if (!isLastAsset) {
539
- writer.setSelection(selection.getLastPosition());
540
- }
541
- }
542
- /**
543
- * Inserts the image by calling the `insertImage` command.
544
- *
545
- * @param asset The asset to be inserted.
546
- */ _insertImage(asset) {
547
- const editor = this.editor;
548
- const { imageFallbackUrl, imageSources, imageTextAlternative, imageWidth, imageHeight, imagePlaceholder } = asset.attributes;
549
- editor.execute('insertImage', {
550
- source: {
551
- src: imageFallbackUrl,
552
- sources: imageSources,
553
- alt: imageTextAlternative,
554
- width: imageWidth,
555
- height: imageHeight,
556
- ...imagePlaceholder ? {
557
- placeholder: imagePlaceholder
558
- } : null
559
- }
560
- });
561
- }
562
- /**
563
- * Inserts the link to the asset by calling the `link` command.
564
- *
565
- * @param asset The asset to be inserted.
566
- * @param writer An instance of the model writer.
567
- * @param isSingleAsset It's true when only one asset is processed.
568
- */ _insertLink(asset, writer, isSingleAsset) {
569
- const editor = this.editor;
570
- const model = editor.model;
571
- const selection = model.document.selection;
572
- const { linkName, linkHref } = asset.attributes;
573
- // If the selection is collapsed, insert the asset name as the link label and select it.
574
- if (selection.isCollapsed) {
575
- const selectionAttributes = toMap(selection.getAttributes());
576
- const textNode = writer.createText(linkName, selectionAttributes);
577
- if (!isSingleAsset) {
578
- const selectionLastPosition = selection.getLastPosition();
579
- const parentElement = selectionLastPosition.parent;
580
- // Insert new `paragraph` when selection is not in an empty `paragraph`.
581
- if (!(parentElement.name === 'paragraph' && parentElement.isEmpty)) {
582
- editor.execute('insertParagraph', {
583
- position: selectionLastPosition
584
- });
585
- }
586
- const range = model.insertContent(textNode);
587
- writer.setSelection(range);
588
- editor.execute('link', linkHref);
589
- return;
590
- }
591
- const range = model.insertContent(textNode);
592
- writer.setSelection(range);
593
- }
594
- editor.execute('link', linkHref);
595
- }
596
- }
311
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
312
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
313
+ */
314
+ const ASSET_INSERTION_WAIT_TIMEOUT = 1e3;
597
315
  /**
598
- * Parses the chosen assets into the internal data format. Filters out chosen assets that are not allowed.
599
- */ function prepareAssets({ downloadableFilesConfig, assets, isImageAllowed, isLinkAllowed }) {
600
- return assets.map((asset)=>isImage(asset) ? {
601
- id: asset.data.id,
602
- type: 'image',
603
- attributes: prepareImageAssetAttributes(asset)
604
- } : {
605
- id: asset.data.id,
606
- type: 'link',
607
- attributes: prepareLinkAssetAttributes(asset, downloadableFilesConfig)
608
- }).filter((asset)=>asset.type === 'image' ? isImageAllowed : isLinkAllowed);
316
+ * The CKBox command. It is used by the {@link module:ckbox/ckboxediting~CKBoxEditing CKBox editing feature} to open the CKBox file manager.
317
+ * The file manager allows inserting an image or a link to a file into the editor content.
318
+ *
319
+ * ```ts
320
+ * editor.execute( 'ckbox' );
321
+ * ```
322
+ *
323
+ * **Note:** This command uses other features to perform the following tasks:
324
+ * - To insert images it uses the {@link module:image/image/insertimagecommand~InsertImageCommand 'insertImage'} command from the
325
+ * {@link module:image/image~Image Image feature}.
326
+ * - To insert links to other files it uses the {@link module:link/linkcommand~LinkCommand 'link'} command from the
327
+ * {@link module:link/link~Link Link feature}.
328
+ */
329
+ var CKBoxCommand = class extends Command {
330
+ /**
331
+ * A set of all chosen assets. They are stored temporarily and they are automatically removed 1 second after being chosen.
332
+ * Chosen assets have to be "remembered" for a while to be able to map the given asset with the element inserted into the model.
333
+ * This association map is then used to set the ID on the model element.
334
+ *
335
+ * All chosen assets are automatically removed after the timeout, because (theoretically) it may happen that they will never be
336
+ * inserted into the model, even if the {@link module:link/linkcommand~LinkCommand `'link'`} command or the
337
+ * {@link module:image/image/insertimagecommand~InsertImageCommand `'insertImage'`} command is enabled. Such a case may arise when
338
+ * another plugin blocks the command execution. Then, in order not to keep the chosen (but not inserted) assets forever, we delete
339
+ * them automatically to prevent memory leakage. The 1 second timeout is enough to insert the asset into the model and extract the
340
+ * ID from the chosen asset.
341
+ *
342
+ * The assets are stored only if
343
+ * the {@link module:ckbox/ckboxconfig~CKBoxConfig#ignoreDataId `config.ckbox.ignoreDataId`} option is set to `false` (by default).
344
+ *
345
+ * @internal
346
+ */
347
+ _chosenAssets = /* @__PURE__ */ new Set();
348
+ /**
349
+ * The DOM element that acts as a mounting point for the CKBox dialog.
350
+ */
351
+ _wrapper = null;
352
+ /**
353
+ * @inheritDoc
354
+ */
355
+ constructor(editor) {
356
+ super(editor);
357
+ this._initListeners();
358
+ }
359
+ /**
360
+ * @inheritDoc
361
+ */
362
+ refresh() {
363
+ this.value = this._getValue();
364
+ this.isEnabled = this._checkEnabled();
365
+ }
366
+ /**
367
+ * @inheritDoc
368
+ */
369
+ execute() {
370
+ this.fire("ckbox:open");
371
+ }
372
+ /**
373
+ * Indicates if the CKBox dialog is already opened.
374
+ *
375
+ * @protected
376
+ * @returns {Boolean}
377
+ */
378
+ _getValue() {
379
+ return this._wrapper !== null;
380
+ }
381
+ /**
382
+ * Checks whether the command can be enabled in the current context.
383
+ */
384
+ _checkEnabled() {
385
+ const imageCommand = this.editor.commands.get("insertImage");
386
+ const linkCommand = this.editor.commands.get("link");
387
+ if (!imageCommand.isEnabled && !linkCommand.isEnabled) return false;
388
+ return true;
389
+ }
390
+ /**
391
+ * Creates the options object for the CKBox dialog.
392
+ *
393
+ * @returns The object with properties:
394
+ * - theme The theme for CKBox dialog.
395
+ * - language The language for CKBox dialog.
396
+ * - tokenUrl The token endpoint URL.
397
+ * - serviceOrigin The base URL of the API service.
398
+ * - forceDemoLabel Whether to force "Powered by CKBox" link.
399
+ * - assets.onChoose The callback function invoked after choosing the assets.
400
+ * - dialog.onClose The callback function invoked after closing the CKBox dialog.
401
+ * - dialog.width The dialog width in pixels.
402
+ * - dialog.height The dialog height in pixels.
403
+ * - categories.icons Allows setting custom icons for categories.
404
+ * - view.openLastView Sets if the last view visited by the user will be reopened
405
+ * on the next startup.
406
+ * - view.startupFolderId Sets the ID of the folder that will be opened on startup.
407
+ * - view.startupCategoryId Sets the ID of the category that will be opened on startup.
408
+ * - view.hideMaximizeButton Sets whether to hide the ‘Maximize’ button.
409
+ * - view.componentsHideTimeout Sets timeout after which upload components are hidden
410
+ * after completed upload.
411
+ * - view.dialogMinimizeTimeout Sets timeout after which upload dialog is minimized
412
+ * after completed upload.
413
+ */
414
+ _prepareOptions() {
415
+ const ckboxConfig = this.editor.config.get("ckbox");
416
+ const dialog = ckboxConfig.dialog;
417
+ const categories = ckboxConfig.categories;
418
+ const view = ckboxConfig.view;
419
+ const upload = ckboxConfig.upload;
420
+ return {
421
+ theme: ckboxConfig.theme,
422
+ language: ckboxConfig.language,
423
+ tokenUrl: ckboxConfig.tokenUrl,
424
+ serviceOrigin: ckboxConfig.serviceOrigin,
425
+ forceDemoLabel: ckboxConfig.forceDemoLabel,
426
+ choosableFileExtensions: ckboxConfig.choosableFileExtensions,
427
+ assets: { onChoose: (assets) => this.fire("ckbox:choose", assets) },
428
+ dialog: {
429
+ onClose: () => this.fire("ckbox:close"),
430
+ width: dialog && dialog.width,
431
+ height: dialog && dialog.height
432
+ },
433
+ categories: categories && { icons: categories.icons },
434
+ view: view && {
435
+ openLastView: view.openLastView,
436
+ startupFolderId: view.startupFolderId,
437
+ startupCategoryId: view.startupCategoryId,
438
+ hideMaximizeButton: view.hideMaximizeButton
439
+ },
440
+ upload: upload && {
441
+ componentsHideTimeout: upload.componentsHideTimeout,
442
+ dialogMinimizeTimeout: upload.dialogMinimizeTimeout
443
+ }
444
+ };
445
+ }
446
+ /**
447
+ * Initializes various event listeners for the `ckbox:*` events, because all functionality of the `ckbox` command is event-based.
448
+ */
449
+ _initListeners() {
450
+ const editor = this.editor;
451
+ const model = editor.model;
452
+ const shouldInsertDataId = !editor.config.get("ckbox.ignoreDataId");
453
+ const downloadableFilesConfig = editor.config.get("ckbox.downloadableFiles");
454
+ this.on("ckbox", () => {
455
+ this.refresh();
456
+ }, { priority: "low" });
457
+ this.on("ckbox:open", () => {
458
+ if (!this.isEnabled || this.value) return;
459
+ this._wrapper = createElement(document, "div", { class: "ck ckbox-wrapper" });
460
+ document.body.appendChild(this._wrapper);
461
+ window.CKBox.mount(this._wrapper, this._prepareOptions());
462
+ });
463
+ this.on("ckbox:close", () => {
464
+ if (!this.value) return;
465
+ this._wrapper.remove();
466
+ this._wrapper = null;
467
+ editor.editing.view.focus();
468
+ });
469
+ this.on("ckbox:choose", (evt, assets) => {
470
+ if (!this.isEnabled) return;
471
+ const imageCommand = editor.commands.get("insertImage");
472
+ const linkCommand = editor.commands.get("link");
473
+ const assetsToProcess = prepareAssets({
474
+ assets,
475
+ downloadableFilesConfig,
476
+ isImageAllowed: imageCommand.isEnabled,
477
+ isLinkAllowed: linkCommand.isEnabled
478
+ });
479
+ const assetsCount = assetsToProcess.length;
480
+ if (assetsCount === 0) return;
481
+ model.change((writer) => {
482
+ for (const asset of assetsToProcess) {
483
+ const isLastAsset = asset === assetsToProcess[assetsCount - 1];
484
+ const isSingleAsset = assetsCount === 1;
485
+ this._insertAsset(asset, isLastAsset, writer, isSingleAsset);
486
+ if (shouldInsertDataId) {
487
+ setTimeout(() => this._chosenAssets.delete(asset), ASSET_INSERTION_WAIT_TIMEOUT);
488
+ this._chosenAssets.add(asset);
489
+ }
490
+ }
491
+ });
492
+ editor.editing.view.focus();
493
+ });
494
+ this.listenTo(editor, "destroy", () => {
495
+ this.fire("ckbox:close");
496
+ this._chosenAssets.clear();
497
+ });
498
+ }
499
+ /**
500
+ * Inserts the asset into the model.
501
+ *
502
+ * @param asset The asset to be inserted.
503
+ * @param isLastAsset Indicates if the current asset is the last one from the chosen set.
504
+ * @param writer An instance of the model writer.
505
+ * @param isSingleAsset It's true when only one asset is processed.
506
+ */
507
+ _insertAsset(asset, isLastAsset, writer, isSingleAsset) {
508
+ const selection = this.editor.model.document.selection;
509
+ writer.removeSelectionAttribute("linkHref");
510
+ if (asset.type === "image") this._insertImage(asset);
511
+ else this._insertLink(asset, writer, isSingleAsset);
512
+ if (!isLastAsset) writer.setSelection(selection.getLastPosition());
513
+ }
514
+ /**
515
+ * Inserts the image by calling the `insertImage` command.
516
+ *
517
+ * @param asset The asset to be inserted.
518
+ */
519
+ _insertImage(asset) {
520
+ const editor = this.editor;
521
+ const { imageFallbackUrl, imageSources, imageTextAlternative, imageWidth, imageHeight, imagePlaceholder } = asset.attributes;
522
+ editor.execute("insertImage", { source: {
523
+ src: imageFallbackUrl,
524
+ sources: imageSources,
525
+ alt: imageTextAlternative,
526
+ width: imageWidth,
527
+ height: imageHeight,
528
+ ...imagePlaceholder ? { placeholder: imagePlaceholder } : null
529
+ } });
530
+ }
531
+ /**
532
+ * Inserts the link to the asset by calling the `link` command.
533
+ *
534
+ * @param asset The asset to be inserted.
535
+ * @param writer An instance of the model writer.
536
+ * @param isSingleAsset It's true when only one asset is processed.
537
+ */
538
+ _insertLink(asset, writer, isSingleAsset) {
539
+ const editor = this.editor;
540
+ const model = editor.model;
541
+ const selection = model.document.selection;
542
+ const { linkName, linkHref } = asset.attributes;
543
+ if (selection.isCollapsed) {
544
+ const selectionAttributes = toMap(selection.getAttributes());
545
+ const textNode = writer.createText(linkName, selectionAttributes);
546
+ if (!isSingleAsset) {
547
+ const selectionLastPosition = selection.getLastPosition();
548
+ const parentElement = selectionLastPosition.parent;
549
+ if (!(parentElement.name === "paragraph" && parentElement.isEmpty)) editor.execute("insertParagraph", { position: selectionLastPosition });
550
+ const range = model.insertContent(textNode);
551
+ writer.setSelection(range);
552
+ editor.execute("link", linkHref);
553
+ return;
554
+ }
555
+ const range = model.insertContent(textNode);
556
+ writer.setSelection(range);
557
+ }
558
+ editor.execute("link", linkHref);
559
+ }
560
+ };
561
+ /**
562
+ * Parses the chosen assets into the internal data format. Filters out chosen assets that are not allowed.
563
+ */
564
+ function prepareAssets({ downloadableFilesConfig, assets, isImageAllowed, isLinkAllowed }) {
565
+ return assets.map((asset) => isImage(asset) ? {
566
+ id: asset.data.id,
567
+ type: "image",
568
+ attributes: prepareImageAssetAttributes(asset)
569
+ } : {
570
+ id: asset.data.id,
571
+ type: "link",
572
+ attributes: prepareLinkAssetAttributes(asset, downloadableFilesConfig)
573
+ }).filter((asset) => asset.type === "image" ? isImageAllowed : isLinkAllowed);
609
574
  }
610
575
  /**
611
- * Parses the assets attributes into the internal data format.
612
- *
613
- * @internal
614
- */ function prepareImageAssetAttributes(asset) {
615
- const { imageFallbackUrl, imageSources } = getImageUrls(asset.data.imageUrls);
616
- const { description, width, height, blurHash } = asset.data.metadata;
617
- const imagePlaceholder = blurHashToDataUrl(blurHash);
618
- return {
619
- imageFallbackUrl,
620
- imageSources,
621
- imageTextAlternative: description || '',
622
- imageWidth: width,
623
- imageHeight: height,
624
- ...imagePlaceholder ? {
625
- imagePlaceholder
626
- } : null
627
- };
576
+ * Parses the assets attributes into the internal data format.
577
+ *
578
+ * @internal
579
+ */
580
+ function prepareImageAssetAttributes(asset) {
581
+ const { imageFallbackUrl, imageSources } = getImageUrls(asset.data.imageUrls);
582
+ const { description, width, height, blurHash } = asset.data.metadata;
583
+ const imagePlaceholder = blurHashToDataUrl(blurHash);
584
+ return {
585
+ imageFallbackUrl,
586
+ imageSources,
587
+ imageTextAlternative: description || "",
588
+ imageWidth: width,
589
+ imageHeight: height,
590
+ ...imagePlaceholder ? { imagePlaceholder } : null
591
+ };
628
592
  }
629
593
  /**
630
- * Parses the assets attributes into the internal data format.
631
- *
632
- * @param asset The asset to prepare the attributes for.
633
- * @param config The CKBox download asset configuration.
634
- */ function prepareLinkAssetAttributes(asset, config) {
635
- return {
636
- linkName: asset.data.name,
637
- linkHref: getAssetUrl(asset, config)
638
- };
594
+ * Parses the assets attributes into the internal data format.
595
+ *
596
+ * @param asset The asset to prepare the attributes for.
597
+ * @param config The CKBox download asset configuration.
598
+ */
599
+ function prepareLinkAssetAttributes(asset, config) {
600
+ return {
601
+ linkName: asset.data.name,
602
+ linkHref: getAssetUrl(asset, config)
603
+ };
639
604
  }
640
605
  /**
641
- * Checks whether the asset is an image.
642
- */ function isImage(asset) {
643
- const metadata = asset.data.metadata;
644
- if (!metadata) {
645
- return false;
646
- }
647
- return metadata.width && metadata.height;
606
+ * Checks whether the asset is an image.
607
+ */
608
+ function isImage(asset) {
609
+ const metadata = asset.data.metadata;
610
+ if (!metadata) return false;
611
+ return metadata.width && metadata.height;
648
612
  }
649
613
  /**
650
- * Creates the URL for the asset.
651
- *
652
- * @param asset The asset to create the URL for.
653
- * @param config The CKBox download asset configuration.
654
- */ function getAssetUrl(asset, config) {
655
- const url = new URL(asset.data.url);
656
- if (isDownloadableAsset(asset, config)) {
657
- url.searchParams.set('download', 'true');
658
- }
659
- return url.toString();
614
+ * Creates the URL for the asset.
615
+ *
616
+ * @param asset The asset to create the URL for.
617
+ * @param config The CKBox download asset configuration.
618
+ */
619
+ function getAssetUrl(asset, config) {
620
+ const url = new URL(asset.data.url);
621
+ if (isDownloadableAsset(asset, config)) url.searchParams.set("download", "true");
622
+ return url.toString();
660
623
  }
661
624
  /**
662
- * Determines if download should be enabled for given asset based on configuration.
663
- *
664
- * @param asset The asset to check.
665
- * @param config The CKBox download asset configuration.
666
- */ function isDownloadableAsset(asset, config) {
667
- if (typeof config === 'function') {
668
- return config(asset);
669
- }
670
- return true;
625
+ * Determines if download should be enabled for given asset based on configuration.
626
+ *
627
+ * @param asset The asset to check.
628
+ * @param config The CKBox download asset configuration.
629
+ */
630
+ function isDownloadableAsset(asset, config) {
631
+ if (typeof config === "function") return config(asset);
632
+ return true;
671
633
  }
672
634
 
673
- const DEFAULT_CKBOX_THEME_NAME = 'lark';
674
635
  /**
675
- * The CKBox utilities plugin.
676
- */ class CKBoxUtils extends Plugin {
677
- /**
678
- * CKEditor Cloud Services access token.
679
- */ _token;
680
- /**
681
- * @inheritDoc
682
- */ static get pluginName() {
683
- return 'CKBoxUtils';
684
- }
685
- /**
686
- * @inheritDoc
687
- */ static get isOfficialPlugin() {
688
- return true;
689
- }
690
- /**
691
- * @inheritDoc
692
- */ static get requires() {
693
- return [
694
- CloudServices
695
- ];
696
- }
697
- /**
698
- * @inheritDoc
699
- */ init() {
700
- const editor = this.editor;
701
- const hasConfiguration = !!editor.config.get('ckbox');
702
- const isLibraryLoaded = !!window.CKBox;
703
- // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
704
- // the CKBox JavaScript library is loaded.
705
- if (!hasConfiguration && !isLibraryLoaded) {
706
- return;
707
- }
708
- editor.config.define('ckbox', {
709
- serviceOrigin: 'https://api.ckbox.io',
710
- defaultUploadCategories: null,
711
- ignoreDataId: false,
712
- language: editor.locale.uiLanguage,
713
- theme: DEFAULT_CKBOX_THEME_NAME,
714
- tokenUrl: editor.config.get('cloudServices.tokenUrl')
715
- });
716
- const cloudServices = editor.plugins.get(CloudServices);
717
- const cloudServicesTokenUrl = editor.config.get('cloudServices.tokenUrl');
718
- const ckboxTokenUrl = editor.config.get('ckbox.tokenUrl');
719
- if (!ckboxTokenUrl) {
720
- /**
721
- * The {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
722
- * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
723
- * configuration is required for the CKBox plugin.
724
- *
725
- * ```ts
726
- * ClassicEditor.create( {
727
- * attachTo: document.createElement( 'div' ),
728
- * ckbox: {
729
- * tokenUrl: "YOUR_TOKEN_URL"
730
- * // ...
731
- * }
732
- * // ...
733
- * } );
734
- * ```
735
- *
736
- * @error ckbox-plugin-missing-token-url
737
- */ throw new CKEditorError('ckbox-plugin-missing-token-url', this);
738
- }
739
- if (ckboxTokenUrl == cloudServicesTokenUrl) {
740
- this._token = Promise.resolve(cloudServices.token);
741
- } else {
742
- this._token = cloudServices.registerTokenUrl(ckboxTokenUrl);
743
- }
744
- // Grant access to private categories after token is fetched. This is done within the same promise chain
745
- // to ensure all services using the token have access to private categories.
746
- // This step is critical as previewing images from private categories requires proper cookies.
747
- this._token = this._token.then(async (token)=>{
748
- await this._authorizePrivateCategoriesAccess(token.value);
749
- return token;
750
- });
751
- }
752
- /**
753
- * Returns a token used by the CKBox plugin for communication with the CKBox service.
754
- */ getToken() {
755
- return this._token;
756
- }
757
- /**
758
- * The ID of workspace to use when uploading an image.
759
- */ async getWorkspaceId() {
760
- const t = this.editor.t;
761
- const cannotAccessDefaultWorkspaceError = t('Cannot access default workspace.');
762
- const defaultWorkspaceId = this.editor.config.get('ckbox.defaultUploadWorkspaceId');
763
- const workspaceId = getWorkspaceId(await this._token, defaultWorkspaceId);
764
- if (workspaceId == null) {
765
- /**
766
- * The user is not authorized to access the workspace defined in the`ckbox.defaultUploadWorkspaceId` configuration.
767
- *
768
- * @error ckbox-access-default-workspace-error
769
- */ logError('ckbox-access-default-workspace-error');
770
- throw cannotAccessDefaultWorkspaceError;
771
- }
772
- return workspaceId;
773
- }
774
- /**
775
- * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
776
- */ async getCategoryIdForFile(fileOrUrl, options) {
777
- const t = this.editor.t;
778
- const cannotFindCategoryError = t('Cannot determine a category for the uploaded file.');
779
- const defaultCategories = this.editor.config.get('ckbox.defaultUploadCategories');
780
- const allCategoriesPromise = this._getAvailableCategories(options);
781
- const extension = typeof fileOrUrl == 'string' ? convertMimeTypeToExtension(await getContentTypeOfUrl(fileOrUrl, options)) : getFileExtension(fileOrUrl);
782
- const allCategories = await allCategoriesPromise;
783
- // Couldn't fetch all categories. Perhaps the authorization token is invalid.
784
- if (!allCategories) {
785
- throw cannotFindCategoryError;
786
- }
787
- // If a user specifies the plugin configuration, find the first category that accepts the uploaded file.
788
- if (defaultCategories) {
789
- const userCategory = Object.keys(defaultCategories).find((category)=>{
790
- return defaultCategories[category].find((e)=>e.toLowerCase() == extension);
791
- });
792
- // If found, return its ID if the category exists on the server side.
793
- if (userCategory) {
794
- const serverCategory = allCategories.find((category)=>category.id === userCategory || category.name === userCategory);
795
- if (!serverCategory) {
796
- throw cannotFindCategoryError;
797
- }
798
- return serverCategory.id;
799
- }
800
- }
801
- // Otherwise, find the first category that accepts the uploaded file and returns its ID.
802
- const category = allCategories.find((category)=>category.extensions.find((e)=>e.toLowerCase() == extension));
803
- if (!category) {
804
- throw cannotFindCategoryError;
805
- }
806
- return category.id;
807
- }
808
- /**
809
- * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
810
- *
811
- * If the API returns limited results, the method will collect all items.
812
- */ async _getAvailableCategories(options) {
813
- const ITEMS_PER_REQUEST = 50;
814
- const editor = this.editor;
815
- const token = this._token;
816
- const { signal } = options;
817
- const serviceOrigin = editor.config.get('ckbox.serviceOrigin');
818
- const workspaceId = await this.getWorkspaceId();
819
- try {
820
- const result = [];
821
- let offset = 0;
822
- let remainingItems;
823
- do {
824
- const data = await fetchCategories(offset);
825
- result.push(...data.items);
826
- remainingItems = data.totalCount - (offset + ITEMS_PER_REQUEST);
827
- offset += ITEMS_PER_REQUEST;
828
- }while (remainingItems > 0)
829
- return result;
830
- } catch {
831
- signal.throwIfAborted();
832
- /**
833
- * Fetching a list of available categories with which an uploaded file can be associated failed.
834
- *
835
- * @error ckbox-fetch-category-http-error
836
- */ logError('ckbox-fetch-category-http-error');
837
- return undefined;
838
- }
839
- async function fetchCategories(offset) {
840
- const categoryUrl = new URL('categories', serviceOrigin);
841
- categoryUrl.searchParams.set('limit', String(ITEMS_PER_REQUEST));
842
- categoryUrl.searchParams.set('offset', String(offset));
843
- categoryUrl.searchParams.set('workspaceId', workspaceId);
844
- return sendHttpRequest({
845
- url: categoryUrl,
846
- signal,
847
- authorization: (await token).value
848
- });
849
- }
850
- }
851
- /**
852
- * Authorize private categories access to the CKBox service. Request sets cookie for the current domain,
853
- * that allows user to preview images from private categories.
854
- */ async _authorizePrivateCategoriesAccess(token) {
855
- const serviceUrl = this.editor.config.get('ckbox.serviceOrigin');
856
- const formData = new FormData();
857
- formData.set('token', token);
858
- await fetch(`${serviceUrl}/categories/authorizePrivateAccess`, {
859
- method: 'POST',
860
- credentials: 'include',
861
- mode: 'no-cors',
862
- body: formData
863
- });
864
- }
865
- }
636
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
637
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
638
+ */
639
+ /**
640
+ * @module ckbox/ckboxutils
641
+ */
642
+ const DEFAULT_CKBOX_THEME_NAME = "lark";
643
+ /**
644
+ * The CKBox utilities plugin.
645
+ */
646
+ var CKBoxUtils = class extends Plugin {
647
+ /**
648
+ * CKEditor Cloud Services access token.
649
+ */
650
+ _token;
651
+ /**
652
+ * @inheritDoc
653
+ */
654
+ static get pluginName() {
655
+ return "CKBoxUtils";
656
+ }
657
+ /**
658
+ * @inheritDoc
659
+ */
660
+ static get isOfficialPlugin() {
661
+ return true;
662
+ }
663
+ /**
664
+ * @inheritDoc
665
+ */
666
+ static get requires() {
667
+ return [CloudServices];
668
+ }
669
+ /**
670
+ * @inheritDoc
671
+ */
672
+ init() {
673
+ const editor = this.editor;
674
+ const hasConfiguration = !!editor.config.get("ckbox");
675
+ const isLibraryLoaded = !!window.CKBox;
676
+ if (!hasConfiguration && !isLibraryLoaded) return;
677
+ editor.config.define("ckbox", {
678
+ serviceOrigin: "https://api.ckbox.io",
679
+ defaultUploadCategories: null,
680
+ ignoreDataId: false,
681
+ language: editor.locale.uiLanguage,
682
+ theme: DEFAULT_CKBOX_THEME_NAME,
683
+ tokenUrl: editor.config.get("cloudServices.tokenUrl")
684
+ });
685
+ const cloudServices = editor.plugins.get(CloudServices);
686
+ const cloudServicesTokenUrl = editor.config.get("cloudServices.tokenUrl");
687
+ const ckboxTokenUrl = editor.config.get("ckbox.tokenUrl");
688
+ if (!ckboxTokenUrl)
689
+ /**
690
+ * The {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
691
+ * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
692
+ * configuration is required for the CKBox plugin.
693
+ *
694
+ * ```ts
695
+ * ClassicEditor.create( {
696
+ * attachTo: document.createElement( 'div' ),
697
+ * ckbox: {
698
+ * tokenUrl: "YOUR_TOKEN_URL"
699
+ * // ...
700
+ * }
701
+ * // ...
702
+ * } );
703
+ * ```
704
+ *
705
+ * @error ckbox-plugin-missing-token-url
706
+ */
707
+ throw new CKEditorError("ckbox-plugin-missing-token-url", this);
708
+ if (ckboxTokenUrl == cloudServicesTokenUrl) this._token = Promise.resolve(cloudServices.token);
709
+ else this._token = cloudServices.registerTokenUrl(ckboxTokenUrl);
710
+ this._token = this._token.then(async (token) => {
711
+ await this._authorizePrivateCategoriesAccess(token.value);
712
+ return token;
713
+ });
714
+ }
715
+ /**
716
+ * Returns a token used by the CKBox plugin for communication with the CKBox service.
717
+ */
718
+ getToken() {
719
+ return this._token;
720
+ }
721
+ /**
722
+ * The ID of workspace to use when uploading an image.
723
+ */
724
+ async getWorkspaceId() {
725
+ const t = this.editor.t;
726
+ const cannotAccessDefaultWorkspaceError = t("Cannot access default workspace.");
727
+ const defaultWorkspaceId = this.editor.config.get("ckbox.defaultUploadWorkspaceId");
728
+ const workspaceId = getWorkspaceId(await this._token, defaultWorkspaceId);
729
+ if (workspaceId == null) {
730
+ /**
731
+ * The user is not authorized to access the workspace defined in the`ckbox.defaultUploadWorkspaceId` configuration.
732
+ *
733
+ * @error ckbox-access-default-workspace-error
734
+ */
735
+ logError("ckbox-access-default-workspace-error");
736
+ throw cannotAccessDefaultWorkspaceError;
737
+ }
738
+ return workspaceId;
739
+ }
740
+ /**
741
+ * Resolves a promise with an object containing a category with which the uploaded file is associated or an error code.
742
+ */
743
+ async getCategoryIdForFile(fileOrUrl, options) {
744
+ const t = this.editor.t;
745
+ const cannotFindCategoryError = t("Cannot determine a category for the uploaded file.");
746
+ const noConnectionError = t("No internet connection. Check your connection and try again.");
747
+ if (isOffline()) throw noConnectionError;
748
+ const defaultCategories = this.editor.config.get("ckbox.defaultUploadCategories");
749
+ const allCategoriesPromise = this._getAvailableCategories(options);
750
+ const extension = typeof fileOrUrl == "string" ? convertMimeTypeToExtension(await getContentTypeOfUrl(fileOrUrl, options)) : getFileExtension(fileOrUrl);
751
+ const allCategories = await allCategoriesPromise;
752
+ if (!allCategories) throw isOffline() ? noConnectionError : cannotFindCategoryError;
753
+ if (defaultCategories) {
754
+ const userCategory = Object.keys(defaultCategories).find((category) => {
755
+ return defaultCategories[category].find((e) => e.toLowerCase() == extension);
756
+ });
757
+ if (userCategory) {
758
+ const serverCategory = allCategories.find((category) => category.id === userCategory || category.name === userCategory);
759
+ if (!serverCategory) throw cannotFindCategoryError;
760
+ return serverCategory.id;
761
+ }
762
+ }
763
+ const category = allCategories.find((category) => category.extensions.find((e) => e.toLowerCase() == extension));
764
+ if (!category) throw isOffline() ? noConnectionError : cannotFindCategoryError;
765
+ return category.id;
766
+ }
767
+ /**
768
+ * Resolves a promise with an array containing available categories with which the uploaded file can be associated.
769
+ *
770
+ * If the API returns limited results, the method will collect all items.
771
+ */
772
+ async _getAvailableCategories(options) {
773
+ const ITEMS_PER_REQUEST = 50;
774
+ const editor = this.editor;
775
+ const token = this._token;
776
+ const { signal } = options;
777
+ const serviceOrigin = editor.config.get("ckbox.serviceOrigin");
778
+ const workspaceId = await this.getWorkspaceId();
779
+ try {
780
+ const result = [];
781
+ let offset = 0;
782
+ let remainingItems;
783
+ do {
784
+ const data = await fetchCategories(offset);
785
+ result.push(...data.items);
786
+ remainingItems = data.totalCount - (offset + ITEMS_PER_REQUEST);
787
+ offset += ITEMS_PER_REQUEST;
788
+ } while (remainingItems > 0);
789
+ return result;
790
+ } catch {
791
+ signal.throwIfAborted();
792
+ /**
793
+ * Fetching a list of available categories with which an uploaded file can be associated failed.
794
+ *
795
+ * @error ckbox-fetch-category-http-error
796
+ */
797
+ logError("ckbox-fetch-category-http-error");
798
+ return;
799
+ }
800
+ async function fetchCategories(offset) {
801
+ const categoryUrl = new URL("categories", serviceOrigin);
802
+ categoryUrl.searchParams.set("limit", String(ITEMS_PER_REQUEST));
803
+ categoryUrl.searchParams.set("offset", String(offset));
804
+ categoryUrl.searchParams.set("workspaceId", workspaceId);
805
+ return sendHttpRequest({
806
+ url: categoryUrl,
807
+ signal,
808
+ authorization: (await token).value
809
+ });
810
+ }
811
+ }
812
+ /**
813
+ * Authorize private categories access to the CKBox service. Request sets cookie for the current domain,
814
+ * that allows user to preview images from private categories.
815
+ */
816
+ async _authorizePrivateCategoriesAccess(token) {
817
+ const serviceUrl = this.editor.config.get("ckbox.serviceOrigin");
818
+ const formData = new FormData();
819
+ formData.set("token", token);
820
+ await fetch(`${serviceUrl}/categories/authorizePrivateAccess`, {
821
+ method: "POST",
822
+ credentials: "include",
823
+ mode: "no-cors",
824
+ body: formData
825
+ });
826
+ }
827
+ };
866
828
 
867
829
  /**
868
- * A plugin that enables file uploads in CKEditor 5 using the CKBox server–side connector.
869
- * See the {@glink features/file-management/ckbox CKBox file manager integration} guide to learn how to configure
870
- * and use this feature as well as find out more about the full integration with the file manager
871
- * provided by the {@link module:ckbox/ckbox~CKBox} plugin.
872
- *
873
- * Check out the {@glink features/images/image-upload/image-upload Image upload overview} guide to learn about
874
- * other ways to upload images into CKEditor 5.
875
- */ class CKBoxUploadAdapter extends Plugin {
876
- /**
877
- * @inheritDoc
878
- */ static get requires() {
879
- return [
880
- ImageUploadEditing,
881
- ImageUploadProgress,
882
- FileRepository,
883
- CKBoxEditing
884
- ];
885
- }
886
- /**
887
- * @inheritDoc
888
- */ static get pluginName() {
889
- return 'CKBoxUploadAdapter';
890
- }
891
- /**
892
- * @inheritDoc
893
- */ static get isOfficialPlugin() {
894
- return true;
895
- }
896
- /**
897
- * @inheritDoc
898
- */ async afterInit() {
899
- const editor = this.editor;
900
- const hasConfiguration = !!editor.config.get('ckbox');
901
- const isLibraryLoaded = !!window.CKBox;
902
- // Editor supports only one upload adapter. Register the CKBox upload adapter (and potentially overwrite other one) only when the
903
- // integrator intentionally wants to use the CKBox plugin, i.e. when the `config.ckbox` exists or the CKBox JavaScript library is
904
- // loaded.
905
- if (!hasConfiguration && !isLibraryLoaded) {
906
- return;
907
- }
908
- const fileRepository = editor.plugins.get(FileRepository);
909
- const ckboxUtils = editor.plugins.get(CKBoxUtils);
910
- fileRepository.createUploadAdapter = (loader)=>new Adapter(loader, editor, ckboxUtils);
911
- const shouldInsertDataId = !editor.config.get('ckbox.ignoreDataId');
912
- const imageUploadEditing = editor.plugins.get(ImageUploadEditing);
913
- // Mark uploaded assets with the `ckboxImageId` attribute. Its value represents an ID in CKBox.
914
- if (shouldInsertDataId) {
915
- imageUploadEditing.on('uploadComplete', (evt, { imageElement, data })=>{
916
- editor.model.change((writer)=>{
917
- writer.setAttribute('ckboxImageId', data.ckboxImageId, imageElement);
918
- });
919
- });
920
- }
921
- }
922
- }
830
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
831
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
832
+ */
923
833
  /**
924
- * Upload adapter for CKBox.
925
- */ class Adapter {
926
- /**
927
- * FileLoader instance to use during the upload.
928
- */ loader;
929
- /**
930
- * CKEditor Cloud Services access token.
931
- */ token;
932
- /**
933
- * The editor instance.
934
- */ editor;
935
- /**
936
- * The abort controller for aborting asynchronous processes.
937
- */ controller;
938
- /**
939
- * The base URL where all requests should be sent.
940
- */ serviceOrigin;
941
- /**
942
- * The reference to CKBoxUtils plugin.
943
- */ ckboxUtils;
944
- /**
945
- * Creates a new adapter instance.
946
- */ constructor(loader, editor, ckboxUtils){
947
- this.loader = loader;
948
- this.token = ckboxUtils.getToken();
949
- this.ckboxUtils = ckboxUtils;
950
- this.editor = editor;
951
- this.controller = new AbortController();
952
- this.serviceOrigin = editor.config.get('ckbox.serviceOrigin');
953
- }
954
- /**
955
- * Starts the upload process.
956
- *
957
- * @see module:upload/filerepository~UploadAdapter#upload
958
- */ async upload() {
959
- const ckboxUtils = this.ckboxUtils;
960
- const t = this.editor.t;
961
- const file = await this.loader.file;
962
- const category = await ckboxUtils.getCategoryIdForFile(file, {
963
- signal: this.controller.signal
964
- });
965
- const uploadUrl = new URL('assets', this.serviceOrigin);
966
- const formData = new FormData();
967
- uploadUrl.searchParams.set('workspaceId', await ckboxUtils.getWorkspaceId());
968
- formData.append('categoryId', category);
969
- formData.append('file', file);
970
- const requestConfig = {
971
- method: 'POST',
972
- url: uploadUrl,
973
- data: formData,
974
- onUploadProgress: (evt)=>{
975
- /* istanbul ignore else -- @preserve */ if (evt.lengthComputable) {
976
- this.loader.uploadTotal = evt.total;
977
- this.loader.uploaded = evt.loaded;
978
- }
979
- },
980
- signal: this.controller.signal,
981
- authorization: (await this.token).value
982
- };
983
- return sendHttpRequest(requestConfig).then(async (data)=>{
984
- const imageUrls = getImageUrls(data.imageUrls);
985
- return {
986
- ckboxImageId: data.id,
987
- default: imageUrls.imageFallbackUrl,
988
- sources: imageUrls.imageSources
989
- };
990
- }).catch(()=>{
991
- const genericError = t('Cannot upload file:') + ` ${file.name}.`;
992
- return Promise.reject(genericError);
993
- });
994
- }
995
- /**
996
- * Aborts the upload process.
997
- *
998
- * @see module:upload/filerepository~UploadAdapter#abort
999
- */ abort() {
1000
- this.controller.abort();
1001
- }
1002
- }
834
+ * @module ckbox/ckboxuploadadapter
835
+ */
836
+ /**
837
+ * A plugin that enables file uploads in CKEditor 5 using the CKBox server–side connector.
838
+ * See the {@glink features/file-management/ckbox CKBox file manager integration} guide to learn how to configure
839
+ * and use this feature as well as find out more about the full integration with the file manager
840
+ * provided by the {@link module:ckbox/ckbox~CKBox} plugin.
841
+ *
842
+ * Check out the {@glink features/images/image-upload/image-upload Image upload overview} guide to learn about
843
+ * other ways to upload images into CKEditor 5.
844
+ */
845
+ var CKBoxUploadAdapter = class extends Plugin {
846
+ /**
847
+ * @inheritDoc
848
+ */
849
+ static get requires() {
850
+ return [
851
+ ImageUploadEditing,
852
+ ImageUploadProgress,
853
+ FileRepository,
854
+ CKBoxEditing
855
+ ];
856
+ }
857
+ /**
858
+ * @inheritDoc
859
+ */
860
+ static get pluginName() {
861
+ return "CKBoxUploadAdapter";
862
+ }
863
+ /**
864
+ * @inheritDoc
865
+ */
866
+ static get isOfficialPlugin() {
867
+ return true;
868
+ }
869
+ /**
870
+ * @inheritDoc
871
+ */
872
+ async afterInit() {
873
+ const editor = this.editor;
874
+ const hasConfiguration = !!editor.config.get("ckbox");
875
+ const isLibraryLoaded = !!window.CKBox;
876
+ if (!hasConfiguration && !isLibraryLoaded) return;
877
+ const fileRepository = editor.plugins.get(FileRepository);
878
+ const ckboxUtils = editor.plugins.get(CKBoxUtils);
879
+ fileRepository.createUploadAdapter = (loader) => new Adapter(loader, editor, ckboxUtils);
880
+ const shouldInsertDataId = !editor.config.get("ckbox.ignoreDataId");
881
+ const imageUploadEditing = editor.plugins.get(ImageUploadEditing);
882
+ if (shouldInsertDataId) imageUploadEditing.on("uploadComplete", (evt, { imageElement, data }) => {
883
+ editor.model.change((writer) => {
884
+ writer.setAttribute("ckboxImageId", data.ckboxImageId, imageElement);
885
+ });
886
+ });
887
+ }
888
+ };
889
+ /**
890
+ * Upload adapter for CKBox.
891
+ */
892
+ var Adapter = class {
893
+ /**
894
+ * FileLoader instance to use during the upload.
895
+ */
896
+ loader;
897
+ /**
898
+ * CKEditor Cloud Services access token.
899
+ */
900
+ token;
901
+ /**
902
+ * The editor instance.
903
+ */
904
+ editor;
905
+ /**
906
+ * The abort controller for aborting asynchronous processes.
907
+ */
908
+ controller;
909
+ /**
910
+ * The base URL where all requests should be sent.
911
+ */
912
+ serviceOrigin;
913
+ /**
914
+ * The reference to CKBoxUtils plugin.
915
+ */
916
+ ckboxUtils;
917
+ /**
918
+ * Creates a new adapter instance.
919
+ */
920
+ constructor(loader, editor, ckboxUtils) {
921
+ this.loader = loader;
922
+ this.token = ckboxUtils.getToken();
923
+ this.ckboxUtils = ckboxUtils;
924
+ this.editor = editor;
925
+ this.controller = new AbortController();
926
+ this.serviceOrigin = editor.config.get("ckbox.serviceOrigin");
927
+ }
928
+ /**
929
+ * Starts the upload process.
930
+ *
931
+ * @see module:upload/filerepository~UploadAdapter#upload
932
+ */
933
+ async upload() {
934
+ const ckboxUtils = this.ckboxUtils;
935
+ const t = this.editor.t;
936
+ const file = await this.loader.file;
937
+ const category = await ckboxUtils.getCategoryIdForFile(file, { signal: this.controller.signal });
938
+ const uploadUrl = new URL("assets", this.serviceOrigin);
939
+ const formData = new FormData();
940
+ uploadUrl.searchParams.set("workspaceId", await ckboxUtils.getWorkspaceId());
941
+ formData.append("categoryId", category);
942
+ formData.append("file", file);
943
+ const requestConfig = {
944
+ method: "POST",
945
+ url: uploadUrl,
946
+ data: formData,
947
+ onUploadProgress: (evt) => {
948
+ /* istanbul ignore else -- @preserve */
949
+ if (evt.lengthComputable) {
950
+ this.loader.uploadTotal = evt.total;
951
+ this.loader.uploaded = evt.loaded;
952
+ }
953
+ },
954
+ signal: this.controller.signal,
955
+ authorization: (await this.token).value
956
+ };
957
+ return sendHttpRequest(requestConfig).then(async (data) => {
958
+ const imageUrls = getImageUrls(data.imageUrls);
959
+ return {
960
+ ckboxImageId: data.id,
961
+ default: imageUrls.imageFallbackUrl,
962
+ sources: imageUrls.imageSources
963
+ };
964
+ }).catch(() => {
965
+ const genericError = t("Cannot upload file:") + ` ${file.name}.`;
966
+ return Promise.reject(genericError);
967
+ });
968
+ }
969
+ /**
970
+ * Aborts the upload process.
971
+ *
972
+ * @see module:upload/filerepository~UploadAdapter#abort
973
+ */
974
+ abort() {
975
+ this.controller.abort();
976
+ }
977
+ };
1003
978
 
1004
- const COMMAND_FORCE_DISABLE_ID = 'NoPermission';
1005
979
  /**
1006
- * The CKBox editing feature. It introduces the {@link module:ckbox/ckboxcommand~CKBoxCommand CKBox command} and
1007
- * {@link module:ckbox/ckboxuploadadapter~CKBoxUploadAdapter CKBox upload adapter}.
1008
- */ class CKBoxEditing extends Plugin {
1009
- /**
1010
- * @inheritDoc
1011
- */ static get pluginName() {
1012
- return 'CKBoxEditing';
1013
- }
1014
- /**
1015
- * @inheritDoc
1016
- */ static get isOfficialPlugin() {
1017
- return true;
1018
- }
1019
- /**
1020
- * @inheritDoc
1021
- */ static get requires() {
1022
- return [
1023
- LinkEditing,
1024
- PictureEditing,
1025
- CKBoxUploadAdapter,
1026
- CKBoxUtils
1027
- ];
1028
- }
1029
- /**
1030
- * @inheritDoc
1031
- */ init() {
1032
- const editor = this.editor;
1033
- if (!this._shouldBeInitialised()) {
1034
- return;
1035
- }
1036
- this._checkImagePlugins();
1037
- // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
1038
- if (isLibraryLoaded()) {
1039
- editor.commands.add('ckbox', new CKBoxCommand(editor));
1040
- }
1041
- // Promise is not handled intentionally. Errors should be displayed in console if there are so.
1042
- isUploadPermissionGranted(editor).then((isCreateAssetAllowed)=>{
1043
- if (!isCreateAssetAllowed) {
1044
- this._blockImageCommands();
1045
- }
1046
- });
1047
- }
1048
- /**
1049
- * @inheritDoc
1050
- */ afterInit() {
1051
- const editor = this.editor;
1052
- if (!this._shouldBeInitialised()) {
1053
- return;
1054
- }
1055
- // Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign
1056
- // the assets ID with the model elements is enabled.
1057
- if (!editor.config.get('ckbox.ignoreDataId')) {
1058
- this._initSchema();
1059
- this._initConversion();
1060
- this._initFixers();
1061
- }
1062
- }
1063
- /**
1064
- * Returns true only when the integrator intentionally wants to use the plugin, i.e. when the `config.ckbox` exists or
1065
- * the CKBox JavaScript library is loaded.
1066
- */ _shouldBeInitialised() {
1067
- const editor = this.editor;
1068
- const hasConfiguration = !!editor.config.get('ckbox');
1069
- return hasConfiguration || isLibraryLoaded();
1070
- }
1071
- /**
1072
- * Blocks `uploadImage` and `ckboxImageEdit` commands.
1073
- */ _blockImageCommands() {
1074
- const editor = this.editor;
1075
- const uploadImageCommand = editor.commands.get('uploadImage');
1076
- const imageEditingCommand = editor.commands.get('ckboxImageEdit');
1077
- if (uploadImageCommand) {
1078
- uploadImageCommand.isAccessAllowed = false;
1079
- uploadImageCommand.forceDisabled(COMMAND_FORCE_DISABLE_ID);
1080
- }
1081
- if (imageEditingCommand) {
1082
- imageEditingCommand.forceDisabled(COMMAND_FORCE_DISABLE_ID);
1083
- }
1084
- }
1085
- /**
1086
- * Checks if at least one image plugin is loaded.
1087
- */ _checkImagePlugins() {
1088
- const editor = this.editor;
1089
- if (!editor.plugins.has('ImageBlockEditing') && !editor.plugins.has('ImageInlineEditing')) {
1090
- /**
1091
- * The CKBox feature requires one of the following plugins to be loaded to work correctly:
1092
- *
1093
- * * {@link module:image/imageblock~ImageBlock},
1094
- * * {@link module:image/imageinline~ImageInline},
1095
- * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
1096
- *
1097
- * Please make sure your editor configuration is correct.
1098
- *
1099
- * @error ckbox-plugin-image-feature-missing
1100
- * @param {module:core/editor/editor~Editor} editor The editor instance.
1101
- */ logError('ckbox-plugin-image-feature-missing', editor);
1102
- }
1103
- }
1104
- /**
1105
- * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
1106
- */ _initSchema() {
1107
- const editor = this.editor;
1108
- const schema = editor.model.schema;
1109
- schema.extend('$text', {
1110
- allowAttributes: 'ckboxLinkId'
1111
- });
1112
- if (schema.isRegistered('imageBlock')) {
1113
- schema.extend('imageBlock', {
1114
- allowAttributes: [
1115
- 'ckboxImageId',
1116
- 'ckboxLinkId'
1117
- ]
1118
- });
1119
- }
1120
- if (schema.isRegistered('imageInline')) {
1121
- schema.extend('imageInline', {
1122
- allowAttributes: [
1123
- 'ckboxImageId',
1124
- 'ckboxLinkId'
1125
- ]
1126
- });
1127
- }
1128
- schema.addAttributeCheck((context)=>{
1129
- // Don't allow `ckboxLinkId` on elements which do not have `linkHref` attribute.
1130
- if (!context.last.getAttribute('linkHref')) {
1131
- return false;
1132
- }
1133
- }, 'ckboxLinkId');
1134
- }
1135
- /**
1136
- * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
1137
- */ _initConversion() {
1138
- const editor = this.editor;
1139
- // Convert `ckboxLinkId` => `data-ckbox-resource-id`.
1140
- editor.conversion.for('downcast').add((dispatcher)=>{
1141
- // Due to custom converters for linked block images, handle the `ckboxLinkId` attribute manually.
1142
- dispatcher.on('attribute:ckboxLinkId:imageBlock', (evt, data, conversionApi)=>{
1143
- const { writer, mapper, consumable } = conversionApi;
1144
- if (!consumable.consume(data.item, evt.name)) {
1145
- return;
1146
- }
1147
- const viewFigure = mapper.toViewElement(data.item);
1148
- const linkInImage = [
1149
- ...viewFigure.getChildren()
1150
- ].find((child)=>child.name === 'a');
1151
- // No link inside an image - no conversion needed.
1152
- if (!linkInImage) {
1153
- return;
1154
- }
1155
- if (data.item.hasAttribute('ckboxLinkId')) {
1156
- writer.setAttribute('data-ckbox-resource-id', data.item.getAttribute('ckboxLinkId'), linkInImage);
1157
- } else {
1158
- writer.removeAttribute('data-ckbox-resource-id', linkInImage);
1159
- }
1160
- }, {
1161
- priority: 'low'
1162
- });
1163
- dispatcher.on('attribute:ckboxLinkId', (evt, data, conversionApi)=>{
1164
- const { writer, mapper, consumable } = conversionApi;
1165
- if (!consumable.consume(data.item, evt.name)) {
1166
- return;
1167
- }
1168
- // Remove the previous attribute value if it was applied.
1169
- if (data.attributeOldValue) {
1170
- const viewElement = createLinkElement(writer, data.attributeOldValue);
1171
- writer.unwrap(mapper.toViewRange(data.range), viewElement);
1172
- }
1173
- // Add the new attribute value if specified in a model element.
1174
- if (data.attributeNewValue) {
1175
- const viewElement = createLinkElement(writer, data.attributeNewValue);
1176
- if (data.item.is('selection')) {
1177
- const viewSelection = writer.document.selection;
1178
- writer.wrap(viewSelection.getFirstRange(), viewElement);
1179
- } else {
1180
- writer.wrap(mapper.toViewRange(data.range), viewElement);
1181
- }
1182
- }
1183
- }, {
1184
- priority: 'low'
1185
- });
1186
- });
1187
- // Convert `data-ckbox-resource-id` => `ckboxLinkId`.
1188
- //
1189
- // The helper conversion does not handle all cases, so take care of the `data-ckbox-resource-id` attribute manually for images
1190
- // and links.
1191
- editor.conversion.for('upcast').add((dispatcher)=>{
1192
- dispatcher.on('element:a', (evt, data, conversionApi)=>{
1193
- const { writer, consumable } = conversionApi;
1194
- // Upcast the `data-ckbox-resource-id` attribute only for valid link elements.
1195
- if (!data.viewItem.getAttribute('href')) {
1196
- return;
1197
- }
1198
- const consumableAttributes = {
1199
- attributes: [
1200
- 'data-ckbox-resource-id'
1201
- ]
1202
- };
1203
- if (!consumable.consume(data.viewItem, consumableAttributes)) {
1204
- return;
1205
- }
1206
- const attributeValue = data.viewItem.getAttribute('data-ckbox-resource-id');
1207
- // Missing the `data-ckbox-resource-id` attribute.
1208
- if (!attributeValue) {
1209
- return;
1210
- }
1211
- if (data.modelRange) {
1212
- // If the `<a>` element contains more than single children (e.g. a linked image), set the `ckboxLinkId` for each
1213
- // allowed child.
1214
- for (let item of data.modelRange.getItems()){
1215
- if (item.is('$textProxy')) {
1216
- item = item.textNode;
1217
- }
1218
- // Do not copy the `ckboxLinkId` attribute when wrapping an element in a block element, e.g. when
1219
- // auto-paragraphing.
1220
- if (shouldUpcastAttributeForNode(item)) {
1221
- writer.setAttribute('ckboxLinkId', attributeValue, item);
1222
- }
1223
- }
1224
- } else {
1225
- // Otherwise, just set the `ckboxLinkId` for the model element.
1226
- const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
1227
- writer.setAttribute('ckboxLinkId', attributeValue, modelElement);
1228
- }
1229
- }, {
1230
- priority: 'low'
1231
- });
1232
- });
1233
- // Convert `ckboxImageId` => `data-ckbox-resource-id`.
1234
- editor.conversion.for('downcast').attributeToAttribute({
1235
- model: 'ckboxImageId',
1236
- view: 'data-ckbox-resource-id'
1237
- });
1238
- // Convert `data-ckbox-resource-id` => `ckboxImageId`.
1239
- editor.conversion.for('upcast').elementToAttribute({
1240
- model: {
1241
- key: 'ckboxImageId',
1242
- value: (viewElement)=>viewElement.getAttribute('data-ckbox-resource-id')
1243
- },
1244
- view: {
1245
- attributes: {
1246
- 'data-ckbox-resource-id': /[\s\S]+/
1247
- }
1248
- }
1249
- });
1250
- const replaceImageSourceCommand = editor.commands.get('replaceImageSource');
1251
- if (replaceImageSourceCommand) {
1252
- this.listenTo(replaceImageSourceCommand, 'cleanupImage', (_, [writer, image])=>{
1253
- writer.removeAttribute('ckboxImageId', image);
1254
- });
1255
- }
1256
- }
1257
- /**
1258
- * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
1259
- */ _initFixers() {
1260
- const editor = this.editor;
1261
- const model = editor.model;
1262
- const selection = model.document.selection;
1263
- // Registers the post-fixer to sync the asset ID with the model elements.
1264
- model.document.registerPostFixer(syncDataIdPostFixer(editor));
1265
- // Registers the post-fixer to remove the `ckboxLinkId` attribute from the model selection.
1266
- model.document.registerPostFixer(injectSelectionPostFixer(selection));
1267
- }
1268
- }
980
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
981
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
982
+ */
983
+ /**
984
+ * @module ckbox/ckboxediting
985
+ */
986
+ const COMMAND_FORCE_DISABLE_ID = "NoPermission";
987
+ /**
988
+ * The CKBox editing feature. It introduces the {@link module:ckbox/ckboxcommand~CKBoxCommand CKBox command} and
989
+ * {@link module:ckbox/ckboxuploadadapter~CKBoxUploadAdapter CKBox upload adapter}.
990
+ */
991
+ var CKBoxEditing = class extends Plugin {
992
+ /**
993
+ * @inheritDoc
994
+ */
995
+ static get pluginName() {
996
+ return "CKBoxEditing";
997
+ }
998
+ /**
999
+ * @inheritDoc
1000
+ */
1001
+ static get isOfficialPlugin() {
1002
+ return true;
1003
+ }
1004
+ /**
1005
+ * @inheritDoc
1006
+ */
1007
+ static get requires() {
1008
+ return [
1009
+ LinkEditing,
1010
+ PictureEditing,
1011
+ CKBoxUploadAdapter,
1012
+ CKBoxUtils
1013
+ ];
1014
+ }
1015
+ /**
1016
+ * @inheritDoc
1017
+ */
1018
+ init() {
1019
+ const editor = this.editor;
1020
+ if (!this._shouldBeInitialised()) return;
1021
+ this._checkImagePlugins();
1022
+ if (isLibraryLoaded()) editor.commands.add("ckbox", new CKBoxCommand(editor));
1023
+ isUploadPermissionGranted(editor).then((isCreateAssetAllowed) => {
1024
+ if (!isCreateAssetAllowed) this._blockImageCommands();
1025
+ });
1026
+ }
1027
+ /**
1028
+ * @inheritDoc
1029
+ */
1030
+ afterInit() {
1031
+ const editor = this.editor;
1032
+ if (!this._shouldBeInitialised()) return;
1033
+ if (!editor.config.get("ckbox.ignoreDataId")) {
1034
+ this._initSchema();
1035
+ this._initConversion();
1036
+ this._initFixers();
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Returns true only when the integrator intentionally wants to use the plugin, i.e. when the `config.ckbox` exists or
1041
+ * the CKBox JavaScript library is loaded.
1042
+ */
1043
+ _shouldBeInitialised() {
1044
+ return !!this.editor.config.get("ckbox") || isLibraryLoaded();
1045
+ }
1046
+ /**
1047
+ * Blocks `uploadImage` and `ckboxImageEdit` commands.
1048
+ */
1049
+ _blockImageCommands() {
1050
+ const editor = this.editor;
1051
+ const uploadImageCommand = editor.commands.get("uploadImage");
1052
+ const imageEditingCommand = editor.commands.get("ckboxImageEdit");
1053
+ if (uploadImageCommand) {
1054
+ uploadImageCommand.isAccessAllowed = false;
1055
+ uploadImageCommand.forceDisabled(COMMAND_FORCE_DISABLE_ID);
1056
+ }
1057
+ if (imageEditingCommand) imageEditingCommand.forceDisabled(COMMAND_FORCE_DISABLE_ID);
1058
+ }
1059
+ /**
1060
+ * Checks if at least one image plugin is loaded.
1061
+ */
1062
+ _checkImagePlugins() {
1063
+ const editor = this.editor;
1064
+ if (!editor.plugins.has("ImageBlockEditing") && !editor.plugins.has("ImageInlineEditing"))
1065
+ /**
1066
+ * The CKBox feature requires one of the following plugins to be loaded to work correctly:
1067
+ *
1068
+ * * {@link module:image/imageblock~ImageBlock},
1069
+ * * {@link module:image/imageinline~ImageInline},
1070
+ * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
1071
+ *
1072
+ * Please make sure your editor configuration is correct.
1073
+ *
1074
+ * @error ckbox-plugin-image-feature-missing
1075
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
1076
+ */
1077
+ logError("ckbox-plugin-image-feature-missing", editor);
1078
+ }
1079
+ /**
1080
+ * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
1081
+ */
1082
+ _initSchema() {
1083
+ const schema = this.editor.model.schema;
1084
+ schema.extend("$text", { allowAttributes: "ckboxLinkId" });
1085
+ if (schema.isRegistered("imageBlock")) schema.extend("imageBlock", { allowAttributes: ["ckboxImageId", "ckboxLinkId"] });
1086
+ if (schema.isRegistered("imageInline")) schema.extend("imageInline", { allowAttributes: ["ckboxImageId", "ckboxLinkId"] });
1087
+ schema.addAttributeCheck((context) => {
1088
+ if (!context.last.getAttribute("linkHref")) return false;
1089
+ }, "ckboxLinkId");
1090
+ }
1091
+ /**
1092
+ * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
1093
+ */
1094
+ _initConversion() {
1095
+ const editor = this.editor;
1096
+ editor.conversion.for("downcast").add((dispatcher) => {
1097
+ dispatcher.on("attribute:ckboxLinkId:imageBlock", (evt, data, conversionApi) => {
1098
+ const { writer, mapper, consumable } = conversionApi;
1099
+ if (!consumable.consume(data.item, evt.name)) return;
1100
+ const linkInImage = [...mapper.toViewElement(data.item).getChildren()].find((child) => child.name === "a");
1101
+ if (!linkInImage) return;
1102
+ if (data.item.hasAttribute("ckboxLinkId")) writer.setAttribute("data-ckbox-resource-id", data.item.getAttribute("ckboxLinkId"), linkInImage);
1103
+ else writer.removeAttribute("data-ckbox-resource-id", linkInImage);
1104
+ }, { priority: "low" });
1105
+ dispatcher.on("attribute:ckboxLinkId", (evt, data, conversionApi) => {
1106
+ const { writer, mapper, consumable } = conversionApi;
1107
+ if (!consumable.consume(data.item, evt.name)) return;
1108
+ if (data.attributeOldValue) {
1109
+ const viewElement = createLinkElement(writer, data.attributeOldValue);
1110
+ writer.unwrap(mapper.toViewRange(data.range), viewElement);
1111
+ }
1112
+ if (data.attributeNewValue) {
1113
+ const viewElement = createLinkElement(writer, data.attributeNewValue);
1114
+ if (data.item.is("selection")) {
1115
+ const viewSelection = writer.document.selection;
1116
+ writer.wrap(viewSelection.getFirstRange(), viewElement);
1117
+ } else writer.wrap(mapper.toViewRange(data.range), viewElement);
1118
+ }
1119
+ }, { priority: "low" });
1120
+ });
1121
+ editor.conversion.for("upcast").add((dispatcher) => {
1122
+ dispatcher.on("element:a", (evt, data, conversionApi) => {
1123
+ const { writer, consumable } = conversionApi;
1124
+ if (!data.viewItem.getAttribute("href")) return;
1125
+ if (!consumable.consume(data.viewItem, { attributes: ["data-ckbox-resource-id"] })) return;
1126
+ const attributeValue = data.viewItem.getAttribute("data-ckbox-resource-id");
1127
+ if (!attributeValue) return;
1128
+ if (data.modelRange) for (let item of data.modelRange.getItems()) {
1129
+ if (item.is("$textProxy")) item = item.textNode;
1130
+ if (shouldUpcastAttributeForNode(item)) writer.setAttribute("ckboxLinkId", attributeValue, item);
1131
+ }
1132
+ else {
1133
+ const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
1134
+ writer.setAttribute("ckboxLinkId", attributeValue, modelElement);
1135
+ }
1136
+ }, { priority: "low" });
1137
+ });
1138
+ editor.conversion.for("downcast").attributeToAttribute({
1139
+ model: "ckboxImageId",
1140
+ view: "data-ckbox-resource-id"
1141
+ });
1142
+ editor.conversion.for("upcast").elementToAttribute({
1143
+ model: {
1144
+ key: "ckboxImageId",
1145
+ value: (viewElement) => viewElement.getAttribute("data-ckbox-resource-id")
1146
+ },
1147
+ view: { attributes: { "data-ckbox-resource-id": /[\s\S]+/ } }
1148
+ });
1149
+ const replaceImageSourceCommand = editor.commands.get("replaceImageSource");
1150
+ if (replaceImageSourceCommand) this.listenTo(replaceImageSourceCommand, "cleanupImage", (_, [writer, image]) => {
1151
+ writer.removeAttribute("ckboxImageId", image);
1152
+ });
1153
+ }
1154
+ /**
1155
+ * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
1156
+ */
1157
+ _initFixers() {
1158
+ const editor = this.editor;
1159
+ const model = editor.model;
1160
+ const selection = model.document.selection;
1161
+ model.document.registerPostFixer(syncDataIdPostFixer(editor));
1162
+ model.document.registerPostFixer(injectSelectionPostFixer(selection));
1163
+ }
1164
+ };
1269
1165
  /**
1270
- * A post-fixer that synchronizes the asset ID with the model element.
1271
- */ function syncDataIdPostFixer(editor) {
1272
- return (writer)=>{
1273
- let changed = false;
1274
- const model = editor.model;
1275
- const ckboxCommand = editor.commands.get('ckbox');
1276
- // The ID from chosen assets are stored in the `CKBoxCommand#_chosenAssets`. If there is no command, it makes no sense to check
1277
- // for changes in the model.
1278
- if (!ckboxCommand) {
1279
- return changed;
1280
- }
1281
- for (const entry of model.document.differ.getChanges()){
1282
- if (entry.type !== 'insert' && entry.type !== 'attribute') {
1283
- continue;
1284
- }
1285
- const range = entry.type === 'insert' ? new ModelRange(entry.position, entry.position.getShiftedBy(entry.length)) : entry.range;
1286
- const isLinkHrefAttributeRemoval = entry.type === 'attribute' && entry.attributeKey === 'linkHref' && entry.attributeNewValue === null;
1287
- for (const item of range.getItems()){
1288
- // If the `linkHref` attribute has been removed, sync the change with the `ckboxLinkId` attribute.
1289
- if (isLinkHrefAttributeRemoval && item.hasAttribute('ckboxLinkId')) {
1290
- writer.removeAttribute('ckboxLinkId', item);
1291
- changed = true;
1292
- continue;
1293
- }
1294
- // Otherwise, the change concerns either a new model element or an attribute change. Try to find the assets for the modified
1295
- // model element.
1296
- const assets = findAssetsForItem(item, ckboxCommand._chosenAssets);
1297
- for (const asset of assets){
1298
- const attributeName = asset.type === 'image' ? 'ckboxImageId' : 'ckboxLinkId';
1299
- if (asset.id === item.getAttribute(attributeName)) {
1300
- continue;
1301
- }
1302
- writer.setAttribute(attributeName, asset.id, item);
1303
- changed = true;
1304
- }
1305
- }
1306
- }
1307
- return changed;
1308
- };
1166
+ * A post-fixer that synchronizes the asset ID with the model element.
1167
+ */
1168
+ function syncDataIdPostFixer(editor) {
1169
+ return (writer) => {
1170
+ let changed = false;
1171
+ const model = editor.model;
1172
+ const ckboxCommand = editor.commands.get("ckbox");
1173
+ if (!ckboxCommand) return changed;
1174
+ for (const entry of model.document.differ.getChanges()) {
1175
+ if (entry.type !== "insert" && entry.type !== "attribute") continue;
1176
+ const range = entry.type === "insert" ? new ModelRange(entry.position, entry.position.getShiftedBy(entry.length)) : entry.range;
1177
+ const isLinkHrefAttributeRemoval = entry.type === "attribute" && entry.attributeKey === "linkHref" && entry.attributeNewValue === null;
1178
+ for (const item of range.getItems()) {
1179
+ if (isLinkHrefAttributeRemoval && item.hasAttribute("ckboxLinkId")) {
1180
+ writer.removeAttribute("ckboxLinkId", item);
1181
+ changed = true;
1182
+ continue;
1183
+ }
1184
+ const assets = findAssetsForItem(item, ckboxCommand._chosenAssets);
1185
+ for (const asset of assets) {
1186
+ const attributeName = asset.type === "image" ? "ckboxImageId" : "ckboxLinkId";
1187
+ if (asset.id === item.getAttribute(attributeName)) continue;
1188
+ writer.setAttribute(attributeName, asset.id, item);
1189
+ changed = true;
1190
+ }
1191
+ }
1192
+ }
1193
+ return changed;
1194
+ };
1309
1195
  }
1310
1196
  /**
1311
- * A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
1312
- */ function injectSelectionPostFixer(selection) {
1313
- return (writer)=>{
1314
- const shouldRemoveLinkIdAttribute = !selection.hasAttribute('linkHref') && selection.hasAttribute('ckboxLinkId');
1315
- if (shouldRemoveLinkIdAttribute) {
1316
- writer.removeSelectionAttribute('ckboxLinkId');
1317
- return true;
1318
- }
1319
- return false;
1320
- };
1197
+ * A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
1198
+ */
1199
+ function injectSelectionPostFixer(selection) {
1200
+ return (writer) => {
1201
+ if (!selection.hasAttribute("linkHref") && selection.hasAttribute("ckboxLinkId")) {
1202
+ writer.removeSelectionAttribute("ckboxLinkId");
1203
+ return true;
1204
+ }
1205
+ return false;
1206
+ };
1321
1207
  }
1322
1208
  /**
1323
- * Tries to find the asset that is associated with the model element by comparing the attributes:
1324
- * - the image fallback URL with the `src` attribute for images,
1325
- * - the link URL with the `href` attribute for links.
1326
- *
1327
- * For any model element, zero, one or more than one asset can be found (e.g. a linked image may be associated with the link asset and the
1328
- * image asset).
1329
- */ function findAssetsForItem(item, assets) {
1330
- const isImageElement = item.is('element', 'imageInline') || item.is('element', 'imageBlock');
1331
- const isLinkElement = item.hasAttribute('linkHref');
1332
- return [
1333
- ...assets
1334
- ].filter((asset)=>{
1335
- if (asset.type === 'image' && isImageElement) {
1336
- return asset.attributes.imageFallbackUrl === item.getAttribute('src');
1337
- }
1338
- if (asset.type === 'link' && isLinkElement) {
1339
- return asset.attributes.linkHref === item.getAttribute('linkHref');
1340
- }
1341
- });
1209
+ * Tries to find the asset that is associated with the model element by comparing the attributes:
1210
+ * - the image fallback URL with the `src` attribute for images,
1211
+ * - the link URL with the `href` attribute for links.
1212
+ *
1213
+ * For any model element, zero, one or more than one asset can be found (e.g. a linked image may be associated with the link asset and the
1214
+ * image asset).
1215
+ */
1216
+ function findAssetsForItem(item, assets) {
1217
+ const isImageElement = item.is("element", "imageInline") || item.is("element", "imageBlock");
1218
+ const isLinkElement = item.hasAttribute("linkHref");
1219
+ return [...assets].filter((asset) => {
1220
+ if (asset.type === "image" && isImageElement) return asset.attributes.imageFallbackUrl === item.getAttribute("src");
1221
+ if (asset.type === "link" && isLinkElement) return asset.attributes.linkHref === item.getAttribute("linkHref");
1222
+ });
1342
1223
  }
1343
1224
  /**
1344
- * Creates view link element with the requested ID.
1345
- */ function createLinkElement(writer, id) {
1346
- // Priority equal 5 is needed to merge adjacent `<a>` elements together.
1347
- const viewElement = writer.createAttributeElement('a', {
1348
- 'data-ckbox-resource-id': id
1349
- }, {
1350
- priority: 5
1351
- });
1352
- writer.setCustomProperty('link', true, viewElement);
1353
- return viewElement;
1225
+ * Creates view link element with the requested ID.
1226
+ */
1227
+ function createLinkElement(writer, id) {
1228
+ const viewElement = writer.createAttributeElement("a", { "data-ckbox-resource-id": id }, { priority: 5 });
1229
+ writer.setCustomProperty("link", true, viewElement);
1230
+ return viewElement;
1354
1231
  }
1355
1232
  /**
1356
- * Checks if the model element may have the `ckboxLinkId` attribute.
1357
- */ function shouldUpcastAttributeForNode(node) {
1358
- if (node.is('$text')) {
1359
- return true;
1360
- }
1361
- if (node.is('element', 'imageInline') || node.is('element', 'imageBlock')) {
1362
- return true;
1363
- }
1364
- return false;
1233
+ * Checks if the model element may have the `ckboxLinkId` attribute.
1234
+ */
1235
+ function shouldUpcastAttributeForNode(node) {
1236
+ if (node.is("$text")) return true;
1237
+ if (node.is("element", "imageInline") || node.is("element", "imageBlock")) return true;
1238
+ return false;
1365
1239
  }
1366
1240
  /**
1367
- * Returns true if the CKBox library is loaded, false otherwise.
1368
- */ function isLibraryLoaded() {
1369
- return !!window.CKBox;
1241
+ * Returns true if the CKBox library is loaded, false otherwise.
1242
+ */
1243
+ function isLibraryLoaded() {
1244
+ return !!window.CKBox;
1370
1245
  }
1371
1246
  /**
1372
- * Checks is access allowed to upload assets.
1373
- */ async function isUploadPermissionGranted(editor) {
1374
- const ckboxUtils = editor.plugins.get(CKBoxUtils);
1375
- const origin = editor.config.get('ckbox.serviceOrigin');
1376
- const url = new URL('permissions', origin);
1377
- const { value } = await ckboxUtils.getToken();
1378
- const response = await sendHttpRequest({
1379
- url,
1380
- authorization: value,
1381
- signal: new AbortController().signal // Aborting is unnecessary.
1382
- });
1383
- return Object.values(response).some((category)=>category['asset:create']);
1247
+ * Checks is access allowed to upload assets.
1248
+ */
1249
+ async function isUploadPermissionGranted(editor) {
1250
+ const ckboxUtils = editor.plugins.get(CKBoxUtils);
1251
+ const origin = editor.config.get("ckbox.serviceOrigin");
1252
+ const url = new URL("permissions", origin);
1253
+ const { value } = await ckboxUtils.getToken();
1254
+ const response = await sendHttpRequest({
1255
+ url,
1256
+ authorization: value,
1257
+ signal: new AbortController().signal
1258
+ });
1259
+ return Object.values(response).some((category) => category["asset:create"]);
1384
1260
  }
1385
1261
 
1386
1262
  /**
1387
- * The CKBox feature, a bridge between the CKEditor 5 WYSIWYG editor and the CKBox file manager and uploader.
1388
- *
1389
- * This is a "glue" plugin which enables:
1390
- *
1391
- * * {@link module:ckbox/ckboxediting~CKBoxEditing},
1392
- * * {@link module:ckbox/ckboxui~CKBoxUI},
1393
- *
1394
- * See the {@glink features/file-management/ckbox CKBox integration} guide to learn how to configure and use this feature.
1395
- *
1396
- * Check out the {@glink features/images/image-upload/image-upload Image upload} guide to learn about other ways to upload
1397
- * images into CKEditor 5.
1398
- */ class CKBox extends Plugin {
1399
- /**
1400
- * @inheritDoc
1401
- */ static get pluginName() {
1402
- return 'CKBox';
1403
- }
1404
- /**
1405
- * @inheritDoc
1406
- */ static get isOfficialPlugin() {
1407
- return true;
1408
- }
1409
- /**
1410
- * @inheritDoc
1411
- */ static get requires() {
1412
- return [
1413
- CKBoxEditing,
1414
- CKBoxUI
1415
- ];
1416
- }
1417
- }
1263
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1264
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1265
+ */
1266
+ /**
1267
+ * @module ckbox/ckbox
1268
+ */
1269
+ /**
1270
+ * The CKBox feature, a bridge between the CKEditor 5 WYSIWYG editor and the CKBox file manager and uploader.
1271
+ *
1272
+ * This is a "glue" plugin which enables:
1273
+ *
1274
+ * * {@link module:ckbox/ckboxediting~CKBoxEditing},
1275
+ * * {@link module:ckbox/ckboxui~CKBoxUI},
1276
+ *
1277
+ * See the {@glink features/file-management/ckbox CKBox integration} guide to learn how to configure and use this feature.
1278
+ *
1279
+ * Check out the {@glink features/images/image-upload/image-upload Image upload} guide to learn about other ways to upload
1280
+ * images into CKEditor 5.
1281
+ */
1282
+ var CKBox = class extends Plugin {
1283
+ /**
1284
+ * @inheritDoc
1285
+ */
1286
+ static get pluginName() {
1287
+ return "CKBox";
1288
+ }
1289
+ /**
1290
+ * @inheritDoc
1291
+ */
1292
+ static get isOfficialPlugin() {
1293
+ return true;
1294
+ }
1295
+ /**
1296
+ * @inheritDoc
1297
+ */
1298
+ static get requires() {
1299
+ return [CKBoxEditing, CKBoxUI];
1300
+ }
1301
+ };
1418
1302
 
1419
1303
  /**
1420
- * @internal
1421
- */ function createEditabilityChecker(allowExternalImagesEditing) {
1422
- const checkUrl = createUrlChecker(allowExternalImagesEditing);
1423
- return (element)=>{
1424
- const isImageElement = element.is('element', 'imageInline') || element.is('element', 'imageBlock');
1425
- if (!isImageElement) {
1426
- return false;
1427
- }
1428
- if (element.hasAttribute('ckboxImageId')) {
1429
- return true;
1430
- }
1431
- if (element.hasAttribute('src')) {
1432
- return checkUrl(element.getAttribute('src'));
1433
- }
1434
- return false;
1435
- };
1304
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1305
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1306
+ */
1307
+ /**
1308
+ * @module ckbox/ckboximageedit/utils
1309
+ */
1310
+ /**
1311
+ * @internal
1312
+ */
1313
+ function createEditabilityChecker(allowExternalImagesEditing) {
1314
+ const checkUrl = createUrlChecker(allowExternalImagesEditing);
1315
+ return (element) => {
1316
+ if (!(element.is("element", "imageInline") || element.is("element", "imageBlock"))) return false;
1317
+ if (element.hasAttribute("ckboxImageId")) return true;
1318
+ if (element.hasAttribute("src")) return checkUrl(element.getAttribute("src"));
1319
+ return false;
1320
+ };
1436
1321
  }
1437
1322
  function createUrlChecker(allowExternalImagesEditing) {
1438
- if (Array.isArray(allowExternalImagesEditing)) {
1439
- const urlMatchers = allowExternalImagesEditing.map(createUrlChecker);
1440
- return (src)=>urlMatchers.some((matcher)=>matcher(src));
1441
- }
1442
- if (allowExternalImagesEditing == 'origin') {
1443
- const origin = global.window.location.origin;
1444
- return (src)=>new URL(src, global.document.baseURI).origin == origin;
1445
- }
1446
- if (typeof allowExternalImagesEditing == 'function') {
1447
- return allowExternalImagesEditing;
1448
- }
1449
- if (allowExternalImagesEditing instanceof RegExp) {
1450
- return (src)=>!!(src.match(allowExternalImagesEditing) || src.replace(/^https?:\/\//, '').match(allowExternalImagesEditing));
1451
- }
1452
- return ()=>false;
1323
+ if (Array.isArray(allowExternalImagesEditing)) {
1324
+ const urlMatchers = allowExternalImagesEditing.map(createUrlChecker);
1325
+ return (src) => urlMatchers.some((matcher) => matcher(src));
1326
+ }
1327
+ if (allowExternalImagesEditing == "origin") {
1328
+ const origin = global.window.location.origin;
1329
+ return (src) => new URL(src, global.document.baseURI).origin == origin;
1330
+ }
1331
+ if (typeof allowExternalImagesEditing == "function") return allowExternalImagesEditing;
1332
+ if (allowExternalImagesEditing instanceof RegExp) return (src) => !!(src.match(allowExternalImagesEditing) || src.replace(/^https?:\/\//, "").match(allowExternalImagesEditing));
1333
+ return () => false;
1453
1334
  }
1454
1335
 
1455
1336
  /**
1456
- * The CKBox edit image command.
1457
- *
1458
- * Opens the CKBox dialog for editing the image.
1459
- */ class CKBoxImageEditCommand extends Command {
1460
- /**
1461
- * The DOM element that acts as a mounting point for the CKBox Edit Image dialog.
1462
- */ _wrapper = null;
1463
- /**
1464
- * The states of image processing in progress.
1465
- */ _processInProgress = new Set();
1466
- /**
1467
- * Determines if the element can be edited.
1468
- */ _canEdit;
1469
- /**
1470
- * A wrapper function to prepare mount options. Ensures that at most one preparation is in-flight.
1471
- */ _prepareOptions;
1472
- /**
1337
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1338
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1339
+ */
1340
+ /**
1341
+ * @module ckbox/ckboximageedit/ckboximageeditcommand
1342
+ */
1343
+ /**
1344
+ * The CKBox edit image command.
1345
+ *
1346
+ * Opens the CKBox dialog for editing the image.
1347
+ */
1348
+ var CKBoxImageEditCommand = class extends Command {
1349
+ /**
1350
+ * The DOM element that acts as a mounting point for the CKBox Edit Image dialog.
1351
+ */
1352
+ _wrapper = null;
1353
+ /**
1354
+ * The states of image processing in progress.
1355
+ */
1356
+ _processInProgress = /* @__PURE__ */ new Set();
1357
+ /**
1358
+ * Determines if the element can be edited.
1359
+ */
1360
+ _canEdit;
1361
+ /**
1362
+ * A wrapper function to prepare mount options. Ensures that at most one preparation is in-flight.
1363
+ */
1364
+ _prepareOptions;
1365
+ /**
1473
1366
  * CKBox's onClose function runs before the final cleanup, potentially causing
1474
1367
  * page layout changes after it finishes. To address this, we use a setTimeout hack
1475
1368
  * to ensure that floating elements on the page maintain their correct position.
1476
1369
  *
1477
1370
  * See: https://github.com/ckeditor/ckeditor5/issues/16153.
1478
- */ _updateUiDelayed = delay(()=>this.editor.ui.update(), 0);
1479
- /**
1480
- * @inheritDoc
1481
- */ constructor(editor){
1482
- super(editor);
1483
- this.value = false;
1484
- this._canEdit = createEditabilityChecker(editor.config.get('ckbox.allowExternalImagesEditing'));
1485
- this._prepareOptions = abortableDebounce((signal, state)=>this._prepareOptionsAbortable(signal, state));
1486
- this._prepareListeners();
1487
- }
1488
- /**
1489
- * @inheritDoc
1490
- */ refresh() {
1491
- const editor = this.editor;
1492
- this.value = this._getValue();
1493
- const selectedElement = editor.model.document.selection.getSelectedElement();
1494
- this.isEnabled = !!selectedElement && this._canEdit(selectedElement) && !this._checkIfElementIsBeingProcessed(selectedElement);
1495
- }
1496
- /**
1497
- * Opens the CKBox Image Editor dialog for editing the image.
1498
- */ execute() {
1499
- if (this._getValue()) {
1500
- return;
1501
- }
1502
- const wrapper = createElement(document, 'div', {
1503
- class: 'ck ckbox-wrapper'
1504
- });
1505
- this._wrapper = wrapper;
1506
- this.value = true;
1507
- document.body.appendChild(this._wrapper);
1508
- const imageElement = this.editor.model.document.selection.getSelectedElement();
1509
- const processingState = {
1510
- element: imageElement,
1511
- controller: new AbortController()
1512
- };
1513
- this._prepareOptions(processingState).then((options)=>window.CKBox.mountImageEditor(wrapper, options), (error)=>{
1514
- const editor = this.editor;
1515
- const t = editor.t;
1516
- const notification = editor.plugins.get(Notification);
1517
- notification.showWarning(t('Failed to determine category of edited image.'), {
1518
- namespace: 'ckbox'
1519
- });
1520
- console.error(error);
1521
- this._handleImageEditorClose();
1522
- });
1523
- }
1524
- /**
1525
- * @inheritDoc
1526
- */ destroy() {
1527
- this._handleImageEditorClose();
1528
- this._prepareOptions.abort();
1529
- this._updateUiDelayed.cancel();
1530
- for (const state of this._processInProgress.values()){
1531
- state.controller.abort();
1532
- }
1533
- super.destroy();
1534
- }
1535
- /**
1536
- * Indicates if the CKBox Image Editor dialog is already opened.
1537
- */ _getValue() {
1538
- return this._wrapper !== null;
1539
- }
1540
- /**
1541
- * Creates the options object for the CKBox Image Editor dialog.
1542
- */ async _prepareOptionsAbortable(signal, state) {
1543
- const editor = this.editor;
1544
- const ckboxConfig = editor.config.get('ckbox');
1545
- const ckboxUtils = editor.plugins.get(CKBoxUtils);
1546
- const { element } = state;
1547
- let imageMountOptions;
1548
- const ckboxImageId = element.getAttribute('ckboxImageId');
1549
- if (ckboxImageId) {
1550
- imageMountOptions = {
1551
- assetId: ckboxImageId
1552
- };
1553
- } else {
1554
- const imageUrl = new URL(element.getAttribute('src'), document.baseURI).href;
1555
- const uploadCategoryId = await ckboxUtils.getCategoryIdForFile(imageUrl, {
1556
- signal
1557
- });
1558
- imageMountOptions = {
1559
- imageUrl,
1560
- uploadCategoryId
1561
- };
1562
- }
1563
- return {
1564
- ...imageMountOptions,
1565
- imageEditing: {
1566
- allowOverwrite: false
1567
- },
1568
- tokenUrl: ckboxConfig.tokenUrl,
1569
- language: ckboxConfig.language,
1570
- ...ckboxConfig.serviceOrigin && {
1571
- serviceOrigin: ckboxConfig.serviceOrigin
1572
- },
1573
- onClose: ()=>this._handleImageEditorClose(),
1574
- onSave: (asset)=>this._handleImageEditorSave(state, asset)
1575
- };
1576
- }
1577
- /**
1578
- * Initializes event lister for an event of removing an image.
1579
- */ _prepareListeners() {
1580
- // Abort editing processing when the image has been removed.
1581
- this.listenTo(this.editor.model.document, 'change:data', ()=>{
1582
- const processingStates = this._getProcessingStatesOfDeletedImages();
1583
- processingStates.forEach((processingState)=>{
1584
- processingState.controller.abort();
1585
- });
1586
- });
1587
- }
1588
- /**
1589
- * Gets processing states of images that have been deleted in the mean time.
1590
- */ _getProcessingStatesOfDeletedImages() {
1591
- const states = [];
1592
- for (const state of this._processInProgress.values()){
1593
- if (state.element.root.rootName == '$graveyard') {
1594
- states.push(state);
1595
- }
1596
- }
1597
- return states;
1598
- }
1599
- _checkIfElementIsBeingProcessed(selectedElement) {
1600
- for (const { element } of this._processInProgress){
1601
- if (isEqual(element, selectedElement)) {
1602
- return true;
1603
- }
1604
- }
1605
- return false;
1606
- }
1607
- /**
1608
- * Closes the CKBox Image Editor dialog.
1609
- */ _handleImageEditorClose() {
1610
- if (!this._wrapper) {
1611
- return;
1612
- }
1613
- this._wrapper.remove();
1614
- this._wrapper = null;
1615
- this.editor.editing.view.focus();
1616
- this._updateUiDelayed();
1617
- this.refresh();
1618
- }
1619
- /**
1620
- * Save edited image. In case server respond with "success" replace with edited image,
1621
- * otherwise show notification error.
1622
- */ _handleImageEditorSave(state, asset) {
1623
- const t = this.editor.locale.t;
1624
- const notification = this.editor.plugins.get(Notification);
1625
- const pendingActions = this.editor.plugins.get(PendingActions);
1626
- const action = pendingActions.add(t('Processing the edited image.'));
1627
- this._processInProgress.add(state);
1628
- this._showImageProcessingIndicator(state.element, asset);
1629
- this.refresh();
1630
- this._waitForAssetProcessed(asset.data.id, state.controller.signal).then((asset)=>{
1631
- this._replaceImage(state.element, asset);
1632
- }, (error)=>{
1633
- // Remove processing indicator. It was added only to ViewElement.
1634
- this.editor.editing.reconvertItem(state.element);
1635
- if (state.controller.signal.aborted) {
1636
- return;
1637
- }
1638
- if (!error || error instanceof CKEditorError) {
1639
- notification.showWarning(t('Server failed to process the image.'), {
1640
- namespace: 'ckbox'
1641
- });
1642
- } else {
1643
- console.error(error);
1644
- }
1645
- }).finally(()=>{
1646
- this._processInProgress.delete(state);
1647
- pendingActions.remove(action);
1648
- this.refresh();
1649
- });
1650
- }
1651
- /**
1652
- * Get asset's status on server. If server responds with "success" status then
1653
- * image is already proceeded and ready for saving.
1654
- */ async _getAssetStatusFromServer(id, signal) {
1655
- const ckboxUtils = this.editor.plugins.get(CKBoxUtils);
1656
- const url = new URL('assets/' + id, this.editor.config.get('ckbox.serviceOrigin'));
1657
- const response = await sendHttpRequest({
1658
- url,
1659
- signal,
1660
- authorization: (await ckboxUtils.getToken()).value
1661
- });
1662
- const status = response.metadata.metadataProcessingStatus;
1663
- if (!status || status == 'queued') {
1664
- /**
1665
- * Image has not been processed yet.
1666
- *
1667
- * @error ckbox-image-not-processed
1668
- */ throw new CKEditorError('ckbox-image-not-processed');
1669
- }
1670
- return {
1671
- data: {
1672
- ...response
1673
- }
1674
- };
1675
- }
1676
- /**
1677
- * Waits for an asset to be processed.
1678
- * It retries retrieving asset status from the server in case of failure.
1679
- */ async _waitForAssetProcessed(id, signal) {
1680
- const result = await retry(()=>this._getAssetStatusFromServer(id, signal), {
1681
- signal,
1682
- maxAttempts: 5
1683
- });
1684
- if (result.data.metadata.metadataProcessingStatus != 'success') {
1685
- /**
1686
- * The image processing failed.
1687
- *
1688
- * @error ckbox-image-processing-failed
1689
- */ throw new CKEditorError('ckbox-image-processing-failed');
1690
- }
1691
- return result;
1692
- }
1693
- /**
1694
- * Shows processing indicator while image is processing.
1695
- *
1696
- * @param asset Data about certain asset.
1697
- */ _showImageProcessingIndicator(element, asset) {
1698
- const editor = this.editor;
1699
- editor.editing.view.change((writer)=>{
1700
- const imageElementView = editor.editing.mapper.toViewElement(element);
1701
- const imageUtils = this.editor.plugins.get('ImageUtils');
1702
- const img = imageUtils.findViewImgElement(imageElementView);
1703
- writer.removeStyle('aspect-ratio', img);
1704
- writer.setAttribute('width', asset.data.metadata.width, img);
1705
- writer.setAttribute('height', asset.data.metadata.height, img);
1706
- writer.setStyle('width', `${asset.data.metadata.width}px`, img);
1707
- writer.setStyle('height', `${asset.data.metadata.height}px`, img);
1708
- writer.addClass('image-processing', imageElementView);
1709
- });
1710
- }
1711
- /**
1712
- * Replace the edited image with the new one.
1713
- */ _replaceImage(element, asset) {
1714
- const editor = this.editor;
1715
- const { imageFallbackUrl, imageSources, imageWidth, imageHeight, imagePlaceholder } = prepareImageAssetAttributes(asset);
1716
- const previousSelectionRanges = Array.from(editor.model.document.selection.getRanges());
1717
- editor.model.change((writer)=>{
1718
- writer.setSelection(element, 'on');
1719
- editor.execute('insertImage', {
1720
- imageType: element.is('element', 'imageInline') ? 'imageInline' : null,
1721
- source: {
1722
- src: imageFallbackUrl,
1723
- sources: imageSources,
1724
- width: imageWidth,
1725
- height: imageHeight,
1726
- ...imagePlaceholder ? {
1727
- placeholder: imagePlaceholder
1728
- } : null,
1729
- ...element.hasAttribute('alt') ? {
1730
- alt: element.getAttribute('alt')
1731
- } : null
1732
- }
1733
- });
1734
- const previousChildren = element.getChildren();
1735
- element = editor.model.document.selection.getSelectedElement();
1736
- for (const child of previousChildren){
1737
- writer.append(writer.cloneElement(child), element);
1738
- }
1739
- writer.setAttribute('ckboxImageId', asset.data.id, element);
1740
- writer.setSelection(previousSelectionRanges);
1741
- });
1742
- }
1743
- }
1371
+ */
1372
+ _updateUiDelayed = delay(() => this.editor.ui.update(), 0);
1373
+ /**
1374
+ * @inheritDoc
1375
+ */
1376
+ constructor(editor) {
1377
+ super(editor);
1378
+ this.value = false;
1379
+ this._canEdit = createEditabilityChecker(editor.config.get("ckbox.allowExternalImagesEditing"));
1380
+ this._prepareOptions = abortableDebounce((signal, state) => this._prepareOptionsAbortable(signal, state));
1381
+ this._prepareListeners();
1382
+ }
1383
+ /**
1384
+ * @inheritDoc
1385
+ */
1386
+ refresh() {
1387
+ const editor = this.editor;
1388
+ this.value = this._getValue();
1389
+ const selectedElement = editor.model.document.selection.getSelectedElement();
1390
+ this.isEnabled = !!selectedElement && this._canEdit(selectedElement) && !this._checkIfElementIsBeingProcessed(selectedElement);
1391
+ }
1392
+ /**
1393
+ * Opens the CKBox Image Editor dialog for editing the image.
1394
+ */
1395
+ execute() {
1396
+ if (this._getValue()) return;
1397
+ const wrapper = createElement(document, "div", { class: "ck ckbox-wrapper" });
1398
+ this._wrapper = wrapper;
1399
+ this.value = true;
1400
+ document.body.appendChild(this._wrapper);
1401
+ const processingState = {
1402
+ element: this.editor.model.document.selection.getSelectedElement(),
1403
+ controller: new AbortController()
1404
+ };
1405
+ this._prepareOptions(processingState).then((options) => window.CKBox.mountImageEditor(wrapper, options), (error) => {
1406
+ if (this._wrapper !== wrapper) return;
1407
+ const editor = this.editor;
1408
+ const t = editor.t;
1409
+ editor.plugins.get(Notification).showWarning(typeof error == "string" ? error : t("Failed to determine category of edited image."), { namespace: "ckbox" });
1410
+ console.error(error);
1411
+ this._handleImageEditorClose();
1412
+ });
1413
+ }
1414
+ /**
1415
+ * @inheritDoc
1416
+ */
1417
+ destroy() {
1418
+ this._handleImageEditorClose();
1419
+ this._prepareOptions.abort();
1420
+ this._updateUiDelayed.cancel();
1421
+ for (const state of this._processInProgress.values()) state.controller.abort();
1422
+ super.destroy();
1423
+ }
1424
+ /**
1425
+ * Indicates if the CKBox Image Editor dialog is already opened.
1426
+ */
1427
+ _getValue() {
1428
+ return this._wrapper !== null;
1429
+ }
1430
+ /**
1431
+ * Creates the options object for the CKBox Image Editor dialog.
1432
+ */
1433
+ async _prepareOptionsAbortable(signal, state) {
1434
+ const editor = this.editor;
1435
+ const ckboxConfig = editor.config.get("ckbox");
1436
+ const ckboxUtils = editor.plugins.get(CKBoxUtils);
1437
+ const { element } = state;
1438
+ let imageMountOptions;
1439
+ const ckboxImageId = element.getAttribute("ckboxImageId");
1440
+ if (ckboxImageId) imageMountOptions = { assetId: ckboxImageId };
1441
+ else {
1442
+ const imageUrl = new URL(element.getAttribute("src"), document.baseURI).href;
1443
+ imageMountOptions = {
1444
+ imageUrl,
1445
+ uploadCategoryId: await ckboxUtils.getCategoryIdForFile(imageUrl, { signal })
1446
+ };
1447
+ }
1448
+ return {
1449
+ ...imageMountOptions,
1450
+ imageEditing: { allowOverwrite: false },
1451
+ tokenUrl: ckboxConfig.tokenUrl,
1452
+ language: ckboxConfig.language,
1453
+ ...ckboxConfig.serviceOrigin && { serviceOrigin: ckboxConfig.serviceOrigin },
1454
+ onClose: () => this._handleImageEditorClose(),
1455
+ onSave: (asset) => this._handleImageEditorSave(state, asset)
1456
+ };
1457
+ }
1458
+ /**
1459
+ * Initializes event lister for an event of removing an image.
1460
+ */
1461
+ _prepareListeners() {
1462
+ this.listenTo(this.editor.model.document, "change:data", () => {
1463
+ this._getProcessingStatesOfDeletedImages().forEach((processingState) => {
1464
+ processingState.controller.abort();
1465
+ });
1466
+ });
1467
+ }
1468
+ /**
1469
+ * Gets processing states of images that have been deleted in the mean time.
1470
+ */
1471
+ _getProcessingStatesOfDeletedImages() {
1472
+ const states = [];
1473
+ for (const state of this._processInProgress.values()) if (state.element.root.rootName == "$graveyard") states.push(state);
1474
+ return states;
1475
+ }
1476
+ _checkIfElementIsBeingProcessed(selectedElement) {
1477
+ for (const { element } of this._processInProgress) if (isEqual(element, selectedElement)) return true;
1478
+ return false;
1479
+ }
1480
+ /**
1481
+ * Closes the CKBox Image Editor dialog.
1482
+ */
1483
+ _handleImageEditorClose() {
1484
+ if (!this._wrapper) return;
1485
+ this._wrapper.remove();
1486
+ this._wrapper = null;
1487
+ this.editor.editing.view.focus();
1488
+ this._updateUiDelayed();
1489
+ this.refresh();
1490
+ }
1491
+ /**
1492
+ * Save edited image. In case server respond with "success" replace with edited image,
1493
+ * otherwise show notification error.
1494
+ */
1495
+ _handleImageEditorSave(state, asset) {
1496
+ const t = this.editor.locale.t;
1497
+ const notification = this.editor.plugins.get(Notification);
1498
+ const pendingActions = this.editor.plugins.get(PendingActions);
1499
+ const action = pendingActions.add(t("Processing the edited image."));
1500
+ this._processInProgress.add(state);
1501
+ this._showImageProcessingIndicator(state.element, asset);
1502
+ this.refresh();
1503
+ this._waitForAssetProcessed(asset.data.id, state.controller.signal).then((asset) => {
1504
+ this._replaceImage(state.element, asset);
1505
+ }, (error) => {
1506
+ this.editor.editing.reconvertItem(state.element);
1507
+ if (state.controller.signal.aborted) return;
1508
+ if (!error || error instanceof CKEditorError) notification.showWarning(isOffline() ? t("No internet connection. Check your connection and try again.") : t("Server failed to process the image."), { namespace: "ckbox" });
1509
+ else console.error(error);
1510
+ }).finally(() => {
1511
+ this._processInProgress.delete(state);
1512
+ pendingActions.remove(action);
1513
+ this.refresh();
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Get asset's status on server. If server responds with "success" status then
1518
+ * image is already proceeded and ready for saving.
1519
+ */
1520
+ async _getAssetStatusFromServer(id, signal) {
1521
+ const ckboxUtils = this.editor.plugins.get(CKBoxUtils);
1522
+ const url = new URL("assets/" + id, this.editor.config.get("ckbox.serviceOrigin"));
1523
+ const response = await sendHttpRequest({
1524
+ url,
1525
+ signal,
1526
+ authorization: (await ckboxUtils.getToken()).value
1527
+ });
1528
+ const status = response.metadata.metadataProcessingStatus;
1529
+ if (!status || status == "queued")
1530
+ /**
1531
+ * Image has not been processed yet.
1532
+ *
1533
+ * @error ckbox-image-not-processed
1534
+ */
1535
+ throw new CKEditorError("ckbox-image-not-processed");
1536
+ return { data: { ...response } };
1537
+ }
1538
+ /**
1539
+ * Waits for an asset to be processed.
1540
+ * It retries retrieving asset status from the server in case of failure.
1541
+ */
1542
+ async _waitForAssetProcessed(id, signal) {
1543
+ const result = await retry(() => this._getAssetStatusFromServer(id, signal), {
1544
+ signal,
1545
+ maxAttempts: 5
1546
+ });
1547
+ if (result.data.metadata.metadataProcessingStatus != "success")
1548
+ /**
1549
+ * The image processing failed.
1550
+ *
1551
+ * @error ckbox-image-processing-failed
1552
+ */
1553
+ throw new CKEditorError("ckbox-image-processing-failed");
1554
+ return result;
1555
+ }
1556
+ /**
1557
+ * Shows processing indicator while image is processing.
1558
+ *
1559
+ * @param asset Data about certain asset.
1560
+ */
1561
+ _showImageProcessingIndicator(element, asset) {
1562
+ const editor = this.editor;
1563
+ editor.editing.view.change((writer) => {
1564
+ const imageElementView = editor.editing.mapper.toViewElement(element);
1565
+ const img = this.editor.plugins.get("ImageUtils").findViewImgElement(imageElementView);
1566
+ writer.removeStyle("aspect-ratio", img);
1567
+ writer.setAttribute("width", asset.data.metadata.width, img);
1568
+ writer.setAttribute("height", asset.data.metadata.height, img);
1569
+ writer.setStyle("width", `${asset.data.metadata.width}px`, img);
1570
+ writer.setStyle("height", `${asset.data.metadata.height}px`, img);
1571
+ writer.addClass("image-processing", imageElementView);
1572
+ });
1573
+ }
1574
+ /**
1575
+ * Replace the edited image with the new one.
1576
+ */
1577
+ _replaceImage(element, asset) {
1578
+ const editor = this.editor;
1579
+ const { imageFallbackUrl, imageSources, imageWidth, imageHeight, imagePlaceholder } = prepareImageAssetAttributes(asset);
1580
+ const previousSelectionRanges = Array.from(editor.model.document.selection.getRanges());
1581
+ editor.model.change((writer) => {
1582
+ writer.setSelection(element, "on");
1583
+ editor.execute("insertImage", {
1584
+ imageType: element.is("element", "imageInline") ? "imageInline" : null,
1585
+ source: {
1586
+ src: imageFallbackUrl,
1587
+ sources: imageSources,
1588
+ width: imageWidth,
1589
+ height: imageHeight,
1590
+ ...imagePlaceholder ? { placeholder: imagePlaceholder } : null,
1591
+ ...element.hasAttribute("alt") ? { alt: element.getAttribute("alt") } : null
1592
+ }
1593
+ });
1594
+ const previousChildren = element.getChildren();
1595
+ element = editor.model.document.selection.getSelectedElement();
1596
+ for (const child of previousChildren) writer.append(writer.cloneElement(child), element);
1597
+ writer.setAttribute("ckboxImageId", asset.data.id, element);
1598
+ writer.setSelection(previousSelectionRanges);
1599
+ });
1600
+ }
1601
+ };
1744
1602
 
1745
1603
  /**
1746
- * The CKBox image edit editing plugin.
1747
- */ class CKBoxImageEditEditing extends Plugin {
1748
- /**
1749
- * @inheritDoc
1750
- */ static get pluginName() {
1751
- return 'CKBoxImageEditEditing';
1752
- }
1753
- /**
1754
- * @inheritDoc
1755
- */ static get isOfficialPlugin() {
1756
- return true;
1757
- }
1758
- /**
1759
- * @inheritDoc
1760
- */ static get requires() {
1761
- return [
1762
- CKBoxEditing,
1763
- CKBoxUtils,
1764
- PendingActions,
1765
- Notification,
1766
- ImageUtils,
1767
- ImageEditing
1768
- ];
1769
- }
1770
- /**
1771
- * @inheritDoc
1772
- */ init() {
1773
- const { editor } = this;
1774
- editor.commands.add('ckboxImageEdit', new CKBoxImageEditCommand(editor));
1775
- }
1776
- }
1604
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1605
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1606
+ */
1607
+ /**
1608
+ * @module ckbox/ckboximageedit/ckboximageeditediting
1609
+ */
1610
+ /**
1611
+ * The CKBox image edit editing plugin.
1612
+ */
1613
+ var CKBoxImageEditEditing = class extends Plugin {
1614
+ /**
1615
+ * @inheritDoc
1616
+ */
1617
+ static get pluginName() {
1618
+ return "CKBoxImageEditEditing";
1619
+ }
1620
+ /**
1621
+ * @inheritDoc
1622
+ */
1623
+ static get isOfficialPlugin() {
1624
+ return true;
1625
+ }
1626
+ /**
1627
+ * @inheritDoc
1628
+ */
1629
+ static get requires() {
1630
+ return [
1631
+ CKBoxEditing,
1632
+ CKBoxUtils,
1633
+ PendingActions,
1634
+ Notification,
1635
+ ImageUtils,
1636
+ ImageEditing
1637
+ ];
1638
+ }
1639
+ /**
1640
+ * @inheritDoc
1641
+ */
1642
+ init() {
1643
+ const { editor } = this;
1644
+ const imageEditCommand = new CKBoxImageEditCommand(editor);
1645
+ editor.commands.add("ckboxImageEdit", imageEditCommand);
1646
+ if (isTrustedTypesEnforced()) {
1647
+ /**
1648
+ * The CKBox image editor is not available, because the application enforces Trusted Types with the
1649
+ * `require-trusted-types-for 'script'` CSP directive. The image editor writes HTML in a way that this directive
1650
+ * rejects, and the editor cannot change how it does that. Choosing assets in CKBox still works.
1651
+ *
1652
+ * The editor disables its own **Edit image** button. The **Edit** button in the CKBox dialog is part of CKBox, so
1653
+ * it stays available, but the image editor that it opens cannot load the image either.
1654
+ *
1655
+ * For a detailed overview, check the {@glink getting-started/setup/csp#known-limitations Content Security
1656
+ * Policy} guide.
1657
+ *
1658
+ * @error ckbox-image-edit-unavailable-with-trusted-types
1659
+ */
1660
+ logWarning("ckbox-image-edit-unavailable-with-trusted-types");
1661
+ imageEditCommand.forceDisabled("trustedTypes");
1662
+ }
1663
+ }
1664
+ };
1777
1665
 
1778
1666
  /**
1779
- * The UI plugin of the CKBox image edit feature.
1780
- *
1781
- * It registers the `'ckboxImageEdit'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
1782
- * that allows you to open the CKBox dialog and edit the image.
1783
- */ class CKBoxImageEditUI extends Plugin {
1784
- /**
1785
- * @inheritDoc
1786
- */ static get pluginName() {
1787
- return 'CKBoxImageEditUI';
1788
- }
1789
- /**
1790
- * @inheritDoc
1791
- */ static get isOfficialPlugin() {
1792
- return true;
1793
- }
1794
- /**
1795
- * @inheritDoc
1796
- */ init() {
1797
- const editor = this.editor;
1798
- editor.ui.componentFactory.add('ckboxImageEdit', (locale)=>{
1799
- const command = editor.commands.get('ckboxImageEdit');
1800
- const uploadImageCommand = editor.commands.get('uploadImage');
1801
- const view = new ButtonView(locale);
1802
- const t = locale.t;
1803
- view.set({
1804
- icon: IconCkboxImageEdit,
1805
- tooltip: true
1806
- });
1807
- view.bind('label').to(uploadImageCommand, 'isAccessAllowed', (isAccessAllowed)=>isAccessAllowed ? t('Edit image') : t('You have no image editing permissions.'));
1808
- view.bind('isOn').to(command, 'value', command, 'isEnabled', (value, isEnabled)=>value && isEnabled);
1809
- view.bind('isEnabled').to(command);
1810
- // Execute the command.
1811
- this.listenTo(view, 'execute', ()=>{
1812
- editor.execute('ckboxImageEdit');
1813
- editor.editing.view.focus();
1814
- });
1815
- return view;
1816
- });
1817
- }
1818
- }
1667
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1668
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1669
+ */
1670
+ /**
1671
+ * @module ckbox/ckboximageedit/ckboximageeditui
1672
+ */
1673
+ /**
1674
+ * The UI plugin of the CKBox image edit feature.
1675
+ *
1676
+ * It registers the `'ckboxImageEdit'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
1677
+ * that allows you to open the CKBox dialog and edit the image.
1678
+ */
1679
+ var CKBoxImageEditUI = class extends Plugin {
1680
+ /**
1681
+ * @inheritDoc
1682
+ */
1683
+ static get pluginName() {
1684
+ return "CKBoxImageEditUI";
1685
+ }
1686
+ /**
1687
+ * @inheritDoc
1688
+ */
1689
+ static get isOfficialPlugin() {
1690
+ return true;
1691
+ }
1692
+ /**
1693
+ * @inheritDoc
1694
+ */
1695
+ init() {
1696
+ const editor = this.editor;
1697
+ editor.ui.componentFactory.add("ckboxImageEdit", (locale) => {
1698
+ const command = editor.commands.get("ckboxImageEdit");
1699
+ const uploadImageCommand = editor.commands.get("uploadImage");
1700
+ const view = new ButtonView(locale);
1701
+ const t = locale.t;
1702
+ view.set({
1703
+ icon: IconCkboxImageEdit,
1704
+ tooltip: true
1705
+ });
1706
+ view.bind("label").to(uploadImageCommand, "isAccessAllowed", (isAccessAllowed) => isAccessAllowed ? t("Edit image") : t("You have no image editing permissions."));
1707
+ view.bind("isOn").to(command, "value", command, "isEnabled", (value, isEnabled) => value && isEnabled);
1708
+ view.bind("isEnabled").to(command);
1709
+ this.listenTo(view, "execute", () => {
1710
+ editor.execute("ckboxImageEdit");
1711
+ editor.editing.view.focus();
1712
+ });
1713
+ return view;
1714
+ });
1715
+ }
1716
+ };
1819
1717
 
1820
1718
  /**
1821
- * The CKBox image edit feature.
1822
- */ class CKBoxImageEdit extends Plugin {
1823
- /**
1824
- * @inheritDoc
1825
- */ static get pluginName() {
1826
- return 'CKBoxImageEdit';
1827
- }
1828
- /**
1829
- * @inheritDoc
1830
- */ static get isOfficialPlugin() {
1831
- return true;
1832
- }
1833
- /**
1834
- * @inheritDoc
1835
- */ static get requires() {
1836
- return [
1837
- CKBoxImageEditEditing,
1838
- CKBoxImageEditUI
1839
- ];
1840
- }
1841
- }
1719
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1720
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1721
+ */
1722
+ /**
1723
+ * @module ckbox/ckboximageedit
1724
+ */
1725
+ /**
1726
+ * The CKBox image edit feature.
1727
+ */
1728
+ var CKBoxImageEdit = class extends Plugin {
1729
+ /**
1730
+ * @inheritDoc
1731
+ */
1732
+ static get pluginName() {
1733
+ return "CKBoxImageEdit";
1734
+ }
1735
+ /**
1736
+ * @inheritDoc
1737
+ */
1738
+ static get isOfficialPlugin() {
1739
+ return true;
1740
+ }
1741
+ /**
1742
+ * @inheritDoc
1743
+ */
1744
+ static get requires() {
1745
+ return [CKBoxImageEditEditing, CKBoxImageEditUI];
1746
+ }
1747
+ };
1748
+
1749
+ /**
1750
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1751
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1752
+ */
1842
1753
 
1843
1754
  export { CKBox, CKBoxCommand, CKBoxEditing, CKBoxImageEdit, CKBoxImageEditCommand, CKBoxImageEditEditing, CKBoxImageEditUI, CKBoxUI, CKBoxUploadAdapter, CKBoxUtils, convertMimeTypeToExtension as _ckBoxConvertMimeTypeToExtension, blurHashToDataUrl as _ckboxBlurHashToDataUrl, createEditabilityChecker as _createCKBoxEditabilityChecker, getContentTypeOfUrl as _getCKBoxContentTypeOfUrl, getFileExtension as _getCKBoxFileExtension, getImageUrls as _getCKBoxImageUrls, getWorkspaceId as _getCKBoxWorkspaceId, prepareImageAssetAttributes as _prepareCKBoxImageAssetAttributes, sendHttpRequest as _sendCKBoxHttpRequest };
1844
- //# sourceMappingURL=index.js.map
1755
+ //# sourceMappingURL=index.js.map