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 | */
|
8 | import { Plugin } from 'ckeditor5/src/core.js';
|
9 | import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
|
10 | import { UpcastWriter } from 'ckeditor5/src/engine.js';
|
11 | import { downcastImageAttribute, downcastSrcsetAttribute } from './converters.js';
|
12 | import ImageEditing from './imageediting.js';
|
13 | import ImageSizeAttributes from '../imagesizeattributes.js';
|
14 | import ImageTypeCommand from './imagetypecommand.js';
|
15 | import ImageUtils from '../imageutils.js';
|
16 | import { getImgViewElementMatcher, createInlineImageViewElement, determineImageTypeForInsertionAtSelection } from './utils.js';
|
17 | import 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 | */
|
28 | export 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 | }
|