UNPKG

11.8 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5/**
6 * @module link/linkimageediting
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import { Matcher } from 'ckeditor5/src/engine';
10import { toMap } from 'ckeditor5/src/utils';
11import LinkEditing from './linkediting';
12/**
13 * The link image engine feature.
14 *
15 * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<imageBlock>`} element
16 * which allows linking images.
17 */
18export default class LinkImageEditing extends Plugin {
19 /**
20 * @inheritDoc
21 */
22 static get requires() {
23 return ['ImageEditing', 'ImageUtils', LinkEditing];
24 }
25 /**
26 * @inheritDoc
27 */
28 static get pluginName() {
29 return 'LinkImageEditing';
30 }
31 /**
32 * @inheritDoc
33 */
34 init() {
35 const editor = this.editor;
36 const schema = editor.model.schema;
37 if (editor.plugins.has('ImageBlockEditing')) {
38 schema.extend('imageBlock', { allowAttributes: ['linkHref'] });
39 }
40 editor.conversion.for('upcast').add(upcastLink(editor));
41 editor.conversion.for('downcast').add(downcastImageLink(editor));
42 // Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
43 this._enableAutomaticDecorators();
44 this._enableManualDecorators();
45 }
46 /**
47 * Processes {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators} definitions and
48 * attaches proper converters that will work when linking an image.`
49 */
50 _enableAutomaticDecorators() {
51 const editor = this.editor;
52 const command = editor.commands.get('link');
53 const automaticDecorators = command.automaticDecorators;
54 if (automaticDecorators.length) {
55 editor.conversion.for('downcast').add(automaticDecorators.getDispatcherForLinkedImage());
56 }
57 }
58 /**
59 * Processes transformed {@link module:link/utils/manualdecorator~ManualDecorator} instances and attaches proper converters
60 * that will work when linking an image.
61 */
62 _enableManualDecorators() {
63 const editor = this.editor;
64 const command = editor.commands.get('link');
65 for (const decorator of command.manualDecorators) {
66 if (editor.plugins.has('ImageBlockEditing')) {
67 editor.model.schema.extend('imageBlock', { allowAttributes: decorator.id });
68 }
69 if (editor.plugins.has('ImageInlineEditing')) {
70 editor.model.schema.extend('imageInline', { allowAttributes: decorator.id });
71 }
72 editor.conversion.for('downcast').add(downcastImageLinkManualDecorator(decorator));
73 editor.conversion.for('upcast').add(upcastImageLinkManualDecorator(editor, decorator));
74 }
75 }
76}
77/**
78 * Returns a converter for linked block images that consumes the "href" attribute
79 * if a link contains an image.
80 *
81 * @param editor The editor instance.
82 */
83function upcastLink(editor) {
84 const isImageInlinePluginLoaded = editor.plugins.has('ImageInlineEditing');
85 const imageUtils = editor.plugins.get('ImageUtils');
86 return dispatcher => {
87 dispatcher.on('element:a', (evt, data, conversionApi) => {
88 const viewLink = data.viewItem;
89 const imageInLink = imageUtils.findViewImgElement(viewLink);
90 if (!imageInLink) {
91 return;
92 }
93 const blockImageView = imageInLink.findAncestor(element => imageUtils.isBlockImageView(element));
94 // There are four possible cases to consider here
95 //
96 // 1. A "root > ... > figure.image > a > img" structure.
97 // 2. A "root > ... > figure.image > a > picture > img" structure.
98 // 3. A "root > ... > block > a > img" structure.
99 // 4. A "root > ... > block > a > picture > img" structure.
100 //
101 // but the last 2 cases should only be considered by this converter when the inline image plugin
102 // is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image).
103 if (isImageInlinePluginLoaded && !blockImageView) {
104 return;
105 }
106 // There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
107 const consumableAttributes = { attributes: ['href'] };
108 // Consume the `href` attribute so the default one will not convert it to $text attribute.
109 if (!conversionApi.consumable.consume(viewLink, consumableAttributes)) {
110 // Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
111 return;
112 }
113 const linkHref = viewLink.getAttribute('href');
114 // Missing the 'href' attribute.
115 if (!linkHref) {
116 return;
117 }
118 // A full definition of the image feature.
119 // figure > a > img: parent of the view link element is an image element (figure).
120 let modelElement = data.modelCursor.parent;
121 if (!modelElement.is('element', 'imageBlock')) {
122 // a > img: parent of the view link is not the image (figure) element. We need to convert it manually.
123 const conversionResult = conversionApi.convertItem(imageInLink, data.modelCursor);
124 // Set image range as conversion result.
125 data.modelRange = conversionResult.modelRange;
126 // Continue conversion where image conversion ends.
127 data.modelCursor = conversionResult.modelCursor;
128 modelElement = data.modelCursor.nodeBefore;
129 }
130 if (modelElement && modelElement.is('element', 'imageBlock')) {
131 // Set the linkHref attribute from link element on model image element.
132 conversionApi.writer.setAttribute('linkHref', linkHref, modelElement);
133 }
134 }, { priority: 'high' });
135 // Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees
136 // that manual decorators will decorate the proper element.
137 };
138}
139/**
140 * Creates a converter that adds `<a>` to linked block image view elements.
141 */
142function downcastImageLink(editor) {
143 const imageUtils = editor.plugins.get('ImageUtils');
144 return dispatcher => {
145 dispatcher.on('attribute:linkHref:imageBlock', (evt, data, conversionApi) => {
146 if (!conversionApi.consumable.consume(data.item, evt.name)) {
147 return;
148 }
149 // The image will be already converted - so it will be present in the view.
150 const viewFigure = conversionApi.mapper.toViewElement(data.item);
151 const writer = conversionApi.writer;
152 // But we need to check whether the link element exists.
153 const linkInImage = Array.from(viewFigure.getChildren())
154 .find((child) => child.is('element', 'a'));
155 const viewImage = imageUtils.findViewImgElement(viewFigure);
156 // <picture>...<img/></picture> or <img/>
157 const viewImgOrPicture = viewImage.parent.is('element', 'picture') ? viewImage.parent : viewImage;
158 // If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
159 if (linkInImage) {
160 if (data.attributeNewValue) {
161 writer.setAttribute('href', data.attributeNewValue, linkInImage);
162 }
163 else {
164 writer.move(writer.createRangeOn(viewImgOrPicture), writer.createPositionAt(viewFigure, 0));
165 writer.remove(linkInImage);
166 }
167 }
168 else {
169 // But if it does not exist. Let's wrap already converted image by newly created link element.
170 // 1. Create an empty link element.
171 const linkElement = writer.createContainerElement('a', { href: data.attributeNewValue });
172 // 2. Insert link inside the associated image.
173 writer.insert(writer.createPositionAt(viewFigure, 0), linkElement);
174 // 3. Move the image to the link.
175 writer.move(writer.createRangeOn(viewImgOrPicture), writer.createPositionAt(linkElement, 0));
176 }
177 }, { priority: 'high' });
178 };
179}
180/**
181 * Returns a converter that decorates the `<a>` element when the image is the link label.
182 */
183function downcastImageLinkManualDecorator(decorator) {
184 return dispatcher => {
185 dispatcher.on(`attribute:${decorator.id}:imageBlock`, (evt, data, conversionApi) => {
186 const viewFigure = conversionApi.mapper.toViewElement(data.item);
187 const linkInImage = Array.from(viewFigure.getChildren())
188 .find((child) => child.is('element', 'a'));
189 // The <a> element was removed by the time this converter is executed.
190 // It may happen when the base `linkHref` and decorator attributes are removed
191 // at the same time (see #8401).
192 if (!linkInImage) {
193 return;
194 }
195 for (const [key, val] of toMap(decorator.attributes)) {
196 conversionApi.writer.setAttribute(key, val, linkInImage);
197 }
198 if (decorator.classes) {
199 conversionApi.writer.addClass(decorator.classes, linkInImage);
200 }
201 for (const key in decorator.styles) {
202 conversionApi.writer.setStyle(key, decorator.styles[key], linkInImage);
203 }
204 });
205 };
206}
207/**
208 * Returns a converter that checks whether manual decorators should be applied to the link.
209 */
210function upcastImageLinkManualDecorator(editor, decorator) {
211 const isImageInlinePluginLoaded = editor.plugins.has('ImageInlineEditing');
212 const imageUtils = editor.plugins.get('ImageUtils');
213 return dispatcher => {
214 dispatcher.on('element:a', (evt, data, conversionApi) => {
215 const viewLink = data.viewItem;
216 const imageInLink = imageUtils.findViewImgElement(viewLink);
217 // We need to check whether an image is inside a link because the converter handles
218 // only manual decorators for linked images. See #7975.
219 if (!imageInLink) {
220 return;
221 }
222 const blockImageView = imageInLink.findAncestor(element => imageUtils.isBlockImageView(element));
223 if (isImageInlinePluginLoaded && !blockImageView) {
224 return;
225 }
226 const matcher = new Matcher(decorator._createPattern());
227 const result = matcher.match(viewLink);
228 // The link element does not have required attributes or/and proper values.
229 if (!result) {
230 return;
231 }
232 // Check whether we can consume those attributes.
233 if (!conversionApi.consumable.consume(viewLink, result.match)) {
234 return;
235 }
236 // At this stage we can assume that we have the `<imageBlock>` element.
237 // `nodeBefore` comes after conversion: `<a><img></a>`.
238 // `parent` comes with full image definition: `<figure><a><img></a></figure>.
239 // See the body of the `upcastLink()` function.
240 const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
241 conversionApi.writer.setAttribute(decorator.id, true, modelElement);
242 }, { priority: 'high' });
243 // Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
244 };
245}