Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Tiny media plugin embed preview and details class.
18
 *
19
 * This handles the embed file/url preview before embedding them into tiny editor.
20
 *
21
 * @module      tiny_media/embed/embedpreview
22
 * @copyright   2024 Stevani Andolo <stevani@hotmail.com.au>
23
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
import Selectors from '../selectors';
27
import {component} from '../common';
28
import {getString} from 'core/str';
29
import {
30
    sourceTypeChecked,
31
    getFileName,
32
    setPropertiesFromData,
33
    showElements,
34
    stopMediaLoading,
35
    hideElements,
36
} from '../helpers';
37
import {EmbedHandler} from './embedhandler';
38
import {MediaBase} from '../mediabase';
39
import Notification from 'core/notification';
40
import EmbedModal from '../embedmodal';
41
import {
42
    getEmbeddedMediaDetails,
43
    insertMediaThumbnailTemplateContext,
44
    fetchPreview,
45
} from './embedhelpers';
46
import {notifyFilterContentUpdated} from 'core_filters/events';
47
 
48
export class EmbedPreview extends MediaBase {
49
 
50
    // Selector type for "EMBED".
51
    selectorType = Selectors.EMBED.type;
52
 
53
    // Fixed aspect ratio used for external media providers.
54
    linkMediaAspectRatio = 1.78;
55
 
56
    constructor(data) {
57
        super();
58
        setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param.
59
    }
60
 
61
    /**
62
     * Init the media details preview.
63
     */
64
    init = async() => {
65
        this.currentModal.setTitle(getString('mediadetails', component));
66
        sourceTypeChecked({
67
            fetchedTitle: this.fetchedMediaLinkTitle ?? null,
68
            source: this.originalUrl,
69
            root: this.root,
70
            urlSelector: Selectors.EMBED.elements.fromUrl,
71
            fileNameSelector: Selectors.EMBED.elements.fileNameLabel,
72
        });
73
        this.setMediaSourceAndPoster();
74
        this.registerMediaDetailsEventListeners(this.currentModal);
75
    };
76
 
77
    /**
78
     * Sets media source and thumbnail for the video.
79
     */
80
    setMediaSourceAndPoster = async() => {
81
        const box = this.root.querySelector(Selectors.EMBED.elements.previewBox);
82
        const previewArea = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);
83
        previewArea.setAttribute('data-original-url', this.originalUrl);
84
 
85
        // Previewing existing media could be a link one.
86
        // Or, new media added using url input and mediaType is neither video or audio.
87
        if (this.mediaType === 'link' || (this.newMediaLink && !['video', 'audio'].includes(this.mediaType))) {
88
            previewArea.setAttribute('data-media-type', 'link');
89
            previewArea.innerHTML = await fetchPreview(this.originalUrl, this.contextId);
90
            notifyFilterContentUpdated(previewArea);
91
        } else if (this.mediaType === 'video') {
92
            const video = document.createElement('video');
93
            video.src = this.originalUrl;
94
 
95
            // Media url can be played using html video.
96
            video.addEventListener('loadedmetadata', () => {
97
                const videoHeight = video.videoHeight;
98
                const videoWidth = video.videoWidth;
99
                const widthProportion = (videoWidth - videoHeight);
100
                const isLandscape = widthProportion > 0;
101
 
102
                // Store dimensions of the raw video.
103
                this.mediaDimensions = {
104
                    width: videoWidth,
105
                    height: videoHeight,
106
                };
107
 
108
                // Set the media preview based on the media dimensions.
109
                if (isLandscape) {
110
                    video.width = box.offsetWidth;
111
                } else {
112
                    video.height = box.offsetHeight;
113
                }
114
 
115
                const height = this.root.querySelector(Selectors.EMBED.elements.height);
116
                const width = this.root.querySelector(Selectors.EMBED.elements.width);
117
 
118
                if (height.value === '' && width.value === '') {
119
                    height.value = videoHeight;
120
                    width.value = videoWidth;
121
                }
122
 
123
                // Size checking and adjustment.
124
                if (videoHeight === parseInt(height.value) && videoWidth === parseInt(width.value)) {
125
                    this.currentWidth = this.mediaDimensions.width;
126
                    this.currentHeight = this.mediaDimensions.height;
127
                    this.sizeChecked('original');
128
                } else {
129
                    this.currentWidth = parseInt(width.value);
130
                    this.currentHeight = parseInt(height.value);
131
                    this.sizeChecked('custom');
132
                }
133
            });
134
 
135
            video.controls = true;
136
            if (this.media.poster) {
137
                previewArea.setAttribute('data-media-poster', this.media.poster);
138
                if (!video.classList.contains('w-100')) {
139
                    video.classList.add('w-100');
140
                }
141
                video.poster = this.media.poster;
142
            }
143
            video.load();
144
 
145
            previewArea.setAttribute('data-media-type', 'video');
146
            previewArea.innerHTML = video.outerHTML;
147
            notifyFilterContentUpdated(previewArea);
148
        } else if (this.mediaType === 'audio') {
149
            const audio = document.createElement('audio');
150
            audio.src = this.originalUrl;
151
            audio.controls = true;
152
            audio.load();
153
 
154
            previewArea.setAttribute('data-media-type', 'audio');
155
            previewArea.innerHTML = audio.outerHTML;
156
            notifyFilterContentUpdated(previewArea);
157
        } else {
158
            // Show warning notification.
159
            const urlWarningLabelEle = this.root.querySelector(Selectors.EMBED.elements.urlWarning);
160
            urlWarningLabelEle.innerHTML = await getString('medianotavailabledesc', component, this.originalUrl);
161
            showElements(Selectors.EMBED.elements.urlWarning, this.root);
162
 
163
            // Stop the spinner.
164
            stopMediaLoading(this.root, Selectors.EMBED.type);
165
 
166
            // Reset the upload form.
167
            (new EmbedHandler(this)).resetUploadForm();
168
            return;
169
        }
170
 
171
        // Stop the loader and display back the body template when the media is loaded.
172
        stopMediaLoading(this.root, Selectors.EMBED.type);
173
        showElements(Selectors.EMBED.elements.mediaDetailsBody, this.root);
174
 
175
        // Set the media name/title.
176
        this.root.querySelector(Selectors.EMBED.elements.title).value = this.setMediaTitle();
177
    };
