@ckeditor/ckeditor5-media-embed
Version:
Media embed feature for CKEditor 5.
232 lines (231 loc) • 9.89 kB
JavaScript
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module media-embed/mediaembedediting
*/
import { Plugin } from 'ckeditor5/src/core';
import { first } from 'ckeditor5/src/utils';
import { modelToViewUrlAttributeConverter } from './converters';
import MediaEmbedCommand from './mediaembedcommand';
import MediaRegistry from './mediaregistry';
import { toMediaWidget, createMediaFigureElement } from './utils';
import '../theme/mediaembedediting.css';
/**
* The media embed editing feature.
*/
export default class MediaEmbedEditing extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'MediaEmbedEditing';
}
/**
* @inheritDoc
*/
constructor(editor) {
super(editor);
editor.config.define('mediaEmbed', {
elementName: 'oembed',
providers: [
{
name: 'dailymotion',
url: /^dailymotion\.com\/video\/(\w+)/,
html: match => {
const id = match[1];
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>');
}
},
{
name: 'spotify',
url: [
/^open\.spotify\.com\/(artist\/\w+)/,
/^open\.spotify\.com\/(album\/\w+)/,
/^open\.spotify\.com\/(track\/\w+)/
],
html: match => {
const id = match[1];
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>');
}
},
{
name: 'youtube',
url: [
/^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/,
/^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/,
/^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/,
/^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/
],
html: match => {
const id = match[1];
const time = match[2];
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>');
}
},
{
name: 'vimeo',
url: [
/^vimeo\.com\/(\d+)/,
/^vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
/^vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
/^vimeo\.com\/channels\/[^/]+\/(\d+)/,
/^vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
/^vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
/^player\.vimeo\.com\/video\/(\d+)/
],
html: match => {
const id = match[1];
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>');
}
},
{
name: 'instagram',
url: /^instagram\.com\/p\/(\w+)/
},
{
name: 'twitter',
url: /^twitter\.com/
},
{
name: 'googleMaps',
url: [
/^google\.com\/maps/,
/^goo\.gl\/maps/,
/^maps\.google\.com/,
/^maps\.app\.goo\.gl/
]
},
{
name: 'flickr',
url: /^flickr\.com/
},
{
name: 'facebook',
url: /^facebook\.com/
}
]
});
this.registry = new MediaRegistry(editor.locale, editor.config.get('mediaEmbed'));
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const schema = editor.model.schema;
const t = editor.t;
const conversion = editor.conversion;
const renderMediaPreview = editor.config.get('mediaEmbed.previewsInData');
const elementName = editor.config.get('mediaEmbed.elementName');
const registry = this.registry;
editor.commands.add('mediaEmbed', new MediaEmbedCommand(editor));
// Configure the schema.
schema.register('media', {
inheritAllFrom: '$blockObject',
allowAttributes: ['url']
});
// Model -> Data
conversion.for('dataDowncast').elementToStructure({
model: 'media',
view: (modelElement, { writer }) => {
const url = modelElement.getAttribute('url');
return createMediaFigureElement(writer, registry, url, {
elementName,
renderMediaPreview: !!url && renderMediaPreview
});
}
});
// Model -> Data (url -> data-oembed-url)
conversion.for('dataDowncast').add(modelToViewUrlAttributeConverter(registry, {
elementName,
renderMediaPreview
}));
// Model -> View (element)
conversion.for('editingDowncast').elementToStructure({
model: 'media',
view: (modelElement, { writer }) => {
const url = modelElement.getAttribute('url');
const figure = createMediaFigureElement(writer, registry, url, {
elementName,
renderForEditingView: true
});
return toMediaWidget(figure, writer, t('media widget'));
}
});
// Model -> View (url -> data-oembed-url)
conversion.for('editingDowncast').add(modelToViewUrlAttributeConverter(registry, {
elementName,
renderForEditingView: true
}));
// View -> Model (data-oembed-url -> url)
conversion.for('upcast')
// Upcast semantic media.
.elementToElement({
view: element => ['oembed', elementName].includes(element.name) && element.getAttribute('url') ?
{ name: true } :
null,
model: (viewMedia, { writer }) => {
const url = viewMedia.getAttribute('url');
if (registry.hasMedia(url)) {
return writer.createElement('media', { url });
}
return null;
}
})
// Upcast non-semantic media.
.elementToElement({
view: {
name: 'div',
attributes: {
'data-oembed-url': true
}
},
model: (viewMedia, { writer }) => {
const url = viewMedia.getAttribute('data-oembed-url');
if (registry.hasMedia(url)) {
return writer.createElement('media', { url });
}
return null;
}
})
// Consume `<figure class="media">` elements, that were left after upcast.
.add(dispatcher => {
const converter = (evt, data, conversionApi) => {
if (!conversionApi.consumable.consume(data.viewItem, { name: true, classes: 'media' })) {
return;
}
const { modelRange, modelCursor } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
data.modelRange = modelRange;
data.modelCursor = modelCursor;
const modelElement = first(modelRange.getItems());
if (!modelElement) {
// Revert consumed figure so other features can convert it.
conversionApi.consumable.revert(data.viewItem, { name: true, classes: 'media' });
}
};
dispatcher.on('element:figure', converter);
});
}
}