lib/editor/tiny/plugins/media/amd/src/helpers.js

// 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 helpers for image and embed.
 *
 * @module      tiny_media/helpers
 * @copyright   2024 Stevani Andolo <stevani@hotmail.com.au>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import Templates from 'core/templates';
import Selectors from './selectors';
import Config from 'core/config';

/**
 * Renders and inserts the body template for inserting an media into the modal.
 *
 * @param {object} templateContext - The context for rendering the template.
 * @param {HTMLElement} root - The root element where the template will be inserted.
 * @returns {Promise<void>}
 */
export const body = async(templateContext, root) => {
    return Templates.renderForPromise(templateContext.bodyTemplate, {...templateContext})
    .then(({html, js}) => {
        Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.bodyTemplate), html, js);
        return;
    })
    .catch(error => {
        window.console.log(error);
    });
};

/**
 * Renders and inserts the footer template for inserting an media into the modal.
 *
 * @param {object} templateContext - The context for rendering the template.
 * @param {HTMLElement} root - The root element where the template will be inserted.
 * @returns {Promise<void>}
 */
export const footer = async(templateContext, root) => {
    return Templates.renderForPromise(templateContext.footerTemplate, {...templateContext})
    .then(({html, js}) => {
        Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.footerTemplate), html, js);
        return;
    })
    .catch(error => {
        window.console.log(error);
    });
};

/**
 * Set extra properties on an instance using incoming data.
 *
 * @param {object} instance
 * @param {object} data
 * @return {object} Modified instance
 */
export const setPropertiesFromData = async(instance, data) => {
    for (const property in data) {
        if (typeof data[property] !== 'function') {
            instance[property] = data[property];
        }
    }
    return instance;
};

/**
 * Check if given string is a valid URL.
 *
 * @param {String} urlString URL the link will point to.
 * @returns {boolean} True is valid, otherwise false.
 */
export const isValidUrl = urlString => {
    const urlPattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
                                '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
                                '((\\d{1,3}\\.){3}\\d{1,3})|localhost)' + // OR ip (v4) address, localhost.
                                '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
    return !!urlPattern.test(urlString);
};

/**
 * Hide the element(s).
 *
 * @param {string|string[]} elements - The CSS selector for the elements to toggle.
 * @param {object} root - The CSS selector for the elements to toggle.
 */
export const hideElements = (elements, root) => {
    if (elements instanceof Array) {
        elements.forEach((elementSelector) => {
            const element = root.querySelector(elementSelector);
            if (element) {
                element.classList.add('d-none');
            }
        });
    } else {
        const element = root.querySelector(elements);
        if (element) {
            element.classList.add('d-none');
        }
    }
};

/**
 * Show the element(s).
 *
 * @param {string|string[]} elements - The CSS selector for the elements to toggle.
 * @param {object} root - The CSS selector for the elements to toggle.
 */
export const showElements = (elements, root) => {
    if (elements instanceof Array) {
        elements.forEach((elementSelector) => {
            const element = root.querySelector(elementSelector);
            if (element) {
                element.classList.remove('d-none');
            }
        });
    } else {
        const element = root.querySelector(elements);
        if (element) {
            element.classList.remove('d-none');
        }
    }
};

/**
 * Displays the upload loader and disables UI elements while loading a file.
 *
 * @param {html} root Modal element
 * @param {string} selector String of type IMAGE/EMBED
 */
export const startMediaLoading = (root, selector) => {
    showElements(Selectors[selector].elements.loaderIcon, root);
    const elementsToHide = [
        Selectors[selector].elements.insertMedia,
        Selectors[selector].elements.urlWarning,
        Selectors[selector].elements.modalFooter,
    ];
    hideElements(elementsToHide, root);
};

/**
 * Hide the upload loader and enable UI elements when loaded.
 *
 * @param {html} root Modal element
 * @param {string} selector String of type IMAGE/EMBED
 */
export const stopMediaLoading = (root, selector) => {
    hideElements(Selectors[selector].elements.loaderIcon, root);
    const elementsToShow = [
        Selectors[selector].elements.insertMedia,
        Selectors[selector].elements.modalFooter,
    ];
    showElements(elementsToShow, root);
};

/**
 * Return true or false if the url is external.
 *
 * @param {string} url
 * @returns {boolean} True if the URL is external, otherwise false.
 */
export const isExternalUrl = (url) => {
    const regex = new RegExp(`${Config.wwwroot}`);

    // True if the URL is from external, otherwise false.
    return regex.test(url) === false;
};

/**
 * Set the string for the URL label element.
 *
 * @param {object} props - The label text to set.
 */
export const setFilenameLabel = (props) => {
    const urlLabelEle = props.root.querySelector(props.fileNameSelector);
    if (urlLabelEle) {
        urlLabelEle.innerHTML = props.label;
        urlLabelEle.setAttribute("title", props.label);
    }
};

/**
 * This function checks whether an image URL is local (within the same website's domain) or external (from an external source).
 * Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.
 * If the image is local then we only show it's filename.
 * If the image is external then it will show full URL and it can be updated.
 *
 * @param {object} props
 */
export const sourceTypeChecked = (props) => {
    if (props.fetchedTitle) {
        props.label = props.fetchedTitle;
    } else {
        if (!isExternalUrl(props.source)) {
            // Split the URL by '/' to get an array of segments.
            const segments = props.source.split('/');
            // Get the last segment, which should be the filename.
            const filename = segments.pop().split('?')[0];
            // Show the file name.
            props.label = decodeURI(filename);
        } else {
            props.label = decodeURI(props.source);
        }
    }
    setFilenameLabel(props);
};

/**
 * Get filename from the name label.
 *
 * @param {string} fileLabel
 * @returns {string}
 */
export const getFileName = (fileLabel) => {
    if (fileLabel.includes('/')) {
        const split = fileLabel.split('/');
        let fileName = split[split.length - 1];
        fileName = fileName.split('.');
        if (fileName.length > 1) {
            return decodeURI(fileName.slice(0, (fileName.length - 1)).join('.'));
        } else {
            return decodeURI(fileName[0]);
        }
    } else {
        return decodeURI(fileLabel.split('.')[0]);
    }
};

/**
 * Return true or false if % is found.
 *
 * @param {string} value
 * @returns {boolean}
 */
export const isPercentageValue = (value) => {
    return value.match(/\d+%/);
};