@ckeditor/ckeditor5-image 27.1.0 → 29.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +3 -3
  3. package/build/image.js +1 -1
  4. package/build/translations/ar.js +1 -0
  5. package/build/translations/ast.js +1 -0
  6. package/build/translations/az.js +1 -0
  7. package/build/translations/bg.js +1 -0
  8. package/build/translations/cs.js +1 -0
  9. package/build/translations/da.js +1 -0
  10. package/build/translations/de-ch.js +1 -0
  11. package/build/translations/de.js +1 -0
  12. package/build/translations/el.js +1 -0
  13. package/build/translations/en-au.js +1 -0
  14. package/build/translations/en-gb.js +1 -0
  15. package/build/translations/eo.js +1 -0
  16. package/build/translations/es.js +1 -0
  17. package/build/translations/et.js +1 -0
  18. package/build/translations/eu.js +1 -0
  19. package/build/translations/fa.js +1 -0
  20. package/build/translations/fi.js +1 -0
  21. package/build/translations/fr.js +1 -0
  22. package/build/translations/gl.js +1 -0
  23. package/build/translations/he.js +1 -0
  24. package/build/translations/hi.js +1 -0
  25. package/build/translations/hr.js +1 -0
  26. package/build/translations/hu.js +1 -0
  27. package/build/translations/id.js +1 -0
  28. package/build/translations/it.js +1 -0
  29. package/build/translations/ja.js +1 -0
  30. package/build/translations/km.js +1 -0
  31. package/build/translations/kn.js +1 -0
  32. package/build/translations/ko.js +1 -0
  33. package/build/translations/ku.js +1 -0
  34. package/build/translations/lt.js +1 -0
  35. package/build/translations/lv.js +1 -0
  36. package/build/translations/nb.js +1 -0
  37. package/build/translations/ne.js +1 -0
  38. package/build/translations/nl.js +1 -0
  39. package/build/translations/no.js +1 -0
  40. package/build/translations/pl.js +1 -0
  41. package/build/translations/pt-br.js +1 -0
  42. package/build/translations/pt.js +1 -0
  43. package/build/translations/ro.js +1 -0
  44. package/build/translations/ru.js +1 -0
  45. package/build/translations/si.js +1 -0
  46. package/build/translations/sk.js +1 -0
  47. package/build/translations/sq.js +1 -0
  48. package/build/translations/sr-latn.js +1 -0
  49. package/build/translations/sr.js +1 -0
  50. package/build/translations/sv.js +1 -0
  51. package/build/translations/th.js +1 -0
  52. package/build/translations/tk.js +1 -0
  53. package/build/translations/tr.js +1 -0
  54. package/build/translations/ug.js +1 -0
  55. package/build/translations/uk.js +1 -0
  56. package/build/translations/vi.js +1 -0
  57. package/build/translations/zh-cn.js +1 -0
  58. package/build/translations/zh.js +1 -0
  59. package/ckeditor5-metadata.json +233 -0
  60. package/lang/contexts.json +3 -0
  61. package/lang/translations/ar.po +12 -0
  62. package/lang/translations/ast.po +12 -0
  63. package/lang/translations/az.po +12 -0
  64. package/lang/translations/bg.po +12 -0
  65. package/lang/translations/cs.po +12 -0
  66. package/lang/translations/da.po +12 -0
  67. package/lang/translations/de-ch.po +113 -0
  68. package/lang/translations/de.po +15 -3
  69. package/lang/translations/el.po +12 -0
  70. package/lang/translations/en-au.po +12 -0
  71. package/lang/translations/en-gb.po +12 -0
  72. package/lang/translations/en.po +12 -0
  73. package/lang/translations/eo.po +12 -0
  74. package/lang/translations/es.po +12 -0
  75. package/lang/translations/et.po +12 -0
  76. package/lang/translations/eu.po +12 -0
  77. package/lang/translations/fa.po +12 -0
  78. package/lang/translations/fi.po +12 -0
  79. package/lang/translations/fr.po +12 -0
  80. package/lang/translations/gl.po +12 -0
  81. package/lang/translations/he.po +12 -0
  82. package/lang/translations/hi.po +12 -0
  83. package/lang/translations/hr.po +12 -0
  84. package/lang/translations/hu.po +13 -1
  85. package/lang/translations/id.po +21 -9
  86. package/lang/translations/it.po +12 -0
  87. package/lang/translations/ja.po +12 -0
  88. package/lang/translations/km.po +12 -0
  89. package/lang/translations/kn.po +12 -0
  90. package/lang/translations/ko.po +12 -0
  91. package/lang/translations/ku.po +12 -0
  92. package/lang/translations/lt.po +12 -0
  93. package/lang/translations/lv.po +12 -0
  94. package/lang/translations/nb.po +12 -0
  95. package/lang/translations/ne.po +12 -0
  96. package/lang/translations/nl.po +12 -0
  97. package/lang/translations/no.po +12 -0
  98. package/lang/translations/pl.po +20 -8
  99. package/lang/translations/pt-br.po +12 -0
  100. package/lang/translations/pt.po +12 -0
  101. package/lang/translations/ro.po +21 -9
  102. package/lang/translations/ru.po +12 -0
  103. package/lang/translations/si.po +12 -0
  104. package/lang/translations/sk.po +12 -0
  105. package/lang/translations/sq.po +12 -0
  106. package/lang/translations/sr-latn.po +12 -0
  107. package/lang/translations/sr.po +12 -0
  108. package/lang/translations/sv.po +12 -0
  109. package/lang/translations/th.po +12 -0
  110. package/lang/translations/tk.po +12 -0
  111. package/lang/translations/tr.po +12 -0
  112. package/lang/translations/ug.po +12 -0
  113. package/lang/translations/uk.po +12 -0
  114. package/lang/translations/vi.po +12 -0
  115. package/lang/translations/zh-cn.po +12 -0
  116. package/lang/translations/zh.po +12 -0
  117. package/package.json +36 -29
  118. package/src/autoimage.js +6 -4
  119. package/src/image/converters.js +192 -13
  120. package/src/image/imageblockediting.js +179 -0
  121. package/src/image/imageediting.js +13 -70
  122. package/src/image/imageinlineediting.js +204 -0
  123. package/src/image/imagetypecommand.js +105 -0
  124. package/src/image/insertimagecommand.js +77 -10
  125. package/src/image/ui/utils.js +3 -3
  126. package/src/image/utils.js +70 -121
  127. package/src/image.js +7 -19
  128. package/src/imageblock.js +46 -0
  129. package/src/imagecaption/imagecaptionediting.js +202 -230
  130. package/src/imagecaption/imagecaptionui.js +78 -0
  131. package/src/imagecaption/toggleimagecaptioncommand.js +165 -0
  132. package/src/imagecaption/utils.js +25 -40
  133. package/src/imagecaption.js +3 -2
  134. package/src/imageinline.js +46 -0
  135. package/src/imageinsert/imageinsertui.js +5 -6
  136. package/src/imageinsert.js +16 -4
  137. package/src/imageresize/imageresizebuttons.js +1 -1
  138. package/src/imageresize/imageresizeediting.js +21 -8
  139. package/src/imageresize/imageresizehandles.js +30 -8
  140. package/src/imageresize/resizeimagecommand.js +8 -5
  141. package/src/imagestyle/converters.js +25 -17
  142. package/src/imagestyle/imagestylecommand.js +73 -33
  143. package/src/imagestyle/imagestyleediting.js +113 -52
  144. package/src/imagestyle/imagestyleui.js +197 -31
  145. package/src/imagestyle/utils.js +300 -85
  146. package/src/imagestyle.js +218 -47
  147. package/src/imagetextalternative/imagetextalternativecommand.js +10 -7
  148. package/src/imagetextalternative/imagetextalternativeediting.js +9 -1
  149. package/src/imagetextalternative/imagetextalternativeui.js +2 -2
  150. package/src/imagetextalternative.js +1 -1
  151. package/src/imagetoolbar.js +33 -11
  152. package/src/imageupload/imageuploadediting.js +91 -31
  153. package/src/imageupload/imageuploadprogress.js +17 -9
  154. package/src/imageupload/imageuploadui.js +1 -1
  155. package/src/imageupload/uploadimagecommand.js +50 -24
  156. package/src/imageupload/utils.js +3 -2
  157. package/src/imageupload.js +1 -1
  158. package/src/imageutils.js +342 -0
  159. package/src/index.js +22 -47
  160. package/src/pictureediting.js +149 -0
  161. package/theme/image.css +101 -21
  162. package/theme/imagecaption.css +24 -2
  163. package/theme/imageresize.css +11 -0
  164. package/theme/imagestyle.css +76 -0
  165. package/theme/imageuploadicon.css +8 -2
  166. package/theme/imageuploadprogress.css +12 -8
  167. package/build/image.js.map +0 -1
