\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from '../selectors';\nimport {\n convertStringUrlToObject,\n createUrlParams,\n} from '../helpers';\nimport {getStrings} from 'core/str';\nimport {component} from \"../common\";\nimport {\n getCurrentLanguage,\n getMoodleLang\n} from 'editor_tiny/options';\n\n/**\n * Return template context for insert media.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const insertMediaTemplateContext = (props) => {\n return {\n mediaType: props.mediaType,\n showDropzone: props.canShowDropZone,\n showFilePicker: props.canShowFilePicker,\n };\n};\n\n/**\n * Return selected media type and element.\n *\n * @param {editor} editor\n * @returns {Array}\n */\nexport const getSelectedMediaElement = (editor) => {\n let mediaType = null;\n let selectedMedia = null;\n const mediaElm = editor.selection.getNode();\n\n if (!mediaElm) {\n mediaType = null;\n selectedMedia = null;\n } else if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {\n mediaType = mediaElm.nodeName.toLowerCase();\n selectedMedia = mediaElm;\n } else if (mediaElm.nodeName.toLowerCase() === 'a') {\n mediaType = 'link';\n selectedMedia = mediaElm;\n } else if (mediaElm.querySelector('video')) {\n mediaType = 'video';\n selectedMedia = mediaElm.querySelector('video');\n } else if (mediaElm.querySelector('audio')) {\n mediaType = 'audio';\n selectedMedia = mediaElm.querySelector('audio');\n }\n\n return [mediaType, selectedMedia];\n};\n\n/**\n * Format url when inserting media link to be previewed.\n *\n * @param {string} url\n * @returns {string}\n */\nexport const formatMediaUrl = (url) => {\n // Convert the string url into url param object.\n const params = convertStringUrlToObject(url);\n\n // Format the url for youtube links.\n if (url.includes(Selectors.EMBED.mediaSites.youtube)) {\n let fetchedUrl = null;\n let fetchedUrlValue = null;\n for (const k in params) {\n if (url.includes(k)) {\n fetchedUrl = k;\n fetchedUrlValue = params[k];\n delete params[k];\n break;\n }\n }\n url = fetchedUrl.replace('watch?v', 'embed/');\n url = url + fetchedUrlValue + '?' + createUrlParams(params);\n }\n return url;\n};\n\n/**\n * Check if the url is from a known media site.\n *\n * @param {string} url\n * @returns {boolean}\n */\nexport const isUrlFromKnownMediaSites = (url) => {\n let state = false;\n const sites = Selectors.EMBED.mediaSites;\n for (const site in sites) {\n if (url.includes(sites[site])) {\n state = true;\n break;\n }\n }\n return state;\n};\n\n/**\n * Return template context for media details.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const mediaDetailsTemplateContext = async(props) => {\n const context = {\n bodyTemplate: Selectors.EMBED.template.body.mediaDetailsBody,\n footerTemplate: Selectors.EMBED.template.footer.mediaDetailsFooter,\n isVideo: (props.mediaType === 'video'),\n isAudio: (props.mediaType === 'audio'),\n isLink: (props.mediaType === 'link'),\n isUpdating: props.isUpdating,\n };\n\n if (props.mediaData) {\n return {\n ...context,\n ...props.mediaData,\n };\n } else {\n return {\n ...context,\n ...await props.mediaTemplateContext,\n };\n }\n};\n\n/**\n * Get help strings.\n *\n * @returns {object}\n */\nexport const getHelpStrings = async() => {\n const [\n customsize,\n ] = await getStrings([\n 'customsize_help',\n ].map((key) => ({\n key,\n component,\n })));\n\n return {\n customsize,\n };\n};\n\n/**\n * Get current moodle languages.\n *\n * @param {editor} editor\n * @returns {object}\n */\nexport const prepareMoodleLang = (editor) => {\n const moodleLangs = getMoodleLang(editor);\n const currentLanguage = getCurrentLanguage(editor);\n\n const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({\n lang,\n code,\n \"default\": lang === currentLanguage,\n }));\n\n const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({\n lang,\n code,\n \"default\": lang === currentLanguage,\n }));\n\n return {\n installed,\n available,\n };\n};\n\n/**\n * Return moodle lang.\n *\n * @param {string} subtitleLang\n * @param {editor} editor\n * @returns {object|null}\n */\nexport const getMoodleLangObj = (subtitleLang, editor) => {\n const {available} = getMoodleLang(editor);\n\n if (available[subtitleLang]) {\n return {\n lang: subtitleLang,\n code: available[subtitleLang],\n };\n }\n\n return null;\n};\n\n/**\n * Get media data from the inserted media.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const getEmbeddedMediaDetails = (props) => {\n const tracks = {\n subtitles: [],\n captions: [],\n descriptions: [],\n chapters: [],\n metadata: []\n };\n\n const mediaMetadata = props.root.querySelectorAll(Selectors.EMBED.elements.mediaMetadataTabPane);\n mediaMetadata.forEach(metaData => {\n const trackElements = metaData.querySelectorAll(Selectors.EMBED.elements.track);\n trackElements.forEach(track => {\n tracks[metaData.dataset.trackKind].push({\n src: track.querySelector(Selectors.EMBED.elements.url).value,\n srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,\n label: track.querySelector(Selectors.EMBED.elements.trackLabel).value,\n defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked,\n });\n });\n });\n\n const querySelector = (element) => props.root.querySelector(element);\n const mediaDataProps = {};\n mediaDataProps.media = {\n type: props.mediaType,\n sources: props.media,\n poster: querySelector(Selectors.EMBED.elements.videoTag).getAttribute('poster'),\n title: querySelector(Selectors.EMBED.elements.title).value,\n width: querySelector(Selectors.EMBED.elements.width).value,\n height: querySelector(Selectors.EMBED.elements.height).value,\n autoplay: querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,\n loop: querySelector(Selectors.EMBED.elements.mediaLoop).checked,\n muted: querySelector(Selectors.EMBED.elements.mediaMute).checked,\n controls: querySelector(Selectors.EMBED.elements.mediaControl).checked,\n tracks,\n };\n mediaDataProps.link = false;\n return mediaDataProps;\n};\n\n/**\n * Get based url from the link.\n *\n * @param {string} url\n * @returns {string}\n */\nexport const getLinkBasedUrl = (url) => {\n if (url.includes(Selectors.EMBED.mediaSites.youtube)) {\n if (url.includes('embed/')) {\n const urlParams = url.split('embed/');\n let id = null;\n if (urlParams[1].includes('?')) {\n id = urlParams[1].split('?')[0];\n } else {\n id = urlParams[1];\n }\n url = `${urlParams[0]}watch?v=${id}&`;\n }\n }\n return url;\n};\n"],"names":["props","mediaType","showDropzone","canShowDropZone","showFilePicker","canShowFilePicker","editor","selectedMedia","mediaElm","selection","getNode","nodeName","toLowerCase","querySelector","url","params","includes","Selectors","EMBED","mediaSites","youtube","fetchedUrl","fetchedUrlValue","k","replace","state","sites","site","async","context","bodyTemplate","template","body","mediaDetailsBody","footerTemplate","footer","mediaDetailsFooter","isVideo","isAudio","isLink","isUpdating","mediaData","mediaTemplateContext","customsize","map","key","component","moodleLangs","currentLanguage","installed","Object","entries","_ref","lang","code","available","_ref2","subtitleLang","tracks","subtitles","captions","descriptions","chapters","metadata","root","querySelectorAll","elements","mediaMetadataTabPane","forEach","metaData","track","dataset","trackKind","push","src","value","srclang","trackLang","label","trackLabel","defaultTrack","trackDefault","checked","element","mediaDataProps","media","type","sources","poster","videoTag","getAttribute","title","width","height","autoplay","mediaAutoplay","loop","mediaLoop","muted","mediaMute","controls","mediaControl","link","urlParams","split","id"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/media/amd/src/embed/embedhandler.js b/lib/editor/tiny/plugins/media/amd/src/embed/embedhandler.js
index 1d4795c635b..25f37367847 100644
--- a/lib/editor/tiny/plugins/media/amd/src/embed/embedhandler.js
+++ b/lib/editor/tiny/plugins/media/amd/src/embed/embedhandler.js
@@ -39,6 +39,7 @@ import {
isValidUrl,
convertStringUrlToObject,
stopMediaLoading,
+ createUrlParams,
} from '../helpers';
import * as ModalEvents from 'core/modal_events';
import {displayFilepicker} from 'editor_tiny/utils';
@@ -47,7 +48,9 @@ import {
getHelpStrings,
prepareMoodleLang,
getMoodleLangObj,
+ getLinkBasedUrl,
} from "./embedhelpers";
+import Templates from 'core/templates';
export class EmbedHandler {
@@ -413,6 +416,119 @@ export class EmbedHandler {
addUrl.disabled = !(url !== "" && isValidUrl(url));
}
+ /**
+ * Get media html to be inserted or updated into tiny.
+ *
+ * @param {html} form Selected element
+ * @returns {string} String of html
+ */
+ getMediaHTML = (form) => {
+ let mediumType = this.root.querySelector(Selectors.EMBED.elements.previewClass).nodeName.toLowerCase();
+ mediumType = (mediumType === 'iframe') ? 'link' : mediumType;
+ const tabContent = form.querySelector('.tab-content');
+ const callback = 'getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1);
+ return this[callback](tabContent);
+ };
+
+ /**
+ * Get media as link.
+ *
+ * @returns {string} String of html
+ */
+ getMediaHTMLLink = () => {
+ const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);
+ const url = details.querySelector(Selectors.EMBED.elements.preview).src ?? null;
+ const urlParamString = createUrlParams({
+ controls: details.querySelector(Selectors.EMBED.elements.mediaControl).checked ? 1 : 0,
+ autoplay: details.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked ? 1 : 0,
+ mute: details.querySelector(Selectors.EMBED.elements.mediaMute).checked ? 1 : 0,
+ });
+ const name = details.querySelector(Selectors.EMBED.elements.title).value || false;
+ const context = {
+ url: getLinkBasedUrl(url) + urlParamString,
+ name: name ? name.trim() : false,
+ };
+
+ return context.url ? Templates.renderForPromise('tiny_media/embed/embed_media_link', context) : '';
+ };
+
+ /**
+ * Get media as video.
+ *
+ * @param {html} tab Selected element
+ * @returns {string} String of html.
+ */
+ getMediaHTMLVideo = (tab) => {
+ const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);
+ const context = this.getContextForMediaHTML(tab, details);
+ context.width = details.querySelector(Selectors.EMBED.elements.width).value || false;
+ context.height = details.querySelector(Selectors.EMBED.elements.height).value || false;
+ context.poster = details.querySelector(Selectors.EMBED.elements.videoTag).poster || false;
+ return context.sources ? Templates.renderForPromise('tiny_media/embed/embed_media_video', context) : '';
+ };
+
+ /**
+ * Get media as audio.
+ *
+ * @param {html} tab Selected element
+ * @returns {string} String of html.
+ */
+ getMediaHTMLAudio = (tab) => {
+ const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);
+ const context = this.getContextForMediaHTML(tab, details);
+ return context.sources.length ? Templates.renderForPromise('tiny_media/embed/embed_media_audio', context) : '';
+ };
+
+ /**
+ * Get previewed media data.
+ *
+ * @param {html} tab Selected element
+ * @param {html} details Selected element
+ * @returns {object}
+ */
+ getContextForMediaHTML = (tab, details) => {
+ const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({
+ track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,
+ kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),
+ label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||
+ track.querySelector(Selectors.EMBED.elements.trackLang).value,
+ srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
+ defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null
+ })).filter((track) => !!track.track);
+
+ const sources = details.querySelector(Selectors.EMBED.elements.preview).src ?? null;
+ const title = details.querySelector(Selectors.EMBED.elements.title).value;
+
+ return {
+ sources,
+ description: tab.querySelector(Selectors.EMBED.elements.url).value || false,
+ tracks,
+ showControls: details.querySelector(Selectors.EMBED.elements.mediaControl).checked,
+ autoplay: details.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
+ muted: details.querySelector(Selectors.EMBED.elements.mediaMute).checked,
+ loop: details.querySelector(Selectors.EMBED.elements.mediaLoop).checked,
+ title: title !== '' ? title.trim() : false,
+ };
+ };
+
+ /**
+ * Handle the insert/update media in tiny editor.
+ *
+ * @param {event} event
+ * @param {object} modal Object of current modal
+ */
+ handleDialogueSubmission = async(event, modal) => {
+ const {html} = await this.getMediaHTML(modal.getRoot()[0]);
+ if (html) {
+ if (this.isUpdating) {
+ this.selectedMedia.outerHTML = html;
+ this.isUpdating = false;
+ } else {
+ this.editor.insertContent(html);
+ }
+ }
+ };
+
registerEventListeners = async(modal) => {
await modal.getBody();
const $root = modal.getRoot();
@@ -440,5 +556,7 @@ export class EmbedHandler {
}
});
});
+
+ $root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));
};
}
diff --git a/lib/editor/tiny/plugins/media/amd/src/embed/embedhelpers.js b/lib/editor/tiny/plugins/media/amd/src/embed/embedhelpers.js
index 64414742edb..6971520d092 100644
--- a/lib/editor/tiny/plugins/media/amd/src/embed/embedhelpers.js
+++ b/lib/editor/tiny/plugins/media/amd/src/embed/embedhelpers.js
@@ -269,3 +269,25 @@ export const getEmbeddedMediaDetails = (props) => {
mediaDataProps.link = false;
return mediaDataProps;
};
+
+/**
+ * Get based url from the link.
+ *
+ * @param {string} url
+ * @returns {string}
+ */
+export const getLinkBasedUrl = (url) => {
+ if (url.includes(Selectors.EMBED.mediaSites.youtube)) {
+ if (url.includes('embed/')) {
+ const urlParams = url.split('embed/');
+ let id = null;
+ if (urlParams[1].includes('?')) {
+ id = urlParams[1].split('?')[0];
+ } else {
+ id = urlParams[1];
+ }
+ url = `${urlParams[0]}watch?v=${id}&`;
+ }
+ }
+ return url;
+};
diff --git a/lib/editor/tiny/plugins/media/templates/embed/footer/media_details_footer.mustache b/lib/editor/tiny/plugins/media/templates/embed/footer/media_details_footer.mustache
index c89a0be9395..051088983e8 100644
--- a/lib/editor/tiny/plugins/media/templates/embed/footer/media_details_footer.mustache
+++ b/lib/editor/tiny/plugins/media/templates/embed/footer/media_details_footer.mustache
@@ -46,6 +46,9 @@
+
{{/isLink}}