UNPKG

9.89 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5/**
6 * @module media-embed/mediaembedediting
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import { first } from 'ckeditor5/src/utils';
10import { modelToViewUrlAttributeConverter } from './converters';
11import MediaEmbedCommand from './mediaembedcommand';
12import MediaRegistry from './mediaregistry';
13import { toMediaWidget, createMediaFigureElement } from './utils';
14import '../theme/mediaembedediting.css';
15/**
16 * The media embed editing feature.
17 */
18export default class MediaEmbedEditing extends Plugin {
19 /**
20 * @inheritDoc
21 */
22 static get pluginName() {
23 return 'MediaEmbedEditing';
24 }
25 /**
26 * @inheritDoc
27 */
28 constructor(editor) {
29 super(editor);
30 editor.config.define('mediaEmbed', {
31 elementName: 'oembed',
32 providers: [
33 {
34 name: 'dailymotion',
35 url: /^dailymotion\.com\/video\/(\w+)/,
36 html: match => {
37 const id = match[1];
38 return ('<div style="position: relative; padding-bottom: 100%; height: 0; ">' +
39 `<iframe src="https://www.dailymotion.com/embed/video/${id}" ` +
40 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
41 'frameborder="0" width="480" height="270" allowfullscreen allow="autoplay">' +
42 '</iframe>' +
43 '</div>');
44 }
45 },
46 {
47 name: 'spotify',
48 url: [
49 /^open\.spotify\.com\/(artist\/\w+)/,
50 /^open\.spotify\.com\/(album\/\w+)/,
51 /^open\.spotify\.com\/(track\/\w+)/
52 ],
53 html: match => {
54 const id = match[1];
55 return ('<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 126%;">' +
56 `<iframe src="https://open.spotify.com/embed/${id}" ` +
57 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
58 'frameborder="0" allowtransparency="true" allow="encrypted-media">' +
59 '</iframe>' +
60 '</div>');
61 }
62 },
63 {
64 name: 'youtube',
65 url: [
66 /^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/,
67 /^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/,
68 /^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/,
69 /^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/
70 ],
71 html: match => {
72 const id = match[1];
73 const time = match[2];
74 return ('<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
75 `<iframe src="https://www.youtube.com/embed/${id}${time ? `?start=${time}` : ''}" ` +
76 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
77 'frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>' +
78 '</iframe>' +
79 '</div>');
80 }
81 },
82 {
83 name: 'vimeo',
84 url: [
85 /^vimeo\.com\/(\d+)/,
86 /^vimeo\.com\/[^/]+\/[^/]+\/video\/(\d+)/,
87 /^vimeo\.com\/album\/[^/]+\/video\/(\d+)/,
88 /^vimeo\.com\/channels\/[^/]+\/(\d+)/,
89 /^vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
90 /^vimeo\.com\/ondemand\/[^/]+\/(\d+)/,
91 /^player\.vimeo\.com\/video\/(\d+)/
92 ],
93 html: match => {
94 const id = match[1];
95 return ('<div style="position: relative; padding-bottom: 100%; height: 0; padding-bottom: 56.2493%;">' +
96 `<iframe src="https://player.vimeo.com/video/${id}" ` +
97 'style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;" ' +
98 'frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>' +
99 '</iframe>' +
100 '</div>');
101 }
102 },
103 {
104 name: 'instagram',
105 url: /^instagram\.com\/p\/(\w+)/
106 },
107 {
108 name: 'twitter',
109 url: /^twitter\.com/
110 },
111 {
112 name: 'googleMaps',
113 url: [
114 /^google\.com\/maps/,
115 /^goo\.gl\/maps/,
116 /^maps\.google\.com/,
117 /^maps\.app\.goo\.gl/
118 ]
119 },
120 {
121 name: 'flickr',
122 url: /^flickr\.com/
123 },
124 {
125 name: 'facebook',
126 url: /^facebook\.com/
127 }
128 ]
129 });
130 this.registry = new MediaRegistry(editor.locale, editor.config.get('mediaEmbed'));
131 }
132 /**
133 * @inheritDoc
134 */
135 init() {
136 const editor = this.editor;
137 const schema = editor.model.schema;
138 const t = editor.t;
139 const conversion = editor.conversion;
140 const renderMediaPreview = editor.config.get('mediaEmbed.previewsInData');
141 const elementName = editor.config.get('mediaEmbed.elementName');
142 const registry = this.registry;
143 editor.commands.add('mediaEmbed', new MediaEmbedCommand(editor));
144 // Configure the schema.
145 schema.register('media', {
146 inheritAllFrom: '$blockObject',
147 allowAttributes: ['url']
148 });
149 // Model -> Data
150 conversion.for('dataDowncast').elementToStructure({
151 model: 'media',
152 view: (modelElement, { writer }) => {
153 const url = modelElement.getAttribute('url');
154 return createMediaFigureElement(writer, registry, url, {
155 elementName,
156 renderMediaPreview: !!url && renderMediaPreview
157 });
158 }
159 });
160 // Model -> Data (url -> data-oembed-url)
161 conversion.for('dataDowncast').add(modelToViewUrlAttributeConverter(registry, {
162 elementName,
163 renderMediaPreview
164 }));
165 // Model -> View (element)
166 conversion.for('editingDowncast').elementToStructure({
167 model: 'media',
168 view: (modelElement, { writer }) => {
169 const url = modelElement.getAttribute('url');
170 const figure = createMediaFigureElement(writer, registry, url, {
171 elementName,
172 renderForEditingView: true
173 });
174 return toMediaWidget(figure, writer, t('media widget'));
175 }
176 });
177 // Model -> View (url -> data-oembed-url)
178 conversion.for('editingDowncast').add(modelToViewUrlAttributeConverter(registry, {
179 elementName,
180 renderForEditingView: true
181 }));
182 // View -> Model (data-oembed-url -> url)
183 conversion.for('upcast')
184 // Upcast semantic media.
185 .elementToElement({
186 view: element => ['oembed', elementName].includes(element.name) && element.getAttribute('url') ?
187 { name: true } :
188 null,
189 model: (viewMedia, { writer }) => {
190 const url = viewMedia.getAttribute('url');
191 if (registry.hasMedia(url)) {
192 return writer.createElement('media', { url });
193 }
194 return null;
195 }
196 })
197 // Upcast non-semantic media.
198 .elementToElement({
199 view: {
200 name: 'div',
201 attributes: {
202 'data-oembed-url': true
203 }
204 },
205 model: (viewMedia, { writer }) => {
206 const url = viewMedia.getAttribute('data-oembed-url');
207 if (registry.hasMedia(url)) {
208 return writer.createElement('media', { url });
209 }
210 return null;
211 }
212 })
213 // Consume `<figure class="media">` elements, that were left after upcast.
214 .add(dispatcher => {
215 const converter = (evt, data, conversionApi) => {
216 if (!conversionApi.consumable.consume(data.viewItem, { name: true, classes: 'media' })) {
217 return;
218 }
219 const { modelRange, modelCursor } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
220 data.modelRange = modelRange;
221 data.modelCursor = modelCursor;
222 const modelElement = first(modelRange.getItems());
223 if (!modelElement) {
224 // Revert consumed figure so other features can convert it.
225 conversionApi.consumable.revert(data.viewItem, { name: true, classes: 'media' });
226 }
227 };
228 dispatcher.on('element:figure', converter);
229 });
230 }
231}