UNPKG

7.08 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/imageinlineediting
8 */
9
10import { Plugin } from 'ckeditor5/src/core';
11import { ClipboardPipeline } from 'ckeditor5/src/clipboard';
12import { UpcastWriter } from 'ckeditor5/src/engine';
13
14import {
15 downcastImageAttribute,
16 downcastSrcsetAttribute
17} from './converters';
18
19import ImageEditing from './imageediting';
20import ImageTypeCommand from './imagetypecommand';
21import ImageUtils from '../imageutils';
22import {
23 getImgViewElementMatcher,
24 createInlineImageViewElement,
25 determineImageTypeForInsertionAtSelection
26} from '../image/utils';
27
28/**
29 * The image inline plugin.
30 *
31 * It registers:
32 *
33 * * `<imageInline>` as an inline element in the document schema, and allows `alt`, `src` and `srcset` attributes.
34 * * converters for editing and data pipelines.
35 * * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeInline'`} command that converts block images into
36 * inline images.
37 *
38 * @extends module:core/plugin~Plugin
39 */
40export default class ImageInlineEditing extends Plugin {
41 /**
42 * @inheritDoc
43 */
44 static get requires() {
45 return [ ImageEditing, ImageUtils, ClipboardPipeline ];
46 }
47
48 /**
49 * @inheritDoc
50 */
51 static get pluginName() {
52 return 'ImageInlineEditing';
53 }
54
55 /**
56 * @inheritDoc
57 */
58 init() {
59 const editor = this.editor;
60 const schema = editor.model.schema;
61
62 // Converters 'alt' and 'srcset' are added in 'ImageEditing' plugin.
63 schema.register( 'imageInline', {
64 inheritAllFrom: '$inlineObject',
65 allowAttributes: [ 'alt', 'src', 'srcset' ]
66 } );
67
68 // Disallow inline images in captions (for now). This is the best spot to do that because
69 // independent packages can introduce captions (ImageCaption, TableCaption, etc.) so better this
70 // be future-proof.
71 schema.addChildCheck( ( context, childDefinition ) => {
72 if ( context.endsWith( 'caption' ) && childDefinition.name === 'imageInline' ) {
73 return false;
74 }
75 } );
76
77 this._setupConversion();
78
79 if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
80 editor.commands.add( 'imageTypeInline', new ImageTypeCommand( this.editor, 'imageInline' ) );
81
82 this._setupClipboardIntegration();
83 }
84 }
85
86 /**
87 * Configures conversion pipelines to support upcasting and downcasting
88 * inline images (inline image widgets) and their attributes.
89 *
90 * @private
91 */
92 _setupConversion() {
93 const editor = this.editor;
94 const t = editor.t;
95 const conversion = editor.conversion;
96 const imageUtils = editor.plugins.get( 'ImageUtils' );
97
98 conversion.for( 'dataDowncast' )
99 .elementToElement( {
100 model: 'imageInline',
101 view: ( modelElement, { writer } ) => writer.createEmptyElement( 'img' )
102 } );
103
104 conversion.for( 'editingDowncast' )
105 .elementToStructure( {
106 model: 'imageInline',
107 view: ( modelElement, { writer } ) => imageUtils.toImageWidget(
108 createInlineImageViewElement( writer ), writer, t( 'image widget' )
109 )
110 } );
111
112 conversion.for( 'downcast' )
113 .add( downcastImageAttribute( imageUtils, 'imageInline', 'src' ) )
114 .add( downcastImageAttribute( imageUtils, 'imageInline', 'alt' ) )
115 .add( downcastSrcsetAttribute( imageUtils, 'imageInline' ) );
116
117 // More image related upcasts are in 'ImageEditing' plugin.
118 conversion.for( 'upcast' )
119 .elementToElement( {
120 view: getImgViewElementMatcher( editor, 'imageInline' ),
121 model: ( viewImage, { writer } ) => writer.createElement(
122 'imageInline',
123 viewImage.hasAttribute( 'src' ) ? { src: viewImage.getAttribute( 'src' ) } : null
124 )
125 } );
126 }
127
128 /**
129 * Integrates the plugin with the clipboard pipeline.
130 *
131 * Idea is that the feature should recognize the user's intent when an **block** image is
132 * pasted or dropped. If such an image is pasted/dropped into a non-empty block
133 * (e.g. a paragraph with some text) it gets converted into an inline image on the fly.
134 *
135 * We assume this is the user's intent if they decided to put their image there.
136 *
137 * **Note**: If a block image has a caption, it will not be converted to an inline image
138 * to avoid the confusion. Captions are added on purpose and they should never be lost
139 * in the clipboard pipeline.
140 *
141 * See the `ImageBlockEditing` for the similar integration that works in the opposite direction.
142 *
143 * @private
144 */
145 _setupClipboardIntegration() {
146 const editor = this.editor;
147 const model = editor.model;
148 const editingView = editor.editing.view;
149 const imageUtils = editor.plugins.get( 'ImageUtils' );
150
151 this.listenTo( editor.plugins.get( 'ClipboardPipeline' ), 'inputTransformation', ( evt, data ) => {
152 const docFragmentChildren = Array.from( data.content.getChildren() );
153 let modelRange;
154
155 // Make sure only <figure class="image"></figure> elements are dropped or pasted. Otherwise, if there some other HTML
156 // mixed up, this should be handled as a regular paste.
157 if ( !docFragmentChildren.every( imageUtils.isBlockImageView ) ) {
158 return;
159 }
160
161 // When drag and dropping, data.targetRanges specifies where to drop because
162 // this is usually a different place than the current model selection (the user
163 // uses a drop marker to specify the drop location).
164 if ( data.targetRanges ) {
165 modelRange = editor.editing.mapper.toModelRange( data.targetRanges[ 0 ] );
166 }
167 // Pasting, however, always occurs at the current model selection.
168 else {
169 modelRange = model.document.selection.getFirstRange();
170 }
171
172 const selection = model.createSelection( modelRange );
173
174 // Convert block images into inline images only when pasting or dropping into non-empty blocks
175 // and when the block is not an object (e.g. pasting to replace another widget).
176 if ( determineImageTypeForInsertionAtSelection( model.schema, selection ) === 'imageInline' ) {
177 const writer = new UpcastWriter( editingView.document );
178
179 // Unwrap <figure class="image"><img .../></figure> -> <img ... />
180 // but <figure class="image"><img .../><figcaption>...</figcaption></figure> -> stays the same
181 const inlineViewImages = docFragmentChildren.map( blockViewImage => {
182 // If there's just one child, it can be either <img /> or <a><img></a>.
183 // If there are other children than <img>, this means that the block image
184 // has a caption or some other features and this kind of image should be
185 // pasted/dropped without modifications.
186 if ( blockViewImage.childCount === 1 ) {
187 // Pass the attributes which are present only in the <figure> to the <img>
188 // (e.g. the style="width:10%" attribute applied by the ImageResize plugin).
189 Array.from( blockViewImage.getAttributes() )
190 .forEach( attribute => writer.setAttribute(
191 ...attribute,
192 imageUtils.findViewImgElement( blockViewImage )
193 ) );
194
195 return blockViewImage.getChild( 0 );
196 } else {
197 return blockViewImage;
198 }
199 } );
200
201 data.content = writer.createDocumentFragment( inlineViewImages );
202 }
203 } );
204 }
205}