178
 
179
    /**
180
     * Set media name/title.
181
     *
182
     * @returns {string}
183
     */
184
    setMediaTitle = () => {
185
        // Getting and setting up media title/name.
186
        let fileName = null;
187
        if (['video', 'audio'].includes(this.mediaType)) {
188
            fileName = getFileName(this.originalUrl); // Get original filename.
189
        } else if (this.fetchedMediaLinkTitle) {
190
            fileName = this.fetchedMediaLinkTitle;
191
        } else {
192
            fileName = this.originalUrl;
193
        }
194
 
195
        if (this.isUpdating) {
196
            if (!this.newMediaLink) {
197
                fileName = this.mediaTitle; // Title from the selected media.
198
            }
199
        }
200
 
201
        return fileName;
202
    };
203
 
204
    /**
205
     * Deletes the media after confirming with the user and loads the insert media page.
206
     */
207
    deleteMedia = () => {
208
        Notification.deleteCancelPromise(
209
            getString('deletemedia', component),
210
            getString('deletemediawarning', component),
211
        ).then(() => {
212
            // Reset media upload form.
213
            (new EmbedHandler(this)).resetUploadForm();
214
 
215
            // Delete any selected media mediaData.
216
            delete this.mediaData;
217
            return;
218
        }).catch(error => {
219
            window.console.log(error);
220
        });
221
    };
222
 
223
    /**
224
     * Delete embedded media thumbnail.
225
     */
226
    deleteEmbeddedThumbnail = () => {
227
        Notification.deleteCancelPromise(
228
            getString('deleteembeddedthumbnail', component),
229
            getString('deleteembeddedthumbnailwarning', component),
230
        ).then(async() => {
231
            if (this.mediaType === 'video') {
232
                const video = this.root.querySelector('video');
233
                if (video) {
234
                    video.removeAttribute('poster');
235
                    const preview = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);
236
                    preview.removeAttribute('data-media-poster');
237
                }
238
            }
239
 
240
            const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail);
241
            deleteCustomThumbnail.remove();
242
 
243
            const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail);
244
            uploadCustomThumbnail.textContent = await getString('uploadthumbnail', component);
245
            return;
246
        }).catch(error => {
247
            window.console.log(error);
248
        });
249
    };
250
 
251
    /**
252
     * Shows the insert thumbnail dialogue.
253
     */
254
    showUploadThumbnail = async() => {
255
        const uploadThumbnailModal = await EmbedModal.create({
256
            large: true,
257
            templateContext: {elementid: this.editor.getElement().id},
258
        });
259
        const root = uploadThumbnailModal.getRoot()[0];
260
 
261
        // Get selected media metadata.
262
        const mediaData = getEmbeddedMediaDetails(this);
263
        mediaData.isUpdating = this.isUpdating;
264
 
265
        const embedHandler = new EmbedHandler(this);
266
        embedHandler.loadInsertThumbnailTemplatePromise(
267
            insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail.
268
            {root, uploadThumbnailModal}, // Required root elements.
269
            await embedHandler.getMediaTemplateContext(mediaData) // Get current media data.
270
        );
271
    };
272
 
273
    /**
274
     * Only registers event listeners for new loaded elements in embed preview modal.
275
     */
