UNPKG

7 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/imageblockediting
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, upcastImageFigure } 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, createBlockImageViewElement, determineImageTypeForInsertionAtSelection } from './utils.js';
17import ImagePlaceholder from './imageplaceholder.js';
18/**
19 * The image block plugin.
20 *
21 * It registers:
22 *
23 * * `<imageBlock>` as a block 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 `'imageTypeBlock'`} command that converts inline images into
26 * block images.
27 */
28export default class ImageBlockEditing 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 'ImageBlockEditing';
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('imageBlock', {
49 inheritAllFrom: '$blockObject',
50 allowAttributes: ['alt', 'src', 'srcset']
51 });
52 this._setupConversion();
53 if (editor.plugins.has('ImageInlineEditing')) {
54 editor.commands.add('imageTypeBlock', new ImageTypeCommand(this.editor, 'imageBlock'));
55 this._setupClipboardIntegration();
56 }
57 }
58 /**
59 * Configures conversion pipelines to support upcasting and downcasting
60 * block images (block image widgets) and their attributes.
61 */
62 _setupConversion() {
63 const editor = this.editor;
64 const t = editor.t;
65 const conversion = editor.conversion;
66 const imageUtils = editor.plugins.get('ImageUtils');
67 conversion.for('dataDowncast')
68 .elementToStructure({
69 model: 'imageBlock',
70 view: (modelElement, { writer }) => createBlockImageViewElement(writer)
71 });
72 conversion.for('editingDowncast')
73 .elementToStructure({
74 model: 'imageBlock',
75 view: (modelElement, { writer }) => imageUtils.toImageWidget(createBlockImageViewElement(writer), writer, t('image widget'))
76 });
77 conversion.for('downcast')
78 .add(downcastImageAttribute(imageUtils, 'imageBlock', 'src'))
79 .add(downcastImageAttribute(imageUtils, 'imageBlock', 'alt'))
80 .add(downcastSrcsetAttribute(imageUtils, 'imageBlock'));
81 // More image related upcasts are in 'ImageEditing' plugin.
82 conversion.for('upcast')
83 .elementToElement({
84 view: getImgViewElementMatcher(editor, 'imageBlock'),
85 model: (viewImage, { writer }) => writer.createElement('imageBlock', viewImage.hasAttribute('src') ? { src: viewImage.getAttribute('src') } : undefined)
86 })
87 .add(upcastImageFigure(imageUtils));
88 }
89 /**
90 * Integrates the plugin with the clipboard pipeline.
91 *
92 * Idea is that the feature should recognize the user's intent when an **inline** image is
93 * pasted or dropped. If such an image is pasted/dropped:
94 *
95 * * into an empty block (e.g. an empty paragraph),
96 * * on another object (e.g. some block widget).
97 *
98 * it gets converted into a block image on the fly. We assume this is the user's intent
99 * if they decided to put their image there.
100 *
101 * See the `ImageInlineEditing` for the similar integration that works in the opposite direction.
102 *
103 * The feature also sets image `width` and `height` attributes on paste.
104 */
105 _setupClipboardIntegration() {
106 const editor = this.editor;
107 const model = editor.model;
108 const editingView = editor.editing.view;
109 const imageUtils = editor.plugins.get('ImageUtils');
110 const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
111 this.listenTo(clipboardPipeline, 'inputTransformation', (evt, data) => {
112 const docFragmentChildren = Array.from(data.content.getChildren());
113 let modelRange;
114 // Make sure only <img> elements are dropped or pasted. Otherwise, if there some other HTML
115 // mixed up, this should be handled as a regular paste.
116 if (!docFragmentChildren.every(imageUtils.isInlineImageView)) {
117 return;
118 }
119 // When drag and dropping, data.targetRanges specifies where to drop because
120 // this is usually a different place than the current model selection (the user
121 // uses a drop marker to specify the drop location).
122 if (data.targetRanges) {
123 modelRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
124 }
125 // Pasting, however, always occurs at the current model selection.
126 else {
127 modelRange = model.document.selection.getFirstRange();
128 }
129 const selection = model.createSelection(modelRange);
130 // Convert inline images into block images only when the currently selected block is empty
131 // (e.g. an empty paragraph) or some object is selected (to replace it).
132 if (determineImageTypeForInsertionAtSelection(model.schema, selection) === 'imageBlock') {
133 const writer = new UpcastWriter(editingView.document);
134 // Wrap <img ... /> -> <figure class="image"><img .../></figure>
135 const blockViewImages = docFragmentChildren.map(inlineViewImage => writer.createElement('figure', { class: 'image' }, inlineViewImage));
136 data.content = writer.createDocumentFragment(blockViewImages);
137 }
138 });
139 this.listenTo(clipboardPipeline, 'contentInsertion', (evt, data) => {
140 if (data.method !== 'paste') {
141 return;
142 }
143 model.change(writer => {
144 const range = writer.createRangeIn(data.content);
145 for (const item of range.getItems()) {
146 if (item.is('element', 'imageBlock')) {
147 imageUtils.setImageNaturalSizeAttributes(item);
148 }
149 }
150 });
151 });
152 }
153}