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

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,465 +2,362 @@
2
2
  * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
- /* globals window */
7
-
8
- /**
9
- * @module ckbox/ckboxediting
10
- */
11
-
12
5
  import { Plugin } from 'ckeditor5/src/core';
13
6
  import { Range } from 'ckeditor5/src/engine';
14
7
  import { CKEditorError, logError } from 'ckeditor5/src/utils';
15
-
16
8
  import CKBoxCommand from './ckboxcommand';
17
9
  import CKBoxUploadAdapter from './ckboxuploadadapter';
18
-
19
10
  /**
20
11
  * The CKBox editing feature. It introduces the {@link module:ckbox/ckboxcommand~CKBoxCommand CKBox command} and
21
12
  * {@link module:ckbox/ckboxuploadadapter~CKBoxUploadAdapter CKBox upload adapter}.
22
- *
23
- * @extends module:core/plugin~Plugin
24
13
  */
25
14
  export default class CKBoxEditing extends Plugin {
26
- /**
27
- * @inheritDoc
28
- */
29
- static get pluginName() {
30
- return 'CKBoxEditing';
31
- }
32
-
33
- /**
34
- * @inheritDoc
35
- */
36
- static get requires() {
37
- return [ 'CloudServices', 'LinkEditing', 'PictureEditing', CKBoxUploadAdapter ];
38
- }
39
-
40
- /**
41
- * @inheritDoc
42
- */
43
- async init() {
44
- const editor = this.editor;
45
- const hasConfiguration = !!editor.config.get( 'ckbox' );
46
- const isLibraryLoaded = !!window.CKBox;
47
-
48
- // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or
49
- // the CKBox JavaScript library is loaded.
50
- if ( !hasConfiguration && !isLibraryLoaded ) {
51
- return;
52
- }
53
-
54
- this._initConfig();
55
-
56
- const cloudServicesCore = editor.plugins.get( 'CloudServicesCore' );
57
- const ckboxTokenUrl = editor.config.get( 'ckbox.tokenUrl' );
58
- const cloudServicesTokenUrl = editor.config.get( 'cloudServices.tokenUrl' );
59
-
60
- // To avoid fetching the same token twice we need to compare the `ckbox.tokenUrl` and `cloudServices.tokenUrl` values.
61
- // If they are equal, it's enough to take the token generated by the `CloudServices` plugin.
62
- if ( ckboxTokenUrl === cloudServicesTokenUrl ) {
63
- /**
64
- * CKEditor Cloud Services access token.
65
- *
66
- * @protected
67
- * @member {module:cloud-services/token~Token} #_token
68
- */
69
- this._token = editor.plugins.get( 'CloudServices' ).token;
70
- }
71
- // Otherwise, create a new token manually.
72
- else {
73
- this._token = await cloudServicesCore.createToken( ckboxTokenUrl ).init();
74
- }
75
-
76
- // Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign
77
- // the assets ID with the model elements is enabled.
78
- if ( !editor.config.get( 'ckbox.ignoreDataId' ) ) {
79
- this._initSchema();
80
- this._initConversion();
81
- this._initFixers();
82
- }
83
-
84
- // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog.
85
- if ( isLibraryLoaded ) {
86
- editor.commands.add( 'ckbox', new CKBoxCommand( editor ) );
87
- }
88
- }
89
-
90
- /**
91
- * Returns a token used by the CKBox plugin for communication with the CKBox service.
92
- *
93
- * @returns {module:cloud-services/token~Token}
94
- */
95
- getToken() {
96
- return this._token;
97
- }
98
-
99
- /**
100
- * Initializes the `ckbox` editor configuration.
101
- *
102
- * @private
103
- */
104
- _initConfig() {
105
- const editor = this.editor;
106
-
107
- editor.config.define( 'ckbox', {
108
- serviceOrigin: 'https://api.ckbox.io',
109
- assetsOrigin: 'https://ckbox.cloud',
110
- defaultUploadCategories: null,
111
- ignoreDataId: false,
112
- language: editor.locale.uiLanguage,
113
- theme: 'default',
114
- tokenUrl: editor.config.get( 'cloudServices.tokenUrl' )
115
- } );
116
-
117
- const tokenUrl = editor.config.get( 'ckbox.tokenUrl' );
118
-
119
- if ( !tokenUrl ) {
120
- /**
121
- * The {@link module:ckbox/ckbox~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
122
- * {@link module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
123
- * configuration is required for the CKBox plugin.
124
- *
125
- * ClassicEditor.create( document.createElement( 'div' ), {
126
- * ckbox: {
127
- * tokenUrl: "YOUR_TOKEN_URL"
128
- * // ...
129
- * }
130
- * // ...
131
- * } );
132
- *
133
- * @error ckbox-plugin-missing-token-url
134
- */
135
- throw new CKEditorError( 'ckbox-plugin-missing-token-url', this );
136
- }
137
-
138
- if ( !editor.plugins.has( 'ImageBlockEditing' ) && !editor.plugins.has( 'ImageInlineEditing' ) ) {
139
- /**
140
- * The CKBox feature requires one of the following plugins to be loaded to work correctly:
141
- *
142
- * * {@link module:image/imageblock~ImageBlock},
143
- * * {@link module:image/imageinline~ImageInline},
144
- * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
145
- *
146
- * Please make sure your editor configuration is correct.
147
- *
148
- * @error ckbox-plugin-image-feature-missing
149
- * @param {module:core/editor/editor~Editor} editor
150
- */
151
- logError( 'ckbox-plugin-image-feature-missing', editor );
152
- }
153
- }
154
-
155
- /**
156
- * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
157
- *
158
- * @private
159
- */
160
- _initSchema() {
161
- const editor = this.editor;
162
- const schema = editor.model.schema;
163
-
164
- schema.extend( '$text', { allowAttributes: 'ckboxLinkId' } );
165
-
166
- if ( schema.isRegistered( 'imageBlock' ) ) {
167
- schema.extend( 'imageBlock', { allowAttributes: [ 'ckboxImageId', 'ckboxLinkId' ] } );
168
- }
169
-
170
- if ( schema.isRegistered( 'imageInline' ) ) {
171
- schema.extend( 'imageInline', { allowAttributes: [ 'ckboxImageId', 'ckboxLinkId' ] } );
172
- }
173
-
174
- schema.addAttributeCheck( ( context, attributeName ) => {
175
- const isLink = !!context.last.getAttribute( 'linkHref' );
176
-
177
- if ( !isLink && attributeName === 'ckboxLinkId' ) {
178
- return false;
179
- }
180
- } );
181
- }
182
-
183
- /**
184
- * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
185
- *
186
- * @private
187
- */
188
- _initConversion() {
189
- const editor = this.editor;
190
-
191
- // Convert `ckboxLinkId` => `data-ckbox-resource-id`.
192
- editor.conversion.for( 'downcast' ).add( dispatcher => {
193
- // Due to custom converters for linked block images, handle the `ckboxLinkId` attribute manually.
194
- dispatcher.on( 'attribute:ckboxLinkId:imageBlock', ( evt, data, conversionApi ) => {
195
- const { writer, mapper, consumable } = conversionApi;
196
-
197
- if ( !consumable.consume( data.item, evt.name ) ) {
198
- return;
199
- }
200
-
201
- const viewFigure = mapper.toViewElement( data.item );
202
- const linkInImage = [ ...viewFigure.getChildren() ].find( child => child.name === 'a' );
203
-
204
- // No link inside an image - no conversion needed.
205
- if ( !linkInImage ) {
206
- return;
207
- }
208
-
209
- if ( data.item.hasAttribute( 'ckboxLinkId' ) ) {
210
- writer.setAttribute( 'data-ckbox-resource-id', data.item.getAttribute( 'ckboxLinkId' ), linkInImage );
211
- } else {
212
- writer.removeAttribute( 'data-ckbox-resource-id', linkInImage );
213
- }
214
- }, { priority: 'low' } );
215
-
216
- dispatcher.on( 'attribute:ckboxLinkId', ( evt, data, conversionApi ) => {
217
- const { writer, mapper, consumable } = conversionApi;
218
-
219
- if ( !consumable.consume( data.item, evt.name ) ) {
220
- return;
221
- }
222
-
223
- // Remove the previous attribute value if it was applied.
224
- if ( data.attributeOldValue ) {
225
- const viewElement = createLinkElement( writer, data.attributeOldValue );
226
-
227
- writer.unwrap( mapper.toViewRange( data.range ), viewElement );
228
- }
229
-
230
- // Add the new attribute value if specified in a model element.
231
- if ( data.attributeNewValue ) {
232
- const viewElement = createLinkElement( writer, data.attributeNewValue );
233
-
234
- if ( data.item.is( 'selection' ) ) {
235
- const viewSelection = writer.document.selection;
236
-
237
- writer.wrap( viewSelection.getFirstRange(), viewElement );
238
- } else {
239
- writer.wrap( mapper.toViewRange( data.range ), viewElement );
240
- }
241
- }
242
- }, { priority: 'low' } );
243
- } );
244
-
245
- // Convert `data-ckbox-resource-id` => `ckboxLinkId`.
246
- //
247
- // The helper conversion does not handle all cases, so take care of the `data-ckbox-resource-id` attribute manually for images
248
- // and links.
249
- editor.conversion.for( 'upcast' ).add( dispatcher => {
250
- dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
251
- const { writer, consumable } = conversionApi;
252
-
253
- // Upcast the `data-ckbox-resource-id` attribute only for valid link elements.
254
- if ( !data.viewItem.getAttribute( 'href' ) ) {
255
- return;
256
- }
257
-
258
- const consumableAttributes = { attributes: [ 'data-ckbox-resource-id' ] };
259
-
260
- if ( !consumable.consume( data.viewItem, consumableAttributes ) ) {
261
- return;
262
- }
263
-
264
- const attributeValue = data.viewItem.getAttribute( 'data-ckbox-resource-id' );
265
-
266
- // Missing the `data-ckbox-resource-id` attribute.
267
- if ( !attributeValue ) {
268
- return;
269
- }
270
-
271
- if ( data.modelRange ) {
272
- // If the `<a>` element contains more than single children (e.g. a linked image), set the `ckboxLinkId` for each
273
- // allowed child.
274
- for ( let item of data.modelRange.getItems() ) {
275
- if ( item.is( '$textProxy' ) ) {
276
- item = item.textNode;
277
- }
278
-
279
- // Do not copy the `ckboxLinkId` attribute when wrapping an element in a block element, e.g. when
280
- // auto-paragraphing.
281
- if ( shouldUpcastAttributeForNode( item ) ) {
282
- writer.setAttribute( 'ckboxLinkId', attributeValue, item );
283
- }
284
- }
285
- } else {
286
- // Otherwise, just set the `ckboxLinkId` for the model element.
287
- const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
288
-
289
- writer.setAttribute( 'ckboxLinkId', attributeValue, modelElement );
290
- }
291
- }, { priority: 'low' } );
292
- } );
293
-
294
- // Convert `ckboxImageId` => `data-ckbox-resource-id`.
295
- editor.conversion.for( 'downcast' ).attributeToAttribute( {
296
- model: 'ckboxImageId',
297
- view: 'data-ckbox-resource-id'
298
- } );
299
-
300
- // Convert `data-ckbox-resource-id` => `ckboxImageId`.
301
- editor.conversion.for( 'upcast' ).elementToAttribute( {
302
- model: {
303
- key: 'ckboxImageId',
304
- value: viewElement => viewElement.getAttribute( 'data-ckbox-resource-id' )
305
- },
306
- view: {
307
- attributes: {
308
- 'data-ckbox-resource-id': /[\s\S]+/
309
- }
310
- }
311
- } );
312
- }
313
-
314
- /**
315
- * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
316
- *
317
- * @private
318
- */
319
- _initFixers() {
320
- const editor = this.editor;
321
- const model = editor.model;
322
- const selection = model.document.selection;
323
-
324
- // Registers the post-fixer to sync the asset ID with the model elements.
325
- model.document.registerPostFixer( syncDataIdPostFixer( editor ) );
326
-
327
- // Registers the post-fixer to remove the `ckboxLinkId` attribute from the model selection.
328
- model.document.registerPostFixer( injectSelectionPostFixer( selection ) );
329
- }
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
+ assetsOrigin: 'https://ckbox.cloud',
79
+ defaultUploadCategories: null,
80
+ ignoreDataId: false,
81
+ language: editor.locale.uiLanguage,
82
+ theme: 'default',
83
+ tokenUrl: editor.config.get('cloudServices.tokenUrl')
84
+ });
85
+ const tokenUrl = editor.config.get('ckbox.tokenUrl');
86
+ if (!tokenUrl) {
87
+ /**
88
+ * The {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} or the
89
+ * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`}
90
+ * configuration is required for the CKBox plugin.
91
+ *
92
+ * ```ts
93
+ * ClassicEditor.create( document.createElement( 'div' ), {
94
+ * ckbox: {
95
+ * tokenUrl: "YOUR_TOKEN_URL"
96
+ * // ...
97
+ * }
98
+ * // ...
99
+ * } );
100
+ * ```
101
+ *
102
+ * @error ckbox-plugin-missing-token-url
103
+ */
104
+ throw new CKEditorError('ckbox-plugin-missing-token-url', this);
105
+ }
106
+ if (!editor.plugins.has('ImageBlockEditing') && !editor.plugins.has('ImageInlineEditing')) {
107
+ /**
108
+ * The CKBox feature requires one of the following plugins to be loaded to work correctly:
109
+ *
110
+ * * {@link module:image/imageblock~ImageBlock},
111
+ * * {@link module:image/imageinline~ImageInline},
112
+ * * {@link module:image/image~Image} (loads both `ImageBlock` and `ImageInline`)
113
+ *
114
+ * Please make sure your editor configuration is correct.
115
+ *
116
+ * @error ckbox-plugin-image-feature-missing
117
+ * @param {module:core/editor/editor~Editor} editor
118
+ */
119
+ logError('ckbox-plugin-image-feature-missing', editor);
120
+ }
121
+ }
122
+ /**
123
+ * Extends the schema to allow the `ckboxImageId` and `ckboxLinkId` attributes for links and images.
124
+ */
125
+ _initSchema() {
126
+ const editor = this.editor;
127
+ const schema = editor.model.schema;
128
+ schema.extend('$text', { allowAttributes: 'ckboxLinkId' });
129
+ if (schema.isRegistered('imageBlock')) {
130
+ schema.extend('imageBlock', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
131
+ }
132
+ if (schema.isRegistered('imageInline')) {
133
+ schema.extend('imageInline', { allowAttributes: ['ckboxImageId', 'ckboxLinkId'] });
134
+ }
135
+ schema.addAttributeCheck((context, attributeName) => {
136
+ const isLink = !!context.last.getAttribute('linkHref');
137
+ if (!isLink && attributeName === 'ckboxLinkId') {
138
+ return false;
139
+ }
140
+ });
141
+ }
142
+ /**
143
+ * Configures the upcast and downcast conversions for the `ckboxImageId` and `ckboxLinkId` attributes.
144
+ */
145
+ _initConversion() {
146
+ const editor = this.editor;
147
+ // Convert `ckboxLinkId` => `data-ckbox-resource-id`.
148
+ editor.conversion.for('downcast').add(dispatcher => {
149
+ // Due to custom converters for linked block images, handle the `ckboxLinkId` attribute manually.
150
+ dispatcher.on('attribute:ckboxLinkId:imageBlock', (evt, data, conversionApi) => {
151
+ const { writer, mapper, consumable } = conversionApi;
152
+ if (!consumable.consume(data.item, evt.name)) {
153
+ return;
154
+ }
155
+ const viewFigure = mapper.toViewElement(data.item);
156
+ const linkInImage = [...viewFigure.getChildren()]
157
+ .find((child) => child.name === 'a');
158
+ // No link inside an image - no conversion needed.
159
+ if (!linkInImage) {
160
+ return;
161
+ }
162
+ if (data.item.hasAttribute('ckboxLinkId')) {
163
+ writer.setAttribute('data-ckbox-resource-id', data.item.getAttribute('ckboxLinkId'), linkInImage);
164
+ }
165
+ else {
166
+ writer.removeAttribute('data-ckbox-resource-id', linkInImage);
167
+ }
168
+ }, { priority: 'low' });
169
+ dispatcher.on('attribute:ckboxLinkId', (evt, data, conversionApi) => {
170
+ const { writer, mapper, consumable } = conversionApi;
171
+ if (!consumable.consume(data.item, evt.name)) {
172
+ return;
173
+ }
174
+ // Remove the previous attribute value if it was applied.
175
+ if (data.attributeOldValue) {
176
+ const viewElement = createLinkElement(writer, data.attributeOldValue);
177
+ writer.unwrap(mapper.toViewRange(data.range), viewElement);
178
+ }
179
+ // Add the new attribute value if specified in a model element.
180
+ if (data.attributeNewValue) {
181
+ const viewElement = createLinkElement(writer, data.attributeNewValue);
182
+ if (data.item.is('selection')) {
183
+ const viewSelection = writer.document.selection;
184
+ writer.wrap(viewSelection.getFirstRange(), viewElement);
185
+ }
186
+ else {
187
+ writer.wrap(mapper.toViewRange(data.range), viewElement);
188
+ }
189
+ }
190
+ }, { priority: 'low' });
191
+ });
192
+ // Convert `data-ckbox-resource-id` => `ckboxLinkId`.
193
+ //
194
+ // The helper conversion does not handle all cases, so take care of the `data-ckbox-resource-id` attribute manually for images
195
+ // and links.
196
+ editor.conversion.for('upcast').add(dispatcher => {
197
+ dispatcher.on('element:a', (evt, data, conversionApi) => {
198
+ const { writer, consumable } = conversionApi;
199
+ // Upcast the `data-ckbox-resource-id` attribute only for valid link elements.
200
+ if (!data.viewItem.getAttribute('href')) {
201
+ return;
202
+ }
203
+ const consumableAttributes = { attributes: ['data-ckbox-resource-id'] };
204
+ if (!consumable.consume(data.viewItem, consumableAttributes)) {
205
+ return;
206
+ }
207
+ const attributeValue = data.viewItem.getAttribute('data-ckbox-resource-id');
208
+ // Missing the `data-ckbox-resource-id` attribute.
209
+ if (!attributeValue) {
210
+ return;
211
+ }
212
+ if (data.modelRange) {
213
+ // If the `<a>` element contains more than single children (e.g. a linked image), set the `ckboxLinkId` for each
214
+ // allowed child.
215
+ for (let item of data.modelRange.getItems()) {
216
+ if (item.is('$textProxy')) {
217
+ item = item.textNode;
218
+ }
219
+ // Do not copy the `ckboxLinkId` attribute when wrapping an element in a block element, e.g. when
220
+ // auto-paragraphing.
221
+ if (shouldUpcastAttributeForNode(item)) {
222
+ writer.setAttribute('ckboxLinkId', attributeValue, item);
223
+ }
224
+ }
225
+ }
226
+ else {
227
+ // Otherwise, just set the `ckboxLinkId` for the model element.
228
+ const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
229
+ writer.setAttribute('ckboxLinkId', attributeValue, modelElement);
230
+ }
231
+ }, { priority: 'low' });
232
+ });
233
+ // Convert `ckboxImageId` => `data-ckbox-resource-id`.
234
+ editor.conversion.for('downcast').attributeToAttribute({
235
+ model: 'ckboxImageId',
236
+ view: 'data-ckbox-resource-id'
237
+ });
238
+ // Convert `data-ckbox-resource-id` => `ckboxImageId`.
239
+ editor.conversion.for('upcast').elementToAttribute({
240
+ model: {
241
+ key: 'ckboxImageId',
242
+ value: (viewElement) => viewElement.getAttribute('data-ckbox-resource-id')
243
+ },
244
+ view: {
245
+ attributes: {
246
+ 'data-ckbox-resource-id': /[\s\S]+/
247
+ }
248
+ }
249
+ });
250
+ }
251
+ /**
252
+ * Registers post-fixers that add or remove the `ckboxLinkId` and `ckboxImageId` attributes.
253
+ */
254
+ _initFixers() {
255
+ const editor = this.editor;
256
+ const model = editor.model;
257
+ const selection = model.document.selection;
258
+ // Registers the post-fixer to sync the asset ID with the model elements.
259
+ model.document.registerPostFixer(syncDataIdPostFixer(editor));
260
+ // Registers the post-fixer to remove the `ckboxLinkId` attribute from the model selection.
261
+ model.document.registerPostFixer(injectSelectionPostFixer(selection));
262
+ }
330
263
  }
