// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin embed helpers.
*
* This provides easy access to any classes without instantiating a new object.
*
* @module tiny_media/embed/embedhelpers
* @copyright 2024 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from '../selectors';
import {getStrings} from 'core/str';
import {component} from "../common";
import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';
import Ajax from 'core/ajax';
import {getFileName} from '../helpers';
/**
* Return template context for insert media.
*
* @param {object} props
* @returns {object}
*/
export const insertMediaTemplateContext = (props) => {
return {
mediaType: props.mediaType,
showDropzone: props.canShowDropZone,
showFilePicker: props.canShowFilePicker,
fileType: 'audio/video',
};
};
/**
* Return template context for insert media.
*
* @param {object} props
* @returns {object}
*/
export const insertMediaThumbnailTemplateContext = (props) => {
return {
elementid: props.editor.id,
showDropzone: props.canShowDropZone,
showImageFilePicker: props.canShowImageFilePicker,
bodyTemplate: Selectors.EMBED.template.body.insertMediaBody,
footerTemplate: Selectors.EMBED.template.footer.insertMediaFooter,
fileType: 'image',
selector: Selectors.EMBED.type,
};
};
/**
* Return selected media type and element.
*
* @param {editor} editor
* @returns {Array}
*/
export const getSelectedMediaElement = (editor) => {
let mediaType = null;
let selectedMedia = null;
const mediaElm = editor.selection.getNode();
if (!mediaElm) {
mediaType = null;
selectedMedia = null;
} else if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {
mediaType = mediaElm.nodeName.toLowerCase();
selectedMedia = mediaElm;
} else if (mediaElm.querySelector('video')) {
mediaType = 'video';
selectedMedia = mediaElm.querySelector('video');
} else if (mediaElm.querySelector('audio')) {
mediaType = 'audio';
selectedMedia = mediaElm.querySelector('audio');
} else if (mediaElm.nodeName.toLowerCase() === 'a') {
selectedMedia = mediaElm;
}
return [mediaType, selectedMedia];
};
/**
* Returns result of media filtering.
*
* @param {string} url
* @param {string} contextId
* @returns {string}
*/
export const fetchPreview = async(url, contextId) => {
const request = {
methodname: 'tiny_media_preview',
args: {
contextid: contextId, // Use the system one.
content: url,
}
};
const responseObj = await Ajax.call([request])[0];
return responseObj.content;
};
/**
* Returns media type.
*
* @param {string} url
* @returns {string|null}
*/
export const checkMediaType = async(url) => {
try {
const response = await fetch(url, {method: 'HEAD'});
const contentType = response.headers.get('Content-type');
if (!contentType) {
return null;
}
if (contentType.startsWith('video/')) {
return 'video';
} else if (contentType.startsWith('audio/')) {
return 'audio';
}
return null;
} catch (e) {
return null;
}
};
/**
* Returns media title.
*
* @param {string} url
* @param {object} props
* @returns {string|null} String of media title.
*/
export const getMediaTitle = async(url, props) => {
const extension = url.split('.').pop().split('?')[0];
if (props.acceptedMediaTypes.includes(`.${extension}`)) {
return getFileName(url);
}
return null;
};
/**
* Return template context for media details.
*
* @param {object} props
* @returns {object}
*/
export const mediaDetailsTemplateContext = async(props) => {
const context = {
bodyTemplate: Selectors.EMBED.template.body.mediaDetailsBody,
footerTemplate: Selectors.EMBED.template.footer.mediaDetailsFooter,
isLink: (props.mediaType === 'link'),
isVideo: (props.mediaType === 'video'),
showControl: (props.mediaType === 'video' || props.mediaType === 'audio'),
isUpdating: props.isUpdating,
isNewFileOrLinkUpload: (props.newMediaLink || props.newFileUpload),
selector: Selectors.EMBED.type,
};
return {...context, ...props};
};
/**
* Get help strings.
*
* @returns {object}
*/
export const getHelpStrings = async() => {
const [
subtitles,
captions,
descriptions,
chapters,
metadata,
customsize,
linkcustomsize,
] = await getStrings([
'subtitles_help',
'captions_help',
'descriptions_help',
'chapters_help',
'metadata_help',
'customsize_help',
'linkcustomsize_help',
].map((key) => ({
key,
component,
})));
return {
subtitles,
captions,
descriptions,
chapters,
metadata,
customsize,
linkcustomsize,
};
};
/**
* Get current moodle languages.
*
* @param {editor} editor
* @returns {object}
*/
export const prepareMoodleLang = (editor) => {
const moodleLangs = getMoodleLang(editor);
const currentLanguage = getCurrentLanguage(editor);
const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
return {
installed,
available,
};
};
/**
* Return moodle lang.
*
* @param {string} subtitleLang
* @param {editor} editor
* @returns {object|null}
*/
export const getMoodleLangObj = (subtitleLang, editor) => {
const {available} = getMoodleLang(editor);
if (available[subtitleLang]) {
return {
lang: subtitleLang,
code: available[subtitleLang],
};
}
return null;
};
/**
* Get media data from the inserted media.
*
* @param {object} props
* @returns {object}
*/
export const getEmbeddedMediaDetails = (props) => {
const tracks = {
subtitles: [],
captions: [],
descriptions: [],
chapters: [],
metadata: []
};
const mediaMetadata = props.root.querySelectorAll(Selectors.EMBED.elements.mediaMetadataTabPane);
mediaMetadata.forEach(metaData => {
const trackElements = metaData.querySelectorAll(Selectors.EMBED.elements.track);
trackElements.forEach(track => {
tracks[metaData.dataset.trackKind].push({
src: track.querySelector(Selectors.EMBED.elements.url).value,
srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
label: track.querySelector(Selectors.EMBED.elements.trackLabel).value,
defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked,
});
});
});
const querySelector = (element) => props.root.querySelector(element);
const mediaDataProps = {};
mediaDataProps.media = {
type: props.mediaType,
sources: props.media,
poster: props.media.poster ?? null,
title: querySelector(Selectors.EMBED.elements.title).value,
width: querySelector(Selectors.EMBED.elements.width).value,
height: querySelector(Selectors.EMBED.elements.height).value,
autoplay: querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
loop: querySelector(Selectors.EMBED.elements.mediaLoop).checked,
muted: querySelector(Selectors.EMBED.elements.mediaMute).checked,
controls: querySelector(Selectors.EMBED.elements.mediaControl).checked,
tracks,
};
mediaDataProps.link = false;
return mediaDataProps;
};
/**
* Check for video/audio attributes.
*
* @param {HTMLElement} elem
* @param {string} attr Attribute name
* @returns {boolean}
*/
export const hasAudioVideoAttr = (elem, attr) => {
// As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
// So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
};