UNPKG

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