Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 class for Moodle.
18
 *
19
 * @module      tiny_media/embed
20
 * @copyright   2022 Huong Nguyen <huongnv13@gmail.com>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import Templates from 'core/templates';
25
import {
26
    getString,
27
    getStrings,
28
} from 'core/str';
29
import * as ModalEvents from 'core/modal_events';
30
import {displayFilepicker} from 'editor_tiny/utils';
31
import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';
32
import {component} from "./common";
33
import EmbedModal from './embedmodal';
34
import Selectors from './selectors';
35
import {getEmbedPermissions} from './options';
36
import {getFilePicker} from 'editor_tiny/options';
37
 
38
export default class MediaEmbed {
39
    editor = null;
40
    canShowFilePicker = false;
41
    canShowFilePickerPoster = false;
42
    canShowFilePickerTrack = false;
43
 
44
    /**
45
     * @property {Object} The names of the alignment options.
46
     */
47
    helpStrings = null;
48
 
49
    /**
50
     * @property {boolean} Indicate that the user is updating the media or not.
51
     */
52
    isUpdating = false;
53
 
54
    /**
55
     * @property {Object} The currently selected media.
56
     */
57
    selectedMedia = null;
58
 
59
    constructor(editor) {
60
        const permissions = getEmbedPermissions(editor);
61
 
62
        // Indicates whether the file picker can be shown.
63
        this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'media') !== 'undefined');
64
        this.canShowFilePickerPoster = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');
65
        this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');
66
 
67
        this.editor = editor;
68
    }
69
 
70
    async getHelpStrings() {
71
        if (!this.helpStrings) {
72
            const [addSource, tracks, subtitles, captions, descriptions, chapters, metadata] = await getStrings([
73
                'addsource_help',
74
                'tracks_help',
75
                'subtitles_help',
76
                'captions_help',
77
                'descriptions_help',
78
                'chapters_help',
79
                'metadata_help',
80
            ].map((key) => ({
81
                key,
82
                component,
83
            })));
84
 
85
            this.helpStrings = {addSource, tracks, subtitles, captions, descriptions, chapters, metadata};
86
        }
87
 
88
        return this.helpStrings;
89
    }
90
 
91
    async getTemplateContext(data) {
92
        const languages = this.prepareMoodleLang();
93
 
94
        const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {
95
            data[`${key.toLowerCase()}helpicon`] = {text};
96
        });
97
 
98
        return Object.assign({}, {
99
            elementid: this.editor.getElement().id,
100
            showfilepicker: this.canShowFilePicker,
101
            showfilepickerposter: this.canShowFilePickerPoster,
102
            showfilepickertrack: this.canShowFilePickerTrack,
103
            langsinstalled: languages.installed,
104
            langsavailable: languages.available,
105
            link: true,
106
            video: false,
107
            audio: false,
108
            isupdating: this.isUpdating,
109
        }, data, helpIcons);
110
    }
111
 
112
    async displayDialogue() {
113
        this.selectedMedia = this.getSelectedMedia();
114
        const data = Object.assign({}, this.getCurrentEmbedData());
115
        this.isUpdating = Object.keys(data).length !== 0;
116
 
117
        this.currentModal = await EmbedModal.create({
118
            title: getString('createmedia', 'tiny_media'),
119
            templateContext: await this.getTemplateContext(data),
120
        });
121
 
122
        await this.registerEventListeners(this.currentModal);
123
    }
124
 
125
    getCurrentEmbedData() {
126
        const properties = this.getMediumProperties();
127
        if (!properties) {
128
            return {};
129
        }
130
 
131
        const processedProperties = {};
132
        processedProperties[properties.type.toLowerCase()] = properties;
133
        processedProperties.link = false;
134
 
135
        return processedProperties;
136
    }
137
 
138
    getSelectedMedia() {
139
        const mediaElm = this.editor.selection.getNode();
140
 
141
        if (!mediaElm) {
142
            return null;
143
        }
144
 
145
        if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {
146
            return mediaElm;
147
        }
148
 
149
        if (mediaElm.querySelector('video')) {
150
            return mediaElm.querySelector('video');
151
        }
152
 
153
        if (mediaElm.querySelector('audio')) {
154
            return mediaElm.querySelector('audio');
155
        }
156
 
157
        return null;
158
    }
159
 
