@ckeditor/ckeditor5-link 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 (73) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +6 -2
  3. package/build/link.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/sk.js +1 -0
  46. package/build/translations/sq.js +1 -0
  47. package/build/translations/sr-latn.js +1 -0
  48. package/build/translations/sr.js +1 -0
  49. package/build/translations/sv.js +1 -0
  50. package/build/translations/tk.js +1 -0
  51. package/build/translations/tr.js +1 -0
  52. package/build/translations/ug.js +1 -0
  53. package/build/translations/uk.js +1 -0
  54. package/build/translations/vi.js +1 -0
  55. package/build/translations/zh-cn.js +1 -0
  56. package/build/translations/zh.js +1 -0
  57. package/ckeditor5-metadata.json +76 -0
  58. package/lang/translations/de-ch.po +53 -0
  59. package/lang/translations/ro.po +1 -1
  60. package/package.json +25 -20
  61. package/src/index.js +7 -17
  62. package/src/link.js +14 -2
  63. package/src/linkcommand.js +13 -15
  64. package/src/linkediting.js +36 -18
  65. package/src/linkimageediting.js +79 -67
  66. package/src/linkimageui.js +21 -19
  67. package/src/linkui.js +20 -12
  68. package/src/unlinkcommand.js +6 -8
  69. package/src/utils/automaticdecorators.js +33 -6
  70. package/src/utils/manualdecorator.js +31 -1
  71. package/src/utils.js +3 -3
  72. package/theme/linkimage.css +9 -12
  73. package/build/link.js.map +0 -1
@@ -13,12 +13,10 @@ import { toMap } from 'ckeditor5/src/utils';
13
13
 
14
14
  import LinkEditing from './linkediting';
15
15
 
16
- import linkIcon from '../theme/icons/link.svg';
17
-
18
16
  /**
19
17
  * The link image engine feature.
20
18
  *
21
- * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<image>`} element
19
+ * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<imageBlock>`} element
22
20
  * which allows linking images.
23
21
  *
24
22
  * @extends module:core/plugin~Plugin
@@ -28,7 +26,7 @@ export default class LinkImageEditing extends Plugin {
28
26
  * @inheritDoc
29
27
  */
30
28
  static get requires() {
31
- return [ 'ImageEditing', LinkEditing ];
29
+ return [ 'ImageEditing', 'ImageUtils', LinkEditing ];
32
30
  }
33
31
 
