MDL-66381 mod_forum: User search and dropdown
authorMathew May <mathewm@hotmail.co.nz>
Fri, 1 Nov 2019 03:56:46 +0000 (11:56 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Tue, 5 Nov 2019 03:58:28 +0000 (11:58 +0800)
16 files changed:
mod/forum/amd/build/local/grades/grader.min.js
mod/forum/amd/build/local/grades/grader.min.js.map
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map
mod/forum/amd/build/local/layout/fullscreen.min.js
mod/forum/amd/build/local/layout/fullscreen.min.js.map
mod/forum/amd/src/local/grades/grader.js
mod/forum/amd/src/local/grades/local/grader/user_picker.js
mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js
mod/forum/amd/src/local/layout/fullscreen.js
mod/forum/lang/en/forum.php
mod/forum/templates/local/grades/local/grader/content.mustache
mod/forum/templates/local/grades/local/grader/user_picker.mustache
mod/forum/templates/local/grades/local/grader/user_picker/user_search.mustache [new file with mode: 0644]

index 3c62a3e..b3d04c0 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/grader.min.js and b/mod/forum/amd/build/local/grades/grader.min.js differ
index 2e24a1f..c40169a 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/grader.min.js.map and b/mod/forum/amd/build/local/grades/grader.min.js.map differ
index 14b67fe..701a7df 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js and b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js differ
index 2cd1640..ad44c62 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map and b/mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map differ
index 99f20c9..15a4202 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js and b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js differ
index 9c9dddc..28e8c47 100644 (file)
Binary files a/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map and b/mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map differ
index d63959b..14669a0 100644 (file)
Binary files a/mod/forum/amd/build/local/layout/fullscreen.min.js and b/mod/forum/amd/build/local/layout/fullscreen.min.js differ
index 567d9e5..83ad24c 100644 (file)
Binary files a/mod/forum/amd/build/local/layout/fullscreen.min.js.map and b/mod/forum/amd/build/local/layout/fullscreen.min.js.map differ
index 39a9d7a..9db5920 100644 (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();
     };
 };
 
index 889f429..815165a 100644 (file)
@@ -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) {
+
+                // 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);
+        };
+    }
 
-                spinner.resolve();
+    /**
+     * 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) {
index 035240f..99777b6 100644 (file)
@@ -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"]',
     }
 };
 
index 048e63b..2aaf122 100644 (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);
index bad04fd..6d3afb9 100644 (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';
index d1e92b8..a31d5cf 100644 (file)
@@ -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>
index c281e10..c84666e 100644 (file)
                         <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>
-
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 (file)
index 0000000..4c01f11
--- /dev/null
@@ -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>