UNPKG

5.5 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, 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 media-embed/automediaembed
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import { LiveRange, LivePosition } from 'ckeditor5/src/engine';
10import { Clipboard } from 'ckeditor5/src/clipboard';
11import { Delete } from 'ckeditor5/src/typing';
12import { Undo } from 'ckeditor5/src/undo';
13import { global } from 'ckeditor5/src/utils';
14import MediaEmbedEditing from './mediaembedediting';
15import { insertMedia } from './utils';
16const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
17/**
18 * The auto-media embed plugin. It recognizes media links in the pasted content and embeds
19 * them shortly after they are injected into the document.
20 */
21export default class AutoMediaEmbed extends Plugin {
22 /**
23 * @inheritDoc
24 */
25 static get requires() {
26 return [Clipboard, Delete, Undo];
27 }
28 /**
29 * @inheritDoc
30 */
31 static get pluginName() {
32 return 'AutoMediaEmbed';
33 }
34 /**
35 * @inheritDoc
36 */
37 constructor(editor) {
38 super(editor);
39 this._timeoutId = null;
40 this._positionToInsert = null;
41 }
42 /**
43 * @inheritDoc
44 */
45 init() {
46 const editor = this.editor;
47 const modelDocument = editor.model.document;
48 // We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
49 // After pasting, the content between those positions will be checked for a URL that could be transformed
50 // into media.
51 const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
52 this.listenTo(clipboardPipeline, 'inputTransformation', () => {
53 const firstRange = modelDocument.selection.getFirstRange();
54 const leftLivePosition = LivePosition.fromPosition(firstRange.start);
55 leftLivePosition.stickiness = 'toPrevious';
56 const rightLivePosition = LivePosition.fromPosition(firstRange.end);
57 rightLivePosition.stickiness = 'toNext';
58 modelDocument.once('change:data', () => {
59 this._embedMediaBetweenPositions(leftLivePosition, rightLivePosition);
60 leftLivePosition.detach();
61 rightLivePosition.detach();
62 }, { priority: 'high' });
63 });
64 const undoCommand = editor.commands.get('undo');
65 undoCommand.on('execute', () => {
66 if (this._timeoutId) {
67 global.window.clearTimeout(this._timeoutId);
68 this._positionToInsert.detach();
69 this._timeoutId = null;
70 this._positionToInsert = null;
71 }
72 }, { priority: 'high' });
73 }
74 /**
75 * Analyzes the part of the document between provided positions in search for a URL representing media.
76 * When the URL is found, it is automatically converted into media.
77 *
78 * @param leftPosition Left position of the selection.
79 * @param rightPosition Right position of the selection.
80 */
81 _embedMediaBetweenPositions(leftPosition, rightPosition) {
82 const editor = this.editor;
83 const mediaRegistry = editor.plugins.get(MediaEmbedEditing).registry;
84 // TODO: Use marker instead of LiveRange & LivePositions.
85 const urlRange = new LiveRange(leftPosition, rightPosition);
86 const walker = urlRange.getWalker({ ignoreElementEnd: true });
87 let url = '';
88 for (const node of walker) {
89 if (node.item.is('$textProxy')) {
90 url += node.item.data;
91 }
92 }
93 url = url.trim();
94 // If the URL does not match to universal URL regexp, let's skip that.
95 if (!url.match(URL_REGEXP)) {
96 urlRange.detach();
97 return;
98 }
99 // If the URL represents a media, let's use it.
100 if (!mediaRegistry.hasMedia(url)) {
101 urlRange.detach();
102 return;
103 }
104 const mediaEmbedCommand = editor.commands.get('mediaEmbed');
105 // Do not anything if media element cannot be inserted at the current position (#47).
106 if (!mediaEmbedCommand.isEnabled) {
107 urlRange.detach();
108 return;
109 }
110 // Position won't be available in the `setTimeout` function so let's clone it.
111 this._positionToInsert = LivePosition.fromPosition(leftPosition);
112 // This action mustn't be executed if undo was called between pasting and auto-embedding.
113 this._timeoutId = global.window.setTimeout(() => {
114 editor.model.change(writer => {
115 this._timeoutId = null;
116 writer.remove(urlRange);
117 urlRange.detach();
118 let insertionPosition = null;
119 // Check if position where the media element should be inserted is still valid.
120 // Otherwise leave it as undefined to use document.selection - default behavior of model.insertContent().
121 if (this._positionToInsert.root.rootName !== '$graveyard') {
122 insertionPosition = this._positionToInsert;
123 }
124 insertMedia(editor.model, url, insertionPosition, false);
125 this._positionToInsert.detach();
126 this._positionToInsert = null;
127 });
128 editor.plugins.get(Delete).requestUndoOnBackspace();
129 }, 100);
130 }
131}