276
    registerMediaDetailsEventListeners = async() => {
277
        // Handle the original size when selected.
278
        const sizeOriginalEle = this.root.querySelector(Selectors.EMBED.elements.sizeOriginal);
279
        if (sizeOriginalEle) {
280
            sizeOriginalEle.addEventListener('change', () => {
281
                this.sizeChecked('original');
282
            });
283
        }
284
 
285
        // Handle the custom size when selected.
286
        const sizeCustomEle = this.root.querySelector(Selectors.EMBED.elements.sizeCustom);
287
        if (sizeCustomEle) {
288
            sizeCustomEle.addEventListener('change', () => {
289
                this.sizeChecked('custom');
290
            });
291
        }
292
 
293
        const widthEle = this.root.querySelector(Selectors.EMBED.elements.width);
294
        const heightEle = this.root.querySelector(Selectors.EMBED.elements.height);
295
 
296
        // Handle the custom with size when inputted.
297
        if (widthEle) {
298
            widthEle.addEventListener('input', () => {
299
                if (this.mediaType === 'link') {
300
                    // Let's apply the 16:9 aspect ratio if it's a link media type.
301
                    heightEle.value = Math.round(widthEle.value / this.linkMediaAspectRatio);
302
                } else {
303
                    // Avoid empty value.
304
                    widthEle.value = widthEle.value === "" ? 0 : Number(widthEle.value);
305
                    this.autoAdjustSize();
306
                }
307
            });
308
        }
309
 
310
        // Handle the custom height size when inputted.
311
        if (heightEle) {
312
            heightEle.addEventListener('input', () => {
313
                if (this.mediaType === 'link') {
314
                    // Let's apply the 16:9 aspect ratio if it's a link media type.
315
                    widthEle.value = Math.round(heightEle.value * this.linkMediaAspectRatio);
316
                } else {
317
                    // Avoid empty value.
318
                    heightEle.value = heightEle.value === "" ? 0 : Number(heightEle.value);
319
                    this.autoAdjustSize(true);
320
                }
321
            });
322
        }
323
 
324
        // Handle media preview delete.
325
        const deleteMedia = this.root.querySelector(Selectors.EMBED.actions.deleteMedia);
326
        if (deleteMedia) {
327
            deleteMedia.addEventListener('click', (e) => {
328
                e.preventDefault();
329
                this.deleteMedia();
330
            });
331
        }
332
 
333
        // Show subtitles and captions settings.
334
        const showSubtitleCaption = this.root.querySelector(Selectors.EMBED.actions.showSubtitleCaption);
335
        if (showSubtitleCaption) {
336
            showSubtitleCaption.addEventListener('click', (e) => {
337
                e.preventDefault();
338
                hideElements([
339
                    Selectors.EMBED.actions.showSubtitleCaption,
340
                    Selectors.EMBED.actions.cancelMediaDetails,
341
                    Selectors.EMBED.elements.mediaDetailsBody,
342
                ], this.root);
343
                showElements([
344
                    Selectors.EMBED.actions.backToMediaDetails,
345
                    Selectors.EMBED.elements.mediaSubtitleCaptionBody,
346
                ], this.root);
347
            });
348
        }
349
 
350
        // Back to media preview.
351
        const backToMediaDetails = this.root.querySelector(Selectors.EMBED.actions.backToMediaDetails);
352
        if (backToMediaDetails) {
353
            backToMediaDetails.addEventListener('click', () => {
354
                hideElements([
355
                    Selectors.EMBED.actions.backToMediaDetails,
356
                    Selectors.EMBED.elements.mediaSubtitleCaptionBody,
357
                ], this.root);
358
                showElements([
359
                    Selectors.EMBED.actions.showSubtitleCaption,
360
                    Selectors.EMBED.actions.cancelMediaDetails,
361
                    Selectors.EMBED.elements.mediaDetailsBody,
362
                ], this.root);
363
            });
364
        }
365
 
366
        // Handles upload media thumbnail.
367
        const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail);
368
        if (uploadCustomThumbnail) {
369
            uploadCustomThumbnail.addEventListener('click', () => {
370
                this.showUploadThumbnail();
371
            });
372
        }
373
 
374
        // Handles delete media thumbnail.
375
        const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail);
376
        if (deleteCustomThumbnail) {
377
            deleteCustomThumbnail.addEventListener('click', () => {
378
                this.deleteEmbeddedThumbnail();
379
            });
380
        }
381
 
382
        // Handles language track selection.
383
        const langTracks = this.root.querySelectorAll(Selectors.EMBED.elements.trackLang);
384
        if (langTracks) {
385
            langTracks.forEach((dropdown) => {
386
                const defaultVal = dropdown.getAttribute('data-value');
387
                if (defaultVal) {
388
                    Array.from(dropdown.options).some(option => {
389
                        // Check if srclang in track is a language code like "en"
390
                        // or language name like "English" prior to MDL-85159.
391
                        if (option.dataset.languageCode === defaultVal || option.value === defaultVal) {
392
                            option.selected = true;
393
                            return true;
394
                        }
395
                        return false;
396
                    });
397
                }
398
            });
399
        }
400
    };
401
}