backup/util/ui/amd/src/schema_backup_form.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/>.

/**
 * Schema selector javascript controls.
 *
 * This module controls:
 * - The select all feature.
 * - Disabling activities checkboxes when the section is not selected.
 *
 * @module     core_backup/schema_backup_form
 * @copyright  2024 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import Notification from 'core/notification';
import * as Templates from 'core/templates';

const Selectors = {
    action: '[data-mdl-action]',
    checkboxes: '#id_coursesettings input[type="checkbox"]',
    firstSection: 'fieldset#id_coursesettings .fcontainer .grouped_settings.section_level',
    modCheckboxes: (modName) => `setting_activity_${modName}_`,
};

const Suffixes = {
    userData: '_userdata',
    userInfo: '_userinfo',
    included: '_included',
};

/**
 * Adds select all/none links to the top of the backup/restore/import schema page.
 */
export default class BackupFormController {

    /**
     * Static module init method.
     * @param {Array<string>} modNames - The names of the modules.
     * @returns {BackupFormController}
     */
    static init(modNames) {
        return new BackupFormController(modNames);
    }

    /**
     * Creates a new instance of the SchemaBackupForm class.
     * @param {Array<string>} modNames - The names of the modules.
     */
    constructor(modNames) {
        this.modNames = modNames;
        this.scanFormUserData();
        this.addSelectorsToPage();
    }

    /**
     * Detect the user data attribute from the form.
     *
     * @private
     */
    scanFormUserData() {
        this.withuserdata = false;
        this.userDataSuffix = Suffixes.userData;

        const checkboxes = document.querySelectorAll(Selectors.checkboxes);
        if (!checkboxes) {
            return;
        }
        // Depending on the form, user data inclusion is called userinfo or userdata.
        for (const checkbox of checkboxes) {
            const name = checkbox.name;
            if (name.endsWith(Suffixes.userData)) {
                this.withuserdata = true;
                break;
            } else if (name.endsWith(Suffixes.userInfo)) {
                this.withuserdata = true;
                this.userDataSuffix = Suffixes.userInfo;
                break;
            }
        }
    }

    /**
     * Initializes all related events.
     *
     * @private
     * @param {HTMLElement} element - The element to attach the events to.
     */
    initEvents(element) {
        element.addEventListener('click', (event) => {
            const action = event.target.closest(Selectors.action);
            if (!action) {
                return;
            }
            event.preventDefault();

            const suffix = (action.dataset?.mdlType == 'userdata') ? this.userDataSuffix : Suffixes.included;

            this.changeSelection(
                action.dataset.mdlAction == 'selectall',
                suffix,
                action.dataset?.mdlMod ?? null
            );
        });
    }

    /**
     * Changes the selection according to the params.
     *
     * @private
     * @param {boolean} checked - The checked state for the checkboxes.
     * @param {string} suffix - The checkboxes suffix
     * @param {string} [modName] - The module name.
     */
    changeSelection(checked, suffix, modName) {
        const prefix = modName ? Selectors.modCheckboxes(modName) : null;

        let formId;

        const checkboxes = document.querySelectorAll(Selectors.checkboxes);
        for (const checkbox of checkboxes) {
            formId = formId ?? checkbox.closest('form').getAttribute('id');

            if (prefix && !checkbox.name.startsWith(prefix)) {
                continue;
            }
            if (checkbox.name.endsWith(suffix)) {
                checkbox.checked = checked;
            }
        }

        // At this point, we really need to persuade the form we are part of to
        // update all of its disabledIf rules. However, as far as I can see,
        // given the way that lib/form/form.js is written, that is impossible.
        if (formId && M.form) {
            M.form.updateFormState(formId);
        }
    }

    /**
     * Generates the full selectors element to add to the page.
     *
     * @private
     * @returns {HTMLElement} The selectors element.
     */
    generateSelectorsElement() {
        const links = document.createElement('div');
        links.id = 'backup_selectors';
        this.initEvents(links);
        this.renderSelectorsTemplate(links);
        return links;
    }

    /**
     * Load the select all template.
     *
     * @private
     * @param {HTMLElement} element the container
     */
    renderSelectorsTemplate(element) {
        const data = {
            modules: this.getModulesTemplateData(),
            withuserdata: (this.withuserdata) ? true : undefined,
        };
        Templates.renderForPromise(
            'core_backup/formselectall',
            data
        ).then(({html, js}) => {
            return Templates.replaceNodeContents(element, html, js);
        }).catch(Notification.exception);
    }

    /**
     * Generate the modules template data.
     *
     * @private
     * @returns {Array} of modules data.
     */
    getModulesTemplateData() {
        const modules = [];
        for (const modName in this.modNames) {
            if (!this.modNames.hasOwnProperty(modName)) {
                continue;
            }
            modules.push({
                modname: modName,
                heading: this.modNames[modName],
            });
        }
        return modules;
    }

    /**
     * Adds select all/none functionality to the backup form.
     *
     * @private
     */
    addSelectorsToPage() {
        const firstSection = document.querySelector(Selectors.firstSection);
        if (!firstSection) {
            // This is not a relevant page.
            return;
        }
        if (!firstSection.querySelector(Selectors.checkboxes)) {
            // No checkboxes.
            return;
        }

        // Add global select all/none options.
        const selector = this.generateSelectorsElement();
        firstSection.parentNode.insertBefore(selector, firstSection);
    }
}