1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | import { Plugin } from 'ckeditor5/src/core';
|
11 | import { first } from 'ckeditor5/src/utils';
|
12 |
|
13 | import { modelToViewUrlAttributeConverter } from './converters';
|
14 | import MediaEmbedCommand from './mediaembedcommand';
|
15 | import MediaRegistry from './mediaregistry';
|
16 | import { toMediaWidget, createMediaFigureElement } from './utils';
|
17 |
|
18 | import '../theme/mediaembedediting.css';
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | export default class MediaEmbedEditing extends Plugin {
|
26 | |
27 |
|
28 |
|
29 | static get pluginName() {
|
30 | return 'MediaEmbedEditing';
|
31 | }
|
32 |
|
33 | |
34 |
|
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 |
|
157 |
|
158 |
|
159 |
|
160 | this.registry = new MediaRegistry( editor.locale, editor.config.get( 'mediaEmbed' ) );
|
161 | }
|
162 |
|
163 | |
164 |
|
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 |
|
179 | schema.register( 'media', {
|
180 | inheritAllFrom: '$blockObject',
|
181 | allowAttributes: [ 'url' ]
|
182 | } );
|
183 |
|
184 |
|
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 |
|
198 | conversion.for( 'dataDowncast' ).add(
|
199 | modelToViewUrlAttributeConverter( registry, {
|
200 | elementName,
|
201 | renderMediaPreview
|
202 | } ) );
|
203 |
|
204 |
|
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 |
|
219 | conversion.for( 'editingDowncast' ).add(
|
220 | modelToViewUrlAttributeConverter( registry, {
|
221 | elementName,
|
222 | renderForEditingView: true
|
223 | } ) );
|
224 |
|
225 |
|
226 | conversion.for( 'upcast' )
|
227 |
|
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 |
|
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 |
|
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 |
|
274 | conversionApi.consumable.revert( data.viewItem, { name: true, classes: 'media' } );
|
275 | }
|
276 | }
|
277 | } );
|
278 | }
|
279 | }
|