| 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 | }
 |