34
32
  /**
@@ -40,12 +38,18 @@ export default class LinkImageEditing extends Plugin {
40
38
 
41
39
  init() {
42
40
  const editor = this.editor;
41
+ const schema = editor.model.schema;
43
42
 
44
- editor.model.schema.extend( 'image', { allowAttributes: [ 'linkHref' ] } );
43
+ if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
44
+ schema.extend( 'imageBlock', { allowAttributes: [ 'linkHref' ] } );
45
+ }
45
46
 
46
- editor.conversion.for( 'upcast' ).add( upcastLink() );
47
- editor.conversion.for( 'editingDowncast' ).add( downcastImageLink( { attachIconIndicator: true } ) );
48
- editor.conversion.for( 'dataDowncast' ).add( downcastImageLink( { attachIconIndicator: false } ) );
47
+ if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
48
+ schema.extend( 'imageInline', { allowAttributes: [ 'linkHref' ] } );
49
+ }
50
+
51
+ editor.conversion.for( 'upcast' ).add( upcastLink( editor ) );
52
+ editor.conversion.for( 'downcast' ).add( downcastImageLink( editor ) );
49
53
 
50
54
  // Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
51
55
  this._enableAutomaticDecorators();
@@ -77,30 +81,56 @@ export default class LinkImageEditing extends Plugin {
77
81
  _enableManualDecorators() {
78
82
  const editor = this.editor;
79
83
  const command = editor.commands.get( 'link' );
80
- const manualDecorators = command.manualDecorators;
81
84
 
82
85
  for ( const decorator of command.manualDecorators ) {
83
- editor.model.schema.extend( 'image', { allowAttributes: decorator.id } );
84
- editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( manualDecorators, decorator ) );
85
- editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( manualDecorators, decorator ) );
86
+ if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
87
+ editor.model.schema.extend( 'imageBlock', { allowAttributes: decorator.id } );
88
+ }
89
+
90
+ if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
91
+ editor.model.schema.extend( 'imageInline', { allowAttributes: decorator.id } );
92
+ }
93
+
94
+ editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( decorator ) );
95
+ editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( editor, decorator ) );
86
96
  }
87
97
  }
88
98
  }
89
99
 
90
- // Returns a converter that consumes the 'href' attribute if a link contains an image.
100
+ // Returns a converter for linked block images that consumes the "href" attribute
101
+ // if a link contains an image.
91
102
  //
92
103
  // @private
104
+ // @param {module:core/editor/editor~Editor} editor The editor instance.
93
105
  // @returns {Function}
94
- function upcastLink() {
106
+ function upcastLink( editor ) {
107
+ const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' );
108
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
109
+
95
110
  return dispatcher => {
96
111
  dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
97
112
  const viewLink = data.viewItem;
98
- const imageInLink = getFirstImage( viewLink );
113
+ const imageInLink = imageUtils.findViewImgElement( viewLink );
99
114
 
100
115
  if ( !imageInLink ) {
101
116
  return;
102
117
  }
103
118
 
119
+ const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) );
120
+
121
+ // There are four possible cases to consider here
122
+ //
123
+ // 1. A "root > ... > figure.image > a > img" structure.
124
+ // 2. A "root > ... > figure.image > a > picture > img" structure.
125
+ // 3. A "root > ... > block > a > img" structure.
126
+ // 4. A "root > ... > block > a > picture > img" structure.
127
+ //
128
+ // but the last 2 cases should only be considered by this converter when the inline image plugin
129
+ // is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image).
130
+ if ( isImageInlinePluginLoaded && !blockImageView ) {
131
+ return;
132
+ }
133
+
104
134
  // There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
105
135
  const consumableAttributes = { attributes: [ 'href' ] };
106
136
 
@@ -121,7 +151,7 @@ function upcastLink() {
121
151
  // figure > a > img: parent of the view link element is an image element (figure).
122
152
  let modelElement = data.modelCursor.parent;
123
153
 
124
- if ( !modelElement.is( 'element', 'image' ) ) {
154
+ if ( !modelElement.is( 'element', 'imageBlock' ) ) {
125
155
  // a > img: parent of the view link is not the image (figure) element. We need to convert it manually.
126
156
  const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );
127
157
 
@@ -134,7 +164,7 @@ function upcastLink() {
134
164
  modelElement = data.modelCursor.nodeBefore;
135
165
  }
136
166
 
137
- if ( modelElement && modelElement.is( 'element', 'image' ) ) {
167
+ if ( modelElement && modelElement.is( 'element', 'imageBlock' ) ) {
138
168
  // Set the linkHref attribute from link element on model image element.
139
169
  conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
140
170
  }
@@ -144,42 +174,34 @@ function upcastLink() {
144
174
  };
145
175
  }
146
176
 
147
- // Return a converter that adds the `<a>` element to data.
177
+ // Creates a converter that adds `<a>` to linked block image view elements.
148
178
  //
149
179
  // @private
150
- // @params {Object} options
151
- // @params {Boolean} options.attachIconIndicator=false If set to `true`, an icon that informs about the linked image will be added.
152
- // @returns {Function}
153
- function downcastImageLink( options ) {
180
+ function downcastImageLink( editor ) {
181
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
182
+
154
183
  return dispatcher => {
155
- dispatcher.on( 'attribute:linkHref:image', ( evt, data, conversionApi ) => {
184
+ dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => {
185
+ if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
186
+ return;
187
+ }
188
+
156
189
  // The image will be already converted - so it will be present in the view.
157
190
  const viewFigure = conversionApi.mapper.toViewElement( data.item );
158
191
  const writer = conversionApi.writer;
159
192
 
160
193
  // But we need to check whether the link element exists.
161
194
  const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
162
-
163
- let linkIconIndicator;
164
-
165
- if ( options.attachIconIndicator ) {
166
- // Create an icon indicator for a linked image.
167
- linkIconIndicator = writer.createUIElement( 'span', { class: 'ck ck-link-image_icon' }, function( domDocument ) {
168
- const domElement = this.toDomElement( domDocument );
169
- domElement.innerHTML = linkIcon;
170
-
171
- return domElement;
172
- } );
173
- }
195
+ const viewImage = imageUtils.findViewImgElement( viewFigure );
196
+ // <picture>...<img/></picture> or <img/>
197
+ const viewImgOrPicture = viewImage.parent.is( 'element', 'picture' ) ? viewImage.parent : viewImage;
174
198
 
175
199
  // If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
176
200
  if ( linkInImage ) {
177
201
  if ( data.attributeNewValue ) {
178
202
  writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
179
203
  } else {
180
- const viewImage = Array.from( linkInImage.getChildren() ).find( child => child.name === 'img' );
181
-
182
- writer.move( writer.createRangeOn( viewImage ), writer.createPositionAt( viewFigure, 0 ) );
204
+ writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( viewFigure, 0 ) );
183
205
  writer.remove( linkInImage );
184
206
  }
185
207
  } else {
@@ -191,14 +213,9 @@ function downcastImageLink( options ) {
191
213
  writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );
192
214
 
193
215
  // 3. Move the image to the link.
194
- writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
195
-
196
- // 4. Inset the linked image icon indicator while downcast to editing.
197
- if ( linkIconIndicator ) {
198
- writer.insert( writer.createPositionAt( linkElement, 'end' ), linkIconIndicator );
199
- }
216
+ writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( linkElement, 0 ) );
200
217
  }
201
- } );
218
+ }, { priority: 'high' } );
202
219
  };
203
220
  }
204
221
 
@@ -206,11 +223,9 @@ function downcastImageLink( options ) {
206
223
  //
207
224
  // @private
208
225
  // @returns {Function}
209
- function downcastImageLinkManualDecorator( manualDecorators, decorator ) {
226
+ function downcastImageLinkManualDecorator( decorator ) {
210
227
  return dispatcher => {
211
- dispatcher.on( `attribute:${ decorator.id }:image`, ( evt, data, conversionApi ) => {
212
- const attributes = manualDecorators.get( decorator.id ).attributes;
213
-
228
+ dispatcher.on( `attribute:${ decorator.id }:imageBlock`, ( evt, data, conversionApi ) => {
214
229
  const viewFigure = conversionApi.mapper.toViewElement( data.item );
215
230
  const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
216
231
 
@@ -221,9 +236,17 @@ function downcastImageLinkManualDecorator( manualDecorators, decorator ) {
221
236
  return;
222
237
  }
223
238
 
224
- for ( const [ key, val ] of toMap( attributes ) ) {
239
+ for ( const [ key, val ] of toMap( decorator.attributes ) ) {
225
240
  conversionApi.writer.setAttribute( key, val, linkInImage );
226
241
  }
242
+
243
+ if ( decorator.classes ) {
244
+ conversionApi.writer.addClass( decorator.classes, linkInImage );
245
+ }
246
+
247
+ for ( const key in decorator.styles ) {
248
+ conversionApi.writer.setStyle( key, decorator.styles[ key ], linkInImage );
249
+ }
227
250
  } );
228
251
  };
229
252
  }
@@ -232,11 +255,13 @@ function downcastImageLinkManualDecorator( manualDecorators, decorator ) {
232
255
  //
233
256
  // @private
234
257
  // @returns {Function}
235
- function upcastImageLinkManualDecorator( manualDecorators, decorator ) {
258
+ function upcastImageLinkManualDecorator( editor, decorator ) {
259
+ const imageUtils = editor.plugins.get( 'ImageUtils' );
260
+
236
261
  return dispatcher => {
237
262
  dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
238
263
  const viewLink = data.viewItem;
239
- const imageInLink = getFirstImage( viewLink );
264
+ const imageInLink = imageUtils.findViewImgElement( viewLink );
240
265
 
241
266
  // We need to check whether an image is inside a link because the converter handles
242
267
  // only manual decorators for linked images. See #7975.
@@ -244,11 +269,7 @@ function upcastImageLinkManualDecorator( manualDecorators, decorator ) {
244
269
  return;
245
270
  }
246
271
 
247
- const consumableAttributes = {
248
- attributes: manualDecorators.get( decorator.id ).attributes
249
- };
250
-
251
- const matcher = new Matcher( consumableAttributes );
272
+ const matcher = new Matcher( decorator._createPattern() );
252
273
  const result = matcher.match( viewLink );
253
274
 
254
275
  // The link element does not have required attributes or/and proper values.
@@ -261,7 +282,7 @@ function upcastImageLinkManualDecorator( manualDecorators, decorator ) {
261
282
  return;
262
283
  }
263
284
 
264
- // At this stage we can assume that we have the `<image>` element.
285
+ // At this stage we can assume that we have the `<imageBlock>` element.
265
286
  // `nodeBefore` comes after conversion: `<a><img></a>`.
266
287
  // `parent` comes with full image definition: `<figure><a><img></a></figure>.
267
288
  // See the body of the `upcastLink()` function.
@@ -272,12 +293,3 @@ function upcastImageLinkManualDecorator( manualDecorators, decorator ) {
272
293
  // Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
273
294
  };
274
295
  }
275
-
276
- // Returns the first image in a given view element.
277
- //
278
- // @private
279
- // @param {module:engine/view/element~Element}
280
- // @returns {module:engine/view/element~Element|undefined}
281
- function getFirstImage( viewElement ) {
282
- return Array.from( viewElement.getChildren() ).find( child => child.name === 'img' );
283
- }
@@ -30,7 +30,7 @@ export default class LinkImageUI extends Plugin {
30
30
  * @inheritDoc
31
31
  */
