diff --git a/mod/forum/amd/build/local/grades/grader.min.js b/mod/forum/amd/build/local/grades/grader.min.js
index 3c62a3e427b..b3d04c024c8 100644
--- a/mod/forum/amd/build/local/grades/grader.min.js
+++ b/mod/forum/amd/build/local/grades/grader.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/grades/grader",["exports","core/templates","./local/grader/selectors","./local/grader/user_picker","mod_forum/local/layout/fullscreen","./local/grader/gradingpanel","core/toast","core/str","core_grades/grades/grader/gradingpanel/normalise"],function(a,b,c,d,e,f,g,h,i){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"getGradingPanelFunctions",{enumerable:!0,get:function get(){return f.default}});a.launch=void 0;b=j(b);c=j(c);d=j(d);f=j(f);function j(a){return a&&a.__esModule?a:{default:a}}function k(a){for(var b=1;b.\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {get_string as getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @return {[*, *]}\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {\n return async(user) => {\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanel), gradingPanelHtml, gradingPanelJS);\n };\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction) => {\n const graderContainer = graderLayout.getContainer();\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n });\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async(user) => {\n try {\n root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel));\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString);\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null, moduleName\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n const [\n graderLayout,\n graderHTML,\n userList,\n ] = await Promise.all([\n createFullScreenWindow({fullscreen: false, showLoader: false}),\n Templates.render(templateNames.grader.app, {moduleName: moduleName}),\n getListOfUsers(),\n ]);\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, graderHTML, '');\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);\n\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n updateUserContent,\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/grades/grader.js"],"names":["templateNames","grader","app","gradingPanel","error","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","Templates","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","user","spinner","Promise","all","id","then","userGrade","moduleReplace","render","templatename","grade","gradingPanelHtml","gradingPanelJS","resolve","registerEventListeners","graderLayout","userPicker","saveGradeFunction","graderContainer","getContainer","addEventListener","e","target","closest","buttons","toggleFullscreen","stopImmediatePropagation","preventDefault","closeGrader","close","saveGrade","currentUser","getSaveUserGradeFunction","setGradeForUser","gradingPanelErrors","innerHTML","result","success","addToast","failed","displayGradingError","err","renderForPromise","message","errorString","launch","getListOfUsers","initialUserId","moduleName","fullscreen","showLoader","graderHTML","userList","updateUserContent","rootNode"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {get_string as getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @return {[*, *]}\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {\n return async(user) => {\n const spinner = addIconToContainerWithPromise(root);\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanel), gradingPanelHtml, gradingPanelJS);\n spinner.resolve();\n };\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction) => {\n const graderContainer = graderLayout.getContainer();\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n });\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async(user) => {\n try {\n root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel));\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString);\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null, moduleName\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n const [\n graderLayout,\n graderHTML,\n userList,\n ] = await Promise.all([\n createFullScreenWindow({fullscreen: false, showLoader: false}),\n Templates.render(templateNames.grader.app, {moduleName: moduleName}),\n getListOfUsers(),\n ]);\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, graderHTML, '');\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser);\n\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n updateUserContent,\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
index 14b67fe6658..701a7df3e5a 100644
--- a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
+++ b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/grades/local/grader/user_picker",["exports","core/templates","./user_picker/selectors","core/loadingicon"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a){for(var b=1;bthis.currentUserIndex){this.currentUserIndex=this.userList.length-1}else if(this.currentUserIndex>this.userList.length-1){this.currentUserIndex=0}return this.currentUserIndex}},{key:"currentUser",get:function get(){return f({},this.userList[this.currentUserIndex],{total:this.userList.length,displayIndex:this.currentUserIndex+1})}},{key:"rootNode",get:function get(){return this.root}}]);return a}(),s=function(){var a=n(regeneratorRuntime.mark(function a(b,c,d){var e,f,g,h,i=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:e=3this.currentUserIndex){this.currentUserIndex=this.userList.length-1}else if(this.currentUserIndex>this.userList.length-1){this.currentUserIndex=0}return this.currentUserIndex}},{key:"currentUser",get:function get(){return e({},this.userList[this.currentUserIndex],{total:this.userList.length,displayIndex:this.currentUserIndex+1})}},{key:"rootNode",get:function get(){return this.root}}]);return a}(),r=function(){var a=m(regeneratorRuntime.mark(function a(b,c,d){var e,f,g,h,i=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:e=3.\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/local/grader/user_picker\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './user_picker/selectors';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\n\nconst templatePath = 'mod_forum/local/grades/local/grader';\n\nclass UserPicker {\n\n /**\n * Constructor for the User Picker.\n *\n * @param {Array} userList List of users\n * @param {Function} showUserCallback The callback used to display the user\n * @param {Function} preChangeUserCallback The callback to use before changing user\n */\n constructor(userList, showUserCallback, preChangeUserCallback) {\n this.userList = userList;\n this.showUserCallback = showUserCallback;\n this.preChangeUserCallback = preChangeUserCallback;\n this.currentUserIndex = 0;\n\n // Ensure that render is bound correctly.\n this.render = this.render.bind(this);\n this.setUserId = this.setUserId.bind(this);\n }\n\n /**\n * Set the current userid without rendering the change.\n * To show the user, call showUser too.\n *\n * @param {Number} userId\n */\n setUserId(userId) {\n // Determine the current index based on the user ID.\n const userIndex = this.userList.findIndex(user => {\n return user.id === parseInt(userId);\n });\n\n if (userIndex === -1) {\n throw Error(`User with id ${userId} not found`);\n }\n\n this.currentUserIndex = userIndex;\n }\n\n /**\n * Render the user picker.\n */\n async render() {\n // Create the root node.\n this.root = document.createElement('div');\n\n const {html, js} = await this.renderNavigator();\n Templates.replaceNodeContents(this.root, html, js);\n\n // Call the showUser function to show the first user immediately.\n await this.showUser(this.currentUser);\n\n // Ensure that the event listeners are all bound.\n this.registerEventListeners();\n }\n\n /**\n * Render the navigator itself.\n *\n * @returns {Promise}\n */\n renderNavigator() {\n return Templates.renderForPromise(`${templatePath}/user_picker`, {});\n }\n\n /**\n * Render the current user details for the picker.\n *\n * @param {Object} context The data used to render the user picker.\n * @returns {Promise}\n */\n renderUserChange(context) {\n return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);\n }\n\n /**\n * Show the specified user in the picker.\n *\n * @param {Object} user\n */\n async showUser(user) {\n const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);\n const userRegion = this.root.querySelector(Selectors.regions.userRegion);\n Templates.replaceNodeContents(userRegion, html, js);\n }\n\n /**\n * Register the event listeners for the user picker.\n */\n registerEventListeners() {\n this.root.addEventListener('click', async(e) => {\n const button = e.target.closest(Selectors.actions.changeUser);\n if (button) {\n const result = await this.preChangeUserCallback(this.currentUser);\n const spinner = addIconToContainerWithPromise(document.querySelector('[data-region=\"unified-grader\"]'));\n\n if (!result.failed) {\n this.updateIndex(parseInt(button.dataset.direction));\n await this.showUser(this.currentUser);\n }\n\n spinner.resolve();\n }\n });\n }\n\n /**\n * Update the current user index.\n *\n * @param {Number} direction\n * @returns {Number}}\n */\n updateIndex(direction) {\n this.currentUserIndex += direction;\n\n // Loop around the edges.\n if (this.currentUserIndex < 0) {\n this.currentUserIndex = this.userList.length - 1;\n } else if (this.currentUserIndex > this.userList.length - 1) {\n this.currentUserIndex = 0;\n }\n\n return this.currentUserIndex;\n }\n\n /**\n * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the\n * current user.\n *\n * @returns {Object}\n */\n get currentUser() {\n return {\n ...this.userList[this.currentUserIndex],\n total: this.userList.length,\n displayIndex: this.currentUserIndex + 1,\n };\n }\n\n /**\n * Get the root node for the User Picker.\n *\n * @returns {HTMLElement}\n */\n get rootNode() {\n return this.root;\n }\n}\n\n/**\n * Create a new user picker.\n *\n * @param {Array} users The list of users\n * @param {Function} showUserCallback The function to call to show a specific user\n * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user\n * @param {Number} [currentUserID] The userid of the current user\n * @returns {UserPicker}\n */\nexport default async(\n users,\n showUserCallback,\n preChangeUserCallback,\n {\n initialUserId = null,\n } = {}\n) => {\n const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);\n if (initialUserId) {\n userPicker.setUserId(initialUserId);\n }\n await userPicker.render();\n\n return userPicker;\n};\n"],"file":"user_picker.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../../../src/local/grades/local/grader/user_picker.js"],"names":["UserPicker","userList","showUserCallback","preChangeUserCallback","currentUserIndex","render","bind","setUserId","userId","userIndex","findIndex","user","id","parseInt","Error","root","document","createElement","renderNavigator","html","js","Templates","replaceNodeContents","showUser","currentUser","renderSearch","searchResultListener","registerEventListeners","renderForPromise","context","Promise","all","renderUserChange","userRegion","querySelector","Selectors","regions","addEventListener","e","button","target","closest","actions","changeUser","input","searchUserInput","result","failed","updateIndex","dataset","direction","onKeyUp","timeout","onkeyup","clearTimeout","setTimeout","userInput","value","results","filter","fullname","toLowerCase","includes","searchUserBox","preventDefault","selectUser","foundUser","item","userid","trimmedUsers","slice","overflowUsers","builtResults","length","searchUserRegion","replaceNode","specificIndex","total","displayIndex","users","initialUserId","userPicker"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/local/grader/user_picker\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './user_picker/selectors';\n\nconst templatePath = 'mod_forum/local/grades/local/grader';\n\nclass UserPicker {\n\n /**\n * Constructor for the User Picker.\n *\n * @param {Array} userList List of users\n * @param {Function} showUserCallback The callback used to display the user\n * @param {Function} preChangeUserCallback The callback to use before changing user\n */\n constructor(userList, showUserCallback, preChangeUserCallback) {\n this.userList = userList;\n this.showUserCallback = showUserCallback;\n this.preChangeUserCallback = preChangeUserCallback;\n this.currentUserIndex = 0;\n\n // Ensure that render is bound correctly.\n this.render = this.render.bind(this);\n this.setUserId = this.setUserId.bind(this);\n }\n\n /**\n * Set the current userid without rendering the change.\n * To show the user, call showUser too.\n *\n * @param {Number} userId\n */\n setUserId(userId) {\n // Determine the current index based on the user ID.\n const userIndex = this.userList.findIndex(user => {\n return user.id === parseInt(userId);\n });\n\n if (userIndex === -1) {\n throw Error(`User with id ${userId} not found`);\n }\n\n this.currentUserIndex = userIndex;\n }\n\n /**\n * Render the user picker.\n */\n async render() {\n // Create the root node.\n this.root = document.createElement('div');\n\n const {html, js} = await this.renderNavigator();\n Templates.replaceNodeContents(this.root, html, js);\n\n // Call the showUser function to show the first user immediately.\n await this.showUser(this.currentUser);\n\n // Show a list of users under the user search box.\n await this.renderSearch(this.userList);\n\n this.searchResultListener();\n // Ensure that the event listeners are all bound.\n this.registerEventListeners();\n }\n\n /**\n * Render the navigator itself.\n *\n * @returns {Promise}\n */\n renderNavigator() {\n return Templates.renderForPromise(`${templatePath}/user_picker`, {});\n }\n\n /**\n * Render the current user details for the picker.\n *\n * @param {Object} context The data used to render the user picker.\n * @returns {Promise}\n */\n renderUserChange(context) {\n return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);\n }\n\n /**\n * Show the specified user in the picker.\n *\n * @param {Object} user\n */\n async showUser(user) {\n const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);\n const userRegion = this.root.querySelector(Selectors.regions.userRegion);\n Templates.replaceNodeContents(userRegion, html, js);\n }\n\n /**\n * Register the event listeners for the user picker.\n */\n registerEventListeners() {\n this.root.addEventListener('click', async(e) => {\n const button = e.target.closest(Selectors.actions.changeUser);\n const input = e.target.closest(Selectors.actions.searchUserInput);\n\n if (button) {\n const result = await this.preChangeUserCallback(this.currentUser);\n\n if (!result.failed) {\n this.updateIndex(parseInt(button.dataset.direction));\n await this.showUser(this.currentUser);\n }\n }\n if (input) {\n\n // Make the key up a seperate function.\n this.onKeyUp(input);\n }\n });\n }\n\n /**\n * Listener for keyboard entry that'll search the user list for matching users.\n *\n * @param {Text} input User entered text of the user to search for.\n */\n onKeyUp(input) {\n // Init a timeout variable to be used below\n let timeout = null;\n // Listen for keystroke events\n input.onkeyup = () => {\n // Clear the timeout if it has already been set.\n clearTimeout(timeout);\n // Make a new timeout set to go off in 300ms\n timeout = setTimeout(async(userList) => {\n const userInput = input.value;\n const results = userList.filter((user) => {\n return user.fullname.toLowerCase().includes(userInput.toLowerCase());\n });\n await this.renderSearch(results);\n this.searchResultListener();\n }, 300, this.userList);\n };\n }\n\n /**\n * Apply the click handler for the users found in the user search area.\n */\n searchResultListener() {\n this.root.querySelector(Selectors.actions.searchUserBox).addEventListener('click', async(e) => {\n e.preventDefault();\n const user = e.target.closest(Selectors.actions.selectUser);\n if (user !== null) {\n const foundUser = this.userList.findIndex(item => parseInt(item.id) === parseInt(user.dataset.userid));\n const result = await this.preChangeUserCallback(this.currentUser);\n\n if (!result.failed) {\n this.updateIndex(0, parseInt(foundUser));\n await this.showUser(this.currentUser);\n }\n }\n });\n }\n\n /**\n * Render the user search results.\n *\n * @param {Array} results List of users\n */\n async renderSearch(results) {\n const trimmedUsers = results.slice(0, 10);\n const overflowUsers = results.slice(10);\n const builtResults = {\n 'expandedUsers': trimmedUsers,\n 'hasCollapsed': overflowUsers.length > 0,\n 'collapsedUsers': overflowUsers,\n };\n const {html, js} = await Templates.renderForPromise(`${templatePath}/user_picker/user_search`, builtResults);\n const searchUserRegion = this.root.querySelector(Selectors.actions.searchUserBox);\n Templates.replaceNode(searchUserRegion, html, js);\n }\n /**\n * Update the current user index.\n *\n * @param {Number} direction\n * @param {Number} specificIndex\n * @returns {Number}}\n */\n updateIndex(direction, specificIndex = null) {\n if (specificIndex) {\n this.currentUserIndex = specificIndex;\n } else {\n this.currentUserIndex += direction;\n }\n\n // Loop around the edges.\n if (this.currentUserIndex < 0) {\n this.currentUserIndex = this.userList.length - 1;\n } else if (this.currentUserIndex > this.userList.length - 1) {\n this.currentUserIndex = 0;\n }\n\n return this.currentUserIndex;\n }\n\n /**\n * Get the details of the user currently shown with the total number of users, and the 1-indexed count of the\n * current user.\n *\n * @returns {Object}\n */\n get currentUser() {\n return {\n ...this.userList[this.currentUserIndex],\n total: this.userList.length,\n displayIndex: this.currentUserIndex + 1,\n };\n }\n\n /**\n * Get the root node for the User Picker.\n *\n * @returns {HTMLElement}\n */\n get rootNode() {\n return this.root;\n }\n}\n\n/**\n * Create a new user picker.\n *\n * @param {Array} users The list of users\n * @param {Function} showUserCallback The function to call to show a specific user\n * @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user\n * @param {Number} [currentUserID] The userid of the current user\n * @returns {UserPicker}\n */\nexport default async(\n users,\n showUserCallback,\n preChangeUserCallback,\n {\n initialUserId = null,\n } = {}\n) => {\n const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);\n if (initialUserId) {\n userPicker.setUserId(initialUserId);\n }\n await userPicker.render();\n\n return userPicker;\n};\n"],"file":"user_picker.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js
index 99f20c9bf41..15a42029242 100644
--- a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js
+++ b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/grades/local/grader/user_picker/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={regions:{userRegion:"[data-region=\"user_picker/user\"]"},actions:{changeUser:"[data-action=\"change-user\"]"}};return a.default});
+define ("mod_forum/local/grades/local/grader/user_picker/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={regions:{userRegion:"[data-region=\"user_picker/user\"]"},actions:{changeUser:"[data-action=\"change-user\"]",selectUser:"[data-action=\"select-user\"]",searchUserBox:"[data-action=\"search-user-box\"]",searchUserInput:"[data-action=\"search-user-input\"]"}};return a.default});
//# sourceMappingURL=selectors.min.js.map
diff --git a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map
index 9c9dddcec17..28e8c47934a 100644
--- a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map
+++ b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../../../../src/local/grades/local/grader/user_picker/selectors.js"],"names":["regions","userRegion","actions","changeUser"],"mappings":"qLAwBe,CACXA,OAAO,CAAE,CACLC,UAAU,CAAE,oCADP,CADE,CAIXC,OAAO,CAAE,CACLC,UAAU,CAAE,+BADP,CAJE,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module mod_forum/local/grades/local/grader/user_picker/selectors\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n regions: {\n userRegion: '[data-region=\"user_picker/user\"]',\n },\n actions: {\n changeUser: '[data-action=\"change-user\"]',\n }\n};\n\n"],"file":"selectors.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../../../../src/local/grades/local/grader/user_picker/selectors.js"],"names":["regions","userRegion","actions","changeUser","selectUser","searchUserBox","searchUserInput"],"mappings":"qLAwBe,CACXA,OAAO,CAAE,CACLC,UAAU,CAAE,oCADP,CADE,CAIXC,OAAO,CAAE,CACLC,UAAU,CAAE,+BADP,CAELC,UAAU,CAAE,+BAFP,CAGLC,aAAa,CAAE,mCAHV,CAILC,eAAe,CAAE,qCAJZ,CAJE,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module mod_forum/local/grades/local/grader/user_picker/selectors\n * @package mod_forum\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n regions: {\n userRegion: '[data-region=\"user_picker/user\"]',\n },\n actions: {\n changeUser: '[data-action=\"change-user\"]',\n selectUser: '[data-action=\"select-user\"]',\n searchUserBox: '[data-action=\"search-user-box\"]',\n searchUserInput: '[data-action=\"search-user-input\"]',\n }\n};\n\n"],"file":"selectors.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/build/local/layout/fullscreen.min.js b/mod/forum/amd/build/local/layout/fullscreen.min.js
index d63959b4668..14669a0f909 100644
--- a/mod/forum/amd/build/local/layout/fullscreen.min.js
+++ b/mod/forum/amd/build/local/layout/fullscreen.min.js
@@ -1,2 +1,2 @@
-define ("mod_forum/local/layout/fullscreen",["exports","core/loadingicon"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.createLayout=void 0;var c=function(a){var c=document.createElement("div");a.append(c);var d=document.createElement("div");a.append(d);var f=function(){if(a.requestFullscreen){a.requestFullscreen()}else if(a.msRequestFullscreen){a.msRequestFullscreen()}else if(a.mozRequestFullscreen){a.mozRequestFullscreen()}else if(a.webkitRequestFullscreen){a.webkitRequestFullscreen()}else{a.setTop(0)}},g=function(){if(document.exitRequestFullScreen){if(document.fullScreenElement!==a){return}document.exitRequestFullScreen()}else if(document.msExitFullscreen){if(document.msFullscreenElement!==a){return}document.msExitFullscreen()}else if(document.mozCancelFullScreen){if(document.mozFullScreenElement!==a){return}document.mozCancelFullScreen()}else if(document.webkitExitFullscreen){if(document.webkitFullscreenElement!==a){return}document.webkitExitFullscreen()}},h=function(){var a=d.lastElementChild;while(a){d.removeChild(a);a=d.lastElementChild}};return{close:function close(){g();e();a.remove()},toggleFullscreen:function toggleFullscreen(){if(document.exitRequestFullScreen){if(document.fullScreenElement===a){g()}else{f()}}else if(document.msExitFullscreen){if(document.msFullscreenElement===a){g()}else{f()}}else if(document.mozCancelFullScreen){if(document.mozFullScreenElement===a){g()}else{f()}}else if(document.webkitExitFullscreen){if(document.webkitFullscreenElement===a){g()}else{f()}}},requestFullscreen:f,exitFullscreen:g,getContainer:function getContainer(){return c},setContent:function setContent(a){h();var b=c.lastElementChild;while(b){c.removeChild(b);b=c.lastElementChild}c.append(a)},showLoadingIcon:function showLoadingIcon(){(0,b.addIconToContainer)(d)},hideLoadingIcon:h}},d=function(){document.querySelector("body").classList.add("overflow-hidden")},e=function(){document.querySelector("body").classList.remove("overflow-hidden")};a.createLayout=function getComposedLayout(){var a=0.\n\n/**\n * Full screen window layout.\n *\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {addIconToContainer} from 'core/loadingicon';\n\n/**\n * @param {string} templateName\n * @param {object} context\n * @return {object}\n */\nconst getComposedLayout = ({\n fullscreen = true,\n showLoader = true,\n} = {}) => {\n const container = document.createElement('div');\n document.body.append(container);\n container.classList.add('layout');\n container.classList.add('fullscreen');\n container.setAttribute('aria-role', 'application');\n\n // Lock scrolling on the document body.\n lockBodyScroll();\n\n const helpers = getLayoutHelpers(container);\n\n if (showLoader) {\n helpers.showLoadingIcon();\n }\n\n if (fullscreen) {\n helpers.requestFullscreen();\n }\n\n return helpers;\n};\n\nconst getLayoutHelpers = (layoutNode) => {\n const contentNode = document.createElement('div');\n layoutNode.append(contentNode);\n\n const loadingNode = document.createElement('div');\n layoutNode.append(loadingNode);\n\n /**\n * Close and destroy the window container.\n */\n const close = () => {\n exitFullscreen();\n unlockBodyScroll();\n\n layoutNode.remove();\n };\n\n /**\n * Attempt to make the conatiner full screen.\n */\n const requestFullscreen = () => {\n if (layoutNode.requestFullscreen) {\n layoutNode.requestFullscreen();\n } else if (layoutNode.msRequestFullscreen) {\n layoutNode.msRequestFullscreen();\n } else if (layoutNode.mozRequestFullscreen) {\n layoutNode.mozRequestFullscreen();\n } else if (layoutNode.webkitRequestFullscreen) {\n layoutNode.webkitRequestFullscreen();\n } else {\n // Not supported.\n // Hack to make this act like full-screen as much as possible.\n layoutNode.setTop(0);\n }\n };\n\n /**\n * Exit full screen but do not close the container fully.\n */\n const exitFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement !== layoutNode) {\n return;\n }\n document.exitRequestFullScreen();\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement !== layoutNode) {\n return;\n }\n document.msExitFullscreen();\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement !== layoutNode) {\n return;\n }\n document.mozCancelFullScreen();\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement !== layoutNode) {\n return;\n }\n document.webkitExitFullscreen();\n }\n };\n\n const toggleFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n }\n };\n\n /**\n * Get the Node which is fullscreen.\n *\n * @return {Element}\n */\n const getContainer = () => {\n return contentNode;\n };\n\n const setContent = (content) => {\n hideLoadingIcon();\n\n // Note: It would be better to use replaceWith, but this is not compatible with IE.\n let child = contentNode.lastElementChild;\n while (child) {\n contentNode.removeChild(child);\n child = contentNode.lastElementChild;\n }\n contentNode.append(content);\n };\n\n const showLoadingIcon = () => {\n addIconToContainer(loadingNode);\n };\n\n const hideLoadingIcon = () => {\n // Hide the loading container.\n let child = loadingNode.lastElementChild;\n while (child) {\n loadingNode.removeChild(child);\n child = loadingNode.lastElementChild;\n }\n };\n\n /**\n * @return {Object}\n */\n return {\n close,\n\n toggleFullscreen,\n requestFullscreen,\n exitFullscreen,\n\n getContainer,\n setContent,\n\n showLoadingIcon,\n hideLoadingIcon,\n };\n};\n\nconst lockBodyScroll = () => {\n document.querySelector('body').classList.add('overflow-hidden');\n};\n\nconst unlockBodyScroll = () => {\n document.querySelector('body').classList.remove('overflow-hidden');\n};\n\nexport const createLayout = getComposedLayout;\n"],"file":"fullscreen.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/local/layout/fullscreen.js"],"names":["getLayoutHelpers","layoutNode","contentNode","document","createElement","append","loadingNode","requestFullscreen","msRequestFullscreen","mozRequestFullscreen","webkitRequestFullscreen","setTop","exitFullscreen","exitRequestFullScreen","fullScreenElement","msExitFullscreen","msFullscreenElement","mozCancelFullScreen","mozFullScreenElement","webkitExitFullscreen","webkitFullscreenElement","hideLoadingIcon","child","lastElementChild","removeChild","close","unlockBodyScroll","remove","toggleFullscreen","getContainer","setContent","content","showLoadingIcon","lockBodyScroll","querySelector","classList","add","getComposedLayout","fullscreen","showLoader","container","body","setAttribute","helpers"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Full screen window layout.\n *\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {addIconToContainer} from 'core/loadingicon';\n\n/**\n * @param {string} templateName\n * @param {object} context\n * @return {object}\n */\nconst getComposedLayout = ({\n fullscreen = true,\n showLoader = false,\n} = {}) => {\n const container = document.createElement('div');\n document.body.append(container);\n container.classList.add('layout');\n container.classList.add('fullscreen');\n container.setAttribute('aria-role', 'application');\n\n // Lock scrolling on the document body.\n lockBodyScroll();\n\n const helpers = getLayoutHelpers(container);\n\n if (showLoader) {\n helpers.showLoadingIcon();\n }\n\n if (fullscreen) {\n helpers.requestFullscreen();\n }\n\n return helpers;\n};\n\nconst getLayoutHelpers = (layoutNode) => {\n const contentNode = document.createElement('div');\n layoutNode.append(contentNode);\n\n const loadingNode = document.createElement('div');\n layoutNode.append(loadingNode);\n\n /**\n * Close and destroy the window container.\n */\n const close = () => {\n exitFullscreen();\n unlockBodyScroll();\n\n layoutNode.remove();\n };\n\n /**\n * Attempt to make the conatiner full screen.\n */\n const requestFullscreen = () => {\n if (layoutNode.requestFullscreen) {\n layoutNode.requestFullscreen();\n } else if (layoutNode.msRequestFullscreen) {\n layoutNode.msRequestFullscreen();\n } else if (layoutNode.mozRequestFullscreen) {\n layoutNode.mozRequestFullscreen();\n } else if (layoutNode.webkitRequestFullscreen) {\n layoutNode.webkitRequestFullscreen();\n } else {\n // Not supported.\n // Hack to make this act like full-screen as much as possible.\n layoutNode.setTop(0);\n }\n };\n\n /**\n * Exit full screen but do not close the container fully.\n */\n const exitFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement !== layoutNode) {\n return;\n }\n document.exitRequestFullScreen();\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement !== layoutNode) {\n return;\n }\n document.msExitFullscreen();\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement !== layoutNode) {\n return;\n }\n document.mozCancelFullScreen();\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement !== layoutNode) {\n return;\n }\n document.webkitExitFullscreen();\n }\n };\n\n const toggleFullscreen = () => {\n if (document.exitRequestFullScreen) {\n if (document.fullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.msExitFullscreen) {\n if (document.msFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.mozCancelFullScreen) {\n if (document.mozFullScreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n } else if (document.webkitExitFullscreen) {\n if (document.webkitFullscreenElement === layoutNode) {\n exitFullscreen();\n } else {\n requestFullscreen();\n }\n }\n };\n\n /**\n * Get the Node which is fullscreen.\n *\n * @return {Element}\n */\n const getContainer = () => {\n return contentNode;\n };\n\n const setContent = (content) => {\n hideLoadingIcon();\n\n // Note: It would be better to use replaceWith, but this is not compatible with IE.\n let child = contentNode.lastElementChild;\n while (child) {\n contentNode.removeChild(child);\n child = contentNode.lastElementChild;\n }\n contentNode.append(content);\n };\n\n const showLoadingIcon = () => {\n addIconToContainer(loadingNode);\n };\n\n const hideLoadingIcon = () => {\n // Hide the loading container.\n let child = loadingNode.lastElementChild;\n while (child) {\n loadingNode.removeChild(child);\n child = loadingNode.lastElementChild;\n }\n };\n\n /**\n * @return {Object}\n */\n return {\n close,\n\n toggleFullscreen,\n requestFullscreen,\n exitFullscreen,\n\n getContainer,\n setContent,\n\n showLoadingIcon,\n hideLoadingIcon,\n };\n};\n\nconst lockBodyScroll = () => {\n document.querySelector('body').classList.add('overflow-hidden');\n};\n\nconst unlockBodyScroll = () => {\n document.querySelector('body').classList.remove('overflow-hidden');\n};\n\nexport const createLayout = getComposedLayout;\n"],"file":"fullscreen.min.js"}
\ No newline at end of file
diff --git a/mod/forum/amd/src/local/grades/grader.js b/mod/forum/amd/src/local/grades/grader.js
index 39a9d7a64b2..9db5920550d 100644
--- a/mod/forum/amd/src/local/grades/grader.js
+++ b/mod/forum/amd/src/local/grades/grader.js
@@ -29,6 +29,7 @@ import getGradingPanelFunctions from './local/grader/gradingpanel';
import {add as addToast} from 'core/toast';
import {get_string as getString} from 'core/str';
import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';
+import {addIconToContainerWithPromise} from 'core/loadingicon';
const templateNames = {
grader: {
@@ -72,6 +73,7 @@ const fetchContentFromRender = (html, js) => {
*/
const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => {
return async(user) => {
+ const spinner = addIconToContainerWithPromise(root);
const [
[html, js],
userGrade,
@@ -86,6 +88,7 @@ const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser)
gradingPanelJS
] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);
Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanel), gradingPanelHtml, gradingPanelJS);
+ spinner.resolve();
};
};
diff --git a/mod/forum/amd/src/local/grades/local/grader/user_picker.js b/mod/forum/amd/src/local/grades/local/grader/user_picker.js
index 889f429fad9..815165a2f25 100644
--- a/mod/forum/amd/src/local/grades/local/grader/user_picker.js
+++ b/mod/forum/amd/src/local/grades/local/grader/user_picker.js
@@ -24,7 +24,6 @@
import Templates from 'core/templates';
import Selectors from './user_picker/selectors';
-import {addIconToContainerWithPromise} from 'core/loadingicon';
const templatePath = 'mod_forum/local/grades/local/grader';
@@ -80,6 +79,10 @@ class UserPicker {
// Call the showUser function to show the first user immediately.
await this.showUser(this.currentUser);
+ // Show a list of users under the user search box.
+ await this.renderSearch(this.userList);
+
+ this.searchResultListener();
// Ensure that the event listeners are all bound.
this.registerEventListeners();
}
@@ -120,28 +123,97 @@ class UserPicker {
registerEventListeners() {
this.root.addEventListener('click', async(e) => {
const button = e.target.closest(Selectors.actions.changeUser);
+ const input = e.target.closest(Selectors.actions.searchUserInput);
+
if (button) {
const result = await this.preChangeUserCallback(this.currentUser);
- const spinner = addIconToContainerWithPromise(document.querySelector('[data-region="unified-grader"]'));
if (!result.failed) {
this.updateIndex(parseInt(button.dataset.direction));
await this.showUser(this.currentUser);
}
+ }
+ if (input) {
- spinner.resolve();
+ // Make the key up a seperate function.
+ this.onKeyUp(input);
}
});
}
+ /**
+ * Listener for keyboard entry that'll search the user list for matching users.
+ *
+ * @param {Text} input User entered text of the user to search for.
+ */
+ onKeyUp(input) {
+ // Init a timeout variable to be used below
+ let timeout = null;
+ // Listen for keystroke events
+ input.onkeyup = () => {
+ // Clear the timeout if it has already been set.
+ clearTimeout(timeout);
+ // Make a new timeout set to go off in 300ms
+ timeout = setTimeout(async(userList) => {
+ const userInput = input.value;
+ const results = userList.filter((user) => {
+ return user.fullname.toLowerCase().includes(userInput.toLowerCase());
+ });
+ await this.renderSearch(results);
+ this.searchResultListener();
+ }, 300, this.userList);
+ };
+ }
+
+ /**
+ * Apply the click handler for the users found in the user search area.
+ */
+ searchResultListener() {
+ this.root.querySelector(Selectors.actions.searchUserBox).addEventListener('click', async(e) => {
+ e.preventDefault();
+ const user = e.target.closest(Selectors.actions.selectUser);
+ if (user !== null) {
+ const foundUser = this.userList.findIndex(item => parseInt(item.id) === parseInt(user.dataset.userid));
+ const result = await this.preChangeUserCallback(this.currentUser);
+
+ if (!result.failed) {
+ this.updateIndex(0, parseInt(foundUser));
+ await this.showUser(this.currentUser);
+ }
+ }
+ });
+ }
+
+ /**
+ * Render the user search results.
+ *
+ * @param {Array} results List of users
+ */
+ async renderSearch(results) {
+ const trimmedUsers = results.slice(0, 10);
+ const overflowUsers = results.slice(10);
+ const builtResults = {
+ 'expandedUsers': trimmedUsers,
+ 'hasCollapsed': overflowUsers.length > 0,
+ 'collapsedUsers': overflowUsers,
+ };
+ const {html, js} = await Templates.renderForPromise(`${templatePath}/user_picker/user_search`, builtResults);
+ const searchUserRegion = this.root.querySelector(Selectors.actions.searchUserBox);
+ Templates.replaceNode(searchUserRegion, html, js);
+ }
/**
* Update the current user index.
*
* @param {Number} direction
+ * @param {Number} specificIndex
* @returns {Number}}
*/
- updateIndex(direction) {
- this.currentUserIndex += direction;
+ updateIndex(direction, specificIndex = null) {
+ if (specificIndex) {
+ this.currentUserIndex = specificIndex;
+ } else {
+ this.currentUserIndex += direction;
+ }
// Loop around the edges.
if (this.currentUserIndex < 0) {
diff --git a/mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js b/mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js
index 035240f0297..99777b6b9e0 100644
--- a/mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js
+++ b/mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js
@@ -28,6 +28,9 @@ export default {
},
actions: {
changeUser: '[data-action="change-user"]',
+ selectUser: '[data-action="select-user"]',
+ searchUserBox: '[data-action="search-user-box"]',
+ searchUserInput: '[data-action="search-user-input"]',
}
};
diff --git a/mod/forum/amd/src/local/layout/fullscreen.js b/mod/forum/amd/src/local/layout/fullscreen.js
index 048e63b3351..2aaf1223f71 100644
--- a/mod/forum/amd/src/local/layout/fullscreen.js
+++ b/mod/forum/amd/src/local/layout/fullscreen.js
@@ -29,7 +29,7 @@ import {addIconToContainer} from 'core/loadingicon';
*/
const getComposedLayout = ({
fullscreen = true,
- showLoader = true,
+ showLoader = false,
} = {}) => {
const container = document.createElement('div');
document.body.append(container);
diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php
index bad04fd65e7..6d3afb9edcb 100644
--- a/mod/forum/lang/en/forum.php
+++ b/mod/forum/lang/en/forum.php
@@ -729,6 +729,8 @@ $string['gradeitemnameforwholeforum'] = 'Whole forum grade for {$a->name}';
$string['gradeitemnameforrating'] = 'Rating grade for {$a->name}';
$string['grades:gradesavedfor'] = 'Grade saved for {$a->fullname}';
$string['grades:gradesavefailed'] = 'Unable to save grade for {$a->fullname}: {$a->error}';
+$string['showmoreusers'] = 'Show more users';
+$string['nousersmatch'] = 'No user(s) found for given criteria';
// Deprecated since Moodle 3.8.
$string['cannotdeletediscussioninsinglediscussion'] = 'You cannot delete the first post in a single discussion';
diff --git a/mod/forum/templates/local/grades/local/grader/content.mustache b/mod/forum/templates/local/grades/local/grader/content.mustache
index d1e92b8b173..a31d5cf4bab 100644
--- a/mod/forum/templates/local/grades/local/grader/content.mustache
+++ b/mod/forum/templates/local/grades/local/grader/content.mustache
@@ -31,7 +31,5 @@
}
}}
-
- {{> core/loading }}
-
+
diff --git a/mod/forum/templates/local/grades/local/grader/user_picker.mustache b/mod/forum/templates/local/grades/local/grader/user_picker.mustache
index c281e10c91d..c84666eed0d 100644
--- a/mod/forum/templates/local/grades/local/grader/user_picker.mustache
+++ b/mod/forum/templates/local/grades/local/grader/user_picker.mustache
@@ -53,16 +53,21 @@
{{#str}} next {{/str}}
-
-
diff --git a/mod/forum/templates/local/grades/local/grader/user_picker/user_search.mustache b/mod/forum/templates/local/grades/local/grader/user_picker/user_search.mustache
new file mode 100644
index 00000000000..4c01f110465
--- /dev/null
+++ b/mod/forum/templates/local/grades/local/grader/user_picker/user_search.mustache
@@ -0,0 +1,95 @@
+{{!
+ 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 .
+}}
+{{!
+ @template mod_forum/local/grades/local/grader/user_picker/user_search
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * data-action="select-user"
+
+
+ Context variables required for this template:
+ * expandedUsers: Array of users to show, limited to 10
+ * profileimage: Profile image for the user
+ * fullname: User's full name
+ * id: User's ID
+ * hasCollapsed: T/F if there are more users to show
+ * collapsedUsers: Array of users after index 9
+
+ Example context (json):
+ {
+ "expandedUsers": [
+ {
+ "id": 4,
+ "fullname": "Phillip J. Fry",
+ "profileimage": "/pluginfile.php/4/user/icon/boost/f1?rev=58"
+ },
+ {
+ "id": 5,
+ "fullname": "Turanga Leela",
+ "profileimage": "/pluginfile.php/5/user/icon/boost/f1?rev=58"
+ }
+ ],
+ "hasCollapsed": true,
+ "collapsedUsers": [
+ {
+ "id": 14,
+ "fullname": "Bender B. Rodriguez",
+ "profileimage": "/pluginfile.php/14/user/icon/boost/f1?rev=58"
+ }
+ ]
+ }
+}}
+