@ckeditor/ckeditor5-image 28.0.0 → 30.0.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 +12 -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 +14 -2
  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 +9 -4
  119. package/src/image/converters.js +191 -15
  120. package/src/image/imageblockediting.js +182 -0
  121. package/src/image/imageediting.js +13 -70
  122. package/src/image/imageinlineediting.js +207 -0
  123. package/src/image/imagetypecommand.js +105 -0
  124. package/src/image/insertimagecommand.js +77 -10
  125. package/src/image/ui/utils.js +5 -4
  126. package/src/image/utils.js +65 -121
  127. package/src/image.js +7 -19
  128. package/src/imageblock.js +46 -0
  129. package/src/imagecaption/imagecaptionediting.js +183 -227
  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 +26 -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 +90 -30
  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/pictureediting.js +149 -0
  160. package/theme/image.css +101 -21
  161. package/theme/imagecaption.css +24 -2
  162. package/theme/imageresize.css +11 -0
  163. package/theme/imagestyle.css +76 -0
  164. package/theme/imageuploadicon.css +8 -2
  165. package/theme/imageuploadprogress.css +12 -8
  166. package/CHANGELOG.md +0 -423
  167. package/build/image.js.map +0 -1
@@ -8,18 +8,31 @@
8
8
  */
9
9
 
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
- import { isImage } from '../image/utils';
12
- import { captionElementCreator, getCaptionFromImage, matchImageCaption } from './utils';
11
+ import { Element, enablePlaceholder } from 'ckeditor5/src/engine';
12
+ import { toWidgetEditable } from 'ckeditor5/src/widget';
13
+
14
+ import ToggleImageCaptionCommand from './toggleimagecaptioncommand';
15
+
16
+ import ImageUtils from '../imageutils';
17
+ import { getCaptionFromImageModelElement, matchImageCaptionViewElement } from './utils';
13
18
 
14
19
  /**
15
- * The image caption engine plugin.
20
+ * The image caption engine plugin. It is responsible for:
16
21
  *
17
- * It registers proper converters. It takes care of adding a caption element if the image without it is inserted
18
- * to the model document.
22
+ * * registering converters for the caption element,
23
+ * * registering converters for the caption model attribute,
24
+ * * registering the {@link module:image/imagecaption/toggleimagecaptioncommand~ToggleImageCaptionCommand `toggleImageCaption`} command.
19
25
  *
20
26
  * @extends module:core/plugin~Plugin
21
27
  */
