UNPKG

@ckeditor/ckeditor5-image

Version:

Image feature for CKEditor 5.

133 lines (132 loc) 5.87 kB
/** * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module image/autoimage */ import { Plugin } from 'ckeditor5/src/core'; import { Clipboard } from 'ckeditor5/src/clipboard'; import { LivePosition, LiveRange } from 'ckeditor5/src/engine'; import { Undo } from 'ckeditor5/src/undo'; import { Delete } from 'ckeditor5/src/typing'; import { global } from 'ckeditor5/src/utils'; import ImageUtils from './imageutils'; // Implements the pattern: http(s)://(www.)example.com/path/to/resource.ext?query=params&maybe=too. const IMAGE_URL_REGEXP = new RegExp(String(/^(http(s)?:\/\/)?[\w-]+\.[\w.~:/[\]@!$&'()*+,;=%-]+/.source + /\.(jpg|jpeg|png|gif|ico|webp|JPG|JPEG|PNG|GIF|ICO|WEBP)/.source + /(\?[\w.~:/[\]@!$&'()*+,;=%-]*)?/.source + /(#[\w.~:/[\]@!$&'()*+,;=%-]*)?$/.source)); /** * The auto-image plugin. It recognizes image links in the pasted content and embeds * them shortly after they are injected into the document. */ export default class AutoImage extends Plugin { /** * @inheritDoc */ static get requires() { return [Clipboard, ImageUtils, Undo, Delete]; } /** * @inheritDoc */ static get pluginName() { return 'AutoImage'; } /** * @inheritDoc */ constructor(editor) { super(editor); this._timeoutId = null; this._positionToInsert = null; } /** * @inheritDoc */ init() { const editor = this.editor; const modelDocument = editor.model.document; const clipboardPipeline = editor.plugins.get('ClipboardPipeline'); // We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection. // After pasting, the content between those positions will be checked for a URL that could be transformed // into an image. this.listenTo(clipboardPipeline, 'inputTransformation', () => { const firstRange = modelDocument.selection.getFirstRange(); const leftLivePosition = LivePosition.fromPosition(firstRange.start); leftLivePosition.stickiness = 'toPrevious'; const rightLivePosition = LivePosition.fromPosition(firstRange.end); rightLivePosition.stickiness = 'toNext'; modelDocument.once('change:data', () => { this._embedImageBetweenPositions(leftLivePosition, rightLivePosition); leftLivePosition.detach(); rightLivePosition.detach(); }, { priority: 'high' }); }); editor.commands.get('undo').on('execute', () => { if (this._timeoutId) { global.window.clearTimeout(this._timeoutId); this._positionToInsert.detach(); this._timeoutId = null; this._positionToInsert = null; } }, { priority: 'high' }); } /** * Analyzes the part of the document between provided positions in search for a URL representing an image. * When the URL is found, it is automatically converted into an image. * * @param leftPosition Left position of the selection. * @param rightPosition Right position of the selection. */ _embedImageBetweenPositions(leftPosition, rightPosition) { const editor = this.editor; // TODO: Use a marker instead of LiveRange & LivePositions. const urlRange = new LiveRange(leftPosition, rightPosition); const walker = urlRange.getWalker({ ignoreElementEnd: true }); const selectionAttributes = Object.fromEntries(editor.model.document.selection.getAttributes()); const imageUtils = this.editor.plugins.get('ImageUtils'); let src = ''; for (const node of walker) { if (node.item.is('$textProxy')) { src += node.item.data; } } src = src.trim(); // If the URL does not match the image URL regexp, let's skip that. if (!src.match(IMAGE_URL_REGEXP)) { urlRange.detach(); return; } // Position will not be available in the `setTimeout` function so let's clone it. this._positionToInsert = LivePosition.fromPosition(leftPosition); // This action mustn't be executed if undo was called between pasting and auto-embedding. this._timeoutId = setTimeout(() => { // Do nothing if image element cannot be inserted at the current position. // See https://github.com/ckeditor/ckeditor5/issues/2763. // Condition must be checked after timeout - pasting may take place on an element, replacing it. The final position matters. const imageCommand = editor.commands.get('insertImage'); if (!imageCommand.isEnabled) { urlRange.detach(); return; } editor.model.change(writer => { this._timeoutId = null; writer.remove(urlRange); urlRange.detach(); let insertionPosition; // Check if the position where the element should be inserted is still valid. // Otherwise leave it as undefined to use the logic of insertImage(). if (this._positionToInsert.root.rootName !== '$graveyard') { insertionPosition = this._positionToInsert.toPosition(); } imageUtils.insertImage({ ...selectionAttributes, src }, insertionPosition); this._positionToInsert.detach(); this._positionToInsert = null; }); const deletePlugin = editor.plugins.get('Delete'); deletePlugin.requestUndoOnBackspace(); }, 100); } }