mediaregistry.js 9.0 KB

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