32
32
  static get requires() {
33
- return [ LinkEditing, LinkUI, 'Image' ];
33
+ return [ LinkEditing, LinkUI, 'ImageBlockEditing' ];
34
34
  }
35
35
 
36
36
  /**
@@ -48,12 +48,15 @@ export default class LinkImageUI extends Plugin {
48
48
  const viewDocument = editor.editing.view.document;
49
49
 
50
50
  this.listenTo( viewDocument, 'click', ( evt, data ) => {
51
- const hasLink = isImageLinked( viewDocument.selection.getSelectedElement(), editor.plugins.get( 'Image' ) );
52
-
53
- if ( hasLink ) {
51
+ if ( this._isSelectedLinkedImage( editor.model.document.selection ) ) {
52
+ // Prevent browser navigation when clicking a linked image.
54
53
  data.preventDefault();
54
+
55
+ // Block the `LinkUI` plugin when an image was clicked.
56
+ // In such a case, we'd like to display the image toolbar.
57
+ evt.stop();
55
58
  }
56
- } );
59
+ }, { priority: 'high' } );
57
60
 
58
61
  this._createToolbarLinkImageButton();
59
62
  }
@@ -91,9 +94,7 @@ export default class LinkImageUI extends Plugin {
91
94
 
92
95
  // Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
93
96
  this.listenTo( button, 'execute', () => {
94
- const hasLink = isImageLinked( editor.editing.view.document.selection.getSelectedElement(), editor.plugins.get( 'Image' ) );
95
-
96
- if ( hasLink ) {
97
+ if ( this._isSelectedLinkedImage( editor.model.document.selection ) ) {
97
98
  plugin._addActionsView();
98
99
  } else {
99
100
  plugin._showUI( true );
@@ -103,18 +104,19 @@ export default class LinkImageUI extends Plugin {
103
104
  return button;
104
105
  } );
105
106
  }
106
- }
107
107
 
108
- // A helper function that checks whether the element is a linked image.
109
- //
110
- // @param {module:engine/model/element~Element} element
111
- // @returns {Boolean}
112
- function isImageLinked( element, image ) {
113
- const isImage = element && image.isImageWidget( element );
108
+ /**
109
+ * Returns true if a linked image (either block or inline) is the only selected element
110
+ * in the model document.
111
+ *
112
+ * @private
113
+ * @param {module:engine/model/selection~Selection} selection
114
+ * @returns {Boolean}
115
+ */
116
+ _isSelectedLinkedImage( selection ) {
117
+ const selectedModelElement = selection.getSelectedElement();
118
+ const imageUtils = this.editor.plugins.get( 'ImageUtils' );
114
119
 
115
- if ( !isImage ) {
116
- return false;
120
+ return imageUtils.isImage( selectedModelElement ) && selectedModelElement.hasAttribute( 'linkHref' );
117
121
  }
118
-
119
- return element.getChild( 0 ).is( 'element', 'a' );
120
122
  }
