UNPKG

8.33 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 */
5import { IconView, Template } from 'ckeditor5/src/ui';
6import { logWarning, toArray } from 'ckeditor5/src/utils';
7import mediaPlaceholderIcon from '../theme/icons/media-placeholder.svg';
8const mediaPlaceholderIconViewBox = '0 0 64 42';
9/**
10 * A bridge between the raw media content provider definitions and the editor view content.
11 *
12 * It helps translating media URLs to corresponding {@link module:engine/view/element~Element view elements}.
13 *
14 * Mostly used by the {@link module:media-embed/mediaembedediting~MediaEmbedEditing} plugin.
15 */
16export default class MediaRegistry {
17 /**
18 * Creates an instance of the {@link module:media-embed/mediaregistry~MediaRegistry} class.
19 *
20 * @param locale The localization services instance.
21 * @param config The configuration of the media embed feature.
22 */
23 constructor(locale, config) {
24 const providers = config.providers;
25 const extraProviders = config.extraProviders || [];
26 const removedProviders = new Set(config.removeProviders);
27 const providerDefinitions = providers
28 .concat(extraProviders)
29 .filter(provider => {
30 const name = provider.name;
31 if (!name) {
32 /**
33 * One of the providers (or extra providers) specified in the media embed configuration
34 * has no name and will not be used by the editor. In order to get this media
35 * provider working, double check your editor configuration.
36 *
37 * @error media-embed-no-provider-name
38 */
39 logWarning('media-embed-no-provider-name', { provider });
40 return false;
41 }
42 return !removedProviders.has(name);
43 });
44 this.locale = locale;
45 this.providerDefinitions = providerDefinitions;
46 }
47 /**
48 * Checks whether the passed URL is representing a certain media type allowed in the editor.
49 *
50 * @param url The URL to be checked
51 */
52 hasMedia(url) {
53 return !!this._getMedia(url);
54 }
55 /**
56 * For the given media URL string and options, it returns the {@link module:engine/view/element~Element view element}
57 * representing that media.
58 *
59 * **Note:** If no URL is specified, an empty view element is returned.
60 *
61 * @param writer The view writer used to produce a view element.
62 * @param url The URL to be translated into a view element.
63 */
64 getMediaViewElement(writer, url, options) {
65 return this._getMedia(url).getViewElement(writer, options);
66 }
67 /**
68 * Returns a `Media` instance for the given URL.
69 *
70 * @param url The URL of the media.
71 * @returns The `Media` instance or `null` when there is none.
72 */
73 _getMedia(url) {
74 if (!url) {
75 return new Media(this.locale);
76 }
77 url = url.trim();
78 for (const definition of this.providerDefinitions) {
79 const previewRenderer = definition.html;
80 const pattern = toArray(definition.url);
81 for (const subPattern of pattern) {
82 const match = this._getUrlMatches(url, subPattern);
83 if (match) {
84 return new Media(this.locale, url, match, previewRenderer);
85 }
86 }
87 }
88 return null;
89 }
90 /**
91 * Tries to match `url` to `pattern`.
92 *
93 * @param url The URL of the media.
94 * @param pattern The pattern that should accept the media URL.
95 */
96 _getUrlMatches(url, pattern) {
97 // 1. Try to match without stripping the protocol and "www" subdomain.
98 let match = url.match(pattern);
99 if (match) {
100 return match;
101 }
102 // 2. Try to match after stripping the protocol.
103 let rawUrl = url.replace(/^https?:\/\//, '');
104 match = rawUrl.match(pattern);
105 if (match) {
106 return match;
107 }
108 // 3. Try to match after stripping the "www" subdomain.
109 rawUrl = rawUrl.replace(/^www\./, '');
110 match = rawUrl.match(pattern);
111 if (match) {
112 return match;
113 }
114 return null;
115 }
116}
117/**
118 * Represents media defined by the provider configuration.
119 *
120 * It can be rendered to the {@link module:engine/view/element~Element view element} and used in the editing or data pipeline.
121 */
122class Media {
123 constructor(locale, url, match, previewRenderer) {
124 this.url = this._getValidUrl(url);
125 this._locale = locale;
126 this._match = match;
127 this._previewRenderer = previewRenderer;
128 }
129 /**
130 * Returns the view element representation of the media.
131 *
132 * @param writer The view writer used to produce a view element.
133 */
134 getViewElement(writer, options) {
135 const attributes = {};
136 let viewElement;
137 if (options.renderForEditingView || (options.renderMediaPreview && this.url && this._previewRenderer)) {
138 if (this.url) {
139 attributes['data-oembed-url'] = this.url;
140 }
141 if (options.renderForEditingView) {
142 attributes.class = 'ck-media__wrapper';
143 }
144 const mediaHtml = this._getPreviewHtml(options);
145 viewElement = writer.createRawElement('div', attributes, (domElement, domConverter) => {
146 domConverter.setContentOf(domElement, mediaHtml);
147 });
148 }
149 else {
150 if (this.url) {
151 attributes.url = this.url;
152 }
153 viewElement = writer.createEmptyElement(options.elementName, attributes);
154 }
155 writer.setCustomProperty('media-content', true, viewElement);
156 return viewElement;
157 }
158 /**
159 * Returns the HTML string of the media content preview.
160 */
161 _getPreviewHtml(options) {
162 if (this._previewRenderer) {
163 return this._previewRenderer(this._match);
164 }
165 else {
166 // The placeholder only makes sense for editing view and media which have URLs.
167 // Placeholder is never displayed in data and URL-less media have no content.
168 if (this.url && options.renderForEditingView) {
169 return this._getPlaceholderHtml();
170 }
171 return '';
172 }
173 }
174 /**
175 * Returns the placeholder HTML when the media has no content preview.
176 */
177 _getPlaceholderHtml() {
178 const icon = new IconView();
179 const t = this._locale.t;
180 icon.content = mediaPlaceholderIcon;
181 icon.viewBox = mediaPlaceholderIconViewBox;
182 const placeholder = new Template({
183 tag: 'div',
184 attributes: {
185 class: 'ck ck-reset_all ck-media__placeholder'
186 },
187 children: [
188 {
189 tag: 'div',
190 attributes: {
191 class: 'ck-media__placeholder__icon'
192 },
193 children: [icon]
194 },
195 {
196 tag: 'a',
197 attributes: {
198 class: 'ck-media__placeholder__url',
199 target: '_blank',
200 rel: 'noopener noreferrer',
201 href: this.url,
202 'data-cke-tooltip-text': t('Open media in new tab')
203 },
204 children: [
205 {
206 tag: 'span',
207 attributes: {
208 class: 'ck-media__placeholder__url__text'
209 },
210 children: [this.url]
211 }
212 ]
213 }
214 ]
215 }).render();
216 return placeholder.outerHTML;
217 }
218 /**
219 * Returns the full URL to the specified media.
220 *
221 * @param url The URL of the media.
222 */
223 _getValidUrl(url) {
224 if (!url) {
225 return null;
226 }
227 if (url.match(/^https?/)) {
228 return url;
229 }
230 return 'https://' + url;
231 }
232}