@ckeditor/ckeditor5-media-embed 0.0.0-nightly-20240602.0 → 0.0.0-nightly-20240604.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ckeditor/ckeditor5-media-embed might be problematic. Click here for more details.

package/dist/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
- import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
5
+ import { Command, Plugin, icons } from '@ckeditor/ckeditor5-core/dist/index.js';
6
6
  import { toWidget, isWidget, findOptimalInsertionRange, Widget, WidgetToolbarRepository } from '@ckeditor/ckeditor5-widget/dist/index.js';
7
- import { logWarning, toArray, first, global, FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils/dist/index.js';
8
- import { IconView, Template, View, submitHandler, LabeledFieldView, createLabeledInputText, Dialog, ButtonView, MenuBarMenuListItemButtonView, CssTransitionDisablerMixin } from '@ckeditor/ckeditor5-ui/dist/index.js';
7
+ import { toArray, logWarning, first, global, FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils/dist/index.js';
8
+ import { IconView, Template, View, submitHandler, LabeledFieldView, createLabeledInputText, ButtonView, ViewCollection, FocusCycler, createDropdown, CssTransitionDisablerMixin } from '@ckeditor/ckeditor5-ui/dist/index.js';
9
9
  import { LivePosition, LiveRange } from '@ckeditor/ckeditor5-engine/dist/index.js';
10
10
  import { Clipboard } from '@ckeditor/ckeditor5-clipboard/dist/index.js';
11
11
  import { Delete } from '@ckeditor/ckeditor5-typing/dist/index.js';
@@ -15,8 +15,6 @@ import { Undo } from '@ckeditor/ckeditor5-undo/dist/index.js';
15
15
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
16
16
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
17
17
  */ /**
18
- * @module media-embed/converters
19
- */ /**
20
18
  * Returns a function that converts the model "url" attribute to the view representation.
21
19
  *
22
20
  * Depending on the configuration, the view representation can be "semantic" (for the data pipeline):
@@ -147,20 +145,10 @@ import { Undo } from '@ckeditor/ckeditor5-undo/dist/index.js';
147
145
  });
148
146
  }
149
147
 
150
- /**
151
- * The insert media command.
152
- *
153
- * The command is registered by the {@link module:media-embed/mediaembedediting~MediaEmbedEditing} as `'mediaEmbed'`.
154
- *
155
- * To insert media at the current selection, execute the command and specify the URL:
156
- *
157
- * ```ts
158
- * editor.execute( 'mediaEmbed', 'http://url.to.the/media' );
159
- * ```
160
- */ class MediaEmbedCommand extends Command {
148
+ class MediaEmbedCommand extends Command {
161
149
  /**
162
- * @inheritDoc
163
- */ refresh() {
150
+ * @inheritDoc
151
+ */ refresh() {
164
152
  const model = this.editor.model;
165
153
  const selection = model.document.selection;
166
154
  const selectedMedia = getSelectedMediaModelWidget(selection);
@@ -168,14 +156,14 @@ import { Undo } from '@ckeditor/ckeditor5-undo/dist/index.js';
168
156
  this.isEnabled = isMediaSelected(selection) || isAllowedInParent(selection, model);
169
157
  }
170
158
  /**
171
- * Executes the command, which either:
172
- *
173
- * * updates the URL of the selected media,
174
- * * inserts the new media into the editor and puts the selection around it.
175
- *
176
- * @fires execute
177
- * @param url The URL of the media.
178
- */ execute(url) {
159
+ * Executes the command, which either:
160
+ *
161
+ * * updates the URL of the selected media,
162
+ * * inserts the new media into the editor and puts the selection around it.
163
+ *
164
+ * @fires execute
165
+ * @param url The URL of the media.
166
+ */ execute(url) {
179
167
  const model = this.editor.model;
180
168
  const selection = model.document.selection;
181
169
  const selectedMedia = getSelectedMediaModelWidget(selection);
@@ -209,72 +197,31 @@ import { Undo } from '@ckeditor/ckeditor5-undo/dist/index.js';
209
197
  var mediaPlaceholderIcon = "<svg viewBox=\"0 0 64 42\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M47.426 17V3.713L63.102 0v19.389h-.001l.001.272c0 1.595-2.032 3.43-4.538 4.098-2.506.668-4.538-.083-4.538-1.678 0-1.594 2.032-3.43 4.538-4.098.914-.244 2.032-.565 2.888-.603V4.516L49.076 7.447v9.556A1.014 1.014 0 0 0 49 17h-1.574zM29.5 17h-8.343a7.073 7.073 0 1 0-4.657 4.06v3.781H3.3a2.803 2.803 0 0 1-2.8-2.804V8.63a2.803 2.803 0 0 1 2.8-2.805h4.082L8.58 2.768A1.994 1.994 0 0 1 10.435 1.5h8.985c.773 0 1.477.448 1.805 1.149l1.488 3.177H26.7c1.546 0 2.8 1.256 2.8 2.805V17zm-11.637 0H17.5a1 1 0 0 0-1 1v.05A4.244 4.244 0 1 1 17.863 17zm29.684 2c.97 0 .953-.048.953.889v20.743c0 .953.016.905-.953.905H19.453c-.97 0-.953.048-.953-.905V19.89c0-.937-.016-.889.97-.889h28.077zm-4.701 19.338V22.183H24.154v16.155h18.692zM20.6 21.375v1.616h1.616v-1.616H20.6zm0 3.231v1.616h1.616v-1.616H20.6zm0 3.231v1.616h1.616v-1.616H20.6zm0 3.231v1.616h1.616v-1.616H20.6zm0 3.231v1.616h1.616v-1.616H20.6zm0 3.231v1.616h1.616V37.53H20.6zm24.233-16.155v1.616h1.615v-1.616h-1.615zm0 3.231v1.616h1.615v-1.616h-1.615zm0 3.231v1.616h1.615v-1.616h-1.615zm0 3.231v1.616h1.615v-1.616h-1.615zm0 3.231v1.616h1.615v-1.616h-1.615zm0 3.231v1.616h1.615V37.53h-1.615zM29.485 25.283a.4.4 0 0 1 .593-.35l9.05 4.977a.4.4 0 0 1 0 .701l-9.05 4.978a.4.4 0 0 1-.593-.35v-9.956z\"/></svg>";
210
198
 
211
199
  const mediaPlaceholderIconViewBox = '0 0 64 42';
212
- /**
213
- * A bridge between the raw media content provider definitions and the editor view content.
214
- *
215
- * It helps translating media URLs to corresponding {@link module:engine/view/element~Element view elements}.
216
- *
217
- * Mostly used by the {@link module:media-embed/mediaembedediting~MediaEmbedEditing} plugin.
218
- */ class MediaRegistry {
219
- /**
220
- * The {@link module:utils/locale~Locale} instance.
221
- */ locale;
222
- /**
223
- * The media provider definitions available for the registry. Usually corresponding with the
224
- * {@link module:media-embed/mediaembedconfig~MediaEmbedConfig media configuration}.
225
- */ providerDefinitions;
200
+ class MediaRegistry {
226
201
  /**
227
- * Creates an instance of the {@link module:media-embed/mediaregistry~MediaRegistry} class.
228
- *
229
- * @param locale The localization services instance.
230
- * @param config The configuration of the media embed feature.
231
- */ constructor(locale, config){
232
- const providers = config.providers;
233
- const extraProviders = config.extraProviders || [];
234
- const removedProviders = new Set(config.removeProviders);
235
- const providerDefinitions = providers.concat(extraProviders).filter((provider)=>{
236
- const name = provider.name;
237
- if (!name) {
238
- /**
239
- * One of the providers (or extra providers) specified in the media embed configuration
240
- * has no name and will not be used by the editor. In order to get this media
241
- * provider working, double check your editor configuration.
242
- *
243
- * @error media-embed-no-provider-name
244
- */ logWarning('media-embed-no-provider-name', {
245
- provider
246
- });
247
- return false;
248
- }
249
- return !removedProviders.has(name);
250
- });
251
- this.locale = locale;
252
- this.providerDefinitions = providerDefinitions;
253
- }
254
- /**
255
- * Checks whether the passed URL is representing a certain media type allowed in the editor.
256
- *
257
- * @param url The URL to be checked
258
- */ hasMedia(url) {
202
+ * Checks whether the passed URL is representing a certain media type allowed in the editor.
203
+ *
204
+ * @param url The URL to be checked
205
+ */ hasMedia(url) {
259
206
  return !!this._getMedia(url);
260
207
  }
261
208
  /**
262
- * For the given media URL string and options, it returns the {@link module:engine/view/element~Element view element}
263
- * representing that media.
264
- *
265
- * **Note:** If no URL is specified, an empty view element is returned.
266
- *
267
- * @param writer The view writer used to produce a view element.
268
- * @param url The URL to be translated into a view element.
269
- */ getMediaViewElement(writer, url, options) {
209
+ * For the given media URL string and options, it returns the {@link module:engine/view/element~Element view element}
210
+ * representing that media.
211
+ *
212
+ * **Note:** If no URL is specified, an empty view element is returned.
213
+ *
214
+ * @param writer The view writer used to produce a view element.
215
+ * @param url The URL to be translated into a view element.
216
+ */ getMediaViewElement(writer, url, options) {
270
217
  return this._getMedia(url).getViewElement(writer, options);
271
218
  }
272
219
  /**
273
- * Returns a `Media` instance for the given URL.
274
- *
275
- * @param url The URL of the media.
276
- * @returns The `Media` instance or `null` when there is none.
277
- */ _getMedia(url) {
220
+ * Returns a `Media` instance for the given URL.
221
+ *
222
+ * @param url The URL of the media.
223
+ * @returns The `Media` instance or `null` when there is none.
224
+ */ _getMedia(url) {
278
225
  if (!url) {
279
226
  return new Media(this.locale);
280
227
  }
@@ -292,11 +239,11 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
292
239
  return null;
293
240
  }
294
241
  /**
295
- * Tries to match `url` to `pattern`.
296
- *
297
- * @param url The URL of the media.
298
- * @param pattern The pattern that should accept the media URL.
299
- */ _getUrlMatches(url, pattern) {
242
+ * Tries to match `url` to `pattern`.
243
+ *
244
+ * @param url The URL of the media.
245
+ * @param pattern The pattern that should accept the media URL.
246
+ */ _getUrlMatches(url, pattern) {
300
247
  // 1. Try to match without stripping the protocol and "www" subdomain.
301
248
  let match = url.match(pattern);
302
249
  if (match) {
@@ -316,6 +263,34 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
316
263
  }
317
264
  return null;
318
265
  }
266
+ /**
267
+ * Creates an instance of the {@link module:media-embed/mediaregistry~MediaRegistry} class.
268
+ *
269
+ * @param locale The localization services instance.
270
+ * @param config The configuration of the media embed feature.
271
+ */ constructor(locale, config){
272
+ const providers = config.providers;
273
+ const extraProviders = config.extraProviders || [];
274
+ const removedProviders = new Set(config.removeProviders);
275
+ const providerDefinitions = providers.concat(extraProviders).filter((provider)=>{
276
+ const name = provider.name;
277
+ if (!name) {
278
+ /**
279
+ * One of the providers (or extra providers) specified in the media embed configuration
280
+ * has no name and will not be used by the editor. In order to get this media
281
+ * provider working, double check your editor configuration.
282
+ *
283
+ * @error media-embed-no-provider-name
284
+ */ logWarning('media-embed-no-provider-name', {
285
+ provider
286
+ });
287
+ return false;
288
+ }
289
+ return !removedProviders.has(name);
290
+ });
291
+ this.locale = locale;
292
+ this.providerDefinitions = providerDefinitions;
293
+ }
319
294
  }
320
295
  /**
321
296
  * Represents media defined by the provider configuration.
@@ -323,30 +298,10 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
323
298
  * It can be rendered to the {@link module:engine/view/element~Element view element} and used in the editing or data pipeline.
324
299
  */ class Media {
325
300
  /**
326
- * The URL this Media instance represents.
327
- */ url;
328
- /**
329
- * Shorthand for {@link module:utils/locale~Locale#t}.
330
- *
331
- * @see module:utils/locale~Locale#t
332
- */ _locale;
333
- /**
334
- * The output of the `RegExp.match` which validated the {@link #url} of this media.
335
- */ _match;
336
- /**
337
- * The function returning the HTML string preview of this media.
338
- */ _previewRenderer;
339
- constructor(locale, url, match, previewRenderer){
340
- this.url = this._getValidUrl(url);
341
- this._locale = locale;
342
- this._match = match;
343
- this._previewRenderer = previewRenderer;
344
- }
345
- /**
346
- * Returns the view element representation of the media.
347
- *
348
- * @param writer The view writer used to produce a view element.
349
- */ getViewElement(writer, options) {
301
+ * Returns the view element representation of the media.
302
+ *
303
+ * @param writer The view writer used to produce a view element.
304
+ */ getViewElement(writer, options) {
350
305
  const attributes = {};
351
306
  let viewElement;
352
307
  if (options.renderForEditingView || options.renderMediaPreview && this.url && this._previewRenderer) {
@@ -370,8 +325,8 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
370
325
  return viewElement;
371
326
  }
372
327
  /**
373
- * Returns the HTML string of the media content preview.
374
- */ _getPreviewHtml(options) {
328
+ * Returns the HTML string of the media content preview.
329
+ */ _getPreviewHtml(options) {
375
330
  if (this._previewRenderer) {
376
331
  return this._previewRenderer(this._match);
377
332
  } else {
@@ -384,8 +339,8 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
384
339
  }
385
340
  }
386
341
  /**
387
- * Returns the placeholder HTML when the media has no content preview.
388
- */ _getPlaceholderHtml() {
342
+ * Returns the placeholder HTML when the media has no content preview.
343
+ */ _getPlaceholderHtml() {
389
344
  const icon = new IconView();
390
345
  const t = this._locale.t;
391
346
  icon.content = mediaPlaceholderIcon;
@@ -431,10 +386,10 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
431
386
  return placeholder.outerHTML;
432
387
  }
433
388
  /**
434
- * Returns the full URL to the specified media.
435
- *
436
- * @param url The URL of the media.
437
- */ _getValidUrl(url) {
389
+ * Returns the full URL to the specified media.
390
+ *
391
+ * @param url The URL of the media.
392
+ */ _getValidUrl(url) {
438
393
  if (!url) {
439
394
  return null;
440
395
  }
@@ -443,111 +398,23 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
443
398
  }
444
399
  return 'https://' + url;
445
400
  }
401
+ constructor(locale, url, match, previewRenderer){
402
+ this.url = this._getValidUrl(url);
403
+ this._locale = locale;
404
+ this._match = match;
405
+ this._previewRenderer = previewRenderer;
406
+ }
446
407
  }
447
408
 
448
- /**
449
- * The media embed editing feature.
450
- */ class MediaEmbedEditing extends Plugin {
409
+ class MediaEmbedEditing extends Plugin {
451
410
  /**
452
- * @inheritDoc
453
- */ static get pluginName() {
411
+ * @inheritDoc
412
+ */ static get pluginName() {
454
413
  return 'MediaEmbedEditing';
455
414
  }
456
415
  /**
457
- * The media registry managing the media providers in the editor.
458
- */ registry;
459
- /**
460
- * @inheritDoc
461
- */ constructor(editor){
462
- super(editor);
463
- editor.config.define('mediaEmbed', {
464
- elementName: 'oembed',
465
- providers: [
466
- {
467
- name: 'dailymotion',
468
- url: [
469
- /^dailymotion\.com\/video\/(\w+)/,
470
- /^dai.ly\/(\w+)/
471
- ],
472
- html: (match)=>{
473
- const id = match[1];
474
- return '<div style="position: relative; padding-bottom: 100%; height: 0; ">' + `<iframe src="https://www.dailymotion.com/embed/video/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" width="480" height="270" allowfullscreen allow="autoplay">' + '</iframe>' + '</div>';
475
- }
476
- },
477
- {
478
- name: 'spotify',
479
- url: [
480
- /^open\.spotify\.com\/(artist\/\w+)/,
481
- /^open\.spotify\.com\/(album\/\w+)/,
482
- /^open\.spotify\.com\/(track\/\w+)/
483
- ],
484
- html: (match)=>{
485
- const id = match[1];
486
- return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 126%;">' + `<iframe src="https://open.spotify.com/embed/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" allowtransparency="true" allow="encrypted-media">' + '</iframe>' + '</div>';
487
- }
488
- },
489
- {
490
- name: 'youtube',
491
- url: [
492
- /^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/,
493
- /^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/,
494
- /^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/,
495
- /^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/
496
- ],
497
- html: (match)=>{
498
- const id = match[1];
499
- const time = match[2];
500
- return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' + `<iframe src="https://www.youtube.com/embed/${id}${time ? `?start=${time}` : ''}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>' + '</iframe>' + '</div>';
501
- }
502
- },
503
- {
504
- name: 'vimeo',
505
- url: [
506
- /^vimeo\.com\/(\d+)/,
507
- /^vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
508
- /^vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
509
- /^vimeo\.com\/channels\/[^/]+\/(\d+)/,
510
- /^vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
511
- /^vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
512
- /^player\.vimeo\.com\/video\/(\d+)/
513
- ],
514
- html: (match)=>{
515
- const id = match[1];
516
- return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' + `<iframe src="https://player.vimeo.com/video/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>' + '</iframe>' + '</div>';
517
- }
518
- },
519
- {
520
- name: 'instagram',
521
- url: /^instagram\.com\/p\/(\w+)/
522
- },
523
- {
524
- name: 'twitter',
525
- url: /^twitter\.com/
526
- },
527
- {
528
- name: 'googleMaps',
529
- url: [
530
- /^google\.com\/maps/,
531
- /^goo\.gl\/maps/,
532
- /^maps\.google\.com/,
533
- /^maps\.app\.goo\.gl/
534
- ]
535
- },
536
- {
537
- name: 'flickr',
538
- url: /^flickr\.com/
539
- },
540
- {
541
- name: 'facebook',
542
- url: /^facebook\.com/
543
- }
544
- ]
545
- });
546
- this.registry = new MediaRegistry(editor.locale, editor.config.get('mediaEmbed'));
547
- }
548
- /**
549
- * @inheritDoc
550
- */ init() {
416
+ * @inheritDoc
417
+ */ init() {
551
418
  const editor = this.editor;
552
419
  const schema = editor.model.schema;
553
420
  const t = editor.t;
@@ -655,16 +522,102 @@ const mediaPlaceholderIconViewBox = '0 0 64 42';
655
522
  dispatcher.on('element:figure', converter);
656
523
  });
657
524
  }
525
+ /**
526
+ * @inheritDoc
527
+ */ constructor(editor){
528
+ super(editor);
529
+ editor.config.define('mediaEmbed', {
530
+ elementName: 'oembed',
531
+ providers: [
532
+ {
533
+ name: 'dailymotion',
534
+ url: [
535
+ /^dailymotion\.com\/video\/(\w+)/,
536
+ /^dai.ly\/(\w+)/
537
+ ],
538
+ html: (match)=>{
539
+ const id = match[1];
540
+ return '<div style="position: relative; padding-bottom: 100%; height: 0; ">' + `<iframe src="https://www.dailymotion.com/embed/video/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" width="480" height="270" allowfullscreen allow="autoplay">' + '</iframe>' + '</div>';
541
+ }
542
+ },
543
+ {
544
+ name: 'spotify',
545
+ url: [
546
+ /^open\.spotify\.com\/(artist\/\w+)/,
547
+ /^open\.spotify\.com\/(album\/\w+)/,
548
+ /^open\.spotify\.com\/(track\/\w+)/
549
+ ],
550
+ html: (match)=>{
551
+ const id = match[1];
552
+ return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 126%;">' + `<iframe src="https://open.spotify.com/embed/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" allowtransparency="true" allow="encrypted-media">' + '</iframe>' + '</div>';
553
+ }
554
+ },
555
+ {
556
+ name: 'youtube',
557
+ url: [
558
+ /^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/,
559
+ /^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/,
560
+ /^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/,
561
+ /^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/
562
+ ],
563
+ html: (match)=>{
564
+ const id = match[1];
565
+ const time = match[2];
566
+ return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' + `<iframe src="https://www.youtube.com/embed/${id}${time ? `?start=${time}` : ''}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>' + '</iframe>' + '</div>';
567
+ }
568
+ },
569
+ {
570
+ name: 'vimeo',
571
+ url: [
572
+ /^vimeo\.com\/(\d+)/,
573
+ /^vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
574
+ /^vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
575
+ /^vimeo\.com\/channels\/[^/]+\/(\d+)/,
576
+ /^vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
577
+ /^vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
578
+ /^player\.vimeo\.com\/video\/(\d+)/
579
+ ],
580
+ html: (match)=>{
581
+ const id = match[1];
582
+ return '<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' + `<iframe src="https://player.vimeo.com/video/${id}" ` + 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' + 'frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>' + '</iframe>' + '</div>';
583
+ }
584
+ },
585
+ {
586
+ name: 'instagram',
587
+ url: /^instagram\.com\/p\/(\w+)/
588
+ },
589
+ {
590
+ name: 'twitter',
591
+ url: /^twitter\.com/
592
+ },
593
+ {
594
+ name: 'googleMaps',
595
+ url: [
596
+ /^google\.com\/maps/,
597
+ /^goo\.gl\/maps/,
598
+ /^maps\.google\.com/,
599
+ /^maps\.app\.goo\.gl/
600
+ ]
601
+ },
602
+ {
603
+ name: 'flickr',
604
+ url: /^flickr\.com/
605
+ },
606
+ {
607
+ name: 'facebook',
608
+ url: /^facebook\.com/
609
+ }
610
+ ]
611
+ });
612
+ this.registry = new MediaRegistry(editor.locale, editor.config.get('mediaEmbed'));
613
+ }
658
614
  }
659
615
 
660
616
  const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
661
- /**
662
- * The auto-media embed plugin. It recognizes media links in the pasted content and embeds
663
- * them shortly after they are injected into the document.
664
- */ class AutoMediaEmbed extends Plugin {
617
+ class AutoMediaEmbed extends Plugin {
665
618
  /**
666
- * @inheritDoc
667
- */ static get requires() {
619
+ * @inheritDoc
620
+ */ static get requires() {
668
621
  return [
669
622
  Clipboard,
670
623
  Delete,
@@ -672,28 +625,13 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
672
625
  ];
673
626
  }
674
627
  /**
675
- * @inheritDoc
676
- */ static get pluginName() {
628
+ * @inheritDoc
629
+ */ static get pluginName() {
677
630
  return 'AutoMediaEmbed';
678
631
  }
679
632
  /**
680
- * The paste–to–embed `setTimeout` ID. Stored as a property to allow
681
- * cleaning of the timeout.
682
- */ _timeoutId;
683
- /**
684
- * The position where the `<media>` element will be inserted after the timeout,
685
- * determined each time the new content is pasted into the document.
686
- */ _positionToInsert;
687
- /**
688
- * @inheritDoc
689
- */ constructor(editor){
690
- super(editor);
691
- this._timeoutId = null;
692
- this._positionToInsert = null;
693
- }
694
- /**
695
- * @inheritDoc
696
- */ init() {
633
+ * @inheritDoc
634
+ */ init() {
697
635
  const editor = this.editor;
698
636
  const modelDocument = editor.model.document;
699
637
  // We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
@@ -727,12 +665,12 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
727
665
  });
728
666
  }
729
667
  /**
730
- * Analyzes the part of the document between provided positions in search for a URL representing media.
731
- * When the URL is found, it is automatically converted into media.
732
- *
733
- * @param leftPosition Left position of the selection.
734
- * @param rightPosition Right position of the selection.
735
- */ _embedMediaBetweenPositions(leftPosition, rightPosition) {
668
+ * Analyzes the part of the document between provided positions in search for a URL representing media.
669
+ * When the URL is found, it is automatically converted into media.
670
+ *
671
+ * @param leftPosition Left position of the selection.
672
+ * @param rightPosition Right position of the selection.
673
+ */ _embedMediaBetweenPositions(leftPosition, rightPosition) {
736
674
  const editor = this.editor;
737
675
  const mediaRegistry = editor.plugins.get(MediaEmbedEditing).registry;
738
676
  // TODO: Use marker instead of LiveRange & LivePositions.
@@ -784,95 +722,71 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
784
722
  editor.plugins.get(Delete).requestUndoOnBackspace();
785
723
  }, 100);
786
724
  }
787
- }
788
-
789
- /**
790
- * The media form view controller class.
791
- *
792
- * See {@link module:media-embed/ui/mediaformview~MediaFormView}.
793
- */ class MediaFormView extends View {
794
- /**
795
- * Tracks information about the DOM focus in the form.
796
- */ focusTracker;
797
- /**
798
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
799
- */ keystrokes;
800
725
  /**
801
- * The URL input view.
802
- */ urlInputView;
803
- /**
804
- * An array of form validators used by {@link #isValid}.
805
- */ _validators;
806
- /**
807
- * The default info text for the {@link #urlInputView}.
808
- */ _urlInputViewInfoDefault;
809
- /**
810
- * The info text with an additional tip for the {@link #urlInputView},
811
- * displayed when the input has some value.
812
- */ _urlInputViewInfoTip;
813
- /**
814
- * @param validators Form validators used by {@link #isValid}.
815
- * @param locale The localization services instance.
816
- */ constructor(validators, locale){
817
- super(locale);
818
- this.focusTracker = new FocusTracker();
819
- this.keystrokes = new KeystrokeHandler();
820
- this.set('mediaURLInputValue', '');
821
- this.urlInputView = this._createUrlInput();
822
- this._validators = validators;
823
- this.setTemplate({
824
- tag: 'form',
825
- attributes: {
826
- class: [
827
- 'ck',
828
- 'ck-media-form',
829
- 'ck-responsive-form'
830
- ],
831
- tabindex: '-1'
832
- },
833
- children: [
834
- this.urlInputView
835
- ]
836
- });
726
+ * @inheritDoc
727
+ */ constructor(editor){
728
+ super(editor);
729
+ this._timeoutId = null;
730
+ this._positionToInsert = null;
837
731
  }
732
+ }
733
+
734
+ class MediaFormView extends View {
838
735
  /**
839
- * @inheritDoc
840
- */ render() {
736
+ * @inheritDoc
737
+ */ render() {
841
738
  super.render();
842
739
  submitHandler({
843
740
  view: this
844
741
  });
845
- // Register the view in the focus tracker.
846
- this.focusTracker.add(this.urlInputView.element);
742
+ const childViews = [
743
+ this.urlInputView,
744
+ this.saveButtonView,
745
+ this.cancelButtonView
746
+ ];
747
+ childViews.forEach((v)=>{
748
+ // Register the view as focusable.
749
+ this._focusables.add(v);
750
+ // Register the view in the focus tracker.
751
+ this.focusTracker.add(v.element);
752
+ });
847
753
  // Start listening for the keystrokes coming from #element.
848
754
  this.keystrokes.listenTo(this.element);
755
+ const stopPropagation = (data)=>data.stopPropagation();
756
+ // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
757
+ // keystroke handler would take over the key management in the URL input. We need to prevent
758
+ // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
759
+ this.keystrokes.set('arrowright', stopPropagation);
760
+ this.keystrokes.set('arrowleft', stopPropagation);
761
+ this.keystrokes.set('arrowup', stopPropagation);
762
+ this.keystrokes.set('arrowdown', stopPropagation);
849
763
  }
850
764
  /**
851
- * @inheritDoc
852
- */ destroy() {
765
+ * @inheritDoc
766
+ */ destroy() {
853
767
  super.destroy();
854
768
  this.focusTracker.destroy();
855
769
  this.keystrokes.destroy();
856
770
  }
857
771
  /**
858
- * Focuses the {@link #urlInputView}.
859
- */ focus() {
860
- this.urlInputView.focus();
772
+ * Focuses the fist {@link #_focusables} in the form.
773
+ */ focus() {
774
+ this._focusCycler.focusFirst();
861
775
  }
862
776
  /**
863
- * The native DOM `value` of the {@link #urlInputView} element.
864
- *
865
- * **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
866
- * which works one way only and may not represent the actual state of the component in the DOM.
867
- */ get url() {
777
+ * The native DOM `value` of the {@link #urlInputView} element.
778
+ *
779
+ * **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
780
+ * which works one way only and may not represent the actual state of the component in the DOM.
781
+ */ get url() {
868
782
  return this.urlInputView.fieldView.element.value.trim();
869
783
  }
870
784
  set url(url) {
871
- this.urlInputView.fieldView.value = url.trim();
785
+ this.urlInputView.fieldView.element.value = url.trim();
872
786
  }
873
787
  /**
874
- * Validates the form and returns `false` when some fields are invalid.
875
- */ isValid() {
788
+ * Validates the form and returns `false` when some fields are invalid.
789
+ */ isValid() {
876
790
  this.resetFormStatus();
877
791
  for (const validator of this._validators){
878
792
  const errorText = validator(this);
@@ -886,19 +800,19 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
886
800
  return true;
887
801
  }
888
802
  /**
889
- * Cleans up the supplementary error and information text of the {@link #urlInputView}
890
- * bringing them back to the state when the form has been displayed for the first time.
891
- *
892
- * See {@link #isValid}.
893
- */ resetFormStatus() {
803
+ * Cleans up the supplementary error and information text of the {@link #urlInputView}
804
+ * bringing them back to the state when the form has been displayed for the first time.
805
+ *
806
+ * See {@link #isValid}.
807
+ */ resetFormStatus() {
894
808
  this.urlInputView.errorText = null;
895
809
  this.urlInputView.infoText = this._urlInputViewInfoDefault;
896
810
  }
897
811
  /**
898
- * Creates a labeled input view.
899
- *
900
- * @returns Labeled input view instance.
901
- */ _createUrlInput() {
812
+ * Creates a labeled input view.
813
+ *
814
+ * @returns Labeled input view instance.
815
+ */ _createUrlInput() {
902
816
  const t = this.locale.t;
903
817
  const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
904
818
  const inputField = labeledInput.fieldView;
@@ -906,7 +820,6 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
906
820
  this._urlInputViewInfoTip = t('Tip: Paste the URL into the content to embed faster.');
907
821
  labeledInput.label = t('Media URL');
908
822
  labeledInput.infoText = this._urlInputViewInfoDefault;
909
- inputField.inputMode = 'url';
910
823
  inputField.on('input', ()=>{
911
824
  // Display the tip text only when there is some value. Otherwise fall back to the default info text.
912
825
  labeledInput.infoText = inputField.element.value ? this._urlInputViewInfoTip : this._urlInputViewInfoDefault;
@@ -914,101 +827,150 @@ const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
914
827
  });
915
828
  return labeledInput;
916
829
  }
830
+ /**
831
+ * Creates a button view.
832
+ *
833
+ * @param label The button label.
834
+ * @param icon The button icon.
835
+ * @param className The additional button CSS class name.
836
+ * @param eventName An event name that the `ButtonView#execute` event will be delegated to.
837
+ * @returns The button view instance.
838
+ */ _createButton(label, icon, className, eventName) {
839
+ const button = new ButtonView(this.locale);
840
+ button.set({
841
+ label,
842
+ icon,
843
+ tooltip: true
844
+ });
845
+ button.extendTemplate({
846
+ attributes: {
847
+ class: className
848
+ }
849
+ });
850
+ if (eventName) {
851
+ button.delegate('execute').to(this, eventName);
852
+ }
853
+ return button;
854
+ }
855
+ /**
856
+ * @param validators Form validators used by {@link #isValid}.
857
+ * @param locale The localization services instance.
858
+ */ constructor(validators, locale){
859
+ super(locale);
860
+ const t = locale.t;
861
+ this.focusTracker = new FocusTracker();
862
+ this.keystrokes = new KeystrokeHandler();
863
+ this.set('mediaURLInputValue', '');
864
+ this.urlInputView = this._createUrlInput();
865
+ this.saveButtonView = this._createButton(t('Save'), icons.check, 'ck-button-save');
866
+ this.saveButtonView.type = 'submit';
867
+ this.cancelButtonView = this._createButton(t('Cancel'), icons.cancel, 'ck-button-cancel', 'cancel');
868
+ this._focusables = new ViewCollection();
869
+ this._focusCycler = new FocusCycler({
870
+ focusables: this._focusables,
871
+ focusTracker: this.focusTracker,
872
+ keystrokeHandler: this.keystrokes,
873
+ actions: {
874
+ // Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
875
+ focusPrevious: 'shift + tab',
876
+ // Navigate form fields forwards using the <kbd>Tab</kbd> key.
877
+ focusNext: 'tab'
878
+ }
879
+ });
880
+ this._validators = validators;
881
+ this.setTemplate({
882
+ tag: 'form',
883
+ attributes: {
884
+ class: [
885
+ 'ck',
886
+ 'ck-media-form',
887
+ 'ck-responsive-form'
888
+ ],
889
+ tabindex: '-1'
890
+ },
891
+ children: [
892
+ this.urlInputView,
893
+ this.saveButtonView,
894
+ this.cancelButtonView
895
+ ]
896
+ });
897
+ }
917
898
  }
918
899
 
919
900
  var mediaIcon = "<svg viewBox=\"0 0 22 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M1.587 1.5c-.612 0-.601-.029-.601.551v14.84c0 .59-.01.559.591.559h18.846c.602 0 .591.03.591-.56V2.052c0-.58.01-.55-.591-.55H1.587Zm.701.971h1.003v1H2.288v-1Zm16.448 0h1.003v1h-1.003v-1Zm-14.24 1h13.008v12H4.467l.029-12Zm-2.208 1h1.003v1H2.288v-1Zm16.448 0h1.003v1h-1.003v-1Zm-16.448 2h1.003v1H2.288v-1Zm16.448 0h1.003v1h-1.003v-1Zm-16.448 2h1.003v1H2.288v-1Zm16.448 0h1.003v1h-1.003v-1Zm-16.448 2h1.003v1H2.288v-1Zm16.448 0h1.003v1h-1.003v-1Zm-16.448 2h1.003l-.029 1h-.974v-1Zm16.448 0h1.003v1h-1.003v-1Zm-16.448 2h.974v1h-.974v-1Zm16.448 0h1.003v1h-1.003v-1Z\"/><path d=\"M8.374 6.648a.399.399 0 0 1 .395-.4.402.402 0 0 1 .2.049l5.148 2.824a.4.4 0 0 1 0 .7l-5.148 2.824a.403.403 0 0 1-.595-.35V6.648Z\"/></svg>";
920
901
 
921
- /**
922
- * The media embed UI plugin.
923
- */ class MediaEmbedUI extends Plugin {
902
+ class MediaEmbedUI extends Plugin {
924
903
  /**
925
- * @inheritDoc
926
- */ static get requires() {
904
+ * @inheritDoc
905
+ */ static get requires() {
927
906
  return [
928
- MediaEmbedEditing,
929
- Dialog
907
+ MediaEmbedEditing
930
908
  ];
931
909
  }
932
910
  /**
933
- * @inheritDoc
934
- */ static get pluginName() {
911
+ * @inheritDoc
912
+ */ static get pluginName() {
935
913
  return 'MediaEmbedUI';
936
914
  }
937
- _formView;
938
- /**
939
- * @inheritDoc
940
- */ init() {
941
- const editor = this.editor;
942
- editor.ui.componentFactory.add('mediaEmbed', ()=>{
943
- const t = this.editor.locale.t;
944
- const button = this._createDialogButton(ButtonView);
945
- button.tooltip = true;
946
- button.label = t('Insert media');
947
- return button;
948
- });
949
- editor.ui.componentFactory.add('menuBar:mediaEmbed', ()=>{
950
- const t = this.editor.locale.t;
951
- const button = this._createDialogButton(MenuBarMenuListItemButtonView);
952
- button.label = t('Media');
953
- return button;
954
- });
955
- }
956
915
  /**
957
- * Creates a button for menu bar that will show media embed dialog.
958
- */ _createDialogButton(ButtonClass) {
916
+ * @inheritDoc
917
+ */ init() {
959
918
  const editor = this.editor;
960
- const buttonView = new ButtonClass(editor.locale);
961
919
  const command = editor.commands.get('mediaEmbed');
962
- const dialogPlugin = this.editor.plugins.get('Dialog');
963
- buttonView.icon = mediaIcon;
964
- buttonView.bind('isEnabled').to(command, 'isEnabled');
965
- buttonView.on('execute', ()=>{
966
- if (dialogPlugin.id === 'mediaEmbed') {
967
- dialogPlugin.hide();
968
- } else {
969
- this._showDialog();
970
- }
920
+ editor.ui.componentFactory.add('mediaEmbed', (locale)=>{
921
+ const dropdown = createDropdown(locale);
922
+ this._setUpDropdown(dropdown, command);
923
+ return dropdown;
971
924
  });
972
- return buttonView;
973
925
  }
974
- _showDialog() {
926
+ _setUpDropdown(dropdown, command) {
975
927
  const editor = this.editor;
976
- const dialog = editor.plugins.get('Dialog');
977
- const command = editor.commands.get('mediaEmbed');
978
- const t = editor.locale.t;
979
- if (!this._formView) {
980
- const registry = editor.plugins.get(MediaEmbedEditing).registry;
981
- this._formView = new (CssTransitionDisablerMixin(MediaFormView))(getFormValidators(editor.t, registry), editor.locale);
982
- }
983
- dialog.show({
984
- id: 'mediaEmbed',
985
- title: t('Insert media'),
986
- content: this._formView,
987
- isModal: true,
988
- onShow: ()=>{
989
- this._formView.url = command.value || '';
990
- this._formView.resetFormStatus();
991
- this._formView.urlInputView.fieldView.select();
992
- },
993
- actionButtons: [
994
- {
995
- label: t('Cancel'),
996
- withText: true,
997
- onExecute: ()=>dialog.hide()
998
- },
999
- {
1000
- label: t('Accept'),
1001
- class: 'ck-button-action',
1002
- withText: true,
1003
- onExecute: ()=>{
1004
- if (this._formView.isValid()) {
1005
- editor.execute('mediaEmbed', this._formView.url);
1006
- dialog.hide();
1007
- editor.editing.view.focus();
1008
- }
1009
- }
928
+ const t = editor.t;
929
+ const button = dropdown.buttonView;
930
+ const registry = editor.plugins.get(MediaEmbedEditing).registry;
931
+ dropdown.once('change:isOpen', ()=>{
932
+ const form = new (CssTransitionDisablerMixin(MediaFormView))(getFormValidators(editor.t, registry), editor.locale);
933
+ dropdown.panelView.children.add(form);
934
+ // Note: Use the low priority to make sure the following listener starts working after the
935
+ // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the
936
+ // invisible form/input cannot be focused/selected.
937
+ button.on('open', ()=>{
938
+ form.disableCssTransitions();
939
+ // Make sure that each time the panel shows up, the URL field remains in sync with the value of
940
+ // the command. If the user typed in the input, then canceled (`urlInputView#fieldView#value` stays
941
+ // unaltered) and re-opened it without changing the value of the media command (e.g. because they
942
+ // didn't change the selection), they would see the old value instead of the actual value of the
943
+ // command.
944
+ form.url = command.value || '';
945
+ form.urlInputView.fieldView.select();
946
+ form.enableCssTransitions();
947
+ }, {
948
+ priority: 'low'
949
+ });
950
+ dropdown.on('submit', ()=>{
951
+ if (form.isValid()) {
952
+ editor.execute('mediaEmbed', form.url);
953
+ editor.editing.view.focus();
1010
954
  }
1011
- ]
955
+ });
956
+ dropdown.on('change:isOpen', ()=>form.resetFormStatus());
957
+ dropdown.on('cancel', ()=>{
958
+ editor.editing.view.focus();
959
+ });
960
+ form.delegate('submit', 'cancel').to(dropdown);
961
+ form.urlInputView.fieldView.bind('value').to(command, 'value');
962
+ // Update balloon position when form error changes.
963
+ form.urlInputView.on('change:errorText', ()=>{
964
+ editor.ui.update();
965
+ });
966
+ // Form elements should be read-only when corresponding commands are disabled.
967
+ form.urlInputView.bind('isEnabled').to(command, 'isEnabled');
968
+ });
969
+ dropdown.bind('isEnabled').to(command);
970
+ button.set({
971
+ label: t('Insert media'),
972
+ icon: mediaIcon,
973
+ tooltip: true
1012
974
  });
1013
975
  }
1014
976
  }
@@ -1027,20 +989,10 @@ function getFormValidators(t, registry) {
1027
989
  ];
1028
990
  }
1029
991
 
1030
- /**
1031
- * The media embed plugin.
1032
- *
1033
- * For a detailed overview, check the {@glink features/media-embed Media Embed feature documentation}.
1034
- *
1035
- * This is a "glue" plugin which loads the following plugins:
1036
- *
1037
- * * The {@link module:media-embed/mediaembedediting~MediaEmbedEditing media embed editing feature},
1038
- * * The {@link module:media-embed/mediaembedui~MediaEmbedUI media embed UI feature} and
1039
- * * The {@link module:media-embed/automediaembed~AutoMediaEmbed auto-media embed feature}.
1040
- */ class MediaEmbed extends Plugin {
992
+ class MediaEmbed extends Plugin {
1041
993
  /**
1042
- * @inheritDoc
1043
- */ static get requires() {
994
+ * @inheritDoc
995
+ */ static get requires() {
1044
996
  return [
1045
997
  MediaEmbedEditing,
1046
998
  MediaEmbedUI,
@@ -1049,33 +1001,28 @@ function getFormValidators(t, registry) {
1049
1001
  ];
1050
1002
  }
1051
1003
  /**
1052
- * @inheritDoc
1053
- */ static get pluginName() {
1004
+ * @inheritDoc
1005
+ */ static get pluginName() {
1054
1006
  return 'MediaEmbed';
1055
1007
  }
1056
1008
  }
1057
1009
 
1058
- /**
1059
- * The media embed toolbar plugin. It creates a toolbar for media embed that shows up when the media element is selected.
1060
- *
1061
- * Instances of toolbar components (e.g. buttons) are created based on the
1062
- * {@link module:media-embed/mediaembedconfig~MediaEmbedConfig#toolbar `media.toolbar` configuration option}.
1063
- */ class MediaEmbedToolbar extends Plugin {
1010
+ class MediaEmbedToolbar extends Plugin {
1064
1011
  /**
1065
- * @inheritDoc
1066
- */ static get requires() {
1012
+ * @inheritDoc
1013
+ */ static get requires() {
1067
1014
  return [
1068
1015
  WidgetToolbarRepository
1069
1016
  ];
1070
1017
  }
1071
1018
  /**
1072
- * @inheritDoc
1073
- */ static get pluginName() {
1019
+ * @inheritDoc
1020
+ */ static get pluginName() {
1074
1021
  return 'MediaEmbedToolbar';
1075
1022
  }
1076
1023
  /**
1077
- * @inheritDoc
1078
- */ afterInit() {
1024
+ * @inheritDoc
1025
+ */ afterInit() {
1079
1026
  const editor = this.editor;
1080
1027
  const t = editor.t;
1081
1028
  const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);