@@ -16,16 +16,16 @@ import { ClipboardPipeline } from 'ckeditor5/src/clipboard';
16
16
  import { FileRepository } from 'ckeditor5/src/upload';
17
17
  import { env } from 'ckeditor5/src/utils';
18
18
 
19
+ import ImageUtils from '../imageutils';
19
20
  import UploadImageCommand from './uploadimagecommand';
20
21
  import { fetchLocalImage, isLocalImage } from '../../src/imageupload/utils';
21
22
  import { createImageTypeRegExp } from './utils';
22
- import { getViewImgFromWidget } from '../image/utils';
23
23
 
24
24
  /**
25
25
  * The editing part of the image upload feature. It registers the `'uploadImage'` command
26
- * and `imageUpload` command as an aliased name.
26
+ * and the `imageUpload` command as an aliased name.
27
27
  *
28
- * When an image is uploaded it fires the {@link ~ImageUploadEditing#event:uploadComplete `uploadComplete` event}
28
+ * When an image is uploaded, it fires the {@link ~ImageUploadEditing#event:uploadComplete `uploadComplete`} event
29
29
  * that allows adding custom attributes to the {@link module:engine/model/element~Element image element}.
30
30
  *
31
31
  * @extends module:core/plugin~Plugin
@@ -35,7 +35,7 @@ export default class ImageUploadEditing extends Plugin {
35
35
  * @inheritDoc
36
36
  */
