@ckeditor/ckeditor5-image
Version:
Image feature for CKEditor 5.
133 lines (132 loc) • 5.87 kB
JavaScript
/**
* @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);
}
}