331
-
332
- // A post-fixer that synchronizes the asset ID with the model element.
333
- //
334
- // @private
335
- // @param {module:core/editor/editor~Editor} editor
336
- // @returns {Function}
337
- function syncDataIdPostFixer( editor ) {
338
- return writer => {
339
- let changed = false;
340
-
341
- const model = editor.model;
342
- const ckboxCommand = editor.commands.get( 'ckbox' );
343
-
344
- // The ID from chosen assets are stored in the `CKBoxCommand#_chosenAssets`. If there is no command, it makes no sense to check
345
- // for changes in the model.
346
- if ( !ckboxCommand ) {
347
- return changed;
348
- }
349
-
350
- for ( const entry of model.document.differ.getChanges() ) {
351
- if ( entry.type !== 'insert' && entry.type !== 'attribute' ) {
352
- continue;
353
- }
354
-
355
- const range = entry.type === 'insert' ?
356
- new Range( entry.position, entry.position.getShiftedBy( entry.length ) ) :
357
- entry.range;
358
-
359
- const isLinkHrefAttributeRemoval = entry.type === 'attribute' &&
360
- entry.attributeKey === 'linkHref' &&
361
- entry.attributeNewValue === null;
362
-
363
- for ( const item of range.getItems() ) {
364
- // If the `linkHref` attribute has been removed, sync the change with the `ckboxLinkId` attribute.
365
- if ( isLinkHrefAttributeRemoval && item.hasAttribute( 'ckboxLinkId' ) ) {
366
- writer.removeAttribute( 'ckboxLinkId', item );
367
-
368
- changed = true;
369
-
370
- continue;
371
- }
372
-
373
- // Otherwise, the change concerns either a new model element or an attribute change. Try to find the assets for the modified
374
- // model element.
375
- const assets = findAssetsForItem( item, ckboxCommand._chosenAssets );
376
-
377
- for ( const asset of assets ) {
378
- const attributeName = asset.type === 'image' ? 'ckboxImageId' : 'ckboxLinkId';
379
-
380
- if ( asset.id === item.getAttribute( attributeName ) ) {
381
- continue;
382
- }
383
-
384
- writer.setAttribute( attributeName, asset.id, item );
385
-
386
- changed = true;
387
- }
388
- }
389
- }
390
-
391
- return changed;
392
- };
264
+ /**
265
+ * A post-fixer that synchronizes the asset ID with the model element.
266
+ */
267
+ function syncDataIdPostFixer(editor) {
268
+ return (writer) => {
269
+ let changed = false;
270
+ const model = editor.model;
271
+ const ckboxCommand = editor.commands.get('ckbox');
272
+ // The ID from chosen assets are stored in the `CKBoxCommand#_chosenAssets`. If there is no command, it makes no sense to check
273
+ // for changes in the model.
274
+ if (!ckboxCommand) {
275
+ return changed;
276
+ }
277
+ for (const entry of model.document.differ.getChanges()) {
278
+ if (entry.type !== 'insert' && entry.type !== 'attribute') {
279
+ continue;
280
+ }
281
+ const range = entry.type === 'insert' ?
282
+ new Range(entry.position, entry.position.getShiftedBy(entry.length)) :
283
+ entry.range;
284
+ const isLinkHrefAttributeRemoval = entry.type === 'attribute' &&
285
+ entry.attributeKey === 'linkHref' &&
286
+ entry.attributeNewValue === null;
287
+ for (const item of range.getItems()) {
288
+ // If the `linkHref` attribute has been removed, sync the change with the `ckboxLinkId` attribute.
289
+ if (isLinkHrefAttributeRemoval && item.hasAttribute('ckboxLinkId')) {
290
+ writer.removeAttribute('ckboxLinkId', item);
291
+ changed = true;
292
+ continue;
293
+ }
294
+ // Otherwise, the change concerns either a new model element or an attribute change. Try to find the assets for the modified
295
+ // model element.
296
+ const assets = findAssetsForItem(item, ckboxCommand._chosenAssets);
297
+ for (const asset of assets) {
298
+ const attributeName = asset.type === 'image' ? 'ckboxImageId' : 'ckboxLinkId';
299
+ if (asset.id === item.getAttribute(attributeName)) {
300
+ continue;
301
+ }
302
+ writer.setAttribute(attributeName, asset.id, item);
303
+ changed = true;
304
+ }
305
+ }
306
+ }
307
+ return changed;
308
+ };
393
309
  }
394
-
395
- // A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
396
- //
397
- // @private
398
- // @param {module:engine/model/selection~Selection} selection
399
- // @returns {Function}
400
- function injectSelectionPostFixer( selection ) {
401
- return writer => {
402
- const shouldRemoveLinkIdAttribute = !selection.hasAttribute( 'linkHref' ) && selection.hasAttribute( 'ckboxLinkId' );
403
-
404
- if ( shouldRemoveLinkIdAttribute ) {
405
- writer.removeSelectionAttribute( 'ckboxLinkId' );
406
- }
407
- };
310
+ /**
311
+ * A post-fixer that removes the `ckboxLinkId` from the selection if it does not represent a link anymore.
312
+ */
313
+ function injectSelectionPostFixer(selection) {
314
+ return (writer) => {
315
+ const shouldRemoveLinkIdAttribute = !selection.hasAttribute('linkHref') && selection.hasAttribute('ckboxLinkId');
316
+ if (shouldRemoveLinkIdAttribute) {
317
+ writer.removeSelectionAttribute('ckboxLinkId');
318
+ return true;
319
+ }
320
+ return false;
321
+ };
408
322
  }
409
-
410
- // Tries to find the asset that is associated with the model element by comparing the attributes:
411
- // - the image fallback URL with the `src` attribute for images,
412
- // - the link URL with the `href` attribute for links.
413
- //
414
- // 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
415
- // image asset).
416
- //
417
- // @private
418
- // @param {module:engine/model/item~Item} item
419
- // @param {Set.<module:ckbox/ckbox~CKBoxAssetDefinition>} assets
420
- // @returns {Array.<module:ckbox/ckbox~CKBoxAssetDefinition>}
421
- function findAssetsForItem( item, assets ) {
422
- const isImageElement = item.is( 'element', 'imageInline' ) || item.is( 'element', 'imageBlock' );
423
- const isLinkElement = item.hasAttribute( 'linkHref' );
424
-
425
- return [ ...assets ].filter( asset => {
426
- if ( asset.type === 'image' && isImageElement ) {
427
- return asset.attributes.imageFallbackUrl === item.getAttribute( 'src' );
428
- }
429
-
430
- if ( asset.type === 'link' && isLinkElement ) {
431
- return asset.attributes.linkHref === item.getAttribute( 'linkHref' );
432
- }
433
- } );
323
+ /**
324
+ * Tries to find the asset that is associated with the model element by comparing the attributes:
325
+ * - the image fallback URL with the `src` attribute for images,
326
+ * - the link URL with the `href` attribute for links.
327
+ *
328
+ * 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
329
+ * image asset).
330
+ */
331
+ function findAssetsForItem(item, assets) {
332
+ const isImageElement = item.is('element', 'imageInline') || item.is('element', 'imageBlock');
333
+ const isLinkElement = item.hasAttribute('linkHref');
334
+ return [...assets].filter(asset => {
335
+ if (asset.type === 'image' && isImageElement) {
336
+ return asset.attributes.imageFallbackUrl === item.getAttribute('src');
337
+ }
338
+ if (asset.type === 'link' && isLinkElement) {
339
+ return asset.attributes.linkHref === item.getAttribute('linkHref');
340
+ }
341
+ });
434
342
  }
435
-
436
- // Creates view link element with the requested ID.
437
- //
438
- // @private
439
- // @param {module:engine/view/downcastwriter~DowncastWriter} writer
440
- // @param {String} id
441
- // @returns {module:engine/view/attributeelement~AttributeElement}
442
- function createLinkElement( writer, id ) {
443
- // Priority equal 5 is needed to merge adjacent `<a>` elements together.
444
- const viewElement = writer.createAttributeElement( 'a', { 'data-ckbox-resource-id': id }, { priority: 5 } );
445
-
446
- writer.setCustomProperty( 'link', true, viewElement );
447
-
448
- return viewElement;
343
+ /**
344
+ * Creates view link element with the requested ID.
345
+ */
346
+ function createLinkElement(writer, id) {
347
+ // Priority equal 5 is needed to merge adjacent `<a>` elements together.
348
+ const viewElement = writer.createAttributeElement('a', { 'data-ckbox-resource-id': id }, { priority: 5 });
349
+ writer.setCustomProperty('link', true, viewElement);
350
+ return viewElement;
449
351
  }
450
-
451
- // Checks if the model element may have the `ckboxLinkId` attribute.
452
- //
453
- // @private
454
- // @param {module:engine/model/node~Node} node
455
- // @returns {Boolean}
456
- function shouldUpcastAttributeForNode( node ) {
457
- if ( node.is( '$text' ) ) {
458
- return true;
459
- }
460
-
461
- if ( node.is( 'element', 'imageInline' ) || node.is( 'element', 'imageBlock' ) ) {
462
- return true;
463
- }
464
-
465
- return false;
352
+ /**
353
+ * Checks if the model element may have the `ckboxLinkId` attribute.
354
+ */
355
+ function shouldUpcastAttributeForNode(node) {
356
+ if (node.is('$text')) {
357
+ return true;
358
+ }
359
+ if (node.is('element', 'imageInline') || node.is('element', 'imageBlock')) {
360
+ return true;
361
+ }
362
+ return false;
466
363
  }