MDL-66074 mod_forum: Implement spinner for user change
[moodle.git] / mod / forum / amd / src / local / grades / local / grader / user_picker.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module will tie together all of the different calls the gradable module will make.
18  *
19  * @module     mod_forum/local/grades/local/grader/user_picker
20  * @package    mod_forum
21  * @copyright  2019 Mathew May <mathew.solutions>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 import Templates from 'core/templates';
26 import Selectors from './user_picker/selectors';
27 import {addIconToContainerWithPromise} from 'core/loadingicon';
29 const templatePath = 'mod_forum/local/grades/local/grader';
31 class UserPicker {
33     /**
34      * Constructor for the User Picker.
35      *
36      * @param {Array} userList List of users
37      * @param {Function} showUserCallback The callback used to display the user
38      * @param {Function} preChangeUserCallback The callback to use before changing user
39      */
40     constructor(userList, showUserCallback, preChangeUserCallback) {
41         this.userList = userList;
42         this.showUserCallback = showUserCallback;
43         this.preChangeUserCallback = preChangeUserCallback;
44         this.currentUserIndex = 0;
46         // Ensure that render is bound correctly.
47         this.render = this.render.bind(this);
48         this.setUserId = this.setUserId.bind(this);
49     }
51     /**
52      * Set the current userid without rendering the change.
53      * To show the user, call showUser too.
54      *
55      * @param {Number} userId
56      */
57     setUserId(userId) {
58         // Determine the current index based on the user ID.
59         const userIndex = this.userList.findIndex(user => {
60             return user.id === parseInt(userId);
61         });
63         if (userIndex === -1) {
64             throw Error(`User with id ${userId} not found`);
65         }
67         this.currentUserIndex = userIndex;
68     }
70     /**
71      * Render the user picker.
72      */
73     async render() {
74         // Create the root node.
75         this.root = document.createElement('div');
77         const {html, js} = await this.renderNavigator();
78         Templates.replaceNodeContents(this.root, html, js);
80         // Call the showUser function to show the first user immediately.
81         await this.showUser(this.currentUser);
83         // Ensure that the event listeners are all bound.
84         this.registerEventListeners();
85     }
87     /**
88      * Render the navigator itself.
89      *
90      * @returns {Promise}
91      */
92     renderNavigator() {
93         return Templates.renderForPromise(`${templatePath}/user_picker`, {});
94     }
96     /**
97      * Render the current user details for the picker.
98      *
99      * @param {Object} context The data used to render the user picker.
100      * @returns {Promise}
101      */
102     renderUserChange(context) {
103         return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);
104     }
106     /**
107      * Show the specified user in the picker.
108      *
109      * @param {Object} user
110      */
111     async showUser(user) {
112         const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);
113         const userRegion = this.root.querySelector(Selectors.regions.userRegion);
114         Templates.replaceNodeContents(userRegion, html, js);
115     }
117     /**
118      * Register the event listeners for the user picker.
119      */
120     registerEventListeners() {
121         this.root.addEventListener('click', async e => {
122             const button = e.target.closest(Selectors.actions.changeUser);
123             if (button) {
124                 const whole = document.querySelector('[data-region="unified-grader"]');
125                 const spinner = addIconToContainerWithPromise(whole);
127                 await this.preChangeUserCallback(this.currentUser);
128                 this.updateIndex(parseInt(button.dataset.direction));
129                 await this.showUser(this.currentUser);
131                 spinner.resolve();
132             }
133         });
134     }
136     /**
137      * Update the current user index.
138      *
139      * @param {Number} direction
140      * @returns {Number}}
141      */
142     updateIndex(direction) {
143         this.currentUserIndex += direction;
145         // Loop around the edges.
146         if (this.currentUserIndex < 0) {
147             this.currentUserIndex = this.userList.length - 1;
148         } else if (this.currentUserIndex > this.userList.length - 1) {
149             this.currentUserIndex = 0;
150         }
152         return this.currentUserIndex;
153     }
155     /**
156      * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the
157      * current user.
158      *
159      * @returns {Object}
160      */
161     get currentUser() {
162         return {
163             ...this.userList[this.currentUserIndex],
164             total: this.userList.length,
165             displayIndex: this.currentUserIndex + 1,
166         };
167     }
169     /**
170      * Get the root node for the User Picker.
171      *
172      * @returns {HTMLElement}
173      */
174     get rootNode() {
175         return this.root;
176     }
179 /**
180  * Create a new user picker.
181  *
182  * @param {Array} users The list of users
183  * @param {Function} showUserCallback The function to call to show a specific user
184  * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user
185  * @param {Number} [currentUserID] The userid of the current user
186  * @returns {UserPicker}
187  */
188 export default async(users, showUserCallback, preChangeUserCallback, {
189         initialUserId = null,
190     } = {}
191 ) => {
192     const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);
193     if (initialUserId) {
194         userPicker.setUserId(initialUserId);
195     }
196     await userPicker.render();
198     return userPicker;
199 };