// 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 upload class.
*
* This handles the embed upload using url, drag-drop and repositories.
*
* @module tiny_media/embed/embedinsert
* @copyright 2024 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {prefetchStrings} from 'core/prefetch';
import {getStrings} from 'core/str';
import {component} from "../common";
import {
setPropertiesFromData,
startMediaLoading,
stopMediaLoading,
showElements,
} from '../helpers';
import Selectors from "../selectors";
import Dropzone from 'core/dropzone';
import uploadFile from 'editor_tiny/uploader';
import {EmbedHandler} from './embedhandler';
import {
getMediaTitle,
mediaDetailsTemplateContext,
checkMediaType,
fetchPreview,
} from './embedhelpers';
import {EmbedPreview} from './embedpreview';
prefetchStrings(component, [
'insertmedia',
'addmediafilesdrop',
'uploading',
'loadingmedia',
]);
export class EmbedInsert {
constructor(data) {
setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param.
}
/**
* Init the dropzone and lang strings.
*/
init = async() => {
const langStringKeys = [
'insertmedia',
'addmediafilesdrop',
'uploading',
'loadingmedia',
];
const langStringValues = await getStrings([...langStringKeys].map((key) => ({key, component})));
this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringValues[index]]));
this.currentModal.setTitle(this.langStrings.insertmedia);
// Let's init the dropzone if canShowDropZone is true and mediaType is null.
if (this.canShowDropZone && !this.mediaType) {
const dropZoneEle = document.querySelector(Selectors.EMBED.elements.dropzoneContainer);
const dropZone = new Dropzone(
dropZoneEle,
this.acceptedMediaTypes,
files => {
this.handleUploadedFile(files);
}
);
dropZone.setLabel(this.langStrings.addmediafilesdrop);
dropZone.init();
}
};
/**
* Loads and displays a preview media based on the provided URL, and handles media loading events.
*
* @param {string} url - The URL of the media to load and display.
*/
loadMediaPreview = async(url) => {
this.originalUrl = url;
this.fetchedMediaLinkTitle = await getMediaTitle(url, this);
if (this.newMediaLink) { // Media added using url input.
this.filteredContent = await fetchPreview(this.originalUrl, this.contextId);
if (!this.mediaType) {
// It means the url points to a physical media file.
if (this.fetchedMediaLinkTitle) {
// Case-insensitive regex for video tag.
const videoRegex = /<video[^>]*>.*<\/video>/i;
// Case-insensitive regex for audio tag.
const audioRegex = /<audio[^>]*>.*<\/audio>/i;
if (videoRegex.test(this.filteredContent)) {
this.mediaType = 'video';
} else if (audioRegex.test(this.filteredContent)) {
this.mediaType = 'audio';
}
} else {
this.mediaType = 'link';
}
}
// Process the media preview.
this.processMediaPreview();
} else { // Media added using dropzone or repositories.
this.mediaType ??= await checkMediaType(url);
// Process the media preview.
this.processMediaPreview();
}
};
/**
* Process the media preview.
*/
processMediaPreview = async() => {
// Let's combine the props.
setPropertiesFromData(
this,
await (new EmbedHandler(this)).getMediaTemplateContext()
);
// Construct templateContext for embed preview.
const templateContext = await mediaDetailsTemplateContext(this);
if (this.isUpdating && !this.newMediaLink) {
// Will be used to set the media title if it's in update state.
this.mediaTitle = templateContext.media.title;
}
// Load the media details and preview of the selected media.
(new EmbedHandler(this)).loadMediaDetails(new EmbedPreview(this), templateContext);
};
/**
* Updates the content of the loader icon.
*
* @param {HTMLElement} root - The root element containing the loader icon.
* @param {object} langStrings - An object containing language strings.
* @param {number|null} progress - The progress percentage (optional).
* @returns {void}
*/
updateLoaderIcon = (root, langStrings, progress = null) => {
const loaderIconState = root.querySelector(Selectors.EMBED.elements.loaderIconContainer + ' div');
loaderIconState.innerHTML = (progress !== null) ?
`${langStrings.uploading} ${Math.round(progress)}%` :
langStrings.loadingmedia;
};
/**
* Handles media preview on file picker callback.
*
* @param {object} params Object of uploaded file
*/
filePickerCallback = (params) => {
if (params.url) {
if (this.mediaType) {
// Set mediaType to "null" if it started with viewing embedded link, otherwise it will not be consistent.
this.mediaType = null;
}
// Flag as new file upload.
this.newFileUpload = true;
// Load the media preview.
this.loadMediaPreview(params.url);
}
};
/**
* Handles the uploaded file, initiates the upload process, and updates the UI during the upload.
*
* @param {FileList} files - The list of files to upload (usually from a file input field).
* @returns {Promise<void>} A promise that resolves when the file is uploaded and processed.
*/
handleUploadedFile = async(files) => {
try {
startMediaLoading(this.root, Selectors.EMBED.type);
const fileURL = await uploadFile(this.editor, 'media', files[0], files[0].name, (progress) => {
this.updateLoaderIcon(this.root, this.langStrings, progress);
});
// Set the loader icon content to "loading" after the file upload completes.
this.updateLoaderIcon(this.root, this.langStrings);
this.filePickerCallback({url: fileURL});
} catch (error) {
// Handle the error.
const urlWarningLabelEle = this.root.querySelector(Selectors.EMBED.elements.urlWarning);
urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;
showElements(Selectors.EMBED.elements.urlWarning, this.root);
stopMediaLoading(this.root, Selectors.EMBED.type);
}
};
}