1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import { Plugin } from 'ckeditor5/src/core';
|
9 | import { first } from 'ckeditor5/src/utils';
|
10 | import { modelToViewUrlAttributeConverter } from './converters';
|
11 | import MediaEmbedCommand from './mediaembedcommand';
|
12 | import MediaRegistry from './mediaregistry';
|
13 | import { toMediaWidget, createMediaFigureElement } from './utils';
|
14 | import '../theme/mediaembedediting.css';
|
15 |
|
16 |
|
17 |
|
18 | export default class MediaEmbedEditing extends Plugin {
|
19 | |
20 |
|
21 |
|
22 | static get pluginName() {
|
23 | return 'MediaEmbedEditing';
|
24 | }
|
25 | |
26 |
|
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 |
|
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 |
|
145 | schema.register('media', {
|
146 | inheritAllFrom: '$blockObject',
|
147 | allowAttributes: ['url']
|
148 | });
|
149 |
|
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 |
|
161 | conversion.for('dataDowncast').add(modelToViewUrlAttributeConverter(registry, {
|
162 | elementName,
|
163 | renderMediaPreview
|
164 | }));
|
165 |
|
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 |
|
178 | conversion.for('editingDowncast').add(modelToViewUrlAttributeConverter(registry, {
|
179 | elementName,
|
180 | renderForEditingView: true
|
181 | }));
|
182 |
|
183 | conversion.for('upcast')
|
184 |
|
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 |
|
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 |
|
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 |
|
225 | conversionApi.consumable.revert(data.viewItem, { name: true, classes: 'media' });
|
226 | }
|
227 | };
|
228 | dispatcher.on('element:figure', converter);
|
229 | });
|
230 | }
|
231 | }
|