UNPKG

10.6 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5import { first } from 'ckeditor5/src/utils.js';
6/**
7 * Returns a function that converts the image view representation:
8 *
9 * ```html
10 * <figure class="image"><img src="..." alt="..."></img></figure>
11 * ```
12 *
13 * to the model representation:
14 *
15 * ```html
16 * <imageBlock src="..." alt="..."></imageBlock>
17 * ```
18 *
19 * The entire content of the `<figure>` element except the first `<img>` is being converted as children
20 * of the `<imageBlock>` model element.
21 *
22 * @internal
23 */
24export function upcastImageFigure(imageUtils) {
25 const converter = (evt, data, conversionApi) => {
26 // Do not convert if this is not an "image figure".
27 if (!conversionApi.consumable.test(data.viewItem, { name: true, classes: 'image' })) {
28 return;
29 }
30 // Find an image element inside the figure element.
31 const viewImage = imageUtils.findViewImgElement(data.viewItem);
32 // Do not convert if image element is absent or was already converted.
33 if (!viewImage || !conversionApi.consumable.test(viewImage, { name: true })) {
34 return;
35 }
36 // Consume the figure to prevent other converters from processing it again.
37 conversionApi.consumable.consume(data.viewItem, { name: true, classes: 'image' });
38 // Convert view image to model image.
39 const conversionResult = conversionApi.convertItem(viewImage, data.modelCursor);
40 // Get image element from conversion result.
41 const modelImage = first(conversionResult.modelRange.getItems());
42 // When image wasn't successfully converted then finish conversion.
43 if (!modelImage) {
44 // Revert consumed figure so other features can convert it.
45 conversionApi.consumable.revert(data.viewItem, { name: true, classes: 'image' });
46 return;
47 }
48 // Convert rest of the figure element's children as an image children.
49 conversionApi.convertChildren(data.viewItem, modelImage);
50 conversionApi.updateConversionResult(modelImage, data);
51 };
52 return dispatcher => {
53 dispatcher.on('element:figure', converter);
54 };
55}
56/**
57 * Returns a function that converts the image view representation:
58 *
59 * ```html
60 * <picture><source ... /><source ... />...<img ... /></picture>
61 * ```
62 *
63 * to the model representation as the `sources` attribute:
64 *
65 * ```html
66 * <image[Block|Inline] ... sources="..."></image[Block|Inline]>
67 * ```
68 *
69 * @internal
70 */
71export function upcastPicture(imageUtils) {
72 const sourceAttributeNames = ['srcset', 'media', 'type', 'sizes'];
73 const converter = (evt, data, conversionApi) => {
74 const pictureViewElement = data.viewItem;
75 // Do not convert <picture> if already consumed.
76 if (!conversionApi.consumable.test(pictureViewElement, { name: true })) {
77 return;
78 }
79 const sources = new Map();
80 // Collect all <source /> elements attribute values.
81 for (const childSourceElement of pictureViewElement.getChildren()) {
82 if (childSourceElement.is('element', 'source')) {
83 const attributes = {};
84 for (const name of sourceAttributeNames) {
85 if (childSourceElement.hasAttribute(name)) {
86 // Don't collect <source /> attribute if already consumed somewhere else.
87 if (conversionApi.consumable.test(childSourceElement, { attributes: name })) {
88 attributes[name] = childSourceElement.getAttribute(name);
89 }
90 }
91 }
92 if (Object.keys(attributes).length) {
93 sources.set(childSourceElement, attributes);
94 }
95 }
96 }
97 const imgViewElement = imageUtils.findViewImgElement(pictureViewElement);
98 // Don't convert when a picture has no <img/> inside (it is broken).
99 if (!imgViewElement) {
100 return;
101 }
102 let modelImage = data.modelCursor.parent;
103 // - In case of an inline image (cursor parent in a <paragraph>), the <img/> must be converted right away
104 // because no converter handled it yet and otherwise there would be no model element to set the sources attribute on.
105 // - In case of a block image, the <figure class="image"> converter (in ImageBlockEditing) converts the
106 // <img/> right away on its own and the modelCursor is already inside an imageBlock and there's nothing special
107 // to do here.
108 if (!modelImage.is('element', 'imageBlock')) {
109 const conversionResult = conversionApi.convertItem(imgViewElement, data.modelCursor);
110 // Set image range as conversion result.
111 data.modelRange = conversionResult.modelRange;
112 // Continue conversion where image conversion ends.
113 data.modelCursor = conversionResult.modelCursor;
114 modelImage = first(conversionResult.modelRange.getItems());
115 }
116 conversionApi.consumable.consume(pictureViewElement, { name: true });
117 // Consume only these <source/> attributes that were actually collected and will be passed on
118 // to the image model element.
119 for (const [sourceElement, attributes] of sources) {
120 conversionApi.consumable.consume(sourceElement, { attributes: Object.keys(attributes) });
121 }
122 if (sources.size) {
123 conversionApi.writer.setAttribute('sources', Array.from(sources.values()), modelImage);
124 }
125 // Convert rest of the <picture> children as an image children. Other converters may want to consume them.
126 conversionApi.convertChildren(pictureViewElement, modelImage);
127 };
128 return dispatcher => {
129 dispatcher.on('element:picture', converter);
130 };
131}
132/**
133 * Converter used to convert the `srcset` model image attribute to the `srcset` and `sizes` attributes in the view.
134 *
135 * @internal
136 * @param imageType The type of the image.
137 */
138export function downcastSrcsetAttribute(imageUtils, imageType) {
139 const converter = (evt, data, conversionApi) => {
140 if (!conversionApi.consumable.consume(data.item, evt.name)) {
141 return;
142 }
143 const writer = conversionApi.writer;
144 const element = conversionApi.mapper.toViewElement(data.item);
145 const img = imageUtils.findViewImgElement(element);
146 if (data.attributeNewValue === null) {
147 writer.removeAttribute('srcset', img);
148 writer.removeAttribute('sizes', img);
149 }
150 else {
151 if (data.attributeNewValue) {
152 writer.setAttribute('srcset', data.attributeNewValue, img);
153 // Always outputting `100vw`. See https://github.com/ckeditor/ckeditor5-image/issues/2.
154 writer.setAttribute('sizes', '100vw', img);
155 }
156 }
157 };
158 return dispatcher => {
159 dispatcher.on(`attribute:srcset:${imageType}`, converter);
160 };
161}
162/**
163 * Converts the `source` model attribute to the `<picture><source /><source />...<img /></picture>`
164 * view structure.
165 *
166 * @internal
167 */
168export function downcastSourcesAttribute(imageUtils) {
169 const converter = (evt, data, conversionApi) => {
170 if (!conversionApi.consumable.consume(data.item, evt.name)) {
171 return;
172 }
173 const viewWriter = conversionApi.writer;
174 const element = conversionApi.mapper.toViewElement(data.item);
175 const imgElement = imageUtils.findViewImgElement(element);
176 const attributeNewValue = data.attributeNewValue;
177 if (attributeNewValue && attributeNewValue.length) {
178 // Make sure <picture> does not break attribute elements, for instance <a> in linked images.
179 const pictureElement = viewWriter.createContainerElement('picture', null, attributeNewValue.map(sourceAttributes => {
180 return viewWriter.createEmptyElement('source', sourceAttributes);
181 }));
182 // Collect all wrapping attribute elements.
183 const attributeElements = [];
184 let viewElement = imgElement.parent;
185 while (viewElement && viewElement.is('attributeElement')) {
186 const parentElement = viewElement.parent;
187 viewWriter.unwrap(viewWriter.createRangeOn(imgElement), viewElement);
188 attributeElements.unshift(viewElement);
189 viewElement = parentElement;
190 }
191 // Insert the picture and move img into it.
192 viewWriter.insert(viewWriter.createPositionBefore(imgElement), pictureElement);
193 viewWriter.move(viewWriter.createRangeOn(imgElement), viewWriter.createPositionAt(pictureElement, 'end'));
194 // Apply collected attribute elements over the new picture element.
195 for (const attributeElement of attributeElements) {
196 viewWriter.wrap(viewWriter.createRangeOn(pictureElement), attributeElement);
197 }
198 }
199 // Both setting "sources" to an empty array and removing the attribute should unwrap the <img />.
200 // Unwrap once if the latter followed the former, though.
201 else if (imgElement.parent.is('element', 'picture')) {
202 const pictureElement = imgElement.parent;
203 viewWriter.move(viewWriter.createRangeOn(imgElement), viewWriter.createPositionBefore(pictureElement));
204 viewWriter.remove(pictureElement);
205 }
206 };
207 return dispatcher => {
208 dispatcher.on('attribute:sources:imageBlock', converter);
209 dispatcher.on('attribute:sources:imageInline', converter);
210 };
211}
212/**
213 * Converter used to convert a given image attribute from the model to the view.
214 *
215 * @internal
216 * @param imageType The type of the image.
217 * @param attributeKey The name of the attribute to convert.
218 */
219export function downcastImageAttribute(imageUtils, imageType, attributeKey) {
220 const converter = (evt, data, conversionApi) => {
221 if (!conversionApi.consumable.consume(data.item, evt.name)) {
222 return;
223 }
224 const viewWriter = conversionApi.writer;
225 const element = conversionApi.mapper.toViewElement(data.item);
226 const img = imageUtils.findViewImgElement(element);
227 viewWriter.setAttribute(data.attributeKey, data.attributeNewValue || '', img);
228 };
229 return dispatcher => {
230 dispatcher.on(`attribute:${attributeKey}:${imageType}`, converter);
231 };
232}