MDL-66381 mod_forum: User search and dropdown

This commit is contained in:
Mathew May
2019-11-05 11:58:28 +08:00
parent 9d752481fc
commit bdcf8908ae
16 changed files with 200 additions and 22 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.\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 <mathew.solutions>\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"}
{"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 <http://www.gnu.org/licenses/>.\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 <mathew.solutions>\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"}
+1 -1
View File
@@ -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<arguments.length&&arguments[0]!==void 0?arguments[0]:{},b=a.fullscreen,e=void 0===b?!0:b,f=a.showLoader,g=void 0===f?!0:f,h=document.createElement("div");document.body.append(h);h.classList.add("layout");h.classList.add("fullscreen");h.setAttribute("aria-role","application");d();var i=c(h);if(g){i.showLoadingIcon()}if(e){i.requestFullscreen()}return i}});
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<arguments.length&&arguments[0]!==void 0?arguments[0]:{},b=a.fullscreen,e=void 0===b?!0:b,f=a.showLoader,g=void 0===f?!1:f,h=document.createElement("div");document.body.append(h);h.classList.add("layout");h.classList.add("fullscreen");h.setAttribute("aria-role","application");d();var i=c(h);if(g){i.showLoadingIcon()}if(e){i.requestFullscreen()}return i}});
//# sourceMappingURL=fullscreen.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -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();
};
};
@@ -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) {
@@ -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"]',
}
};
+1 -1
View File
@@ -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);
+2
View File
@@ -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';
@@ -31,7 +31,5 @@
}
}}
<div class="grader-module-content col-sm-12 col-md-8 mb-3">
<div data-region="module_content" class="grader-module-content-display col-sm-12">
{{> core/loading }}
</div>
<div data-region="module_content" class="grader-module-content-display col-sm-12"></div>
</div>
@@ -53,16 +53,21 @@
<span class="sr-only">{{#str}} next {{/str}}</span>
</a>
</li>
<li class="page-item">
<a class="page-link disabled" href="#" aria-label="Search" data-action="search-user">
<span class="fa fa-search" aria-hidden="true"></span>
<li>
<button class="btn btn-icon icon-size-4 icon-no-margin colour-inherit" aria-label="{{#str}} search {{/str}}" data-target="#searchbox" aria-expanded="false" aria-controls="searchbox" data-toggle="collapse">
<i class="icon fa fa-search fa-fw" aria-hidden="true"></i>
<span class="sr-only">{{#str}} search {{/str}}</span>
</a>
</button>
</li>
</ul>
</div>
<div id="searchbox" class="col-md-12 collapse">
<form action="" class="search-form my-3 w-100">
<input type="text" data-action="search-user-input" class="form-control input-lg" placeholder="Search user">
</form>
<div data-action="search-user-box"></div>
</div>
</div>
<hr>
</div>
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
]
}
}}
<div data-action="search-user-box">
{{#expandedUsers}}
<div class="mt-2">
<img class="rounded-circle userpicture" src="{{profileimage}}"
alt="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
title="{{#str}}pictureof, moodle, {{fullname}}{{/str}}" >
<a href="#" class="font-weight-bold" data-action="select-user" data-userid="{{id}}">{{fullname}}</a>
</div>
{{/expandedUsers}}
{{^expandedUsers}}
<h5>{{#str}}nousersmatch, mod_forum{{/str}}</h5>
{{/expandedUsers}}
{{#hasCollapsed}}
<button
class="btn btn-icon mt-2 text-muted icon-no-margin icon-size-3"
type="button"
id="grader-users-menu-{{uniqid}}"
aria-label="{{#str}} showmoreusers, mod_forum {{/str}}"
data-toggle="collapse"
data-target="#collapsed-users-{{uniqid}}"
aria-expanded="false"
aria-controls="collapsed-users-{{uniqid}}"
>
{{#pix}} i/moremenu {{/pix}}
</button>
<div id="collapsed-users-{{uniqid}}" class="collapse" aria-labelledby="grader-users-menu-{{uniqid}}">
{{#collapsedUsers}}
<div class="mt-2">
<img class="rounded-circle userpicture" src="{{profileimage}}"
alt="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
title="{{#str}}pictureof, moodle, {{fullname}}{{/str}}" >
<a href="#" class="font-weight-bold" data-action="select-user" data-userid="{{id}}">{{fullname}}</a>
</div>
{{/collapsedUsers}}
</div>
{{/hasCollapsed}}
</div>