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 | import { IconView, Template } from 'ckeditor5/src/ui';
|
6 | import { logWarning, toArray } from 'ckeditor5/src/utils';
|
7 | import mediaPlaceholderIcon from '../theme/icons/media-placeholder.svg';
|
8 | const 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 | */
|
16 | export 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 | */
|
122 | class 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 | }
|