160
    getMediumProperties() {
161
        const boolAttr = (elem, attr) => {
162
            // As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
163
            // So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
164
            return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
165
        };
166
 
167
        const tracks = {
168
            subtitles: [],
169
            captions: [],
170
            descriptions: [],
171
            chapters: [],
172
            metadata: []
173
        };
174
        const sources = [];
175
 
176
        const medium = this.selectedMedia;
177
        if (!medium) {
178
            return null;
179
        }
180
        medium.querySelectorAll('track').forEach((track) => {
181
            tracks[track.getAttribute('kind')].push({
182
                src: track.getAttribute('src'),
183
                srclang: track.getAttribute('srclang'),
184
                label: track.getAttribute('label'),
185
                defaultTrack: boolAttr(track, 'default')
186
            });
187
        });
188
 
189
        medium.querySelectorAll('source').forEach((source) => {
190
            sources.push(source.src);
191
        });
192
 
193
        return {
194
            type: medium.nodeName.toLowerCase() === 'video' ? Selectors.EMBED.mediaTypes.video : Selectors.EMBED.mediaTypes.audio,
195
            sources,
196
            poster: medium.getAttribute('poster'),
197
            title: medium.getAttribute('title'),
198
            width: medium.getAttribute('width'),
199
            height: medium.getAttribute('height'),
200
            autoplay: boolAttr(medium, 'autoplay'),
201
            loop: boolAttr(medium, 'loop'),
202
            muted: boolAttr(medium, 'muted'),
203
            controls: boolAttr(medium, 'controls'),
204
            tracks,
205
        };
206
    }
207
 
208
    prepareMoodleLang() {
209
        const moodleLangs = getMoodleLang(this.editor);
210
        const currentLanguage = getCurrentLanguage(this.editor);
211
 
212
        const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({
213
            lang,
214
            code,
215
            "default": lang === currentLanguage,
216
        }));
217
 
218
        const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({
219
            lang,
220
            code,
221
            "default": lang === currentLanguage,
222
        }));
223
 
224
        return {
225
            installed,
226
            available,
227
        };
228
    }
229
 
230
    getMoodleLangObj(subtitleLang) {
231
        const {available} = getMoodleLang(this.editor);
232
 
233
        if (available[subtitleLang]) {
234
            return {
235
                lang: subtitleLang,
236
                code: available[subtitleLang],
237
            };
238
        }
239
 
240
        return null;
241
    }
242
 
243
    filePickerCallback(params, element, fpType) {
244
        if (params.url !== '') {
245
            const tabPane = element.closest('.tab-pane');
246
            element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;
247
 
248
            if (tabPane.id === this.editor.getElement().id + '_' + Selectors.EMBED.mediaTypes.link.toLowerCase()) {
249
                tabPane.querySelector(Selectors.EMBED.elements.name).value = params.file;
250
            }
251
 
252
            if (fpType === 'subtitle') {
253
                // If the file is subtitle file. We need to match the language and label for that file.
254
                const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];
255
                const langObj = this.getMoodleLangObj(subtitleLang);
256
                if (langObj) {
257
                    const track = element.closest(Selectors.EMBED.elements.track);
258
                    track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();
259
                    track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;
260
                }
261
            }
262
        }
263
    }
264
 
265
    addMediaSourceComponent(element, callback) {
266
        const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
267
        const clone = sourceElement.cloneNode(true);
268
 
269
        sourceElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
270
        sourceElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
271
 
272
        sourceElement.parentNode.insertBefore(clone, sourceElement.nextSibling);
273
 
274
        if (callback) {
275
            callback(clone);
276
        }
277
    }
278
 
279
    removeMediaSourceComponent(element) {
280
        const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
281
        sourceElement.remove();
282
    }
283
 
284
    addTrackComponent(element, callback) {
285
        const trackElement = element.closest(Selectors.EMBED.elements.track);
286
        const clone = trackElement.cloneNode(true);
287
 
288
        trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
289
        trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
290
 
291
        trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);
292
 
293
        if (callback) {
294
            callback(clone);
295
        }
296
    }
297
 
298
    removeTrackComponent(element) {
299
        const sourceElement = element.closest(Selectors.EMBED.elements.track);
300
        sourceElement.remove();
301
    }
302
 
303
    getMediumTypeFromTabPane(tabPane) {
304
        return tabPane.getAttribute('data-medium-type');
305
    }
306
 
307
    getTrackTypeFromTabPane(tabPane) {
308
        return tabPane.getAttribute('data-track-kind');
309
    }
310
 
311
    getMediaHTML(form) {
312
        const mediumType = this.getMediumTypeFromTabPane(form.querySelector('.root.tab-content > .tab-pane.active'));
313
        const tabContent = form.querySelector(Selectors.EMBED.elements[mediumType.toLowerCase() + 'Pane']);
314
 
315
        return this['getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);
316
    }
317
 
318
    getMediaHTMLLink(tab) {
319
        const context = {
320
            url: tab.querySelector(Selectors.EMBED.elements.url).value,
321
            name: tab.querySelector(Selectors.EMBED.elements.name).value || false
322
        };
323
 
324
        return context.url ? Templates.renderForPromise('tiny_media/embed_media_link', context) : '';
325
    }
326
 
327
    getMediaHTMLVideo(tab) {
328
        const context = this.getContextForMediaHTML(tab);
329
        context.width = tab.querySelector(Selectors.EMBED.elements.width).value || false;
330
        context.height = tab.querySelector(Selectors.EMBED.elements.height).value || false;
331
        context.poster = tab.querySelector(
332
            `${Selectors.EMBED.elements.posterSource} ${Selectors.EMBED.elements.url}`
333
        ).value || false;
334
 
335
        return context.sources.length ? Templates.renderForPromise('tiny_media/embed_media_video', context) : '';
336
    }
337
 
338
    getMediaHTMLAudio(tab) {
339
        const context = this.getContextForMediaHTML(tab);
340
 
341
        return context.sources.length ? Templates.renderForPromise('tiny_media/embed_media_audio', context) : '';
342
    }
343
 
344
    getContextForMediaHTML(tab) {
345
        const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({
346
            track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,
347
            kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),
348
            label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||
349
                track.querySelector(Selectors.EMBED.elements.trackLang).value,
350
            srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
351
            defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null
352
        })).filter((track) => !!track.track);