37
37
  static get requires() {
38
- return [ FileRepository, Notification, ClipboardPipeline ];
38
+ return [ FileRepository, Notification, ClipboardPipeline, ImageUtils ];
39
39
  }
40
40
 
41
41
  static get pluginName() {
@@ -53,6 +53,21 @@ export default class ImageUploadEditing extends Plugin {
53
53
  types: [ 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff' ]
54
54
  }
55
55
  } );
56
+
57
+ /**
58
+ * An internal mapping of {@link module:upload/filerepository~FileLoader#id file loader UIDs} and
59
+ * model elements during the upload.
60
+ *
61
+ * Model element of the uploaded image can change, for instance, when {@link module:image/image/imagetypecommand~ImageTypeCommand}
62
+ * is executed as a result of adding caption or changing image style. As a result, the upload logic must keep track of the model
63
+ * element (reference) and resolve the upload for the correct model element (instead of the one that landed in the `$graveyard`
64
+ * after image type changed).
65
+ *
66
+ * @private
67
+ * @readonly
68
+ * @member {Map.<String,module:engine/model/element~Element>}
69
+ */
70
+ this._uploadImageElements = new Map();
56
71
  }
57
72
 
58
73
  /**
@@ -61,17 +76,10 @@ export default class ImageUploadEditing extends Plugin {
61
76
  init() {
62
77
  const editor = this.editor;
63
78
  const doc = editor.model.document;
64
- const schema = editor.model.schema;
65
79
  const conversion = editor.conversion;
66
80
  const fileRepository = editor.plugins.get( FileRepository );
67
-
81
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
68
82
  const imageTypes = createImageTypeRegExp( editor.config.get( 'image.upload.types' ) );
69
-
70
- // Setup schema to allow uploadId and uploadStatus for images.
71
- schema.extend( 'image', {
72
- allowAttributes: [ 'uploadId', 'uploadStatus' ]
73
- } );
74
-
75
83
  const uploadImageCommand = new UploadImageCommand( editor );
76
84
 
77
85
  // Register `uploadImage` command and add `imageUpload` command as an alias for backward compatibility.
@@ -133,7 +141,7 @@ export default class ImageUploadEditing extends Plugin {
133
141
  // (see Document#change listener below).
134
142
  this.listenTo( editor.plugins.get( 'ClipboardPipeline' ), 'inputTransformation', ( evt, data ) => {
135
143
  const fetchableImages = Array.from( editor.editing.view.createRangeIn( data.content ) )
136
- .filter( value => isLocalImage( value.item ) && !value.item.getAttribute( 'uploadProcessed' ) )
144
+ .filter( value => isLocalImage( imageUtils, value.item ) && !value.item.getAttribute( 'uploadProcessed' ) )
137
145
  .map( value => { return { promise: fetchLocalImage( value.item ), imageElement: value.item }; } );
138
146
 
139
147
  if ( !fetchableImages.length ) {
@@ -162,16 +170,20 @@ export default class ImageUploadEditing extends Plugin {
162
170
 
163
171
  // Upload placeholder images that appeared in the model.
164
172
  doc.on( 'change', () => {
165
- const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } );
173
+ // Note: Reversing changes to start with insertions and only then handle removals. If it was the other way around,
174
+ // loaders for **all** images that land in the $graveyard would abort while in fact only those that were **not** replaced
175
+ // by other images should be aborted.
176
+ const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } ).reverse();
177
+ const insertedImagesIds = new Set();
166
178
 
167
179
  for ( const entry of changes ) {
168
180
  if ( entry.type == 'insert' && entry.name != '$text' ) {
169
181
  const item = entry.position.nodeAfter;
170
- const isInGraveyard = entry.position.root.rootName == '$graveyard';
182
+ const isInsertedInGraveyard = entry.position.root.rootName == '$graveyard';
171
183
 
172
- for ( const image of getImagesFromChangeItem( editor, item ) ) {
184
+ for ( const imageElement of getImagesFromChangeItem( editor, item ) ) {
173
185
  // Check if the image element still has upload id.
174
- const uploadId = image.getAttribute( 'uploadId' );
186
+ const uploadId = imageElement.getAttribute( 'uploadId' );
175
187
 
176
188
  if ( !uploadId ) {
177
189
  continue;
@@ -184,12 +196,28 @@ export default class ImageUploadEditing extends Plugin {
184
196
  continue;
185
197
  }
186
198
 
187
- if ( isInGraveyard ) {
188
- // If the image was inserted to the graveyard - abort the loading process.
189
- loader.abort();
190
- } else if ( loader.status == 'idle' ) {
191
- // If the image was inserted into content and has not been loaded yet, start loading it.
192
- this._readAndUpload( loader, image );
199
+ if ( isInsertedInGraveyard ) {
200
+ // If the image was inserted to the graveyard for good (**not** replaced by another image),
201
+ // only then abort the loading process.
202
+ if ( !insertedImagesIds.has( uploadId ) ) {
203
+ loader.abort();
204
+ }
205
+ } else {
206
+ // Remember the upload id of the inserted image. If it acted as a replacement for another
207
+ // image (which landed in the $graveyard), the related loader will not be aborted because
208
+ // this is still the same image upload.
209
+ insertedImagesIds.add( uploadId );
210
+
211
+ // Keep the mapping between the upload ID and the image model element so the upload
212
+ // can later resolve in the context of the correct model element. The model element could
213
+ // change for the same upload if one image was replaced by another (e.g. image type was changed),
214
+ // so this may also replace an existing mapping.
215
+ this._uploadImageElements.set( uploadId, imageElement );
216
+
217
+ if ( loader.status == 'idle' ) {
218
+ // If the image was inserted into content and has not been loaded yet, start loading it.
219
+ this._readAndUpload( loader );
220
+ }
193
221
  }
194
222
  }
195
223
  }
@@ -207,6 +235,28 @@ export default class ImageUploadEditing extends Plugin {
207
235
  }, { priority: 'low' } );
208
236
  }
209
237
 
238
+ /**
239
+ * @inheritDoc
240
+ */
241
+ afterInit() {
242
+ const schema = this.editor.model.schema;
243
+
244
+ // Setup schema to allow uploadId and uploadStatus for images.
245
+ // Wait for ImageBlockEditing or ImageInlineEditing to register their elements first,
246
+ // that's why doing this in afterInit() instead of init().
247
+ if ( this.editor.plugins.has( 'ImageBlockEditing' ) ) {
248
+ schema.extend( 'imageBlock', {
249
+ allowAttributes: [ 'uploadId', 'uploadStatus' ]
250
+ } );
251
+ }
252
+
253
+ if ( this.editor.plugins.has( 'ImageInlineEditing' ) ) {
254
+ schema.extend( 'imageInline', {
255
+ allowAttributes: [ 'uploadId', 'uploadStatus' ]
256
+ } );
257
+ }
258
+ }
259
+
210
260
  /**
211
261
  * Reads and uploads an image.
212
262
  *
@@ -216,30 +266,32 @@ export default class ImageUploadEditing extends Plugin {
216
266
  *
217
267
  * @protected
218
268
  * @param {module:upload/filerepository~FileLoader} loader
219
- * @param {module:engine/model/element~Element} imageElement
220
269
  * @returns {Promise}
221
270
  */
