UNPKG

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