@ckeditor/ckeditor5-ckbox 40.0.0 → 40.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. package/LICENSE.md +6 -2
  2. package/build/ckbox.js +2 -2
  3. package/build/translations/ar.js +1 -1
  4. package/build/translations/az.js +1 -1
  5. package/build/translations/bg.js +1 -1
  6. package/build/translations/bn.js +1 -1
  7. package/build/translations/ca.js +1 -1
  8. package/build/translations/cs.js +1 -1
  9. package/build/translations/da.js +1 -1
  10. package/build/translations/de.js +1 -1
  11. package/build/translations/el.js +1 -1
  12. package/build/translations/en-au.js +1 -1
  13. package/build/translations/es-co.js +1 -1
  14. package/build/translations/es.js +1 -1
  15. package/build/translations/et.js +1 -1
  16. package/build/translations/fa.js +1 -1
  17. package/build/translations/fi.js +1 -1
  18. package/build/translations/fr.js +1 -1
  19. package/build/translations/gl.js +1 -1
  20. package/build/translations/he.js +1 -1
  21. package/build/translations/hi.js +1 -1
  22. package/build/translations/hr.js +1 -1
  23. package/build/translations/hu.js +1 -1
  24. package/build/translations/id.js +1 -1
  25. package/build/translations/it.js +1 -1
  26. package/build/translations/ja.js +1 -1
  27. package/build/translations/ko.js +1 -1
  28. package/build/translations/lt.js +1 -1
  29. package/build/translations/lv.js +1 -1
  30. package/build/translations/ms.js +1 -1
  31. package/build/translations/nl.js +1 -1
  32. package/build/translations/no.js +1 -1
  33. package/build/translations/pl.js +1 -1
  34. package/build/translations/pt-br.js +1 -1
  35. package/build/translations/pt.js +1 -1
  36. package/build/translations/ro.js +1 -1
  37. package/build/translations/ru.js +1 -1
  38. package/build/translations/sk.js +1 -1
  39. package/build/translations/sq.js +1 -1
  40. package/build/translations/sr-latn.js +1 -1
  41. package/build/translations/sr.js +1 -1
  42. package/build/translations/sv.js +1 -1
  43. package/build/translations/th.js +1 -1
  44. package/build/translations/tr.js +1 -1
  45. package/build/translations/ug.js +1 -1
  46. package/build/translations/uk.js +1 -1
  47. package/build/translations/ur.js +1 -1
  48. package/build/translations/uz.js +1 -1
  49. package/build/translations/vi.js +1 -1
  50. package/build/translations/zh-cn.js +1 -1
  51. package/build/translations/zh.js +1 -1
  52. package/ckeditor5-metadata.json +17 -0
  53. package/lang/contexts.json +6 -2
  54. package/lang/translations/ar.po +18 -2
  55. package/lang/translations/az.po +18 -2
  56. package/lang/translations/bg.po +18 -2
  57. package/lang/translations/bn.po +18 -2
  58. package/lang/translations/ca.po +18 -2
  59. package/lang/translations/cs.po +18 -2
  60. package/lang/translations/da.po +18 -2
  61. package/lang/translations/de.po +18 -2
  62. package/lang/translations/el.po +18 -2
  63. package/lang/translations/en-au.po +18 -2
  64. package/lang/translations/en.po +18 -2
  65. package/lang/translations/es-co.po +18 -2
  66. package/lang/translations/es.po +18 -2
  67. package/lang/translations/et.po +18 -2
  68. package/lang/translations/fa.po +18 -2
  69. package/lang/translations/fi.po +18 -2
  70. package/lang/translations/fr.po +18 -2
  71. package/lang/translations/gl.po +18 -2
  72. package/lang/translations/he.po +18 -2
  73. package/lang/translations/hi.po +18 -2
  74. package/lang/translations/hr.po +18 -2
  75. package/lang/translations/hu.po +18 -2
  76. package/lang/translations/id.po +18 -2
  77. package/lang/translations/it.po +18 -2
  78. package/lang/translations/ja.po +18 -2
  79. package/lang/translations/ko.po +18 -2
  80. package/lang/translations/lt.po +18 -2
  81. package/lang/translations/lv.po +18 -2
  82. package/lang/translations/ms.po +18 -2
  83. package/lang/translations/nl.po +18 -2
  84. package/lang/translations/no.po +18 -2
  85. package/lang/translations/pl.po +18 -2
  86. package/lang/translations/pt-br.po +18 -2
  87. package/lang/translations/pt.po +18 -2
  88. package/lang/translations/ro.po +18 -2
  89. package/lang/translations/ru.po +18 -2
  90. package/lang/translations/sk.po +18 -2
  91. package/lang/translations/sq.po +18 -2
  92. package/lang/translations/sr-latn.po +18 -2
  93. package/lang/translations/sr.po +18 -2
  94. package/lang/translations/sv.po +18 -2
  95. package/lang/translations/th.po +18 -2
  96. package/lang/translations/tr.po +18 -2
  97. package/lang/translations/ug.po +18 -2
  98. package/lang/translations/uk.po +18 -2
  99. package/lang/translations/ur.po +18 -2
  100. package/lang/translations/uz.po +18 -2
  101. package/lang/translations/vi.po +18 -2
  102. package/lang/translations/zh-cn.po +18 -2
  103. package/lang/translations/zh.po +18 -2
  104. package/package.json +4 -2
  105. package/src/augmentation.d.ts +32 -22
  106. package/src/augmentation.js +5 -5
  107. package/src/ckbox.d.ts +33 -33
  108. package/src/ckbox.js +37 -37
  109. package/src/ckboxcommand.d.ts +114 -110
  110. package/src/ckboxcommand.js +332 -302
  111. package/src/ckboxconfig.d.ts +325 -283
  112. package/src/ckboxconfig.js +5 -5
  113. package/src/ckboxediting.d.ts +45 -52
  114. package/src/ckboxediting.js +321 -362
  115. package/src/ckboximageedit/ckboximageeditcommand.d.ts +97 -0
  116. package/src/ckboximageedit/ckboximageeditcommand.js +298 -0
  117. package/src/ckboximageedit/ckboximageeditediting.d.ts +28 -0
  118. package/src/ckboximageedit/ckboximageeditediting.js +36 -0
  119. package/src/ckboximageedit/ckboximageeditui.d.ts +24 -0
  120. package/src/ckboximageedit/ckboximageeditui.js +48 -0
  121. package/src/ckboximageedit/utils.d.ts +10 -0
  122. package/src/ckboximageedit/utils.js +48 -0
  123. package/src/ckboximageedit.d.ts +24 -0
  124. package/src/ckboximageedit.js +28 -0
  125. package/src/ckboxui.d.ts +21 -21
  126. package/src/ckboxui.js +74 -47
  127. package/src/ckboxuploadadapter.d.ts +33 -38
  128. package/src/ckboxuploadadapter.js +130 -275
  129. package/src/ckboxutils.d.ts +50 -0
  130. package/src/ckboxutils.js +183 -0
  131. package/src/index.d.ts +17 -13
  132. package/src/index.js +14 -11
  133. package/src/utils.d.ts +63 -28
  134. package/src/utils.js +175 -49
  135. package/theme/ckboximageedit.css +53 -0
  136. package/theme/icons/ckbox-image-edit.svg +1 -0
  137. package/build/ckbox.js.map +0 -1