package/src/linkui.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
11
  import { ClickObserver } from 'ckeditor5/src/engine';
12
12
  import { ButtonView, ContextualBalloon, clickOutsideHandler } from 'ckeditor5/src/ui';
13
-
13
+ import { isWidget } from 'ckeditor5/src/widget';
14
14
  import LinkFormView from './ui/linkformview';
15
15
  import LinkActionsView from './ui/linkactionsview';
16
16
  import { addLinkProtocolIfApplicable, isLinkElement, LINK_KEYSTROKE } from './utils';
@@ -593,14 +593,19 @@ export default class LinkUI extends Plugin {
593
593
 
594
594
  target = view.domConverter.viewRangeToDom( newRange );
595
595
  } else {
596
- const targetLink = this._getSelectedLinkElement();
597
- const range = viewDocument.selection.getFirstRange();
598
-
599
- target = targetLink ?
600
- // When selection is inside link element, then attach panel to this element.
601
- view.domConverter.mapViewToDom( targetLink ) :
602
- // Otherwise attach panel to the selection.
603
- view.domConverter.viewRangeToDom( range );
596
+ // Make sure the target is calculated on demand at the last moment because a cached DOM range
597
+ // (which is very fragile) can desynchronize with the state of the editing view if there was
598
+ // any rendering done in the meantime. This can happen, for instance, when an inline widget
599
+ // gets unlinked.
600
+ target = () => {
601
+ const targetLink = this._getSelectedLinkElement();
602
+
603
+ return targetLink ?
604
+ // When selection is inside link element, then attach panel to this element.
605
+ view.domConverter.mapViewToDom( targetLink ) :
606
+ // Otherwise attach panel to the selection.
607
+ view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
608
+ };
604
609
  }