22
28
  export default class ImageCaptionEditing extends Plugin {
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ static get requires() {
33
+ return [ ImageUtils ];
34
+ }
35
+
23
36
  /**
24
37
  * @inheritDoc
25
38
  */
@@ -30,286 +43,229 @@ export default class ImageCaptionEditing extends Plugin {
30
43
  /**
31
44
  * @inheritDoc
32
45
  */
33
- init() {
34
- const editor = this.editor;
35
- const view = editor.editing.view;
36
- const schema = editor.model.schema;
37
- const data = editor.data;
38
- const editing = editor.editing;
39
- const t = editor.t;
46
+ constructor( editor ) {
47
+ super( editor );
40
48
 
41
49
  /**
42
- * The last selected caption editable.
43
- * It is used for hiding the editable when it is empty and the image widget is no longer selected.
50
+ * A map that keeps saved JSONified image captions and image model elements they are
51
+ * associated with.
52
+ *
53
+ * To learn more about this system, see {@link #_saveCaption}.
44
54
  *
45
- * @private
46
- * @member {module:engine/view/editableelement~EditableElement} #_lastSelectedCaption
55
+ * @member {WeakMap.<module:engine/model/element~Element,Object>}
47
56
  */
57
+ this._savedCaptionsMap = new WeakMap();
58
+ }
59
+
60
+ /**
61
+ * @inheritDoc
62
+ */
63
+ init() {
64
+ const editor = this.editor;
65
+ const schema = editor.model.schema;
48
66
 
49
67
  // Schema configuration.
50
68
  if ( !schema.isRegistered( 'caption' ) ) {
51
69
  schema.register( 'caption', {
52
- allowIn: 'image',
70
+ allowIn: 'imageBlock',
53
71
  allowContentOf: '$block',
54
72
  isLimit: true
55
73
  } );
56
74
  } else {
57
75
  schema.extend( 'caption', {
58
- allowIn: 'image'
76
+ allowIn: 'imageBlock'
59
77
  } );
60
78
  }
61
79
 
62
- // Add caption element to each image inserted without it.
63
- editor.model.document.registerPostFixer( writer => this._insertMissingModelCaptionElement( writer ) );
80
+ editor.commands.add( 'toggleImageCaption', new ToggleImageCaptionCommand( this.editor ) );
64
81
 
65
- // View to model converter for the data pipeline.
82
+ this._setupConversion();
83
+ this._setupImageTypeCommandsIntegration();
84
+ }
85
+
86
+ /**
87
+ * Configures conversion pipelines to support upcasting and downcasting
88
+ * image captions.
89
+ *
90
+ * @private
91
+ */
92
+ _setupConversion() {
93
+ const editor = this.editor;
94
+ const view = editor.editing.view;
95
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
96
+ const t = editor.t;
97
+
98
+ // View -> model converter for the data pipeline.
66
99
  editor.conversion.for( 'upcast' ).elementToElement( {
67
- view: matchImageCaption,
100
+ view: element => matchImageCaptionViewElement( imageUtils, element ),
68
101
  model: 'caption'
69
102
  } );
70
103
 
71
- // Model to view converter for the data pipeline.
72
- const createCaptionForData = writer => writer.createContainerElement( 'figcaption' );
73
- data.downcastDispatcher.on( 'insert:caption', captionModelToView( createCaptionForData, false ) );
104
+ // Model -> view converter for the data pipeline.
105
+ editor.conversion.for( 'dataDowncast' ).elementToElement( {
106
+ model: 'caption',
107
+ view: ( modelElement, { writer } ) => {
108
+ if ( !imageUtils.isBlockImage( modelElement.parent ) ) {
109
+ return null;
110
+ }
74
111
 
75
- // Model to view converter for the editing pipeline.
76
- const createCaptionForEditing = captionElementCreator( view, t( 'Enter image caption' ) );
77
- editing.downcastDispatcher.on( 'insert:caption', captionModelToView( createCaptionForEditing ) );
112
+ return writer.createContainerElement( 'figcaption' );
113
+ }
114
+ } );
78
115
 
79
- // Always show caption in view when something is inserted in model.
80
- editing.downcastDispatcher.on(
81
- 'insert',
82
- this._fixCaptionVisibility( data => data.item ),
83
- { priority: 'high' }
84
- );
116
+ // Model -> view converter for the editing pipeline.
117
+ editor.conversion.for( 'editingDowncast' ).elementToElement( {
118
+ model: 'caption',
119
+ view: ( modelElement, { writer } ) => {
120
+ if ( !imageUtils.isBlockImage( modelElement.parent ) ) {
121
+ return null;
122
+ }
123
+
124
+ const figcaptionElement = writer.createEditableElement( 'figcaption' );
125
+ writer.setCustomProperty( 'imageCaption', true, figcaptionElement );
126
+
127
+ enablePlaceholder( {
128
+ view,
129
+ element: figcaptionElement,
130
+ text: t( 'Enter image caption' ),
131
+ keepOnFocus: true
132
+ } );
85
133
 
86
- // Hide caption when everything is removed from it.
87
- editing.downcastDispatcher.on( 'remove', this._fixCaptionVisibility( data => data.position.parent ), { priority: 'high' } );
134
+ return toWidgetEditable( figcaptionElement, writer );
135
+ }
136
+ } );
88
137
 
89
- // Update caption visibility on view in post fixer.
90
- view.document.registerPostFixer( writer => this._updateCaptionVisibility( writer ) );
138
+ editor.editing.mapper.on( 'modelToViewPosition', mapModelPositionToView( view ) );
139
+ editor.data.mapper.on( 'modelToViewPosition', mapModelPositionToView( view ) );
91
140
  }
92
141
 
93
142
  /**
94
- * Updates the view before each rendering, making sure that empty captions (so unnecessary ones) are hidden
95
- * and then visible when the image is selected.
143
+ * Integrates with {@link module:image/image/imagetypecommand~ImageTypeCommand image type commands}
144
+ * to make sure the caption is preserved when the type of an image changes so it can be restored
145
+ * in the future if the user decides they want their caption back.
96
146
  *
97
147
  * @private
98
- * @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
99
- * @returns {Boolean} Returns `true` when the view is updated.
100
148
  */
101
- _updateCaptionVisibility( viewWriter ) {
102
- const mapper = this.editor.editing.mapper;
103
- const lastCaption = this._lastSelectedCaption;
104
- let viewCaption;
105
-
106
- // If whole image is selected.
107
- const modelSelection = this.editor.model.document.selection;
108
- const selectedElement = modelSelection.getSelectedElement();
109
-
110
- if ( selectedElement && selectedElement.is( 'element', 'image' ) ) {
111
- const modelCaption = getCaptionFromImage( selectedElement );
112
- viewCaption = mapper.toViewElement( modelCaption );
113
- }
149
+ _setupImageTypeCommandsIntegration() {
150
+ const editor = this.editor;
151
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
152
+ const imageTypeInlineCommand = editor.commands.get( 'imageTypeInline' );
153
+ const imageTypeBlockCommand = editor.commands.get( 'imageTypeBlock' );
114
154
 
115
- // If selection is placed inside caption.
116
- const position = modelSelection.getFirstPosition();
117
- const modelCaption = getParentCaption( position.parent );
155
+ const handleImageTypeChange = evt => {
156
+ // The image type command execution can be unsuccessful.
157
+ if ( !evt.return ) {
158
+ return;
159
+ }
118
160
 
119
- if ( modelCaption ) {
120
- viewCaption = mapper.toViewElement( modelCaption );
121
- }
161
+ const { oldElement, newElement } = evt.return;
162
+
163
+ /* istanbul ignore if: paranoid check */
164
+ if ( !oldElement ) {
165
+ return;
166
+ }
122
167
 
123
- // Is currently any caption selected?
124
- if ( viewCaption && !this.editor.isReadOnly ) {
125
- // Was any caption selected before?
126
- if ( lastCaption ) {
127
- // Same caption as before?
128
- if ( lastCaption === viewCaption ) {
129
- return showCaption( viewCaption, viewWriter );
130
- } else {
131
- hideCaptionIfEmpty( lastCaption, viewWriter );
132
- this._lastSelectedCaption = viewCaption;
133
-
134
- return showCaption( viewCaption, viewWriter );
168
+ if ( imageUtils.isBlockImage( oldElement ) ) {
169
+ const oldCaptionElement = getCaptionFromImageModelElement( oldElement );
170
+
171
+ // If the old element was a captioned block image (the caption was visible),
172
+ // simply save it so it can be restored.
173
+ if ( oldCaptionElement ) {
174
+ this._saveCaption( newElement, oldCaptionElement );
175
+
176
+ return;
135
177
  }
136
- } else {
137
- this._lastSelectedCaption = viewCaption;
138
- return showCaption( viewCaption, viewWriter );
139
178
  }
140
- } else {
141
- // Was any caption selected before?
142
- if ( lastCaption ) {
143
- const viewModified = hideCaptionIfEmpty( lastCaption, viewWriter );
144
- this._lastSelectedCaption = null;
145
-
146
- return viewModified;
147
- } else {
148
- return false;
179
+
180
+ const savedOldElementCaption = this._getSavedCaption( oldElement );
181
+
182
+ // If either:
183
+ //
184
+ // * the block image didn't have a visible caption,
185
+ // * the block image caption was hidden (and already saved),
186
+ // * the inline image was passed
187
+ //
188
+ // just try to "pass" the saved caption from the old image to the new image
189
+ // so it can be retrieved in the future if the user wants it back.
190
+ if ( savedOldElementCaption ) {
191
+ // Note: Since we're writing to a WeakMap, we don't bother with removing the
192
+ // [ oldElement, savedOldElementCaption ] pair from it.
193
+ this._saveCaption( newElement, savedOldElementCaption );
149
194
  }
195
+ };
196
+
197
+ // Presence of the commands depends on the Image(Inline|Block)Editing plugins loaded in the editor.
198
+ if ( imageTypeInlineCommand ) {
199
+ this.listenTo( imageTypeInlineCommand, 'execute', handleImageTypeChange, { priority: 'low' } );
200
+ }
201
+
202
+ if ( imageTypeBlockCommand ) {
203
+ this.listenTo( imageTypeBlockCommand, 'execute', handleImageTypeChange, { priority: 'low' } );
150
204
  }
151
205
  }
152
206
 
153
207
  /**
154
- * Returns a converter that fixes caption visibility during the model-to-view conversion.
155
- * Checks if the changed node is placed inside the caption element and fixes its visibility in the view.
208
+ * Returns the saved {@link module:engine/model/element~Element#toJSON JSONified} caption
209
+ * of an image model element.
156
210
  *
157
- * @private
158
- * @param {Function} nodeFinder
159
- * @returns {Function}
211
+ * See {@link #_saveCaption}.
212
+ *
213
+ * @protected
214
+ * @param {module:engine/model/element~Element} imageModelElement The model element the
215
+ * caption should be returned for.
216
+ * @returns {module:engine/model/element~Element|null} The model caption element or `null` if there is none.
160
217
  */
161
- _fixCaptionVisibility( nodeFinder ) {
162
- return ( evt, data, conversionApi ) => {
163
- const node = nodeFinder( data );
164
- const modelCaption = getParentCaption( node );
165
- const mapper = this.editor.editing.mapper;
166
- const viewWriter = conversionApi.writer;
167
-
168
- if ( modelCaption ) {
169
- const viewCaption = mapper.toViewElement( modelCaption );
170
-
171
- if ( viewCaption ) {
172
- if ( modelCaption.childCount ) {
173
- viewWriter.removeClass( 'ck-hidden', viewCaption );
174
- } else {
175
- viewWriter.addClass( 'ck-hidden', viewCaption );
176
- }
177
- }
178
- }
179
- };
218
+ _getSavedCaption( imageModelElement ) {
219
+ const jsonObject = this._savedCaptionsMap.get( imageModelElement );
220
+
221
+ return jsonObject ? Element.fromJSON( jsonObject ) : null;
180
222
  }
181
223
 
182
224
  /**
183
- * Checks whether the data inserted to the model document have an image element that has no caption element inside it.
184
- * If there is none, it adds it to the image element.
225
+ * Saves a {@link module:engine/model/element~Element#toJSON JSONified} caption for
226
+ * an image element to allow restoring it in the future.
185
227
  *
186
- * @private
187
- * @param {module:engine/model/writer~Writer} writer The writer to make changes with.
188
- * @returns {Boolean} `true` if any change was applied, `false` otherwise.
228
+ * A caption is saved every time it gets hidden and/or the type of an image changes. The
229
+ * user should be able to restore it on demand.
230
+ *
231
+ * **Note**: The caption cannot be stored in the image model element attribute because,
232
+ * for instance, when the model state propagates to collaborators, the attribute would get
233
+ * lost (mainly because it does not convert to anything when the caption is hidden) and
234
+ * the states of collaborators' models would de-synchronize causing numerous issues.
235
+ *
236
+ * See {@link #_getSavedCaption}.
237
+ *
238
+ * @protected
239
+ * @param {module:engine/model/element~Element} imageModelElement The model element the
240
+ * caption is saved for.
241
+ * @param {module:engine/model/element~Element} caption The caption model element to be saved.
189
242
  */
190
- _insertMissingModelCaptionElement( writer ) {
191
- const model = this.editor.model;
192
- const changes = model.document.differ.getChanges();
193
-
194
- const imagesWithoutCaption = [];
195
-
196
- for ( const entry of changes ) {
197
- if ( entry.type == 'insert' && entry.name != '$text' ) {
198
- const item = entry.position.nodeAfter;
199
-
200
- if ( item.is( 'element', 'image' ) && !getCaptionFromImage( item ) ) {
201
- imagesWithoutCaption.push( item );
202
- }
203
-
204
- // Check elements with children for nested images.
205
- if ( !item.is( 'element', 'image' ) && item.childCount ) {
206
- for ( const nestedItem of model.createRangeIn( item ).getItems() ) {
207
- if ( nestedItem.is( 'element', 'image' ) && !getCaptionFromImage( nestedItem ) ) {
208
- imagesWithoutCaption.push( nestedItem );
209
- }
210
- }
211
- }
212
- }
213
- }
214
-
215
- for ( const image of imagesWithoutCaption ) {
216
- writer.appendElement( 'caption', image );
217
- }
218
-
219
- return !!imagesWithoutCaption.length;
243
+ _saveCaption( imageModelElement, caption ) {
244
+ this._savedCaptionsMap.set( imageModelElement, caption.toJSON() );
220
245
  }
221
246
  }
222
247
 
223
- // Creates a converter that converts image caption model element to view element.
248
+ // Creates a mapper callback that reverses the order of `<img>` and `<figcaption>` in the image.
249
+ // Without it, `<figcaption>` would precede the `<img>` in the conversion.
250
+ //
251
+ // <imageBlock>^</imageBlock> -> <figure><img>^<caption></caption></figure>
224
252
  //
225
253
  // @private
226
- // @param {Function} elementCreator
227
- // @param {Boolean} [hide=true] When set to `false` view element will not be inserted when it's empty.
254
+ // @param {module:engine/view/view~View} editingView
228
255
  // @returns {Function}
229
- function captionModelToView( elementCreator, hide = true ) {
230
- return ( evt, data, conversionApi ) => {
231
- const captionElement = data.item;
256
+ function mapModelPositionToView( editingView ) {
257
+ return ( evt, data ) => {
258
+ const modelPosition = data.modelPosition;
259
+ const parent = modelPosition.parent;
232
260
 
233
- // Return if element shouldn't be present when empty.
234
- if ( !captionElement.childCount && !hide ) {
261
+ if ( !parent.is( 'element', 'imageBlock' ) ) {
235
262
  return;
236
263
  }
237
264
 
238
- if ( isImage( captionElement.parent ) ) {
239
- if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
240
- return;
241
- }
242
-
243
- const viewImage = conversionApi.mapper.toViewElement( data.range.start.parent );
244
- const viewCaption = elementCreator( conversionApi.writer );
245
- const viewWriter = conversionApi.writer;
246
-
247
- // Hide if empty.
248
- if ( !captionElement.childCount ) {
249
- viewWriter.addClass( 'ck-hidden', viewCaption );
250
- }
265
+ const viewElement = data.mapper.toViewElement( parent );
251
266
 
252
- insertViewCaptionAndBind( viewCaption, data.item, viewImage, conversionApi );
253
- }
267
+ // The "img" element is inserted by ImageBlockEditing during the downcast conversion via
268
+ // an explicit view position so the "0" position does not need any mapping.
269
+ data.viewPosition = editingView.createPositionAt( viewElement, modelPosition.offset + 1 );
254
270
  };
255
271
  }
256
-
257
- // Inserts `viewCaption` at the end of `viewImage` and binds it to `modelCaption`.
258
- //
259
- // @private
260
- // @param {module:engine/view/containerelement~ContainerElement} viewCaption
261
- // @param {module:engine/model/element~Element} modelCaption
262
- // @param {module:engine/view/containerelement~ContainerElement} viewImage
263
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
264
- function insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, conversionApi ) {
265
- const viewPosition = conversionApi.writer.createPositionAt( viewImage, 'end' );
266
-
267
- conversionApi.writer.insert( viewPosition, viewCaption );
268
- conversionApi.mapper.bindElements( modelCaption, viewCaption );
269
- }
270
-
271
- // Checks if the provided node or one of its ancestors is a caption element, and returns it.
272
- //
273
- // @private
274
- // @param {module:engine/model/node~Node} node
275
- // @returns {module:engine/model/element~Element|null}
276
- function getParentCaption( node ) {
277
- const ancestors = node.getAncestors( { includeSelf: true } );
278
- const caption = ancestors.find( ancestor => ancestor.name == 'caption' );
279
-
280
- if ( caption && caption.parent && caption.parent.name == 'image' ) {
281
- return caption;
282
- }
283
-
284
- return null;
285
- }
286
-
287
- // Hides a given caption in the view if it is empty.
288
- //
289
- // @private
290
- // @param {module:engine/view/containerelement~ContainerElement} caption
291
- // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
292
- // @returns {Boolean} Returns `true` if the view was modified.
293
- function hideCaptionIfEmpty( caption, viewWriter ) {
294
- if ( !caption.childCount && !caption.hasClass( 'ck-hidden' ) ) {
295
- viewWriter.addClass( 'ck-hidden', caption );
296
- return true;
297
- }
298
-
299
- return false;
300
- }
301
-
302
- // Shows the caption.
303
- //
304
- // @private
305
- // @param {module:engine/view/containerelement~ContainerElement} caption
306
- // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
307
- // @returns {Boolean} Returns `true` if the view was modified.
308
- function showCaption( caption, viewWriter ) {
309
- if ( caption.hasClass( 'ck-hidden' ) ) {
310
- viewWriter.removeClass( 'ck-hidden', caption );
311
- return true;
312
- }
313
-
314
- return false;
315
- }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module image/imagecaption/imagecaptionui
8
+ */
9
+
10
+ import { Plugin, icons } from 'ckeditor5/src/core';
11
+ import { ButtonView } from 'ckeditor5/src/ui';
12
+ import ImageUtils from '../imageutils';
13
+
14
+ import { getCaptionFromModelSelection } from './utils';
15
+
16
+ /**
17
+ * The image caption UI plugin. It introduces the `'toggleImageCaption'` UI button.
18
+ *
19
+ * @extends module:core/plugin~Plugin
20
+ */
21
+ export default class ImageCaptionUI extends Plugin {
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires() {
26
+ return [ ImageUtils ];
27
+ }
28
+
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ static get pluginName() {
33
+ return 'ImageCaptionUI';
34
+ }
35
+
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ init() {
40
+ const editor = this.editor;
41
+ const editingView = editor.editing.view;
42
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
43
+ const t = editor.t;
44
+
45
+ editor.ui.componentFactory.add( 'toggleImageCaption', locale => {
46
+ const command = editor.commands.get( 'toggleImageCaption' );
47
+ const view = new ButtonView( locale );
48
+
49
+ view.set( {
50
+ icon: icons.caption,
51
+ tooltip: true,
52
+ isToggleable: true
53
+ } );
54
+
55
+ view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
56
+ view.bind( 'label' ).to( command, 'value', value => value ? t( 'Toggle caption off' ) : t( 'Toggle caption on' ) );
57
+
58
+ this.listenTo( view, 'execute', () => {
59
+ editor.execute( 'toggleImageCaption', { focusCaptionOnShow: true } );
60
+
61
+ // Scroll to the selection and highlight the caption if the caption showed up.
62
+ const modelCaptionElement = getCaptionFromModelSelection( imageUtils, editor.model.document.selection );
63
+
64
+ if ( modelCaptionElement ) {
65
+ const figcaptionElement = editor.editing.mapper.toViewElement( modelCaptionElement );
66
+
67
+ editingView.scrollToTheSelection();
68
+
69
+ editingView.change( writer => {
70
+ writer.addClass( 'image__caption_highlighted', figcaptionElement );
71
+ } );
72
+ }
73
+ } );
74
+
75
+ return view;
76
+ } );
77
+ }
78
+ }