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/mediaregistry
|
8 | */
|
9 |
|
10 | import { TooltipView, IconView, Template } from 'ckeditor5/src/ui';
|
11 | import { logWarning, toArray } from 'ckeditor5/src/utils';
|
12 |
|
13 | import mediaPlaceholderIcon from '../theme/icons/media-placeholder.svg';
|
14 |
|
15 | const mediaPlaceholderIconViewBox = '0 0 64 42';
|
16 |
|
17 | /**
|
18 | * A bridge between the raw media content provider definitions and the editor view content.
|
19 | *
|
20 | * It helps translating media URLs to corresponding {@link module:engine/view/element~Element view elements}.
|
21 | *
|
22 | * Mostly used by the {@link module:media-embed/mediaembedediting~MediaEmbedEditing} plugin.
|
23 | */
|
24 | export default class MediaRegistry {
|
25 | /**
|
26 | * Creates an instance of the {@link module:media-embed/mediaregistry~MediaRegistry} class.
|
27 | *
|
28 | * @param {module:utils/locale~Locale} locale The localization services instance.
|
29 | * @param {module:media-embed/mediaembed~MediaEmbedConfig} config The configuration of the media embed feature.
|
30 | */
|
31 | constructor( locale, config ) {
|
32 | const providers = config.providers;
|
33 | const extraProviders = config.extraProviders || [];
|
34 | const removedProviders = new Set( config.removeProviders );
|
35 | const providerDefinitions = providers
|
36 | .concat( extraProviders )
|
37 | .filter( provider => {
|
38 | const name = provider.name;
|
39 |
|
40 | if ( !name ) {
|
41 | /**
|
42 | * One of the providers (or extra providers) specified in the media embed configuration
|
43 | * has no name and will not be used by the editor. In order to get this media
|
44 | * provider working, double check your editor configuration.
|
45 | *
|
46 | * @error media-embed-no-provider-name
|
47 | */
|
48 | logWarning( 'media-embed-no-provider-name', { provider } );
|
49 |
|
50 | return false;
|
51 | }
|
52 |
|
53 | return !removedProviders.has( name );
|
54 | } );
|
55 |
|
56 | /**
|
57 | * The {@link module:utils/locale~Locale} instance.
|
58 | *
|
59 | * @member {module:utils/locale~Locale}
|
60 | */
|
61 | this.locale = locale;
|
62 |
|
63 | /**
|
64 | * The media provider definitions available for the registry. Usually corresponding with the
|
65 | * {@link module:media-embed/mediaembed~MediaEmbedConfig media configuration}.
|
66 | *
|
67 | * @member {Array}
|
68 | */
|
69 | this.providerDefinitions = providerDefinitions;
|
70 | }
|
71 |
|
72 | /**
|
73 | * Checks whether the passed URL is representing a certain media type allowed in the editor.
|
74 | *
|
75 | * @param {String} url The URL to be checked
|
76 | * @returns {Boolean}
|
77 | */
|
78 | hasMedia( url ) {
|
79 | return !!this._getMedia( url );
|
80 | }
|
81 |
|
82 | /**
|
83 | * For the given media URL string and options, it returns the {@link module:engine/view/element~Element view element}
|
84 | * representing that media.
|
85 | *
|
86 | * **Note:** If no URL is specified, an empty view element is returned.
|
87 | *
|
88 | * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer used to produce a view element.
|
89 | * @param {String} url The URL to be translated into a view element.
|
90 | * @param {Object} options
|
91 | * @param {String} [options.elementName]
|
92 | * @param {Boolean} [options.renderMediaPreview]
|
93 | * @param {Boolean} [options.renderForEditingView]
|
94 | * @returns {module:engine/view/element~Element}
|
95 | */
|
96 | getMediaViewElement( writer, url, options ) {
|
97 | return this._getMedia( url ).getViewElement( writer, options );
|
98 | }
|
99 |
|
100 | /**
|
101 | * Returns a `Media` instance for the given URL.
|
102 | *
|
103 | * @protected
|
104 | * @param {String} url The URL of the media.
|
105 | * @returns {module:media-embed/mediaregistry~Media|null} The `Media` instance or `null` when there is none.
|
106 | */
|
107 | _getMedia( url ) {
|
108 | if ( !url ) {
|
109 | return new Media( this.locale );
|
110 | }
|
111 |
|
112 | url = url.trim();
|
113 |
|
114 | for ( const definition of this.providerDefinitions ) {
|
115 | const previewRenderer = definition.html;
|
116 | const pattern = toArray( definition.url );
|
117 |
|
118 | for ( const subPattern of pattern ) {
|
119 | const match = this._getUrlMatches( url, subPattern );
|
120 |
|
121 | if ( match ) {
|
122 | return new Media( this.locale, url, match, previewRenderer );
|
123 | }
|
124 | }
|
125 | }
|
126 |
|
127 | return null;
|
128 | }
|
129 |
|
130 | /**
|
131 | * Tries to match `url` to `pattern`.
|
132 | *
|
133 | * @private
|
134 | * @param {String} url The URL of the media.
|
135 | * @param {RegExp} pattern The pattern that should accept the media URL.
|
136 | * @returns {Array|null}
|
137 | */
|
138 | _getUrlMatches( url, pattern ) {
|
139 | // 1. Try to match without stripping the protocol and "www" subdomain.
|
140 | let match = url.match( pattern );
|
141 |
|
142 | if ( match ) {
|
143 | return match;
|
144 | }
|
145 |
|
146 | // 2. Try to match after stripping the protocol.
|
147 | let rawUrl = url.replace( /^https?:\/\//, '' );
|
148 | match = rawUrl.match( pattern );
|
149 |
|
150 | if ( match ) {
|
151 | return match;
|
152 | }
|
153 |
|
154 | // 3. Try to match after stripping the "www" subdomain.
|
155 | rawUrl = rawUrl.replace( /^www\./, '' );
|
156 | match = rawUrl.match( pattern );
|
157 |
|
158 | if ( match ) {
|
159 | return match;
|
160 | }
|
161 |
|
162 | return null;
|
163 | }
|
164 | }
|
165 |
|
166 | /**
|
167 | * Represents media defined by the provider configuration.
|
168 | *
|
169 | * It can be rendered to the {@link module:engine/view/element~Element view element} and used in the editing or data pipeline.
|
170 | *
|
171 | * @private
|
172 | */
|
173 | class Media {
|
174 | constructor( locale, url, match, previewRenderer ) {
|
175 | /**
|
176 | * The URL this Media instance represents.
|
177 | *
|
178 | * @member {String}
|
179 | */
|
180 | this.url = this._getValidUrl( url );
|
181 |
|
182 | /**
|
183 | * Shorthand for {@link module:utils/locale~Locale#t}.
|
184 | *
|
185 | * @see module:utils/locale~Locale#t
|
186 | * @method
|
187 | */
|
188 | this._t = locale.t;
|
189 |
|
190 | /**
|
191 | * The output of the `RegExp.match` which validated the {@link #url} of this media.
|
192 | *
|
193 | * @member {Object}
|
194 | */
|
195 | this._match = match;
|
196 |
|
197 | /**
|
198 | * The function returning the HTML string preview of this media.
|
199 | *
|
200 | * @member {Function}
|
201 | */
|
202 | this._previewRenderer = previewRenderer;
|
203 | }
|
204 |
|
205 | /**
|
206 | * Returns the view element representation of the media.
|
207 | *
|
208 | * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer used to produce a view element.
|
209 | * @param {Object} options
|
210 | * @param {String} [options.elementName]
|
211 | * @param {Boolean} [options.renderMediaPreview]
|
212 | * @param {Boolean} [options.renderForEditingView]
|
213 | * @returns {module:engine/view/element~Element}
|
214 | */
|
215 | getViewElement( writer, options ) {
|
216 | const attributes = {};
|
217 | let viewElement;
|
218 |
|
219 | if ( options.renderForEditingView || ( options.renderMediaPreview && this.url && this._previewRenderer ) ) {
|
220 | if ( this.url ) {
|
221 | attributes[ 'data-oembed-url' ] = this.url;
|
222 | }
|
223 |
|
224 | if ( options.renderForEditingView ) {
|
225 | attributes.class = 'ck-media__wrapper';
|
226 | }
|
227 |
|
228 | const mediaHtml = this._getPreviewHtml( options );
|
229 |
|
230 | viewElement = writer.createRawElement( 'div', attributes, ( domElement, domConverter ) => {
|
231 | domConverter.setContentOf( domElement, mediaHtml );
|
232 | } );
|
233 | } else {
|
234 | if ( this.url ) {
|
235 | attributes.url = this.url;
|
236 | }
|
237 |
|
238 | viewElement = writer.createEmptyElement( options.elementName, attributes );
|
239 | }
|
240 |
|
241 | writer.setCustomProperty( 'media-content', true, viewElement );
|
242 |
|
243 | return viewElement;
|
244 | }
|
245 |
|
246 | /**
|
247 | * Returns the HTML string of the media content preview.
|
248 | *
|
249 | * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer used to produce a view element.
|
250 | * @param {Object} options
|
251 | * @param {Boolean} [options.renderForEditingView]
|
252 | * @returns {String}
|
253 | */
|
254 | _getPreviewHtml( options ) {
|
255 | if ( this._previewRenderer ) {
|
256 | return this._previewRenderer( this._match );
|
257 | } else {
|
258 | // The placeholder only makes sense for editing view and media which have URLs.
|
259 | // Placeholder is never displayed in data and URL-less media have no content.
|
260 | if ( this.url && options.renderForEditingView ) {
|
261 | return this._getPlaceholderHtml();
|
262 | }
|
263 |
|
264 | return '';
|
265 | }
|
266 | }
|
267 |
|
268 | /**
|
269 | * Returns the placeholder HTML when the media has no content preview.
|
270 | *
|
271 | * @returns {String}
|
272 | */
|
273 | _getPlaceholderHtml() {
|
274 | const tooltip = new TooltipView();
|
275 | const icon = new IconView();
|
276 |
|
277 | tooltip.text = this._t( 'Open media in new tab' );
|
278 | icon.content = mediaPlaceholderIcon;
|
279 | icon.viewBox = mediaPlaceholderIconViewBox;
|
280 |
|
281 | const placeholder = new Template( {
|
282 | tag: 'div',
|
283 | attributes: {
|
284 | class: 'ck ck-reset_all ck-media__placeholder'
|
285 | },
|
286 | children: [
|
287 | {
|
288 | tag: 'div',
|
289 | attributes: {
|
290 | class: 'ck-media__placeholder__icon'
|
291 | },
|
292 | children: [ icon ]
|
293 | },
|
294 | {
|
295 | tag: 'a',
|
296 | attributes: {
|
297 | class: 'ck-media__placeholder__url',
|
298 | target: '_blank',
|
299 | rel: 'noopener noreferrer',
|
300 | href: this.url
|
301 | },
|
302 | children: [
|
303 | {
|
304 | tag: 'span',
|
305 | attributes: {
|
306 | class: 'ck-media__placeholder__url__text'
|
307 | },
|
308 | children: [ this.url ]
|
309 | },
|
310 | tooltip
|
311 | ]
|
312 | }
|
313 | ]
|
314 | } ).render();
|
315 |
|
316 | return placeholder.outerHTML;
|
317 | }
|
318 |
|
319 | /**
|
320 | * Returns the full URL to the specified media.
|
321 | *
|
322 | * @param {String} url The URL of the media.
|
323 | * @returns {String|null}
|
324 | */
|
325 | _getValidUrl( url ) {
|
326 | if ( !url ) {
|
327 | return null;
|
328 | }
|
329 |
|
330 | if ( url.match( /^https?/ ) ) {
|
331 | return url;
|
332 | }
|
333 |
|
334 | return 'https://' + url;
|
335 | }
|
336 | }
|