222
- _readAndUpload( loader, imageElement ) {
271
+ _readAndUpload( loader ) {
223
272
  const editor = this.editor;
224
273
  const model = editor.model;
225
274
  const t = editor.locale.t;
226
275
  const fileRepository = editor.plugins.get( FileRepository );
227
276
  const notification = editor.plugins.get( Notification );
277
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
278
+ const imageUploadElements = this._uploadImageElements;
228
279
 
229
280
  model.enqueueChange( 'transparent', writer => {
230
- writer.setAttribute( 'uploadStatus', 'reading', imageElement );
281
+ writer.setAttribute( 'uploadStatus', 'reading', imageUploadElements.get( loader.id ) );
231
282
  } );
232
283
 
233
284
  return loader.read()
234
285
  .then( () => {
235
286
  const promise = loader.upload();
287
+ const imageElement = imageUploadElements.get( loader.id );
236
288
 
237
289
  // Force re–paint in Safari. Without it, the image will display with a wrong size.
238
290
  // https://github.com/ckeditor/ckeditor5/issues/1975
239
291
  /* istanbul ignore next */
240
292
  if ( env.isSafari ) {
241
293
  const viewFigure = editor.editing.mapper.toViewElement( imageElement );
242
- const viewImg = getViewImgFromWidget( viewFigure );
294
+ const viewImg = imageUtils.findViewImgElement( viewFigure );
243
295
 
244
296
  editor.editing.view.once( 'render', () => {
245
297
  // Early returns just to be safe. There might be some code ran
@@ -273,6 +325,8 @@ export default class ImageUploadEditing extends Plugin {
273
325
  } )
274
326
  .then( data => {
275
327
  model.enqueueChange( 'transparent', writer => {
328
+ const imageElement = imageUploadElements.get( loader.id );
329
+
276
330
  writer.setAttribute( 'uploadStatus', 'complete', imageElement );
277
331
 
278
332
  /**
@@ -323,18 +377,22 @@ export default class ImageUploadEditing extends Plugin {
323
377
  } );
324
378
  }
325
379
 
326
- clean();
327
-
328
380
  // Permanently remove image from insertion batch.
329
381
  model.enqueueChange( 'transparent', writer => {
330
- writer.remove( imageElement );
382
+ writer.remove( imageUploadElements.get( loader.id ) );
331
383
  } );
384
+
385
+ clean();
332
386
  } );
333
387
 
334
388
  function clean() {
335
389
  model.enqueueChange( 'transparent', writer => {
390
+ const imageElement = imageUploadElements.get( loader.id );
391
+
336
392
  writer.removeAttribute( 'uploadId', imageElement );
337
393
  writer.removeAttribute( 'uploadStatus', imageElement );
394
+
395
+ imageUploadElements.delete( loader.id );
338
396
  } );
339
397
 
340
398
  fileRepository.destroyLoader( loader );
@@ -389,7 +447,9 @@ export function isHtmlIncluded( dataTransfer ) {
389
447
  }
390
448
 
391
449
  function getImagesFromChangeItem( editor, item ) {
450
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
451
+
392
452
  return Array.from( editor.model.createRangeOn( item ) )
393
- .filter( value => value.item.is( 'element', 'image' ) )
453
+ .filter( value => imageUtils.isImage( value.item ) )
394
454
  .map( value => value.item );
395
455
  }
@@ -11,7 +11,6 @@
11
11
 
12
12
  import { Plugin } from 'ckeditor5/src/core';
13
13
  import { FileRepository } from 'ckeditor5/src/upload';
14
- import { getViewImgFromWidget } from '../image/utils';
15
14
 
16
15
  import uploadingPlaceholder from '../../theme/icons/image_placeholder.svg';
17
16
 
@@ -55,7 +54,13 @@ export default class ImageUploadProgress extends Plugin {
55
54
  const editor = this.editor;
56
55
 
57
56
  // Upload status change - update image's view according to that status.
58
- editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:image', ( ...args ) => this.uploadStatusChange( ...args ) );
57
+ if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
58
+ editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:imageBlock', ( ...args ) => this.uploadStatusChange( ...args ) );
59
+ }
60
+
61
+ if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
62
+ editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:imageInline', ( ...args ) => this.uploadStatusChange( ...args ) );
63
+ }
59
64
  }
60
65
 
61
66
  /**
@@ -74,6 +79,7 @@ export default class ImageUploadProgress extends Plugin {
74
79
  return;
75
80
  }
76
81
 
82
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
77
83
  const fileRepository = editor.plugins.get( FileRepository );
78
84
  const status = uploadId ? data.attributeNewValue : null;
79
85
  const placeholder = this.placeholder;
@@ -84,7 +90,7 @@ export default class ImageUploadProgress extends Plugin {
84
90
  // Start "appearing" effect and show placeholder with infinite progress bar on the top
85
91
  // while image is read from disk.
86
92
  _startAppearEffect( viewFigure, viewWriter );
87
- _showPlaceholder( placeholder, viewFigure, viewWriter );
93
+ _showPlaceholder( imageUtils, placeholder, viewFigure, viewWriter );
88
94
 
89
95
  return;
90
96
  }
@@ -100,12 +106,12 @@ export default class ImageUploadProgress extends Plugin {
100
106
  // There is no loader associated with uploadId - this means that image came from external changes.
101
107
  // In such cases we still want to show the placeholder until image is fully uploaded.
102
108
  // Show placeholder if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
103
- _showPlaceholder( placeholder, viewFigure, viewWriter );
109
+ _showPlaceholder( imageUtils, placeholder, viewFigure, viewWriter );
104
110
  } else {
105
111
  // Hide placeholder and initialize progress bar showing upload progress.
106
112
  _hidePlaceholder( viewFigure, viewWriter );
107
113
  _showProgressBar( viewFigure, viewWriter, loader, editor.editing.view );
108
- _displayLocalImage( viewFigure, viewWriter, loader );
114
+ _displayLocalImage( imageUtils, viewFigure, viewWriter, loader );
109
115
  }
110
116
 
111
117
  return;
@@ -142,15 +148,16 @@ function _stopAppearEffect( viewFigure, writer ) {
142
148
 
143
149
  // Shows placeholder together with infinite progress bar on given image figure.
144
150
  //
151
+ // @param {module:image/imageutils~ImageUtils} imageUtils
145
152
  // @param {String} Data-uri with a svg placeholder.
146
153
  // @param {module:engine/view/containerelement~ContainerElement} viewFigure
147
154
  // @param {module:engine/view/downcastwriter~DowncastWriter} writer
148
- function _showPlaceholder( placeholder, viewFigure, writer ) {
155
+ function _showPlaceholder( imageUtils, placeholder, viewFigure, writer ) {
149
156
  if ( !viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
150
157
  writer.addClass( 'ck-image-upload-placeholder', viewFigure );
151
158
  }
152
159
 
153
- const viewImg = getViewImgFromWidget( viewFigure );
160
+ const viewImg = imageUtils.findViewImgElement( viewFigure );
154
161
 
155
162
  if ( viewImg.getAttribute( 'src' ) !== placeholder ) {
156
163
  writer.setAttribute( 'src', placeholder, viewImg );
@@ -272,12 +279,13 @@ function _removeUIElement( viewFigure, writer, uniqueProperty ) {
272
279
 
273
280
  // Displays local data from file loader.
274
281
  //
282
+ // @param {module:image/imageutils~ImageUtils} imageUtils
275
283
  // @param {module:engine/view/element~Element} imageFigure
276
284
  // @param {module:engine/view/downcastwriter~DowncastWriter} writer
277
285
  // @param {module:upload/filerepository~FileLoader} loader
278
- function _displayLocalImage( viewFigure, writer, loader ) {
286
+ function _displayLocalImage( imageUtils, viewFigure, writer, loader ) {
279
287
  if ( loader.data ) {
280
- const viewImg = getViewImgFromWidget( viewFigure );
288
+ const viewImg = imageUtils.findViewImgElement( viewFigure );
281
289
 
282
290
  writer.setAttribute( 'src', loader.data, viewImg );
283
291
  }
@@ -14,7 +14,7 @@ import { createImageTypeRegExp } from './utils';
14
14
  /**
15
15
  * The image upload button plugin.
16
16
  *
17
- * For a detailed overview, check the {@glink features/image-upload/image-upload Image upload feature} documentation.
17
+ * For a detailed overview, check the {@glink features/images/image-upload/image-upload Image upload feature} documentation.
18
18
  *
19
19
  * Adds the `'uploadImage'` button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}
20
20
  * and also the `imageUpload` button as an alias for backward compatibility.
@@ -7,8 +7,6 @@ import { FileRepository } from 'ckeditor5/src/upload';
7
7
  import { Command } from 'ckeditor5/src/core';
8
8
  import { toArray } from 'ckeditor5/src/utils';
9
9
 
10
- import { insertImage, isImageAllowed } from '../image/utils';
11
-
12
10
  /**
13
11
  * @module image/imageupload/uploadimagecommand
14
12
  */
@@ -20,7 +18,7 @@ import { insertImage, isImageAllowed } from '../image/utils';
20
18
  * and it is also available via aliased `imageUpload` name.
21
19
  *
22
20
  * In order to upload an image at the current selection position
23
- * (according to the {@link module:widget/utils~findOptimalInsertionPosition} algorithm),
21
+ * (according to the {@link module:widget/utils~findOptimalInsertionRange} algorithm),
24
22
  * execute the command and pass the native image file instance:
25
23
  *
26
24
  * this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
@@ -47,10 +45,12 @@ export default class UploadImageCommand extends Command {
47
45
  * @inheritDoc
48
46
  */
49
47
  refresh() {
50
- const imageElement = this.editor.model.document.selection.getSelectedElement();
51
- const isImage = imageElement && imageElement.name === 'image' || false;
48
+ const editor = this.editor;
49
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
50
+ const selectedElement = editor.model.document.selection.getSelectedElement();
52
51
 
53
- this.isEnabled = isImageAllowed( this.editor.model ) || isImage;
52
+ // TODO: This needs refactoring.
53
+ this.isEnabled = imageUtils.isImageAllowed() || imageUtils.isImage( selectedElement );
54
54
  }
55
55
 
56
56
  /**
@@ -61,28 +61,54 @@ export default class UploadImageCommand extends Command {
61
61
  * @param {File|Array.<File>} options.file The image file or an array of image files to upload.
62
62
  */
63
63
  execute( options ) {
64
- const editor = this.editor;
65
- const model = editor.model;
64
+ const files = toArray( options.file );
65
+ const selection = this.editor.model.document.selection;
66
+ const imageUtils = this.editor.plugins.get( 'ImageUtils' );
66
67
 
67
- const fileRepository = editor.plugins.get( FileRepository );
68
+ // In case of multiple files, each file (starting from the 2nd) will be inserted at a position that
69
+ // follows the previous one. That will move the selection and, to stay on the safe side and make sure
70
+ // all images inherit the same selection attributes, they are collected beforehand.
71
+ //
72
+ // Applying these attributes ensures, for instance, that inserting an (inline) image into a link does
73
+ // not split that link but preserves its continuity.
74
+ //
75
+ // Note: Selection attributes that do not make sense for images will be filtered out by insertImage() anyway.
76
+ const selectionAttributes = Object.fromEntries( selection.getAttributes() );
68
77
 
69
- for ( const file of toArray( options.file ) ) {
70
- uploadImage( model, fileRepository, file );
71
- }
72
- }
73
- }
78
+ files.forEach( ( file, index ) => {
79
+ const selectedElement = selection.getSelectedElement();
74
80
 
75
- // Handles uploading single file.
76
- //
77
- // @param {module:engine/model/model~Model} model
78
- // @param {File} file
79
- function uploadImage( model, fileRepository, file ) {
80
- const loader = fileRepository.createLoader( file );
81
+ // Inserting of an inline image replace the selected element and make a selection on the inserted image.
82
+ // Therefore inserting multiple inline images requires creating position after each element.
83
+ if ( index && selectedElement && imageUtils.isImage( selectedElement ) ) {
84
+ const position = this.editor.model.createPositionAfter( selectedElement );
81
85
 
82
- // Do not throw when upload adapter is not set. FileRepository will log an error anyway.
83
- if ( !loader ) {
84
- return;
86
+ this._uploadImage( file, selectionAttributes, position );
87
+ } else {
88
+ this._uploadImage( file, selectionAttributes );
89
+ }
90
+ } );
85
91
  }
86
92
 
87
- insertImage( model, { uploadId: loader.id } );
93
+ /**
94
+ * Handles uploading single file.
95
+ *
96
+ * @private
97
+ * @param {File} file
98
+ * @param {Object} attributes
99
+ * @param {module:engine/model/position~Position} position
100
+ */
101
+ _uploadImage( file, attributes, position ) {
102
+ const editor = this.editor;
103
+ const fileRepository = editor.plugins.get( FileRepository );
104
+ const loader = fileRepository.createLoader( file );
105
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
106
+
107
+ // Do not throw when upload adapter is not set. FileRepository will log an error anyway.
108
+ if ( !loader ) {
109
+ return;
110
+ }
111
+
112
+ imageUtils.insertImage( { ...attributes, uploadId: loader.id }, position );
113
+ }
88
114
  }
@@ -64,11 +64,12 @@ export function fetchLocalImage( image ) {
64
64
  /**
65
65
  * Checks whether a given node is an image element with a local source (Base64 or blob).
66
66
  *
67
+ * @param {module:image/imageutils~ImageUtils} imageUtils
67
68
  * @param {module:engine/view/node~Node} node The node to check.
68
69
  * @returns {Boolean}
69
70
  */
70
- export function isLocalImage( node ) {
71
- if ( !node.is( 'element', 'img' ) || !node.getAttribute( 'src' ) ) {
71
+ export function isLocalImage( imageUtils, node ) {
72
+ if ( !imageUtils.isInlineImageView( node ) || !node.getAttribute( 'src' ) ) {
72
73
  return false;
73
74
  }
74
75
 
@@ -15,7 +15,7 @@ import ImageUploadEditing from './imageupload/imageuploadediting';
15
15
  /**
16
16
  * The image upload plugin.
17
17
  *
18
- * For a detailed overview, check the {@glink features/image-upload/image-upload image upload feature} documentation.
18
+ * For a detailed overview, check the {@glink features/images/image-upload/image-upload image upload feature} documentation.
19
19
  *
20
20
  * This plugin does not do anything directly, but it loads a set of specific plugins to enable image uploading:
21
21
  *