UNPKG

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