353
 
354
        const sources = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.mediaSource + ' '
355
            + Selectors.EMBED.elements.url))
356
                .filter((source) => !!source.value)
357
                .map((source) => source.value);
358
 
359
        return {
360
            sources,
361
            description: tab.querySelector(Selectors.EMBED.elements.mediaSource + ' '
362
                + Selectors.EMBED.elements.url).value || false,
363
            tracks,
364
            showControls: tab.querySelector(Selectors.EMBED.elements.mediaControl).checked,
365
            autoplay: tab.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
366
            muted: tab.querySelector(Selectors.EMBED.elements.mediaMute).checked,
367
            loop: tab.querySelector(Selectors.EMBED.elements.mediaLoop).checked,
368
            title: tab.querySelector(Selectors.EMBED.elements.title).value || false
369
        };
370
    }
371
 
372
    getFilepickerTypeFromElement(element) {
373
        if (element.closest(Selectors.EMBED.elements.posterSource)) {
374
            return 'image';
375
        }
376
        if (element.closest(Selectors.EMBED.elements.trackSource)) {
377
            return 'subtitle';
378
        }
379
 
380
        return 'media';
381
    }
382
 
383
    async clickHandler(e) {
384
        const element = e.target;
385
 
386
        const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);
387
        if (mediaBrowser) {
388
            e.preventDefault();
389
            const fpType = this.getFilepickerTypeFromElement(element);
390
            const params = await displayFilepicker(this.editor, fpType);
391
            this.filePickerCallback(params, element, fpType);
392
        }
393
 
394
        const addComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .addcomponent');
395
        if (addComponentSourceAction) {
396
            e.preventDefault();
397
            this.addMediaSourceComponent(element);
398
        }
399
 
400
        const removeComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .removecomponent');
401
        if (removeComponentSourceAction) {
402
            e.preventDefault();
403
            this.removeMediaSourceComponent(element);
404
        }
405
 
406
        const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');
407
        if (addComponentTrackAction) {
408
            e.preventDefault();
409
            this.addTrackComponent(element);
410
        }
411
 
412
        const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');
413
        if (removeComponentTrackAction) {
414
            e.preventDefault();
415
            this.removeTrackComponent(element);
416
        }
417
 
418
        // Only allow one track per tab to be selected as "default".
419
        const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);
420
        if (trackDefaultAction && trackDefaultAction.checked) {
421
            const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));
422
 
423
            element.parentElement
424
                .closest('.root.tab-content')
425
                .querySelectorAll(Selectors.EMBED.elements.trackDefault)
426
                .forEach((select) => {
427
                    if (select !== element && getKind(element) === getKind(select)) {
428
                        select.checked = false;
429
                    }
430
                });
431
        }
432
    }
433
 
434
    async handleDialogueSubmission(event, modal) {
435
        const {html} = await this.getMediaHTML(modal.getRoot()[0]);
436
        if (html) {
437
            if (this.isUpdating) {
438
                this.selectedMedia.outerHTML = html;
439
                this.isUpdating = false;
440
            } else {
441
                this.editor.insertContent(html);
442
            }
443
        }
444
    }
445
 
446
    async registerEventListeners(modal) {
447
        await modal.getBody();
448
        const $root = modal.getRoot();
449
        const root = $root[0];
450
        if (this.canShowFilePicker || this.canShowFilePickerPoster || this.canShowFilePickerTrack) {
451
            root.addEventListener('click', this.clickHandler.bind(this));
452
        }
453
 
454
        $root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));
455
        $root.on(ModalEvents.hidden, () => {
456
            this.currentModal.destroy();
457
        });
458
        $root.on(ModalEvents.shown, () => {
459
            root.querySelectorAll(Selectors.EMBED.elements.trackLang).forEach((dropdown) => {
460
                const defaultVal = dropdown.getAttribute('data-value');
461
                if (defaultVal) {
462
                    dropdown.value = defaultVal;
463
                }
464
            });
465
        });
466
    }
467
}