UNPKG

@ckeditor/ckeditor5-media-embed

Version:

Media embed feature for CKEditor 5.

232 lines (231 loc) • 9.89 kB
/** * @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); }); } }