UNPKG

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