mod/forum/amd/src/local/grades/local/grader/user_picker.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/>.

/**
 * This module will tie together all of the different calls the gradable module will make.
 *
 * @module     mod_forum/local/grades/local/grader/user_picker
 * @copyright  2019 Mathew May <mathew.solutions>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import Templates from 'core/templates';
import Selectors from './user_picker/selectors';
import {getString} from 'core/str';

const templatePath = 'mod_forum/local/grades/local/grader';

/**
 * The Grader User Picker.
 *
 * @class mod_forum/local/grades/local/grader/user_picker
 */
class UserPicker {

    /**
     * Constructor for the User Picker.
     *
     * @constructor mod_forum/local/grades/local/grader/user_picker
     * @param {Array} userList List of users
     * @param {Function} showUserCallback The callback used to display the user
     * @param {Function} preChangeUserCallback The callback to use before changing user
     */
    constructor(userList, showUserCallback, preChangeUserCallback) {
        this.userList = userList;
        this.showUserCallback = showUserCallback;
        this.preChangeUserCallback = preChangeUserCallback;
        this.currentUserIndex = 0;

        // Ensure that render is bound correctly.
        this.render = this.render.bind(this);
        this.setUserId = this.setUserId.bind(this);
    }

    /**
     * Set the current userid without rendering the change.
     * To show the user, call showUser too.
     *
     * @param {Number} userId
     */
    setUserId(userId) {
        // Determine the current index based on the user ID.
        const userIndex = this.userList.findIndex(user => {
            return user.id === parseInt(userId);
        });

        if (userIndex === -1) {
            throw Error(`User with id ${userId} not found`);
        }

        this.currentUserIndex = userIndex;
    }

    /**
     * Render the user picker.
     */
    async render() {
        // Create the root node.
        this.root = document.createElement('div');

        const {html, js} = await this.renderNavigator();
        Templates.replaceNodeContents(this.root, html, js);

        // Call the showUser function to show the first user immediately.
        await this.showUser(this.currentUser);

        // Ensure that the event listeners are all bound.
        this.registerEventListeners();
    }

    /**
     * Render the navigator itself.
     *
     * @returns {Promise}
     */
    renderNavigator() {
        return Templates.renderForPromise(`${templatePath}/user_picker`, {});
    }

    /**
     * Render the current user details for the picker.
     *
     * @param {Object} context The data used to render the user picker.
     * @returns {Promise}
     */
    renderUserChange(context) {
        return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);
    }

    /**
     * Show the specified user in the picker.
     *
     * @param {Object} user
     */
    async showUser(user) {
        const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);
        const userRegion = this.root.querySelector(Selectors.regions.userRegion);
        Templates.replaceNodeContents(userRegion, html, js);

        // Update the hidden now-grading region so screen readers can announce the user that's currently being graded.
        const currentUserRegion = this.root.querySelector(Selectors.regions.currentUser);
        currentUserRegion.textContent = await getString('nowgradinguser', 'mod_forum', user.fullname);
    }

    /**
     * Register the event listeners for the user picker.
     */
    registerEventListeners() {
        this.root.addEventListener('click', async(e) => {
            const button = e.target.closest(Selectors.actions.changeUser);

            if (button) {
                const result = await this.preChangeUserCallback(this.currentUser);

                if (!result.failed) {
                    this.updateIndex(parseInt(button.dataset.direction));
                    await this.showUser(this.currentUser);
                }
            }
        });
    }

    /**
     * Update the current user index.
     *
     * @param {Number} direction
     * @returns {Number}}
     */
    updateIndex(direction) {
        this.currentUserIndex += direction;

        // Loop around the edges.
        if (this.currentUserIndex < 0) {
            this.currentUserIndex = this.userList.length - 1;
        } else if (this.currentUserIndex > this.userList.length - 1) {
            this.currentUserIndex = 0;
        }

        return this.currentUserIndex;
    }

    /**
     * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the
     * current user.
     *
     * @returns {Object}
     */
    get currentUser() {
        return {
            ...this.userList[this.currentUserIndex],
            total: this.userList.length,
            displayIndex: this.currentUserIndex + 1,
        };
    }

    /**
     * Get the root node for the User Picker.
     *
     * @returns {HTMLElement}
     */
    get rootNode() {
        return this.root;
    }
}

/**
 * Create a new user picker.
 *
 * @param {Array} users The list of users
 * @param {Function} showUserCallback The function to call to show a specific user
 * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user
 * @param {Number} [currentUserID] The userid of the current user
 * @returns {UserPicker}
 */
export default async(
    users,
    showUserCallback,
    preChangeUserCallback,
    {
        initialUserId = null,
    } = {}
) => {
    const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);
    if (initialUserId) {
        userPicker.setUserId(initialUserId);
    }
    await userPicker.render();

    return userPicker;
};