UNPKG

8.88 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/mediaregistry
8 */
9
10import { TooltipView, IconView, Template } from 'ckeditor5/src/ui';
11import { logWarning, toArray } from 'ckeditor5/src/utils';
12
13import mediaPlaceholderIcon from '../theme/icons/media-placeholder.svg';
14
15const 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 */
24export 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 */
173class 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}