@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.
- package/LICENSE.md +1 -1
- package/README.md +6 -2
- package/build/link.js +1 -1
- package/build/translations/ar.js +1 -0
- package/build/translations/ast.js +1 -0
- package/build/translations/az.js +1 -0
- package/build/translations/bg.js +1 -0
- package/build/translations/cs.js +1 -0
- package/build/translations/da.js +1 -0
- package/build/translations/de-ch.js +1 -0
- package/build/translations/de.js +1 -0
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -0
- package/build/translations/en-gb.js +1 -0
- package/build/translations/eo.js +1 -0
- package/build/translations/es.js +1 -0
- package/build/translations/et.js +1 -0
- package/build/translations/eu.js +1 -0
- package/build/translations/fa.js +1 -0
- package/build/translations/fi.js +1 -0
- package/build/translations/fr.js +1 -0
- package/build/translations/gl.js +1 -0
- package/build/translations/he.js +1 -0
- package/build/translations/hi.js +1 -0
- package/build/translations/hr.js +1 -0
- package/build/translations/hu.js +1 -0
- package/build/translations/id.js +1 -0
- package/build/translations/it.js +1 -0
- package/build/translations/ja.js +1 -0
- package/build/translations/km.js +1 -0
- package/build/translations/kn.js +1 -0
- package/build/translations/ko.js +1 -0
- package/build/translations/ku.js +1 -0
- package/build/translations/lt.js +1 -0
- package/build/translations/lv.js +1 -0
- package/build/translations/nb.js +1 -0
- package/build/translations/ne.js +1 -0
- package/build/translations/nl.js +1 -0
- package/build/translations/no.js +1 -0
- package/build/translations/pl.js +1 -0
- package/build/translations/pt-br.js +1 -0
- package/build/translations/pt.js +1 -0
- package/build/translations/ro.js +1 -0
- package/build/translations/ru.js +1 -0
- package/build/translations/sk.js +1 -0
- package/build/translations/sq.js +1 -0
- package/build/translations/sr-latn.js +1 -0
- package/build/translations/sr.js +1 -0
- package/build/translations/sv.js +1 -0
- package/build/translations/tk.js +1 -0
- package/build/translations/tr.js +1 -0
- package/build/translations/ug.js +1 -0
- package/build/translations/uk.js +1 -0
- package/build/translations/vi.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/build/translations/zh.js +1 -0
- package/ckeditor5-metadata.json +76 -0
- package/lang/translations/de-ch.po +53 -0
- package/lang/translations/ro.po +1 -1
- package/package.json +25 -20
- package/src/index.js +7 -17
- package/src/link.js +14 -2
- package/src/linkcommand.js +13 -15
- package/src/linkediting.js +36 -18
- package/src/linkimageediting.js +79 -67
- package/src/linkimageui.js +21 -19
- package/src/linkui.js +20 -12
- package/src/unlinkcommand.js +6 -8
- package/src/utils/automaticdecorators.js +33 -6
- package/src/utils/manualdecorator.js +31 -1
- package/src/utils.js +3 -3
- package/theme/linkimage.css +9 -12
- package/build/link.js.map +0 -1
package/src/linkimageediting.js
CHANGED
|
@@ -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 `<
|
|
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.
|
|
43
|
+
if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
|
|
44
|
+
schema.extend( 'imageBlock', { allowAttributes: [ 'linkHref' ] } );
|
|
45
|
+
}
|
|
45
46
|
|
|
46
|
-
editor.
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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 =
|
|
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', '
|
|
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', '
|
|
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
|
-
//
|
|
177
|
+
// Creates a converter that adds `<a>` to linked block image view elements.
|
|
148
178
|
//
|
|
149
179
|
// @private
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
226
|
+
function downcastImageLinkManualDecorator( decorator ) {
|
|
210
227
|
return dispatcher => {
|
|
211
|
-
dispatcher.on( `attribute:${ decorator.id }:
|
|
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(
|
|
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 =
|
|
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
|
|
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 `<
|
|
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
|
-
}
|
package/src/linkimageui.js
CHANGED
|
@@ -30,7 +30,7 @@ export default class LinkImageUI extends Plugin {
|
|
|
30
30
|
* @inheritDoc
|
|
31
31
|
*/
|
|
32
32
|
static get requires() {
|
|
33
|
-
return [ LinkEditing, LinkUI, '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
package/src/unlinkcommand.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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 (
|
|
31
|
+
if ( isLinkableElement( selectedElement, model.schema ) ) {
|
|
34
32
|
this.isEnabled = model.schema.checkAttribute( selectedElement, 'linkHref' );
|
|
35
33
|
} else {
|
|
36
|
-
this.isEnabled = model.schema.checkAttributeInSelection(
|
|
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:
|
|
101
|
-
const viewFigure =
|
|
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
|
-
|
|
121
|
+
writer.addClass( val, linkInImage );
|
|
111
122
|
} else {
|
|
112
|
-
|
|
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
|
-
|
|
137
|
+
writer.removeClass( val, linkInImage );
|
|
119
138
|
} else {
|
|
120
|
-
|
|
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`
|
|
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
|
|
138
|
+
export function isLinkableElement( element, schema ) {
|
|
139
139
|
if ( !element ) {
|
|
140
140
|
return false;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
return
|
|
143
|
+
return schema.checkAttribute( element.name, 'linkHref' );
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
package/theme/linkimage.css
CHANGED
|
@@ -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-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
|