UNPKG

@ckeditor/ckeditor5-image

Version:

Image feature for CKEditor 5.

178 lines (177 loc) • 8.63 kB
/** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module image/image/imageinlineediting */ import { Plugin } from 'ckeditor5/src/core.js'; import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js'; import { UpcastWriter } from 'ckeditor5/src/engine.js'; import { downcastImageAttribute, downcastSrcsetAttribute } from './converters.js'; import ImageEditing from './imageediting.js'; import ImageSizeAttributes from '../imagesizeattributes.js'; import ImageTypeCommand from './imagetypecommand.js'; import ImageUtils from '../imageutils.js'; import { getImgViewElementMatcher, createInlineImageViewElement, determineImageTypeForInsertionAtSelection } from './utils.js'; import ImagePlaceholder from './imageplaceholder.js'; /** * The image inline plugin. * * It registers: * * * `<imageInline>` as an inline element in the document schema, and allows `alt`, `src` and `srcset` attributes. * * converters for editing and data pipelines. * * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeInline'`} command that converts block images into * inline images. */ export default class ImageInlineEditing extends Plugin { /** * @inheritDoc */ static get requires() { return [ImageEditing, ImageSizeAttributes, ImageUtils, ImagePlaceholder, ClipboardPipeline]; } /** * @inheritDoc */ static get pluginName() { return 'ImageInlineEditing'; } /** * @inheritDoc */ init() { const editor = this.editor; const schema = editor.model.schema; // Converters 'alt' and 'srcset' are added in 'ImageEditing' plugin. schema.register('imageInline', { inheritAllFrom: '$inlineObject', allowAttributes: ['alt', 'src', 'srcset'] }); // Disallow inline images in captions (for now). This is the best spot to do that because // independent packages can introduce captions (ImageCaption, TableCaption, etc.) so better this // be future-proof. schema.addChildCheck((context, childDefinition) => { if (context.endsWith('caption') && childDefinition.name === 'imageInline') { return false; } }); this._setupConversion(); if (editor.plugins.has('ImageBlockEditing')) { editor.commands.add('imageTypeInline', new ImageTypeCommand(this.editor, 'imageInline')); this._setupClipboardIntegration(); } } /** * Configures conversion pipelines to support upcasting and downcasting * inline images (inline image widgets) and their attributes. */ _setupConversion() { const editor = this.editor; const t = editor.t; const conversion = editor.conversion; const imageUtils = editor.plugins.get('ImageUtils'); conversion.for('dataDowncast') .elementToElement({ model: 'imageInline', view: (modelElement, { writer }) => writer.createEmptyElement('img') }); conversion.for('editingDowncast') .elementToStructure({ model: 'imageInline', view: (modelElement, { writer }) => imageUtils.toImageWidget(createInlineImageViewElement(writer), writer, t('image widget')) }); conversion.for('downcast') .add(downcastImageAttribute(imageUtils, 'imageInline', 'src')) .add(downcastImageAttribute(imageUtils, 'imageInline', 'alt')) .add(downcastSrcsetAttribute(imageUtils, 'imageInline')); // More image related upcasts are in 'ImageEditing' plugin. conversion.for('upcast') .elementToElement({ view: getImgViewElementMatcher(editor, 'imageInline'), model: (viewImage, { writer }) => writer.createElement('imageInline', viewImage.hasAttribute('src') ? { src: viewImage.getAttribute('src') } : undefined) }); } /** * Integrates the plugin with the clipboard pipeline. * * Idea is that the feature should recognize the user's intent when an **block** image is * pasted or dropped. If such an image is pasted/dropped into a non-empty block * (e.g. a paragraph with some text) it gets converted into an inline image on the fly. * * We assume this is the user's intent if they decided to put their image there. * * **Note**: If a block image has a caption, it will not be converted to an inline image * to avoid the confusion. Captions are added on purpose and they should never be lost * in the clipboard pipeline. * * See the `ImageBlockEditing` for the similar integration that works in the opposite direction. * * The feature also sets image `width` and `height` attributes when pasting. */ _setupClipboardIntegration() { const editor = this.editor; const model = editor.model; const editingView = editor.editing.view; const imageUtils = editor.plugins.get('ImageUtils'); const clipboardPipeline = editor.plugins.get('ClipboardPipeline'); this.listenTo(clipboardPipeline, 'inputTransformation', (evt, data) => { const docFragmentChildren = Array.from(data.content.getChildren()); let modelRange; // Make sure only <figure class="image"></figure> elements are dropped or pasted. Otherwise, if there some other HTML // mixed up, this should be handled as a regular paste. if (!docFragmentChildren.every(imageUtils.isBlockImageView)) { return; } // When drag and dropping, data.targetRanges specifies where to drop because // this is usually a different place than the current model selection (the user // uses a drop marker to specify the drop location). if (data.targetRanges) { modelRange = editor.editing.mapper.toModelRange(data.targetRanges[0]); } // Pasting, however, always occurs at the current model selection. else { modelRange = model.document.selection.getFirstRange(); } const selection = model.createSelection(modelRange); // Convert block images into inline images only when pasting or dropping into non-empty blocks // and when the block is not an object (e.g. pasting to replace another widget). if (determineImageTypeForInsertionAtSelection(model.schema, selection) === 'imageInline') { const writer = new UpcastWriter(editingView.document); // Unwrap <figure class="image"><img .../></figure> -> <img ... /> // but <figure class="image"><img .../><figcaption>...</figcaption></figure> -> stays the same const inlineViewImages = docFragmentChildren.map(blockViewImage => { // If there's just one child, it can be either <img /> or <a><img></a>. // If there are other children than <img>, this means that the block image // has a caption or some other features and this kind of image should be // pasted/dropped without modifications. if (blockViewImage.childCount === 1) { // Pass the attributes which are present only in the <figure> to the <img> // (e.g. the style="width:10%" attribute applied by the ImageResize plugin). Array.from(blockViewImage.getAttributes()) .forEach(attribute => writer.setAttribute(...attribute, imageUtils.findViewImgElement(blockViewImage))); return blockViewImage.getChild(0); } else { return blockViewImage; } }); data.content = writer.createDocumentFragment(inlineViewImages); } }); this.listenTo(clipboardPipeline, 'contentInsertion', (evt, data) => { if (data.method !== 'paste') { return; } model.change(writer => { const range = writer.createRangeIn(data.content); for (const item of range.getItems()) { if (item.is('element', 'imageInline')) { imageUtils.setImageNaturalSizeAttributes(item); } } }); }); } }