UNPKG

5.89 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/autoimage
7 */
8import { Plugin } from 'ckeditor5/src/core.js';
9import { Clipboard } from 'ckeditor5/src/clipboard.js';
10import { LivePosition, LiveRange } from 'ckeditor5/src/engine.js';
11import { Undo } from 'ckeditor5/src/undo.js';
12import { Delete } from 'ckeditor5/src/typing.js';
13import { global } from 'ckeditor5/src/utils.js';
14import ImageUtils from './imageutils.js';
15// Implements the pattern: http(s)://(www.)example.com/path/to/resource.ext?query=params&maybe=too.
16const IMAGE_URL_REGEXP = new RegExp(String(/^(http(s)?:\/\/)?[\w-]+\.[\w.~:/[\]@!$&'()*+,;=%-]+/.source +
17 /\.(jpg|jpeg|png|gif|ico|webp|JPG|JPEG|PNG|GIF|ICO|WEBP)/.source +
18 /(\?[\w.~:/[\]@!$&'()*+,;=%-]*)?/.source +
19 /(#[\w.~:/[\]@!$&'()*+,;=%-]*)?$/.source));
20/**
21 * The auto-image plugin. It recognizes image links in the pasted content and embeds
22 * them shortly after they are injected into the document.
23 */
24export default class AutoImage extends Plugin {
25 /**
26 * @inheritDoc
27 */
28 static get requires() {
29 return [Clipboard, ImageUtils, Undo, Delete];
30 }
31 /**
32 * @inheritDoc
33 */
34 static get pluginName() {
35 return 'AutoImage';
36 }
37 /**
38 * @inheritDoc
39 */
40 constructor(editor) {
41 super(editor);
42 this._timeoutId = null;
43 this._positionToInsert = null;
44 }
45 /**
46 * @inheritDoc
47 */
48 init() {
49 const editor = this.editor;
50 const modelDocument = editor.model.document;
51 const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
52 // We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
53 // After pasting, the content between those positions will be checked for a URL that could be transformed
54 // into an image.
55 this.listenTo(clipboardPipeline, 'inputTransformation', () => {
56 const firstRange = modelDocument.selection.getFirstRange();
57 const leftLivePosition = LivePosition.fromPosition(firstRange.start);
58 leftLivePosition.stickiness = 'toPrevious';
59 const rightLivePosition = LivePosition.fromPosition(firstRange.end);
60 rightLivePosition.stickiness = 'toNext';
61 modelDocument.once('change:data', () => {
62 this._embedImageBetweenPositions(leftLivePosition, rightLivePosition);
63 leftLivePosition.detach();
64 rightLivePosition.detach();
65 }, { priority: 'high' });
66 });
67 editor.commands.get('undo').on('execute', () => {
68 if (this._timeoutId) {
69 global.window.clearTimeout(this._timeoutId);
70 this._positionToInsert.detach();
71 this._timeoutId = null;
72 this._positionToInsert = null;
73 }
74 }, { priority: 'high' });
75 }
76 /**
77 * Analyzes the part of the document between provided positions in search for a URL representing an image.
78 * When the URL is found, it is automatically converted into an image.
79 *
80 * @param leftPosition Left position of the selection.
81 * @param rightPosition Right position of the selection.
82 */
83 _embedImageBetweenPositions(leftPosition, rightPosition) {
84 const editor = this.editor;
85 // TODO: Use a marker instead of LiveRange & LivePositions.
86 const urlRange = new LiveRange(leftPosition, rightPosition);
87 const walker = urlRange.getWalker({ ignoreElementEnd: true });
88 const selectionAttributes = Object.fromEntries(editor.model.document.selection.getAttributes());
89 const imageUtils = this.editor.plugins.get('ImageUtils');
90 let src = '';
91 for (const node of walker) {
92 if (node.item.is('$textProxy')) {
93 src += node.item.data;
94 }
95 }
96 src = src.trim();
97 // If the URL does not match the image URL regexp, let's skip that.
98 if (!src.match(IMAGE_URL_REGEXP)) {
99 urlRange.detach();
100 return;
101 }
102 // Position will not be available in the `setTimeout` function so let's clone it.
103 this._positionToInsert = LivePosition.fromPosition(leftPosition);
104 // This action mustn't be executed if undo was called between pasting and auto-embedding.
105 this._timeoutId = setTimeout(() => {
106 // Do nothing if image element cannot be inserted at the current position.
107 // See https://github.com/ckeditor/ckeditor5/issues/2763.
108 // Condition must be checked after timeout - pasting may take place on an element, replacing it. The final position matters.
109 const imageCommand = editor.commands.get('insertImage');
110 if (!imageCommand.isEnabled) {
111 urlRange.detach();
112 return;
113 }
114 editor.model.change(writer => {
115 this._timeoutId = null;
116 writer.remove(urlRange);
117 urlRange.detach();
118 let insertionPosition;
119 // Check if the position where the element should be inserted is still valid.
120 // Otherwise leave it as undefined to use the logic of insertImage().
121 if (this._positionToInsert.root.rootName !== '$graveyard') {
122 insertionPosition = this._positionToInsert.toPosition();
123 }
124 imageUtils.insertImage({ ...selectionAttributes, src }, insertionPosition);
125 this._positionToInsert.detach();
126 this._positionToInsert = null;
127 });
128 const deletePlugin = editor.plugins.get('Delete');
129 deletePlugin.requestUndoOnBackspace();
130 }, 100);
131 }
132}