@@ -1,362 +1,321 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- import { Plugin } from 'ckeditor5/src/core';
6
- import { Range } from 'ckeditor5/src/engine';
7
- import { CKEditorError, logError } from 'ckeditor5/src/utils';
8
- import CKBoxCommand from './ckboxcommand';
9
- import CKBoxUploadAdapter from './ckboxuploadadapter';
10
- /**
11
- * The CKBox editing feature. It introduces the {@link module:ckbox/ckboxcommand~CKBoxCommand CKBox command} and
12
- * {@link module:ckbox/ckboxuploadadapter~CKBoxUploadAdapter CKBox upload adapter}.
13
- */
14
- export default class CKBoxEditing extends Plugin {
15
- /**
16
- * @inheritDoc
17
- */
18
- static get pluginName() {
19
- return 'CKBoxEditing';
20
- }
21
- /**
22
- * @inheritDoc
23
- */
24
- static get requires() {
25
- return ['CloudServices', 'LinkEditing', 'PictureEditing', CKBoxUploadAdapter];
26
- }
27
- /**
28
- * @inheritDoc
29
- */
30
- async init() {
31
- const editor = this.editor;
32
- const hasConfiguration = !!editor.config.get('ckbox');
33
- const isLibraryLoaded = !!window.CKBox;
34
- // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
35
- // the CKBox JavaScript library is loaded.
36
- if (!hasConfiguration && !isLibraryLoaded) {
37
- return;
38
- }
39
- this._initConfig();
40
- const cloudServicesCore = editor.plugins.get('CloudServicesCore');
41
- const ckboxTokenUrl = editor.config.get('ckbox.tokenUrl');
42
- const cloudServicesTokenUrl = editor.config.get('cloudServices.tokenUrl');
43
- // To avoid fetching the same token twice we need to compare the `ckbox.tokenUrl` and `cloudServices.tokenUrl` values.
44
- // If they are equal, it's enough to take the token generated by the `CloudServices` plugin.
45
- if (ckboxTokenUrl === cloudServicesTokenUrl) {
46
- const cloudServices = editor.plugins.get('CloudServices');
47
- this._token = cloudServices.token;
48
- }
49
- // Otherwise, create a new token manually.
50
- else {
51
- this._token = await cloudServicesCore.createToken(ckboxTokenUrl).init();
52
- }
53
- // Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign
54
- // the assets ID with the model elements is enabled.
55
- if (!editor.config.get('ckbox.ignoreDataId')) {
56
- this._initSchema();
57
- this._initConversion();
58
- this._initFixers();
59
- }
60
- // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
61
- if (isLibraryLoaded) {
62
- editor.commands.add('ckbox', new CKBoxCommand(editor));
63
- }
64
- }
65
- /**
66
- * Returns a token used by the CKBox plugin for communication with the CKBox service.
67
- */
68
- getToken() {
69
- return this._token;
70
- }
71
- /**
72
- * Initializes the `ckbox` editor configuration.
73
- */
74
- _initConfig() {
75
- const editor = this.editor;
76
- editor.config.define('ckbox', {
77
- serviceOrigin: 'https://api.ckbox.io',
78
- defaultUploadCategories: null,
79
- ignoreDataId: false,
80
- language: editor.locale.uiLanguage,
81
- theme: 'default',
82
- tokenUrl: editor.config.get('cloudServices.tokenUrl')
83
- });
84
- const tokenUrl = editor.config.get('ckbox.tokenUrl');
85
- if (!tokenUrl) {
86
- /**
87
- * The {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
88
- * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
89
- * configuration is required for the CKBox plugin.
90
- *
91
- * ```ts
92
- * ClassicEditor.create( document.createElement( 'div' ), {
93
- * ckbox: {
94
- * tokenUrl: "YOUR_TOKEN_URL"
95
- * // ...
96
- * }
97
- * // ...
98
- * } );
99
- * ```
100
- *
101
- * @error ckbox-plugin-missing-token-url
102
- */
103
- throw new CKEditorError('ckbox-plugin-missing-token-url', this);
104
- }
105
- if (!editor.plugins.has('ImageBlockEditing') && !editor.plugins.has('ImageInlineEditing')) {
106
- /**
107
- * The CKBox feature requires one of the following plugins to be loaded to work correctly:
108
- *
109
- * * {@link module:image/imageblock~ImageBlock},
110
- * * {@link module:image/imageinline~ImageInline},
111
- * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
112
- *
113
- * Please make sure your editor configuration is correct.
114
- *
115
- * @error ckbox-plugin-image-feature-missing
116
- * @param {module:core/editor/editor~Editor} editor
117
- */
118
- logError('ckbox-plugin-image-feature-missing', editor);
119
- }
120
- }
121
- /**
122
- * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
123
- */
124
- _initSchema() {
125
- const editor = this.editor;
126
- const schema = editor.model.schema;
127
- schema.extend('$text', { allowAttributes: 'ckboxLinkId' });
128
- if (schema.isRegistered('imageBlock')) {
129
- schema.extend('imageBlock', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
130
- }
131
- if (schema.isRegistered('imageInline')) {
132
- schema.extend('imageInline', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
133
- }
134
- schema.addAttributeCheck((context, attributeName) => {
135
- const isLink = !!context.last.getAttribute('linkHref');
136
- if (!isLink && attributeName === 'ckboxLinkId') {
137
- return false;
138
- }
139
- });
140
- }
141
- /**
142
- * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
143
- */
144
- _initConversion() {
145
- const editor = this.editor;
146
- // Convert `ckboxLinkId` => `data-ckbox-resource-id`.
147
- editor.conversion.for('downcast').add(dispatcher => {
148
- // Due to custom converters for linked block images, handle the `ckboxLinkId` attribute manually.
149
- dispatcher.on('attribute:ckboxLinkId:imageBlock', (evt, data, conversionApi) => {
150
- const { writer, mapper, consumable } = conversionApi;
151
- if (!consumable.consume(data.item, evt.name)) {
152
- return;
153
- }
154
- const viewFigure = mapper.toViewElement(data.item);
155
- const linkInImage = [...viewFigure.getChildren()]
156
- .find((child) => child.name === 'a');
157
- // No link inside an image - no conversion needed.
158
- if (!linkInImage) {
159
- return;
160
- }
161
- if (data.item.hasAttribute('ckboxLinkId')) {
162
- writer.setAttribute('data-ckbox-resource-id', data.item.getAttribute('ckboxLinkId'), linkInImage);
163
- }
164
- else {
165
- writer.removeAttribute('data-ckbox-resource-id', linkInImage);
166
- }
167
- }, { priority: 'low' });
168
- dispatcher.on('attribute:ckboxLinkId', (evt, data, conversionApi) => {
169
- const { writer, mapper, consumable } = conversionApi;
170
- if (!consumable.consume(data.item, evt.name)) {
171
- return;
172
- }
173
- // Remove the previous attribute value if it was applied.
174
- if (data.attributeOldValue) {
175
- const viewElement = createLinkElement(writer, data.attributeOldValue);
176
- writer.unwrap(mapper.toViewRange(data.range), viewElement);
177
- }
178
- // Add the new attribute value if specified in a model element.
179
- if (data.attributeNewValue) {
180
- const viewElement = createLinkElement(writer, data.attributeNewValue);
181
- if (data.item.is('selection')) {
182
- const viewSelection = writer.document.selection;
183
- writer.wrap(viewSelection.getFirstRange(), viewElement);
184
- }
185
- else {
186
- writer.wrap(mapper.toViewRange(data.range), viewElement);
187
- }
188
- }
189
- }, { priority: 'low' });
190
- });
191
- // Convert `data-ckbox-resource-id` => `ckboxLinkId`.
192
- //
193
- // The helper conversion does not handle all cases, so take care of the `data-ckbox-resource-id` attribute manually for images
194
- // and links.
195
- editor.conversion.for('upcast').add(dispatcher => {
196
- dispatcher.on('element:a', (evt, data, conversionApi) => {
197
- const { writer, consumable } = conversionApi;
198
- // Upcast the `data-ckbox-resource-id` attribute only for valid link elements.
199
- if (!data.viewItem.getAttribute('href')) {
200
- return;
201
- }
202
- const consumableAttributes = { attributes: ['data-ckbox-resource-id'] };
203
- if (!consumable.consume(data.viewItem, consumableAttributes)) {
204
- return;
205
- }
206
- const attributeValue = data.viewItem.getAttribute('data-ckbox-resource-id');
207
- // Missing the `data-ckbox-resource-id` attribute.
208
- if (!attributeValue) {
209
- return;
210
- }
211
- if (data.modelRange) {
212
- // If the `<a>` element contains more than single children (e.g. a linked image), set the `ckboxLinkId` for each
213
- // allowed child.
214
- for (let item of data.modelRange.getItems()) {
215
- if (item.is('$textProxy')) {
216
- item = item.textNode;
217
- }
218
- // Do not copy the `ckboxLinkId` attribute when wrapping an element in a block element, e.g. when
219
- // auto-paragraphing.
220
- if (shouldUpcastAttributeForNode(item)) {
221
- writer.setAttribute('ckboxLinkId', attributeValue, item);
222
- }
223
- }
224
- }
225
- else {
226
- // Otherwise, just set the `ckboxLinkId` for the model element.
227
- const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
228
- writer.setAttribute('ckboxLinkId', attributeValue, modelElement);
229
- }
230
- }, { priority: 'low' });
231
- });
232
- // Convert `ckboxImageId` => `data-ckbox-resource-id`.
233
- editor.conversion.for('downcast').attributeToAttribute({
234
- model: 'ckboxImageId',
235
- view: 'data-ckbox-resource-id'
236
- });
237
- // Convert `data-ckbox-resource-id` => `ckboxImageId`.
238
- editor.conversion.for('upcast').elementToAttribute({
239
- model: {
240
- key: 'ckboxImageId',
241
- value: (viewElement) => viewElement.getAttribute('data-ckbox-resource-id')
242
- },
243
- view: {
244
- attributes: {
245
- 'data-ckbox-resource-id': /[\s\S]+/
246
- }
247
- }
248
- });
249
- }
250
- /**
251
- * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
252
- */
253
- _initFixers() {
254
- const editor = this.editor;
255
- const model = editor.model;
256
- const selection = model.document.selection;
257
- // Registers the post-fixer to sync the asset ID with the model elements.
258
- model.document.registerPostFixer(syncDataIdPostFixer(editor));
259
- // Registers the post-fixer to remove the `ckboxLinkId` attribute from the model selection.
260
- model.document.registerPostFixer(injectSelectionPostFixer(selection));
261
- }
262
- }
263
- /**
264
- * A post-fixer that synchronizes the asset ID with the model element.
265
- */
266
- function syncDataIdPostFixer(editor) {
267
- return (writer) => {
268
- let changed = false;
269
- const model = editor.model;
270
- const ckboxCommand = editor.commands.get('ckbox');
271
- // The ID from chosen assets are stored in the `CKBoxCommand#_chosenAssets`. If there is no command, it makes no sense to check
272
- // for changes in the model.
273
- if (!ckboxCommand) {
274
- return changed;
275
- }
276
- for (const entry of model.document.differ.getChanges()) {
277
- if (entry.type !== 'insert' && entry.type !== 'attribute') {
278
- continue;
279
- }
280
- const range = entry.type === 'insert' ?
281
- new Range(entry.position, entry.position.getShiftedBy(entry.length)) :
282
- entry.range;
283
- const isLinkHrefAttributeRemoval = entry.type === 'attribute' &&
284
- entry.attributeKey === 'linkHref' &&
285
- entry.attributeNewValue === null;
286
- for (const item of range.getItems()) {
287
- // If the `linkHref` attribute has been removed, sync the change with the `ckboxLinkId` attribute.
288
- if (isLinkHrefAttributeRemoval && item.hasAttribute('ckboxLinkId')) {
289
- writer.removeAttribute('ckboxLinkId', item);
290
- changed = true;
291
- continue;
292
- }
293
- // Otherwise, the change concerns either a new model element or an attribute change. Try to find the assets for the modified
294
- // model element.
295
- const assets = findAssetsForItem(item, ckboxCommand._chosenAssets);
296
- for (const asset of assets) {
297
- const attributeName = asset.type === 'image' ? 'ckboxImageId' : 'ckboxLinkId';
298
- if (asset.id === item.getAttribute(attributeName)) {
299
- continue;
300
- }
301
- writer.setAttribute(attributeName, asset.id, item);
302
- changed = true;
303
- }
304
- }
305
- }
306
- return changed;
307
- };
308
- }
309
- /**
310
- * A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
311
- */
312
- function injectSelectionPostFixer(selection) {
313
- return (writer) => {
314
- const shouldRemoveLinkIdAttribute = !selection.hasAttribute('linkHref') && selection.hasAttribute('ckboxLinkId');
315
- if (shouldRemoveLinkIdAttribute) {
316
- writer.removeSelectionAttribute('ckboxLinkId');
317
- return true;
318
- }
319
- return false;
320
- };
321
- }
322
- /**
323
- * Tries to find the asset that is associated with the model element by comparing the attributes:
324
- * - the image fallback URL with the `src` attribute for images,
325
- * - the link URL with the `href` attribute for links.
326
- *
327
- * 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
328
- * image asset).
329
- */
330
- function findAssetsForItem(item, assets) {
331
- const isImageElement = item.is('element', 'imageInline') || item.is('element', 'imageBlock');
332
- const isLinkElement = item.hasAttribute('linkHref');
333
- return [...assets].filter(asset => {
334
- if (asset.type === 'image' && isImageElement) {
335
- return asset.attributes.imageFallbackUrl === item.getAttribute('src');
336
- }
337
- if (asset.type === 'link' && isLinkElement) {
338
- return asset.attributes.linkHref === item.getAttribute('linkHref');
339
- }
340
- });
341
- }
342
- /**
343
- * Creates view link element with the requested ID.
344
- */
345
- function createLinkElement(writer, id) {
346
- // Priority equal 5 is needed to merge adjacent `<a>` elements together.
347
- const viewElement = writer.createAttributeElement('a', { 'data-ckbox-resource-id': id }, { priority: 5 });
348
- writer.setCustomProperty('link', true, viewElement);
349
- return viewElement;
350
- }
351
- /**
352
- * Checks if the model element may have the `ckboxLinkId` attribute.
353
- */
354
- function shouldUpcastAttributeForNode(node) {
355
- if (node.is('$text')) {
356
- return true;
357
- }
358
- if (node.is('element', 'imageInline') || node.is('element', 'imageBlock')) {
359
- return true;
360
- }
361
- return false;
362
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core';
6
+ import { Range } from 'ckeditor5/src/engine';
7
+ import { logError } from 'ckeditor5/src/utils';
8
+ import CKBoxCommand from './ckboxcommand';
9
+ import CKBoxUploadAdapter from './ckboxuploadadapter';
10
+ import CKBoxUtils from './ckboxutils';
11
+ /**
12
+ * The CKBox editing feature. It introduces the {@link module:ckbox/ckboxcommand~CKBoxCommand CKBox command} and
13
+ * {@link module:ckbox/ckboxuploadadapter~CKBoxUploadAdapter CKBox upload adapter}.
14
+ */
15
+ export default class CKBoxEditing extends Plugin {
16
+ /**
17
+ * @inheritDoc
18
+ */
19
+ static get pluginName() {
20
+ return 'CKBoxEditing';
21
+ }
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires() {
26
+ return ['LinkEditing', 'PictureEditing', CKBoxUploadAdapter, CKBoxUtils];
27
+ }
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ init() {
32
+ const editor = this.editor;
33
+ const hasConfiguration = !!editor.config.get('ckbox');
34
+ const isLibraryLoaded = !!window.CKBox;
35
+ // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
36
+ // the CKBox JavaScript library is loaded.
37
+ if (!hasConfiguration && !isLibraryLoaded) {
38
+ return;
39
+ }
40
+ this._checkImagePlugins();
41
+ // Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign
42
+ // the assets ID with the model elements is enabled.
43
+ if (!editor.config.get('ckbox.ignoreDataId')) {
44
+ this._initSchema();
45
+ this._initConversion();
46
+ this._initFixers();
47
+ }
48
+ // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
49
+ if (isLibraryLoaded) {
50
+ editor.commands.add('ckbox', new CKBoxCommand(editor));
51
+ }
52
+ }
53
+ /**
54
+ * Checks if at least one image plugin is loaded.
55
+ */
56
+ _checkImagePlugins() {
57
+ const editor = this.editor;
58
+ if (!editor.plugins.has('ImageBlockEditing') && !editor.plugins.has('ImageInlineEditing')) {
59
+ /**
60
+ * The CKBox feature requires one of the following plugins to be loaded to work correctly:
61
+ *
62
+ * * {@link module:image/imageblock~ImageBlock},
63
+ * * {@link module:image/imageinline~ImageInline},
64
+ * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
65
+ *
66
+ * Please make sure your editor configuration is correct.
67
+ *
68
+ * @error ckbox-plugin-image-feature-missing
69
+ * @param {module:core/editor/editor~Editor} editor
70
+ */
71
+ logError('ckbox-plugin-image-feature-missing', editor);
72
+ }
73
+ }
74
+ /**
75
+ * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
76
+ */
77
+ _initSchema() {
78
+ const editor = this.editor;
79
+ const schema = editor.model.schema;
80
+ schema.extend('$text', { allowAttributes: 'ckboxLinkId' });
81
+ if (schema.isRegistered('imageBlock')) {
82
+ schema.extend('imageBlock', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
83
+ }
84
+ if (schema.isRegistered('imageInline')) {
85
+ schema.extend('imageInline', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
86
+ }
87
+ schema.addAttributeCheck((context, attributeName) => {
88
+ const isLink = !!context.last.getAttribute('linkHref');
89
+ if (!isLink && attributeName === 'ckboxLinkId') {
90
+ return false;
91
+ }
92
+ });
93
+ }
94
+ /**
95
+ * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
96
+ */
97
+ _initConversion() {
98
+ const editor = this.editor;
99
+ // Convert `ckboxLinkId` => `data-ckbox-resource-id`.
100
+ editor.conversion.for('downcast').add(dispatcher => {
101
+ // Due to custom converters for linked block images, handle the `ckboxLinkId` attribute manually.
102
+ dispatcher.on('attribute:ckboxLinkId:imageBlock', (evt, data, conversionApi) => {
103
+ const { writer, mapper, consumable } = conversionApi;
104
+ if (!consumable.consume(data.item, evt.name)) {
105
+ return;
106
+ }
107
+ const viewFigure = mapper.toViewElement(data.item);
108
+ const linkInImage = [...viewFigure.getChildren()]
109
+ .find((child) => child.name === 'a');
110
+ // No link inside an image - no conversion needed.
111
+ if (!linkInImage) {
112
+ return;
113
+ }
114
+ if (data.item.hasAttribute('ckboxLinkId')) {
115
+ writer.setAttribute('data-ckbox-resource-id', data.item.getAttribute('ckboxLinkId'), linkInImage);
116
+ }
117
+ else {
118
+ writer.removeAttribute('data-ckbox-resource-id', linkInImage);
119
+ }
120
+ }, { priority: 'low' });
121
+ dispatcher.on('attribute:ckboxLinkId', (evt, data, conversionApi) => {
122
+ const { writer, mapper, consumable } = conversionApi;
123
+ if (!consumable.consume(data.item, evt.name)) {
124
+ return;
125
+ }
126
+ // Remove the previous attribute value if it was applied.
127
+ if (data.attributeOldValue) {
128
+ const viewElement = createLinkElement(writer, data.attributeOldValue);
129
+ writer.unwrap(mapper.toViewRange(data.range), viewElement);
130
+ }
131
+ // Add the new attribute value if specified in a model element.
132
+ if (data.attributeNewValue) {
133
+ const viewElement = createLinkElement(writer, data.attributeNewValue);
134
+ if (data.item.is('selection')) {
135
+ const viewSelection = writer.document.selection;
136
+ writer.wrap(viewSelection.getFirstRange(), viewElement);
137
+ }
138
+ else {
139
+ writer.wrap(mapper.toViewRange(data.range), viewElement);
140
+ }
141
+ }
142
+ }, { priority: 'low' });
143
+ });
144
+ // Convert `data-ckbox-resource-id` => `ckboxLinkId`.
145
+ //
146
+ // The helper conversion does not handle all cases, so take care of the `data-ckbox-resource-id` attribute manually for images
147
+ // and links.
148
+ editor.conversion.for('upcast').add(dispatcher => {
149
+ dispatcher.on('element:a', (evt, data, conversionApi) => {
150
+ const { writer, consumable } = conversionApi;
151
+ // Upcast the `data-ckbox-resource-id` attribute only for valid link elements.
152
+ if (!data.viewItem.getAttribute('href')) {
153
+ return;
154
+ }
155
+ const consumableAttributes = { attributes: ['data-ckbox-resource-id'] };
156
+ if (!consumable.consume(data.viewItem, consumableAttributes)) {
157
+ return;
158
+ }
159
+ const attributeValue = data.viewItem.getAttribute('data-ckbox-resource-id');
160
+ // Missing the `data-ckbox-resource-id` attribute.
161
+ if (!attributeValue) {
162
+ return;
163
+ }
164
+ if (data.modelRange) {
165
+ // If the `<a>` element contains more than single children (e.g. a linked image), set the `ckboxLinkId` for each
166
+ // allowed child.
167
+ for (let item of data.modelRange.getItems()) {
168
+ if (item.is('$textProxy')) {
169
+ item = item.textNode;
170
+ }
171
+ // Do not copy the `ckboxLinkId` attribute when wrapping an element in a block element, e.g. when
172
+ // auto-paragraphing.
173
+ if (shouldUpcastAttributeForNode(item)) {
174
+ writer.setAttribute('ckboxLinkId', attributeValue, item);
175
+ }
176
+ }
177
+ }
178
+ else {
179
+ // Otherwise, just set the `ckboxLinkId` for the model element.
180
+ const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
181
+ writer.setAttribute('ckboxLinkId', attributeValue, modelElement);
182
+ }
183
+ }, { priority: 'low' });
184
+ });
185
+ // Convert `ckboxImageId` => `data-ckbox-resource-id`.
186
+ editor.conversion.for('downcast').attributeToAttribute({
187
+ model: 'ckboxImageId',
188
+ view: 'data-ckbox-resource-id'
189
+ });
190
+ // Convert `data-ckbox-resource-id` => `ckboxImageId`.
191
+ editor.conversion.for('upcast').elementToAttribute({
192
+ model: {
193
+ key: 'ckboxImageId',
194
+ value: (viewElement) => viewElement.getAttribute('data-ckbox-resource-id')
195
+ },
196
+ view: {
197
+ attributes: {
198
+ 'data-ckbox-resource-id': /[\s\S]+/
199
+ }
200
+ }
201
+ });
202
+ const replaceImageSourceCommand = editor.commands.get('replaceImageSource');
203
+ if (replaceImageSourceCommand) {
204
+ this.listenTo(replaceImageSourceCommand, 'cleanupImage', (_, [writer, image]) => {
205
+ writer.removeAttribute('ckboxImageId', image);
206
+ });
207
+ }
208
+ }
209
+ /**
210
+ * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
211
+ */
212
+ _initFixers() {
213
+ const editor = this.editor;
214
+ const model = editor.model;
215
+ const selection = model.document.selection;
216
+ // Registers the post-fixer to sync the asset ID with the model elements.
217
+ model.document.registerPostFixer(syncDataIdPostFixer(editor));
218
+ // Registers the post-fixer to remove the `ckboxLinkId` attribute from the model selection.
219
+ model.document.registerPostFixer(injectSelectionPostFixer(selection));
220
+ }
221
+ }
222
+ /**
223
+ * A post-fixer that synchronizes the asset ID with the model element.
224
+ */
225
+ function syncDataIdPostFixer(editor) {
226
+ return (writer) => {
227
+ let changed = false;
228
+ const model = editor.model;
229
+ const ckboxCommand = editor.commands.get('ckbox');
230
+ // The ID from chosen assets are stored in the `CKBoxCommand#_chosenAssets`. If there is no command, it makes no sense to check
231
+ // for changes in the model.
232
+ if (!ckboxCommand) {
233
+ return changed;
234
+ }
235
+ for (const entry of model.document.differ.getChanges()) {
236
+ if (entry.type !== 'insert' && entry.type !== 'attribute') {
237
+ continue;
238
+ }
239
+ const range = entry.type === 'insert' ?
240
+ new Range(entry.position, entry.position.getShiftedBy(entry.length)) :
241
+ entry.range;
242
+ const isLinkHrefAttributeRemoval = entry.type === 'attribute' &&
243
+ entry.attributeKey === 'linkHref' &&
244
+ entry.attributeNewValue === null;
245
+ for (const item of range.getItems()) {
246
+ // If the `linkHref` attribute has been removed, sync the change with the `ckboxLinkId` attribute.
247
+ if (isLinkHrefAttributeRemoval && item.hasAttribute('ckboxLinkId')) {
248
+ writer.removeAttribute('ckboxLinkId', item);
249
+ changed = true;
250
+ continue;
251
+ }
252
+ // Otherwise, the change concerns either a new model element or an attribute change. Try to find the assets for the modified
253
+ // model element.
254
+ const assets = findAssetsForItem(item, ckboxCommand._chosenAssets);
255
+ for (const asset of assets) {
256
+ const attributeName = asset.type === 'image' ? 'ckboxImageId' : 'ckboxLinkId';
257
+ if (asset.id === item.getAttribute(attributeName)) {
258
+ continue;
259
+ }
260
+ writer.setAttribute(attributeName, asset.id, item);
261
+ changed = true;
262
+ }
263
+ }
264
+ }
265
+ return changed;
266
+ };
267
+ }
268
+ /**
269
+ * A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
270
+ */
271
+ function injectSelectionPostFixer(selection) {
272
+ return (writer) => {
273
+ const shouldRemoveLinkIdAttribute = !selection.hasAttribute('linkHref') && selection.hasAttribute('ckboxLinkId');
274
+ if (shouldRemoveLinkIdAttribute) {
275
+ writer.removeSelectionAttribute('ckboxLinkId');
276
+ return true;
277
+ }
278
+ return false;
279
+ };
280
+ }
281
+ /**
282
+ * Tries to find the asset that is associated with the model element by comparing the attributes:
283
+ * - the image fallback URL with the `src` attribute for images,
284
+ * - the link URL with the `href` attribute for links.
285
+ *
286
+ * 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
287
+ * image asset).
288
+ */
289
+ function findAssetsForItem(item, assets) {
290
+ const isImageElement = item.is('element', 'imageInline') || item.is('element', 'imageBlock');
291
+ const isLinkElement = item.hasAttribute('linkHref');
292
+ return [...assets].filter(asset => {
293
+ if (asset.type === 'image' && isImageElement) {
294
+ return asset.attributes.imageFallbackUrl === item.getAttribute('src');
295
+ }
296
+ if (asset.type === 'link' && isLinkElement) {
297
+ return asset.attributes.linkHref === item.getAttribute('linkHref');
298
+ }
299
+ });
300
+ }
301
+ /**
302
+ * Creates view link element with the requested ID.
303
+ */
304
+ function createLinkElement(writer, id) {
305
+ // Priority equal 5 is needed to merge adjacent `<a>` elements together.
306
+ const viewElement = writer.createAttributeElement('a', { 'data-ckbox-resource-id': id }, { priority: 5 });
307
+ writer.setCustomProperty('link', true, viewElement);
308
+ return viewElement;
309
+ }
310
+ /**
311
+ * Checks if the model element may have the `ckboxLinkId` attribute.
312
+ */
313
+ function shouldUpcastAttributeForNode(node) {
314
+ if (node.is('$text')) {
315
+ return true;
316
+ }
317
+ if (node.is('element', 'imageInline') || node.is('element', 'imageBlock')) {
318
+ return true;
319
+ }
320
+ return false;
321
+ }