Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"embedhandler.min.js","sources":["../../src/embed/embedhandler.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny media plugin embed handler class.\n *\n * This handles anything that embed requires like:\n * - Calling the media preview in embedPreview.\n * - Loading the embed insert.\n * - Getting selected media data.\n * - Handles url and repository uploads.\n * - Reset embed insert when embed preview is deleted.\n * - Handles media embedding into tiny and etc.\n *\n * @module      tiny_media/embed/embedhandler\n * @copyright   2024 Stevani Andolo <stevani@hotmail.com.au>\n * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from \"../selectors\";\nimport {EmbedInsert} from './embedinsert';\nimport {\n    body,\n    footer,\n    setPropertiesFromData,\n    isValidUrl,\n    stopMediaLoading,\n    startMediaLoading,\n} from '../helpers';\nimport * as ModalEvents from 'core/modal_events';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport {\n    insertMediaTemplateContext,\n    getHelpStrings,\n    prepareMoodleLang,\n    getMoodleLangObj,\n    hasAudioVideoAttr,\n    insertMediaThumbnailTemplateContext,\n} from \"./embedhelpers\";\nimport Templates from 'core/templates';\nimport {EmbedThumbnailInsert} from './embedthumbnailinsert';\n\nexport class EmbedHandler {\n\n    constructor(data) {\n        setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n    }\n\n    /**\n     * Load the media insert dialogue.\n     *\n     * @param {object} templateContext Object template context\n     */\n    loadTemplatePromise = (templateContext) => {\n        templateContext.elementid = this.editor.id;\n        templateContext.bodyTemplate = Selectors.EMBED.template.body.insertMediaBody;\n        templateContext.footerTemplate = Selectors.EMBED.template.footer.insertMediaFooter;\n        templateContext.selector = Selectors.EMBED.type;\n\n        Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n            .then(() => {\n                (new EmbedInsert(this)).init();\n                return;\n            })\n            .catch(error => {\n                window.console.log(error);\n            });\n    };\n\n    /**\n     * Load the media thumbnail insert dialogue.\n     *\n     * @param {object} templateContext Object template context\n     * @param {HTMLElement} root\n     * @param {object} mediaData\n     */\n    loadInsertThumbnailTemplatePromise = async(templateContext, root, mediaData) => {\n        Promise.all([body(templateContext, root.root), footer(templateContext, root.root)])\n            .then(() => {\n                if (!this.currentModal.insertMediaModal) {\n                    this.currentModal.insertMediaModal = this.currentModal;\n                }\n\n                if (root.uploadThumbnailModal) {\n                    this.currentModal.uploadThumbnailModal = root.uploadThumbnailModal;\n                }\n\n                this.thumbnailModalRoot = root.root;\n                (new EmbedThumbnailInsert(this)).init(mediaData);\n                return;\n            })\n            .catch(error => {\n                window.console.log(error);\n            });\n    };\n\n    /**\n     * Loads the media preview dialogue.\n     *\n     * @param {object} embedPreview Object of embedPreview\n     * @param {object} templateContext Object of template context\n     */\n    loadMediaDetails = async(embedPreview, templateContext) => {\n        Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n            .then(() => {\n                embedPreview.init();\n                return;\n            })\n            .catch(error => {\n                stopMediaLoading(this.root, Selectors.EMBED.type);\n                window.console.log(error);\n            });\n    };\n\n    /**\n     * Reset the media/thumbnail insert modal form.\n     *\n     * @param {boolean} isMediaInsert Is current state media insert or thumbnail insert?\n     */\n    resetUploadForm = (isMediaInsert = true) => {\n        if (isMediaInsert) {\n            this.newMediaLink = false;\n            this.fetchedMediaLinkTitle = null;\n            this.resetCurrentMediaData();\n            this.loadTemplatePromise(insertMediaTemplateContext(this));\n        } else {\n            this.loadInsertThumbnailTemplatePromise(\n                insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail.\n                {root: this.thumbnailModalRoot}, // Required root elements.\n                this.mediaData // Get current media data.\n            );\n        }\n    };\n\n    /**\n     * Get selected media data.\n     *\n     * @returns {null|object}\n     */\n    getMediaProperties = () => {\n        const media = this.selectedMedia;\n        if (!media) {\n            return null;\n        }\n\n        const tracks = {\n            subtitles: [],\n            captions: [],\n            descriptions: [],\n            chapters: [],\n            metadata: []\n        };\n        const sources = [];\n\n        media.querySelectorAll('track').forEach((track) => {\n            tracks[track.getAttribute('kind')].push({\n                src: track.getAttribute('src'),\n                srclang: track.getAttribute('srclang'),\n                label: track.getAttribute('label'),\n                defaultTrack: hasAudioVideoAttr(track, 'default')\n            });\n        });\n\n        media.querySelectorAll('source').forEach((source) => {\n            sources.push(source.src);\n        });\n        const title = media.getAttribute('title') ?? media.textContent;\n\n        return {\n            type: this.mediaType,\n            sources,\n            poster: media.getAttribute('poster'),\n            title: title ? title.trim() : false,\n            width: media.getAttribute('width'),\n            height: media.getAttribute('height'),\n            autoplay: hasAudioVideoAttr(media, 'autoplay'),\n            loop: hasAudioVideoAttr(media, 'loop'),\n            muted: hasAudioVideoAttr(media, 'muted'),\n            controls: hasAudioVideoAttr(media, 'controls'),\n            tracks,\n        };\n    };\n\n    /**\n     * Get selected media data.\n     *\n     * @returns {object}\n     */\n    getCurrentEmbedData = () => {\n        const properties = this.getMediaProperties();\n        if (!properties || this.newMediaLink) {\n            return {media: {}};\n        }\n\n        const processedProperties = {};\n        processedProperties.media = properties;\n        processedProperties.link = false;\n\n        return processedProperties;\n    };\n\n    /**\n     * Get help strings for media subtitles and captions.\n     *\n     * @returns {null|object}\n     */\n    getHelpStrings = async() => {\n        if (!this.helpStrings) {\n            this.helpStrings = await getHelpStrings();\n        }\n\n        return this.helpStrings;\n    };\n\n    /**\n     * Set template context for insert media dialogue.\n     *\n     * @param {object} data Object of media data\n     * @returns {object}\n     */\n    getTemplateContext = async(data) => {\n        const languages = prepareMoodleLang(this.editor);\n        const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {\n            data[`${key.toLowerCase()}helpicon`] = {text};\n        });\n\n        return Object.assign({}, {\n            elementid: this.editor.getElement().id,\n            showFilePickerTrack: this.canShowFilePickerTrack,\n            langsInstalled: languages.installed,\n            langsAvailable: languages.available,\n            media: true,\n            isUpdating: this.isUpdating,\n        }, data, helpIcons);\n    };\n\n    /**\n     * Set and get media template context.\n     *\n     * @param {null|object} data Null or object of media data\n     * @returns {Promise<object>} A promise that resolves template context.\n     */\n    getMediaTemplateContext = async(data = null) => {\n        if (!data) {\n            data = Object.assign({}, this.getCurrentEmbedData());\n        } else {\n            if (data.hasOwnProperty('isUpdating')) {\n                this.isUpdating = data.isUpdating;\n            } else {\n                this.isUpdating = Object.keys(data).length > 1;\n            }\n        }\n        return await this.getTemplateContext(data);\n    };\n\n    /**\n     * Handles changes in the media URL input field and loads a preview of the media if the URL has changed.\n     */\n    urlChanged() {\n        const url = this.root.querySelector(Selectors.EMBED.elements.fromUrl).value;\n        if (url && url !== this.currentUrl) {\n            // Set to null on new url change.\n            this.mediaType = null;\n\n            // Flag as new media link insert.\n            this.newMediaLink = true;\n            this.loadMediaPreview(url);\n        }\n    }\n\n    /**\n     * Load the media preview dialogue.\n     *\n     * @param {string} url String of media url\n     */\n    loadMediaPreview = (url) => {\n        (new EmbedInsert(this)).loadMediaPreview(url);\n    };\n\n    /**\n     * Callback for file picker that previews the media or add the captions and subtitles.\n     *\n     * @param {object} params Object of media url and etc\n     * @param {html} element Selected element.\n     * @param {string} fpType Caption type.\n     */\n    trackFilePickerCallback(params, element, fpType) {\n        if (params.url !== '') {\n            const tabPane = element.closest('.tab-pane');\n            if (tabPane) {\n                element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;\n\n                if (fpType === 'subtitle') {\n                    // If the file is subtitle file. We need to match the language and label for that file.\n                    const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];\n                    const langObj = getMoodleLangObj(subtitleLang, this.editor);\n                    if (langObj) {\n                        const track = element.closest(Selectors.EMBED.elements.track);\n                        track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();\n                        track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;\n                    }\n                }\n            } else {\n                // Flag as new file upload.\n                this.newFileUpload = true;\n                this.resetCurrentMediaData();\n                this.loadMediaPreview(params.url);\n            }\n        }\n    }\n\n    /**\n     * Reset current media data.\n     */\n    resetCurrentMediaData = () => {\n        // Reset the value of the following props.\n        this.media = {};\n        this.mediaType = null;\n        this.selectedMedia = null;\n    };\n\n    /**\n     * Add new html track element.\n     *\n     * @param {html} element\n     */\n    addTrackComponent(element) {\n        const trackElement = element.closest(Selectors.EMBED.elements.track);\n        const clone = trackElement.cloneNode(true);\n\n        trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');\n        trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');\n        trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);\n    }\n\n    /**\n     * Remove added html track element.\n     *\n     * @param {html} element\n     */\n    removeTrackComponent(element) {\n        const sourceElement = element.closest(Selectors.EMBED.elements.track);\n        sourceElement.remove();\n    }\n\n    /**\n     * Get picker type based on the selected element.\n     *\n     * @param {html} element Selected element\n     * @returns {string}\n     */\n    getFilePickerTypeFromElement = (element) => {\n        if (element.closest(Selectors.EMBED.elements.posterSource)) {\n            return 'image';\n        }\n        if (element.closest(Selectors.EMBED.elements.trackSource)) {\n            return 'subtitle';\n        }\n\n        return 'media';\n    };\n\n    /**\n     * Get captions/subtitles type.\n     *\n     * @param {html} tabPane\n     * @returns {string}\n     */\n    getTrackTypeFromTabPane = (tabPane) => {\n        return tabPane.getAttribute('data-track-kind');\n    };\n\n    /**\n     * Handle click events.\n     *\n     * @param {html} e Selected element\n     */\n    clickHandler = async(e) => {\n        const element = e.target;\n\n        // Handle repository browsing.\n        const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);\n        if (mediaBrowser) {\n            e.preventDefault();\n            const fpType = this.getFilePickerTypeFromElement(element);\n            const params = await displayFilepicker(this.editor, fpType);\n            this.trackFilePickerCallback(params, element, fpType);\n        }\n\n        // Handles add media url.\n        const addUrlEle = e.target.closest(Selectors.EMBED.actions.addUrl);\n        if (addUrlEle) {\n            startMediaLoading(this.root, Selectors.EMBED.type);\n            this.urlChanged();\n        }\n\n        // Handles adding tracks.\n        const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');\n        if (addComponentTrackAction) {\n            e.preventDefault();\n            this.addTrackComponent(element);\n        }\n\n        // Handles removing added tracks.\n        const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');\n        if (removeComponentTrackAction) {\n            e.preventDefault();\n            this.removeTrackComponent(element);\n        }\n\n        // Only allow one track per tab to be selected as \"default\".\n        const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);\n        if (trackDefaultAction && trackDefaultAction.checked) {\n            const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));\n\n            element.parentElement\n                .closest('.tab-content')\n                .querySelectorAll(Selectors.EMBED.elements.trackDefault)\n                .forEach((select) => {\n                    if (select !== element && getKind(element) === getKind(select)) {\n                        select.checked = false;\n                    }\n                });\n        }\n    };\n\n    /**\n     * Enables or disables the URL-related buttons in the footer based on the current URL and input value.\n     *\n     * @param {html} input Url input field\n     * @param {object} root\n     */\n    toggleUrlButton(input, root) {\n        const url = input.value;\n        const addUrl = root.querySelector(Selectors.EMBED.actions.addUrl);\n        addUrl.disabled = !(url !== \"\" && isValidUrl(url));\n    }\n\n    /**\n     * Get media html to be inserted or updated into tiny.\n     *\n     * @param {html} form Selected element\n     * @returns {string} String of html\n     */\n    getMediaHTML = (form) => {\n        this.mediaType = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer).dataset.mediaType;\n        const tabContent = form.querySelector('.tab-content');\n        const callback = 'getMediaHTML' + this.mediaType[0].toUpperCase() + this.mediaType.substr(1);\n        return this[callback](tabContent);\n    };\n\n    /**\n     * Get media as link.\n     *\n     * @returns {string} String of html.\n     */\n    getMediaHTMLLink() {\n        const mediaPreviewContainer = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n        const context = {\n            name: document.querySelector(Selectors.EMBED.elements.title).value ?? mediaPreviewContainer.dataset.originalUrl,\n            url: mediaPreviewContainer.dataset.originalUrl || false\n        };\n\n        return context.url ? Templates.renderForPromise('tiny_media/embed/embed_media_link', context) : '';\n    }\n\n    /**\n     * Get media as video.\n     *\n     * @param {html} tab Selected element\n     * @returns {string} String of html.\n     */\n    getMediaHTMLVideo = (tab) => {\n        const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);\n        const context = this.getContextForMediaHTML(tab, details);\n        context.width = details.querySelector(Selectors.EMBED.elements.width).value || false;\n        context.height = details.querySelector(Selectors.EMBED.elements.height).value || false;\n\n        const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n        context.poster = mediaPreviewContainer.dataset.mediaPoster || false;\n        return context.sources ? Templates.renderForPromise('tiny_media/embed/embed_media_video', context) : '';\n    };\n\n    /**\n     * Get media as audio.\n     *\n     * @param {html} tab Selected element\n     * @returns {string} String of html.\n     */\n    getMediaHTMLAudio = (tab) => {\n        const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);\n        const context = this.getContextForMediaHTML(tab, details);\n        return context.sources.length ? Templates.renderForPromise('tiny_media/embed/embed_media_audio', context) : '';\n    };\n\n    /**\n     * Get previewed media data.\n     *\n     * @param {html} tab Selected element\n     * @param {html} details Selected element\n     * @returns {object}\n     */\n    getContextForMediaHTML = (tab, details) => {\n        const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => {\n            const langTrack = track.querySelector(Selectors.EMBED.elements.trackLang);\n            const selectedLangTrack = langTrack.options[langTrack.selectedIndex];\n\n            return {\n                track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,\n                kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),\n                label: track.querySelector(Selectors.EMBED.elements.trackLabel).value || langTrack.value,\n                srclang: selectedLangTrack.dataset.languageCode ?? false,\n                defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? \"true\" : null\n            };\n        }).filter((track) => !!track.track);\n\n        const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n        let sources = mediaPreviewContainer.dataset.originalUrl ?? null;\n\n        // Let's check if media has more than one sources.\n        if (this.alternativeSources) {\n            // Always update the first item in this.alternativeSources to the new one.\n            this.alternativeSources[0] = sources;\n            // Override the sources to have all the updated sources.\n            sources = this.alternativeSources;\n        }\n\n        const title = details.querySelector(Selectors.EMBED.elements.title).value;\n        // Remove data-original-url attribute once it's extracted.\n        mediaPreviewContainer.removeAttribute('data-original-url');\n\n        const templateContext = {\n            sources,\n            tracks,\n            showControls: details.querySelector(Selectors.EMBED.elements.mediaControl).checked,\n            autoplay: details.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,\n            muted: details.querySelector(Selectors.EMBED.elements.mediaMute).checked,\n            loop: details.querySelector(Selectors.EMBED.elements.mediaLoop).checked,\n            title: title !== '' ? title.trim() : false,\n        };\n\n        // Add description prop to templateContext if media type is \"link\".\n        if (this.mediaType === 'link') {\n            // Let's form an alternative title.\n            templateContext.description = Array.isArray(sources) ? sources[0] : sources;\n        }\n\n        return templateContext;\n    };\n\n    /**\n     * Handle the insert/update media in tiny editor.\n     *\n     * @param {event} event\n     * @param {object} modal Object of current modal\n     */\n    handleDialogueSubmission = async(event, modal) => {\n        const {html} = await this.getMediaHTML(modal.getRoot()[0]);\n        if (html) {\n            if (this.isUpdating) {\n                this.selectedMedia.outerHTML = html;\n                this.isUpdating = false;\n            } else {\n                this.editor.insertContent(html);\n            }\n        }\n    };\n\n    /**\n     * Register insert media modal elements' events.\n     */\n    registerEventListeners = async() => {\n        // Handles click events for insert media modal.\n        if (this.canShowFilePickerTrack) {\n            this.root.addEventListener('click', this.clickHandler.bind(this));\n        }\n\n        // Handles media adding using url input.\n        this.root.addEventListener('input', (e) => {\n            const urlEle = e.target.closest(Selectors.EMBED.elements.fromUrl);\n            if (urlEle) {\n                this.toggleUrlButton(urlEle, this.root);\n            }\n        });\n\n        // Destroy created modal when it's closed.\n        this.modalRoot.on(ModalEvents.hidden, () => {\n            this.currentModal.destroy();\n        });\n\n        // Handles media insert to editor.\n        this.modalRoot.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));\n    };\n}\n"],"names":["constructor","data","templateContext","elementid","this","editor","id","bodyTemplate","Selectors","EMBED","template","body","insertMediaBody","footerTemplate","footer","insertMediaFooter","selector","type","Promise","all","root","then","EmbedInsert","init","catch","error","window","console","log","async","mediaData","currentModal","insertMediaModal","uploadThumbnailModal","thumbnailModalRoot","EmbedThumbnailInsert","embedPreview","isMediaInsert","_this","newMediaLink","fetchedMediaLinkTitle","resetCurrentMediaData","loadTemplatePromise","loadInsertThumbnailTemplatePromise","media","selectedMedia","tracks","subtitles","captions","descriptions","chapters","metadata","sources","querySelectorAll","forEach","track","getAttribute","push","src","srclang","label","defaultTrack","source","title","textContent","mediaType","poster","trim","width","height","autoplay","loop","muted","controls","properties","getMediaProperties","processedProperties","link","helpStrings","languages","helpIcons","Array","from","Object","entries","getHelpStrings","_ref","key","text","toLowerCase","assign","getElement","showFilePickerTrack","canShowFilePickerTrack","langsInstalled","installed","langsAvailable","available","isUpdating","hasOwnProperty","keys","length","getCurrentEmbedData","getTemplateContext","url","loadMediaPreview","element","closest","elements","posterSource","trackSource","tabPane","e","target","actions","mediaBrowser","preventDefault","fpType","getFilePickerTypeFromElement","params","trackFilePickerCallback","addUrl","urlChanged","addTrackComponent","removeTrackComponent","trackDefaultAction","trackDefault","checked","getKind","el","getTrackTypeFromTabPane","parentElement","select","form","querySelector","mediaPreviewContainer","dataset","tabContent","toUpperCase","substr","tab","details","document","mediaDetailsBody","context","getContextForMediaHTML","value","mediaPoster","Templates","renderForPromise","map","langTrack","trackLang","selectedLangTrack","options","selectedIndex","kind","trackLabel","languageCode","filter","originalUrl","alternativeSources","removeAttribute","showControls","mediaControl","mediaAutoplay","mediaMute","mediaLoop","description","isArray","event","modal","html","getMediaHTML","getRoot","outerHTML","insertContent","addEventListener","clickHandler","bind","urlEle","fromUrl","toggleUrlButton","modalRoot","on","ModalEvents","hidden","destroy","save","handleDialogueSubmission","currentUrl","subtitleLang","file","split","slice","langObj","lang","code","newFileUpload","trackElement","clone","cloneNode","classList","remove","add","parentNode","insertBefore","nextSibling","input","disabled","getMediaHTMLLink","name"],"mappings":"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"}