605
610
 
606
611
  return { target };
@@ -611,8 +616,9 @@ export default class LinkUI extends Plugin {
611
616
  * the {@link module:engine/view/document~Document editing view's} selection or `null`
612
617
  * if there is none.
613
618
  *
614
- * **Note**: For a non–collapsed selection, the link element is only returned when **fully**
615
- * selected and the **only** element within the selection boundaries.
619
+ * **Note**: For a non–collapsed selection, the link element is returned when **fully**
620
+ * selected and the **only** element within the selection boundaries, or when
621
+ * a linked widget is selected.
616
622
  *
617
623
  * @private
618
624
  * @returns {module:engine/view/attributeelement~AttributeElement|null}
@@ -620,8 +626,10 @@ export default class LinkUI extends Plugin {
620
626
  _getSelectedLinkElement() {
621
627
  const view = this.editor.editing.view;
622
628
  const selection = view.document.selection;
629
+ const selectedElement = selection.getSelectedElement();
623
630
 
624
- if ( selection.isCollapsed ) {
631
+ // The selection is collapsed or some widget is selected (especially inline widget).
632
+ if ( selection.isCollapsed || selectedElement && isWidget( selectedElement ) ) {
625
633
  return findLinkElementAncestor( selection.getFirstPosition() );
626
634
  } else {
627
635
  // The range for fully selected link is usually anchored in adjacent text nodes.
@@ -9,9 +9,8 @@
9
9
 
10
10
  import { Command } from 'ckeditor5/src/core';
11
11
  import { findAttributeRange } from 'ckeditor5/src/typing';
12
- import { first } from 'ckeditor5/src/utils';
13
12
 
14
- import { isImageAllowed } from './utils';
13
+ import { isLinkableElement } from './utils';
15
14
 
16
15
  /**
17
16
  * The unlink command. It is used by the {@link module:link/link~Link link plugin}.
@@ -24,16 +23,15 @@ export default class UnlinkCommand extends Command {
24
23
  */
25
24
  refresh() {
26
25
  const model = this.editor.model;
27
- const doc = model.document;
28
-
29
- const selectedElement = first( doc.selection.getSelectedBlocks() );
26
+ const selection = model.document.selection;
27
+ const selectedElement = selection.getSelectedElement();
30
28
 
31
- // A check for the `LinkImage` plugin. If the selection contains an image element, get values from the element.
29
+ // A check for any integration that allows linking elements (e.g. `LinkImage`).
32
30
  // Currently the selection reads attributes from text nodes only. See #7429 and #7465.
33
- if ( isImageAllowed( selectedElement, model.schema ) ) {
31
+ if ( isLinkableElement( selectedElement, model.schema ) ) {
34
32
  this.isEnabled = model.schema.checkAttribute( selectedElement, 'linkHref' );
35
33
  } else {
36
- this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' );
34
+ this.isEnabled = model.schema.checkAttributeInSelection( selection, 'linkHref' );
37
35
  }
38
36
  }
39
37
 
@@ -73,6 +73,15 @@ export default class AutomaticDecorators {
73
73
  const viewElement = viewWriter.createAttributeElement( 'a', item.attributes, {
74
74
  priority: 5
75
75
  } );
76
+
77
+ if ( item.classes ) {
78
+ viewWriter.addClass( item.classes, viewElement );
79
+ }
80
+
81
+ for ( const key in item.styles ) {
82
+ viewWriter.setStyle( key, item.styles[ key ], viewElement );
83
+ }
84
+
76
85
  viewWriter.setCustomProperty( 'link', true, viewElement );
77
86
  if ( item.callback( data.attributeNewValue ) ) {
78
87
  if ( data.item.is( 'selection' ) ) {
@@ -97,8 +106,8 @@ export default class AutomaticDecorators {
97
106
  */
98
107
  getDispatcherForLinkedImage() {
99
108
  return dispatcher => {
100
- dispatcher.on( 'attribute:linkHref:image', ( evt, data, conversionApi ) => {
101
- const viewFigure = conversionApi.mapper.toViewElement( data.item );
109
+ dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, { writer, mapper } ) => {
110
+ const viewFigure = mapper.toViewElement( data.item );
102
111
  const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
103
112
 
104
113
  for ( const item of this._definitions ) {
@@ -106,20 +115,38 @@ export default class AutomaticDecorators {
106
115
 
107
116
  if ( item.callback( data.attributeNewValue ) ) {
108
117
  for ( const [ key, val ] of attributes ) {
118
+ // Left for backward compatibility. Since v30 decorator should
119
+ // accept `classes` and `styles` separately from `attributes`.
109
120
  if ( key === 'class' ) {
110
- conversionApi.writer.addClass( val, linkInImage );
121
+ writer.addClass( val, linkInImage );
111
122
  } else {
112
- conversionApi.writer.setAttribute( key, val, linkInImage );
123
+ writer.setAttribute( key, val, linkInImage );
113
124
  }
114
125
  }
126
+
127
+ if ( item.classes ) {
128
+ writer.addClass( item.classes, linkInImage );
129
+ }
130
+
131
+ for ( const key in item.styles ) {
132
+ writer.setStyle( key, item.styles[ key ], linkInImage );
133
+ }
115
134
  } else {
116
135
  for ( const [ key, val ] of attributes ) {
117
136
  if ( key === 'class' ) {
118
- conversionApi.writer.removeClass( val, linkInImage );
137
+ writer.removeClass( val, linkInImage );
119
138
  } else {
120
- conversionApi.writer.removeAttribute( key, linkInImage );
139
+ writer.removeAttribute( key, linkInImage );
121
140
  }
122
141
  }
142
+
143
+ if ( item.classes ) {
144
+ writer.removeClass( item.classes, linkInImage );
145
+ }
146
+
147
+ for ( const key in item.styles ) {
148
+ writer.removeStyle( key, linkInImage );
149
+ }
123
150
  }
124
151
  }
125
152
  } );
@@ -28,7 +28,7 @@ export default class ManualDecorator {
28
28
  * Attributes should keep the format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
29
29
  * @param {Boolean} [config.defaultValue] Controls whether the decorator is "on" by default.
30
30
  */
31
- constructor( { id, label, attributes, defaultValue } ) {
31
+ constructor( { id, label, attributes, classes, styles, defaultValue } ) {
32
32
  /**
33
33
  * An ID of a manual decorator which is the name of the attribute in the model, for example: 'linkManualDecorator0'.
34
34
  *
@@ -65,6 +65,36 @@ export default class ManualDecorator {
65
65
  * @type {Object}
66
66
  */
67
67
  this.attributes = attributes;
68
+
69
+ /**
70
+ * A set of classes added to downcasted data when the decorator is activated for a specific link.
71
+ * Classes should be added in a form of classes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
72
+ *
73
+ * @type {Object}
74
+ */
75
+ this.classes = classes;
76
+
77
+ /**
78
+ * A set of styles added to downcasted data when the decorator is activated for a specific link.
79
+ * Styles should be added in a form of styles defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
80
+ *
81
+ * @type {Object}
82
+ */
83
+ this.styles = styles;
84
+ }
85
+
86
+ /**
87
+ * Returns {@link module:engine/view/matcher~MatcherPattern} with decorator attributes.
88
+ *
89
+ * @protected
90
+ * @returns {module:engine/view/matcher~MatcherPattern}
91
+ */
92
+ _createPattern() {
93
+ return {
94
+ attributes: this.attributes,
95
+ classes: this.classes,
96
+ styles: this.styles
97
+ };
68
98
  }
69
99
  }
70
100
 
package/src/utils.js CHANGED
@@ -129,18 +129,18 @@ export function normalizeDecorators( decorators ) {
129
129
  }
130
130
 
131
131
  /**
132
- * Returns `true` if the specified `element` is an image and it can be linked (the element allows having the `linkHref` attribute).
132
+ * Returns `true` if the specified `element` can be linked (the element allows the `linkHref` attribute).
133
133
  *
134
134
  * @params {module:engine/model/element~Element|null} element
135
135
  * @params {module:engine/model/schema~Schema} schema
136
136
  * @returns {Boolean}
137
137
  */
138
- export function isImageAllowed( element, schema ) {
138
+ export function isLinkableElement( element, schema ) {
139
139
  if ( !element ) {
140
140
  return false;
141
141
  }
142
142
 
143
- return element.is( 'element', 'image' ) && schema.checkAttribute( 'image', 'linkHref' );
143
+ return schema.checkAttribute( element.name, 'linkHref' );
144
144
  }
145
145
 
146
146
  /**
@@ -3,17 +3,14 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
6
- .ck.ck-link-image_icon {
7
- position: absolute;
8
- top: var(--ck-spacing-medium);
9
- right: var(--ck-spacing-medium);
10
- width: 28px;
11
- height: 28px;
12
- padding: 4px;
13
- box-sizing: border-box;
14
- border-radius: var(--ck-border-radius);
15
-
16
- & svg {
17
- fill: currentColor;
6
+ .ck.ck-editor__editable {
7
+ /* Linked image indicator */
8
+ & figure.image > a,
9
+ & a span.image-inline {
10
+ &::after {
11
+ display: block;
12
+ position: absolute;
13
+ }
18
14
  }
19
15
  }
16
+