@ckeditor/ckeditor5-media-embed 0.0.0-nightly-20240509.0 → 0.0.0-nightly-20240511.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/build/media-embed.js +1 -1
- package/dist/index.js +396 -289
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
*/
|
5
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 {
|
8
|
-
import { IconView, Template, View, submitHandler, LabeledFieldView, createLabeledInputText, ButtonView,
|
7
|
+
import { logWarning, toArray, first, global, FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils/dist/index.js';
|
8
|
+
import { IconView, Template, View, ViewCollection, FocusCycler, submitHandler, LabeledFieldView, createLabeledInputText, ButtonView, 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,6 +15,8 @@ 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
|
+
*/ /**
|
18
20
|
* Returns a function that converts the model "url" attribute to the view representation.
|
19
21
|
*
|
20
22
|
* Depending on the configuration, the view representation can be "semantic" (for the data pipeline):
|
@@ -145,10 +147,20 @@ import { Undo } from '@ckeditor/ckeditor5-undo/dist/index.js';
|
|
145
147
|
});
|
146
148
|
}
|
147
149
|
|
148
|
-
|
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 {
|
149
161
|
/**
|
150
|
-
|
151
|
-
|
162
|
+
* @inheritDoc
|
163
|
+
*/ refresh() {
|
152
164
|
const model = this.editor.model;
|
153
165
|
const selection = model.document.selection;
|
154
166
|
const selectedMedia = getSelectedMediaModelWidget(selection);
|
@@ -156,14 +168,14 @@ class MediaEmbedCommand extends Command {
|
|
156
168
|
this.isEnabled = isMediaSelected(selection) || isAllowedInParent(selection, model);
|
157
169
|
}
|
158
170
|
/**
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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) {
|
167
179
|
const model = this.editor.model;
|
168
180
|
const selection = model.document.selection;
|
169
181
|
const selectedMedia = getSelectedMediaModelWidget(selection);
|
@@ -197,31 +209,72 @@ class MediaEmbedCommand extends Command {
|
|
197
209
|
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>";
|
198
210
|
|
199
211
|
const mediaPlaceholderIconViewBox = '0 0 64 42';
|
200
|
-
|
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;
|
226
|
+
/**
|
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
|
+
}
|
201
254
|
/**
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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) {
|
206
259
|
return !!this._getMedia(url);
|
207
260
|
}
|
208
261
|
/**
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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) {
|
217
270
|
return this._getMedia(url).getViewElement(writer, options);
|
218
271
|
}
|
219
272
|
/**
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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) {
|
225
278
|
if (!url) {
|
226
279
|
return new Media(this.locale);
|
227
280
|
}
|
@@ -239,11 +292,11 @@ class MediaRegistry {
|
|
239
292
|
return null;
|
240
293
|
}
|
241
294
|
/**
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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) {
|
247
300
|
// 1. Try to match without stripping the protocol and "www" subdomain.
|
248
301
|
let match = url.match(pattern);
|
249
302
|
if (match) {
|
@@ -263,34 +316,6 @@ class MediaRegistry {
|
|
263
316
|
}
|
264
317
|
return null;
|
265
318
|
}
|
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
|
-
}
|
294
319
|
}
|
295
320
|
/**
|
296
321
|
* Represents media defined by the provider configuration.
|
@@ -298,10 +323,30 @@ class MediaRegistry {
|
|
298
323
|
* It can be rendered to the {@link module:engine/view/element~Element view element} and used in the editing or data pipeline.
|
299
324
|
*/ class Media {
|
300
325
|
/**
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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) {
|
305
350
|
const attributes = {};
|
306
351
|
let viewElement;
|
307
352
|
if (options.renderForEditingView || options.renderMediaPreview && this.url && this._previewRenderer) {
|
@@ -325,8 +370,8 @@ class MediaRegistry {
|
|
325
370
|
return viewElement;
|
326
371
|
}
|
327
372
|
/**
|
328
|
-
|
329
|
-
|
373
|
+
* Returns the HTML string of the media content preview.
|
374
|
+
*/ _getPreviewHtml(options) {
|
330
375
|
if (this._previewRenderer) {
|
331
376
|
return this._previewRenderer(this._match);
|
332
377
|
} else {
|
@@ -339,8 +384,8 @@ class MediaRegistry {
|
|
339
384
|
}
|
340
385
|
}
|
341
386
|
/**
|
342
|
-
|
343
|
-
|
387
|
+
* Returns the placeholder HTML when the media has no content preview.
|
388
|
+
*/ _getPlaceholderHtml() {
|
344
389
|
const icon = new IconView();
|
345
390
|
const t = this._locale.t;
|
346
391
|
icon.content = mediaPlaceholderIcon;
|
@@ -386,10 +431,10 @@ class MediaRegistry {
|
|
386
431
|
return placeholder.outerHTML;
|
387
432
|
}
|
388
433
|
/**
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
434
|
+
* Returns the full URL to the specified media.
|
435
|
+
*
|
436
|
+
* @param url The URL of the media.
|
437
|
+
*/ _getValidUrl(url) {
|
393
438
|
if (!url) {
|
394
439
|
return null;
|
395
440
|
}
|
@@ -398,23 +443,111 @@ class MediaRegistry {
|
|
398
443
|
}
|
399
444
|
return 'https://' + url;
|
400
445
|
}
|
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
|
-
}
|
407
446
|
}
|
408
447
|
|
409
|
-
|
448
|
+
/**
|
449
|
+
* The media embed editing feature.
|
450
|
+
*/ class MediaEmbedEditing extends Plugin {
|
410
451
|
/**
|
411
|
-
|
412
|
-
|
452
|
+
* @inheritDoc
|
453
|
+
*/ static get pluginName() {
|
413
454
|
return 'MediaEmbedEditing';
|
414
455
|
}
|
415
456
|
/**
|
416
|
-
|
417
|
-
|
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() {
|
418
551
|
const editor = this.editor;
|
419
552
|
const schema = editor.model.schema;
|
420
553
|
const t = editor.t;
|
@@ -522,102 +655,16 @@ class MediaEmbedEditing extends Plugin {
|
|
522
655
|
dispatcher.on('element:figure', converter);
|
523
656
|
});
|
524
657
|
}
|
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
|
-
}
|
614
658
|
}
|
615
659
|
|
616
660
|
const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;
|
617
|
-
|
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 {
|
618
665
|
/**
|
619
|
-
|
620
|
-
|
666
|
+
* @inheritDoc
|
667
|
+
*/ static get requires() {
|
621
668
|
return [
|
622
669
|
Clipboard,
|
623
670
|
Delete,
|
@@ -625,13 +672,28 @@ class AutoMediaEmbed extends Plugin {
|
|
625
672
|
];
|
626
673
|
}
|
627
674
|
/**
|
628
|
-
|
629
|
-
|
675
|
+
* @inheritDoc
|
676
|
+
*/ static get pluginName() {
|
630
677
|
return 'AutoMediaEmbed';
|
631
678
|
}
|
632
679
|
/**
|
633
|
-
|
634
|
-
|
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() {
|
635
697
|
const editor = this.editor;
|
636
698
|
const modelDocument = editor.model.document;
|
637
699
|
// We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
|
@@ -665,12 +727,12 @@ class AutoMediaEmbed extends Plugin {
|
|
665
727
|
});
|
666
728
|
}
|
667
729
|
/**
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
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) {
|
674
736
|
const editor = this.editor;
|
675
737
|
const mediaRegistry = editor.plugins.get(MediaEmbedEditing).registry;
|
676
738
|
// TODO: Use marker instead of LiveRange & LivePositions.
|
@@ -722,19 +784,90 @@ class AutoMediaEmbed extends Plugin {
|
|
722
784
|
editor.plugins.get(Delete).requestUndoOnBackspace();
|
723
785
|
}, 100);
|
724
786
|
}
|
725
|
-
/**
|
726
|
-
* @inheritDoc
|
727
|
-
*/ constructor(editor){
|
728
|
-
super(editor);
|
729
|
-
this._timeoutId = null;
|
730
|
-
this._positionToInsert = null;
|
731
|
-
}
|
732
787
|
}
|
733
788
|
|
734
|
-
|
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
|
+
/**
|
801
|
+
* The URL input view.
|
802
|
+
*/ urlInputView;
|
803
|
+
/**
|
804
|
+
* The Save button view.
|
805
|
+
*/ saveButtonView;
|
806
|
+
/**
|
807
|
+
* The Cancel button view.
|
808
|
+
*/ cancelButtonView;
|
809
|
+
/**
|
810
|
+
* A collection of views that can be focused in the form.
|
811
|
+
*/ _focusables;
|
812
|
+
/**
|
813
|
+
* Helps cycling over {@link #_focusables} in the form.
|
814
|
+
*/ _focusCycler;
|
815
|
+
/**
|
816
|
+
* An array of form validators used by {@link #isValid}.
|
817
|
+
*/ _validators;
|
818
|
+
/**
|
819
|
+
* The default info text for the {@link #urlInputView}.
|
820
|
+
*/ _urlInputViewInfoDefault;
|
821
|
+
/**
|
822
|
+
* The info text with an additional tip for the {@link #urlInputView},
|
823
|
+
* displayed when the input has some value.
|
824
|
+
*/ _urlInputViewInfoTip;
|
735
825
|
/**
|
736
|
-
|
737
|
-
|
826
|
+
* @param validators Form validators used by {@link #isValid}.
|
827
|
+
* @param locale The localization services instance.
|
828
|
+
*/ constructor(validators, locale){
|
829
|
+
super(locale);
|
830
|
+
const t = locale.t;
|
831
|
+
this.focusTracker = new FocusTracker();
|
832
|
+
this.keystrokes = new KeystrokeHandler();
|
833
|
+
this.set('mediaURLInputValue', '');
|
834
|
+
this.urlInputView = this._createUrlInput();
|
835
|
+
this.saveButtonView = this._createButton(t('Save'), icons.check, 'ck-button-save');
|
836
|
+
this.saveButtonView.type = 'submit';
|
837
|
+
this.cancelButtonView = this._createButton(t('Cancel'), icons.cancel, 'ck-button-cancel', 'cancel');
|
838
|
+
this._focusables = new ViewCollection();
|
839
|
+
this._focusCycler = new FocusCycler({
|
840
|
+
focusables: this._focusables,
|
841
|
+
focusTracker: this.focusTracker,
|
842
|
+
keystrokeHandler: this.keystrokes,
|
843
|
+
actions: {
|
844
|
+
// Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
|
845
|
+
focusPrevious: 'shift + tab',
|
846
|
+
// Navigate form fields forwards using the <kbd>Tab</kbd> key.
|
847
|
+
focusNext: 'tab'
|
848
|
+
}
|
849
|
+
});
|
850
|
+
this._validators = validators;
|
851
|
+
this.setTemplate({
|
852
|
+
tag: 'form',
|
853
|
+
attributes: {
|
854
|
+
class: [
|
855
|
+
'ck',
|
856
|
+
'ck-media-form',
|
857
|
+
'ck-responsive-form'
|
858
|
+
],
|
859
|
+
tabindex: '-1'
|
860
|
+
},
|
861
|
+
children: [
|
862
|
+
this.urlInputView,
|
863
|
+
this.saveButtonView,
|
864
|
+
this.cancelButtonView
|
865
|
+
]
|
866
|
+
});
|
867
|
+
}
|
868
|
+
/**
|
869
|
+
* @inheritDoc
|
870
|
+
*/ render() {
|
738
871
|
super.render();
|
739
872
|
submitHandler({
|
740
873
|
view: this
|
@@ -762,31 +895,31 @@ class MediaFormView extends View {
|
|
762
895
|
this.keystrokes.set('arrowdown', stopPropagation);
|
763
896
|
}
|
764
897
|
/**
|
765
|
-
|
766
|
-
|
898
|
+
* @inheritDoc
|
899
|
+
*/ destroy() {
|
767
900
|
super.destroy();
|
768
901
|
this.focusTracker.destroy();
|
769
902
|
this.keystrokes.destroy();
|
770
903
|
}
|
771
904
|
/**
|
772
|
-
|
773
|
-
|
905
|
+
* Focuses the fist {@link #_focusables} in the form.
|
906
|
+
*/ focus() {
|
774
907
|
this._focusCycler.focusFirst();
|
775
908
|
}
|
776
909
|
/**
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
910
|
+
* The native DOM `value` of the {@link #urlInputView} element.
|
911
|
+
*
|
912
|
+
* **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
|
913
|
+
* which works one way only and may not represent the actual state of the component in the DOM.
|
914
|
+
*/ get url() {
|
782
915
|
return this.urlInputView.fieldView.element.value.trim();
|
783
916
|
}
|
784
917
|
set url(url) {
|
785
918
|
this.urlInputView.fieldView.element.value = url.trim();
|
786
919
|
}
|
787
920
|
/**
|
788
|
-
|
789
|
-
|
921
|
+
* Validates the form and returns `false` when some fields are invalid.
|
922
|
+
*/ isValid() {
|
790
923
|
this.resetFormStatus();
|
791
924
|
for (const validator of this._validators){
|
792
925
|
const errorText = validator(this);
|
@@ -800,19 +933,19 @@ class MediaFormView extends View {
|
|
800
933
|
return true;
|
801
934
|
}
|
802
935
|
/**
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
936
|
+
* Cleans up the supplementary error and information text of the {@link #urlInputView}
|
937
|
+
* bringing them back to the state when the form has been displayed for the first time.
|
938
|
+
*
|
939
|
+
* See {@link #isValid}.
|
940
|
+
*/ resetFormStatus() {
|
808
941
|
this.urlInputView.errorText = null;
|
809
942
|
this.urlInputView.infoText = this._urlInputViewInfoDefault;
|
810
943
|
}
|
811
944
|
/**
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
945
|
+
* Creates a labeled input view.
|
946
|
+
*
|
947
|
+
* @returns Labeled input view instance.
|
948
|
+
*/ _createUrlInput() {
|
816
949
|
const t = this.locale.t;
|
817
950
|
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
|
818
951
|
const inputField = labeledInput.fieldView;
|
@@ -828,14 +961,14 @@ class MediaFormView extends View {
|
|
828
961
|
return labeledInput;
|
829
962
|
}
|
830
963
|
/**
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
964
|
+
* Creates a button view.
|
965
|
+
*
|
966
|
+
* @param label The button label.
|
967
|
+
* @param icon The button icon.
|
968
|
+
* @param className The additional button CSS class name.
|
969
|
+
* @param eventName An event name that the `ButtonView#execute` event will be delegated to.
|
970
|
+
* @returns The button view instance.
|
971
|
+
*/ _createButton(label, icon, className, eventName) {
|
839
972
|
const button = new ButtonView(this.locale);
|
840
973
|
button.set({
|
841
974
|
label,
|
@@ -852,69 +985,28 @@ class MediaFormView extends View {
|
|
852
985
|
}
|
853
986
|
return button;
|
854
987
|
}
|
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
|
-
}
|
898
988
|
}
|
899
989
|
|
900
990
|
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>";
|
901
991
|
|
902
|
-
|
992
|
+
/**
|
993
|
+
* The media embed UI plugin.
|
994
|
+
*/ class MediaEmbedUI extends Plugin {
|
903
995
|
/**
|
904
|
-
|
905
|
-
|
996
|
+
* @inheritDoc
|
997
|
+
*/ static get requires() {
|
906
998
|
return [
|
907
999
|
MediaEmbedEditing
|
908
1000
|
];
|
909
1001
|
}
|
910
1002
|
/**
|
911
|
-
|
912
|
-
|
1003
|
+
* @inheritDoc
|
1004
|
+
*/ static get pluginName() {
|
913
1005
|
return 'MediaEmbedUI';
|
914
1006
|
}
|
915
1007
|
/**
|
916
|
-
|
917
|
-
|
1008
|
+
* @inheritDoc
|
1009
|
+
*/ init() {
|
918
1010
|
const editor = this.editor;
|
919
1011
|
const command = editor.commands.get('mediaEmbed');
|
920
1012
|
editor.ui.componentFactory.add('mediaEmbed', (locale)=>{
|
@@ -989,10 +1081,20 @@ function getFormValidators(t, registry) {
|
|
989
1081
|
];
|
990
1082
|
}
|
991
1083
|
|
992
|
-
|
1084
|
+
/**
|
1085
|
+
* The media embed plugin.
|
1086
|
+
*
|
1087
|
+
* For a detailed overview, check the {@glink features/media-embed Media Embed feature documentation}.
|
1088
|
+
*
|
1089
|
+
* This is a "glue" plugin which loads the following plugins:
|
1090
|
+
*
|
1091
|
+
* * The {@link module:media-embed/mediaembedediting~MediaEmbedEditing media embed editing feature},
|
1092
|
+
* * The {@link module:media-embed/mediaembedui~MediaEmbedUI media embed UI feature} and
|
1093
|
+
* * The {@link module:media-embed/automediaembed~AutoMediaEmbed auto-media embed feature}.
|
1094
|
+
*/ class MediaEmbed extends Plugin {
|
993
1095
|
/**
|
994
|
-
|
995
|
-
|
1096
|
+
* @inheritDoc
|
1097
|
+
*/ static get requires() {
|
996
1098
|
return [
|
997
1099
|
MediaEmbedEditing,
|
998
1100
|
MediaEmbedUI,
|
@@ -1001,28 +1103,33 @@ class MediaEmbed extends Plugin {
|
|
1001
1103
|
];
|
1002
1104
|
}
|
1003
1105
|
/**
|
1004
|
-
|
1005
|
-
|
1106
|
+
* @inheritDoc
|
1107
|
+
*/ static get pluginName() {
|
1006
1108
|
return 'MediaEmbed';
|
1007
1109
|
}
|
1008
1110
|
}
|
1009
1111
|
|
1010
|
-
|
1112
|
+
/**
|
1113
|
+
* The media embed toolbar plugin. It creates a toolbar for media embed that shows up when the media element is selected.
|
1114
|
+
*
|
1115
|
+
* Instances of toolbar components (e.g. buttons) are created based on the
|
1116
|
+
* {@link module:media-embed/mediaembedconfig~MediaEmbedConfig#toolbar `media.toolbar` configuration option}.
|
1117
|
+
*/ class MediaEmbedToolbar extends Plugin {
|
1011
1118
|
/**
|
1012
|
-
|
1013
|
-
|
1119
|
+
* @inheritDoc
|
1120
|
+
*/ static get requires() {
|
1014
1121
|
return [
|
1015
1122
|
WidgetToolbarRepository
|
1016
1123
|
];
|
1017
1124
|
}
|
1018
1125
|
/**
|
1019
|
-
|
1020
|
-
|
1126
|
+
* @inheritDoc
|
1127
|
+
*/ static get pluginName() {
|
1021
1128
|
return 'MediaEmbedToolbar';
|
1022
1129
|
}
|
1023
1130
|
/**
|
1024
|
-
|
1025
|
-
|
1131
|
+
* @inheritDoc
|
1132
|
+
*/ afterInit() {
|
1026
1133
|
const editor = this.editor;
|
1027
1134
|
const t = editor.t;
|
1028
1135
|
const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
|