UNPKG

@ckeditor/ckeditor5-image

Version:

Image feature for CKEditor 5.

160 lines (159 loc) • 7.1 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module image/image/imageblockediting */ import { Plugin } from 'ckeditor5/src/core.js'; import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js'; import { UpcastWriter } from 'ckeditor5/src/engine.js'; import { downcastImageAttribute, downcastSrcsetAttribute, upcastImageFigure } 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, createBlockImageViewElement, determineImageTypeForInsertionAtSelection } from './utils.js'; import ImagePlaceholder from './imageplaceholder.js'; /** * The image block plugin. * * It registers: * * * `<imageBlock>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes. * * converters for editing and data pipelines., * * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeBlock'`} command that converts inline images into * block images. */ export default class ImageBlockEditing extends Plugin { /** * @inheritDoc */ static get requires() { return [ImageEditing, ImageSizeAttributes, ImageUtils, ImagePlaceholder, ClipboardPipeline]; } /** * @inheritDoc */ static get pluginName() { return 'ImageBlockEditing'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ init() { const editor = this.editor; const schema = editor.model.schema; // Converters 'alt' and 'srcset' are added in 'ImageEditing' plugin. schema.register('imageBlock', { inheritAllFrom: '$blockObject', allowAttributes: ['alt', 'src', 'srcset'] }); this._setupConversion(); if (editor.plugins.has('ImageInlineEditing')) { editor.commands.add('imageTypeBlock', new ImageTypeCommand(this.editor, 'imageBlock')); this._setupClipboardIntegration(); } } /** * Configures conversion pipelines to support upcasting and downcasting * block images (block 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') .elementToStructure({ model: 'imageBlock', view: (modelElement, { writer }) => createBlockImageViewElement(writer) }); conversion.for('editingDowncast') .elementToStructure({ model: 'imageBlock', view: (modelElement, { writer }) => imageUtils.toImageWidget(createBlockImageViewElement(writer), writer, t('image widget')) }); conversion.for('downcast') .add(downcastImageAttribute(imageUtils, 'imageBlock', 'src')) .add(downcastImageAttribute(imageUtils, 'imageBlock', 'alt')) .add(downcastSrcsetAttribute(imageUtils, 'imageBlock')); // More image related upcasts are in 'ImageEditing' plugin. conversion.for('upcast') .elementToElement({ view: getImgViewElementMatcher(editor, 'imageBlock'), model: (viewImage, { writer }) => writer.createElement('imageBlock', viewImage.hasAttribute('src') ? { src: viewImage.getAttribute('src') } : undefined) }) .add(upcastImageFigure(imageUtils)); } /** * Integrates the plugin with the clipboard pipeline. * * Idea is that the feature should recognize the user's intent when an **inline** image is * pasted or dropped. If such an image is pasted/dropped: * * * into an empty block (e.g. an empty paragraph), * * on another object (e.g. some block widget). * * it gets converted into a block image on the fly. We assume this is the user's intent * if they decided to put their image there. * * See the `ImageInlineEditing` for the similar integration that works in the opposite direction. * * The feature also sets image `width` and `height` attributes on paste. */ _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 <img> 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.isInlineImageView)) { 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 inline images into block images only when the currently selected block is empty // (e.g. an empty paragraph) or some object is selected (to replace it). if (determineImageTypeForInsertionAtSelection(model.schema, selection) === 'imageBlock') { const writer = new UpcastWriter(editingView.document); // Wrap <img ... /> -> <figure class="image"><img .../></figure> const blockViewImages = docFragmentChildren.map(inlineViewImage => writer.createElement('figure', { class: 'image' }, inlineViewImage)); data.content = writer.createDocumentFragment(blockViewImages); } }); 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', 'imageBlock')) { imageUtils.setImageNaturalSizeAttributes(item); } } }); }); } }