MDL-75837 core_grades: Adapt the base search widget to support dropdowns

This commit is contained in:
Mihail Geshoski
2022-11-14 10:11:42 +08:00
parent 7c3188b2ca
commit 09d0a20d45
8 changed files with 186 additions and 132 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
define("core_grades/searchwidget/basewidget",["exports","core/modal_factory","core/modal_events","core/utils","core/templates","core/aria"],(function(_exports,ModalFactory,ModalEvents,_utils,Templates,_aria){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
define("core_grades/searchwidget/basewidget",["exports","core/utils","core/templates","core/aria","core_grades/searchwidget/selectors","core/notification"],(function(_exports,_utils,Templates,_aria,Selectors,_notification){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* A small modal to search users or grade items within the gradebook.
* A widget to search users or grade items within the gradebook.
*
* @module core_grades/searchwidget/basewidget
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.promisesAndResolvers=_exports.init=void 0,ModalFactory=_interopRequireWildcard(ModalFactory),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates);_exports.init=function(bodyPromise,data,searchFunc,modalTitle){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;const modal=buildModal(bodyPromise,modalTitle);registerListenerEvents(modal,data,searchFunc,unsearchableContent)};const registerListenerEvents=(modal,data,searchFunc,unsearchableContent)=>{modal.then((modal=>{modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal.getBodyPromise().then((body=>body[0])).then((body=>{const searchInput=body.querySelector('input[data-action="search"]'),searchResultsContainer=body.querySelector('[data-region="search-results-container-widget"]');if(renderSearchResults(searchResultsContainer,data),unsearchableContent){body.querySelector('[data-region="unsearchable-content-container-widget"]').innerHTML+=unsearchableContent}return searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),(0,_aria.comboBox)(searchInput),body})).catch()})).catch()},buildModal=(bodyPromise,modalTitle)=>ModalFactory.create({type:ModalFactory.types.DEFAULT,title:modalTitle,body:bodyPromise,small:!0,scrollable:!1,templateContext:{classes:"reportdatasearch modal-sm"}}).then((modal=>(modal.show(),modal))),debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showLoader=_exports.registerListenerEvents=_exports.promisesAndResolvers=_exports.init=void 0,Templates=_interopRequireWildcard(Templates),Selectors=_interopRequireWildcard(Selectors),_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};_exports.init=async function(widgetContentContainer,bodyPromise,data,searchFunc){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;bodyPromise.then((async bodyContent=>{if(widgetContentContainer.innerHTML=bodyContent,unsearchableContent){widgetContentContainer.querySelector(Selectors.regions.unsearchableContent).innerHTML+=unsearchableContent}const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults);await showLoader(searchResultsContainer),await renderSearchResults(searchResultsContainer,data),registerListenerEvents(widgetContentContainer,data,searchFunc)})).catch(_notification.default.exception)};const registerListenerEvents=(widgetContentContainer,data,searchFunc)=>{const searchResultsContainer=widgetContentContainer.querySelector(Selectors.regions.searchResults),searchInput=widgetContentContainer.querySelector(Selectors.actions.search);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),(0,_aria.comboBox)(searchInput)};_exports.registerListenerEvents=registerListenerEvents;const showLoader=async container=>{const{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/loading",{});Templates.replaceNodeContents(container,html,js)};_exports.showLoader=showLoader;const debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}}));
//# sourceMappingURL=basewidget.min.js.map
File diff suppressed because one or more lines are too long
+53 -71
View File
@@ -14,105 +14,87 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A small modal to search users or grade items within the gradebook.
* A widget to search users or grade items within the gradebook.
*
* @module core_grades/searchwidget/basewidget
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as ModalFactory from 'core/modal_factory';
import * as ModalEvents from 'core/modal_events';
import {debounce} from 'core/utils';
import * as Templates from 'core/templates';
import {comboBox} from 'core/aria';
import * as Selectors from 'core_grades/searchwidget/selectors';
import Notification from 'core/notification';
/**
* Build the base searching widget.
*
* @method init
* @param {Promise} bodyPromise The promise from the callee of the contents to place in the modal body.
* @param {HTMLElement} widgetContentContainer The selector for the widget container element.
* @param {Promise} bodyPromise The promise from the callee of the contents to place in the widget container.
* @param {Array} data An array of all the data generated by the callee.
* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
* @param {string} modalTitle The name of the search widget.
* @param {string|null} unsearchableContent The content rendered in a non-searchable area.
*/
export const init = (bodyPromise, data, searchFunc, modalTitle, unsearchableContent = null) => {
const modal = buildModal(bodyPromise, modalTitle);
registerListenerEvents(modal, data, searchFunc, unsearchableContent);
export const init = async(widgetContentContainer, bodyPromise, data, searchFunc, unsearchableContent = null) => {
bodyPromise.then(async(bodyContent) => {
// Render the body content.
widgetContentContainer.innerHTML = bodyContent;
// Render the unsearchable content if defined.
if (unsearchableContent) {
const unsearchableContentContainer = widgetContentContainer.querySelector(Selectors.regions.unsearchableContent);
unsearchableContentContainer.innerHTML += unsearchableContent;
}
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
// Display a loader until the search results are rendered.
await showLoader(searchResultsContainer);
// Render the search results.
await renderSearchResults(searchResultsContainer, data);
registerListenerEvents(widgetContentContainer, data, searchFunc);
}).catch(Notification.exception);
};
/**
* Register chooser related event listeners.
* Register the event listeners for the search widget.
*
* @method registerListenerEvents
* @param {Promise} modal Our modal that we are working with.
* @param {HTMLElement} widgetContentContainer The selector for the widget container element.
* @param {Array} data An array of all the data generated by the callee.
* @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.
* @param {string|null} unsearchableContent The content rendered in a non-searchable area.
*/
const registerListenerEvents = (modal, data, searchFunc, unsearchableContent) => {
modal.then(modal => {
// We want to destroy this when the dialog is closed.
modal.getRoot().on(ModalEvents.hidden, () => {
modal.destroy();
});
// Once the body of the modal has been resolved, add more features.
modal.getBodyPromise()
// The return value of getBodyPromise is a jquery object containing the body NodeElement.
.then(body => body[0])
.then(body => {
const searchInput = body.querySelector('input[data-action="search"]');
const searchResultsContainer = body.querySelector('[data-region="search-results-container-widget"]');
export const registerListenerEvents = (widgetContentContainer, data, searchFunc) => {
const searchResultsContainer = widgetContentContainer.querySelector(Selectors.regions.searchResults);
const searchInput = widgetContentContainer.querySelector(Selectors.actions.search);
renderSearchResults(searchResultsContainer, data);
if (unsearchableContent) {
const unsearchableContentContainer = body.querySelector(
'[data-region="unsearchable-content-container-widget"]');
unsearchableContentContainer.innerHTML += unsearchableContent;
}
// The search input is triggered.
searchInput.addEventListener('input', debounce(async() => {
// Display the search results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
}, 300));
// Trigger event handling for the results in line with aria guidelines.
comboBox(searchInput);
return body;
}).catch();
}).catch();
// The search input is triggered.
searchInput.addEventListener('input', debounce(async() => {
// Display the search results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
}, 300));
// Trigger event handling for the results in line with aria guidelines.
comboBox(searchInput);
};
/**
* Given an object we want to build a modal ready to show.
* Renders the loading placeholder for the search widget.
*
* @method buildModal
* @param {Promise} bodyPromise Body promise that the caller should resolve.
* @param {string} modalTitle The name of the search widget.
* @return {Object} The modal ready to display immediately and render body in later.
* @method showLoader
* @param {HTMLElement} container The DOM node where we'll render the loading placeholder.
*/
const buildModal = (bodyPromise, modalTitle) => {
return ModalFactory.create({
type: ModalFactory.types.DEFAULT,
title: modalTitle,
body: bodyPromise,
small: true,
scrollable: false,
templateContext: {
classes: 'reportdatasearch modal-sm'
}
}).then(modal => {
modal.show();
return modal;
});
export const showLoader = async(container) => {
const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/loading', {});
Templates.replaceNodeContents(container, html, js);
};
/**
@@ -133,7 +115,7 @@ const debounceCallee = (searchValue, data, searchFunction) => {
};
/**
* Given the output of the callers' search function, render out the results into the modal.
* Given the output of the callers' search function, render out the results into the search results container.
*
* @method renderSearchResults
* @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results.
@@ -149,14 +131,14 @@ const renderSearchResults = async(searchResultsContainer, searchResultsData) =>
};
/**
* We want to create the basic promises and hooks that the caller will implement, so we can build modals
* We want to create the basic promises and hooks that the caller will implement, so we can build the search widget
* ahead of time and allow the caller to resolve their promises once complete.
*
* @method promisesAndResolvers
* @returns {{bodyPromise: Promise, bodyPromiseResolver}}
*/
export const promisesAndResolvers = () => {
// We want to show the modal instantly but loading whilst waiting for our data.
// We want to show the widget instantly but loading whilst waiting for our data.
let bodyPromiseResolver;
const bodyPromise = new Promise(resolve => {
bodyPromiseResolver = resolve;
@@ -0,0 +1,27 @@
{{!
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 core_grades/searchwidget/loading
This template renders the loading placeholder for the search widget.
Example context (json):
{}
}}
<div class="d-flex w-100 h-100 justify-content-center align-items-center">
{{> core/loading }}
</div>
@@ -27,22 +27,20 @@
"fullname": "Cameron Greeve"
}
}}
<div class="w-100 result-row" role="row" id="result-row-{{id}}">
{{#name}}
<a role="option" id="result-item-{{id}}x{{id}}" href="{{url}}" class="d-flex px-2 py-1">
<span class="w-50 pull-left result-cell text-truncate text-body mr-2">
{{name}}
</span>
</a>
{{/name}}
{{^name}}
<a role="option" id="user-{{id}}" href="{{url}}" class="d-flex px-2 py-1">
<span class="w-50 pull-left result-cell text-truncate text-body mr-2">
{{fullname}}
</span>
<span class="w-50 pull-left result-cell text-truncate text-muted">
<span id="email-user-{{id}}" role="gridcell">{{email}}</span>
</span>
</a>
{{/name}}
</div>
{{#name}}
<a role="option" href="{{url}}" class="dropdown-item d-flex px-2 py-1">
<span class="pull-left result-cell text-truncate mr-2">
{{name}}
</span>
</a>
{{/name}}
{{^name}}
<a role="option" href="{{url}}" class="dropdown-item d-flex px-2 py-1 align-items-center">
<span class="w-50 pull-left text-truncate mr-2">
{{fullname}}
</span>
<span class="w-50 pull-left text-truncate small email">
{{email}}
</span>
</a>
{{/name}}
+41 -14
View File
@@ -530,21 +530,48 @@
}
}
.searchresultitemscontainer {
max-height: 150px;
overflow: auto;
.search-widget {
.dropdown-menu {
padding: 0.8rem 1.2rem;
a:hover {
background-color: $gray-100;
}
&.wide {
width: 350px;
}
.focused-cell {
outline-style: dotted;
outline-color: green;
&.narrow {
width: 250px;
}
.dropdown-item {
span {
&.email {
color: $text-muted;
}
}
}
.dropdown-item:hover,
.dropdown-item:active {
span {
color: #fff;
}
}
.searchresultscontainer {
height: 178px;
font-size: 90%;
.searchresultitemscontainer {
height: 178px;
max-height: 178px;
overflow: auto;
}
}
.unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px;
font-size: 90%;
}
}
}
.unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px;
}
+22 -12
View File
@@ -19747,18 +19747,28 @@ p.arrow_button {
.path-grade-report-singleview .reporttable .dropdown-toggle::after {
display: none; }
.searchresultitemscontainer {
max-height: 150px;
overflow: auto; }
.searchresultitemscontainer a:hover {
background-color: #f8f9fa; }
.searchresultitemscontainer .focused-cell {
outline-style: dotted;
outline-color: green; }
.unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px; }
.search-widget .dropdown-menu {
padding: 0.8rem 1.2rem; }
.search-widget .dropdown-menu.wide {
width: 350px; }
.search-widget .dropdown-menu.narrow {
width: 250px; }
.search-widget .dropdown-menu .dropdown-item span.email {
color: #6a737b; }
.search-widget .dropdown-menu .dropdown-item:hover span,
.search-widget .dropdown-menu .dropdown-item:active span {
color: #fff; }
.search-widget .dropdown-menu .searchresultscontainer {
height: 178px;
font-size: 90%; }
.search-widget .dropdown-menu .searchresultscontainer .searchresultitemscontainer {
height: 178px;
max-height: 178px;
overflow: auto; }
.search-widget .dropdown-menu .unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px;
font-size: 90%; }
.columns-autoflow-1to1to1 {
column-count: 3; }
+22 -12
View File
@@ -19747,18 +19747,28 @@ p.arrow_button {
.path-grade-report-singleview .reporttable .dropdown-toggle::after {
display: none; }
.searchresultitemscontainer {
max-height: 150px;
overflow: auto; }
.searchresultitemscontainer a:hover {
background-color: #f8f9fa; }
.searchresultitemscontainer .focused-cell {
outline-style: dotted;
outline-color: green; }
.unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px; }
.search-widget .dropdown-menu {
padding: 0.8rem 1.2rem; }
.search-widget .dropdown-menu.wide {
width: 350px; }
.search-widget .dropdown-menu.narrow {
width: 250px; }
.search-widget .dropdown-menu .dropdown-item span.email {
color: #6a737b; }
.search-widget .dropdown-menu .dropdown-item:hover span,
.search-widget .dropdown-menu .dropdown-item:active span {
color: #fff; }
.search-widget .dropdown-menu .searchresultscontainer {
height: 178px;
font-size: 90%; }
.search-widget .dropdown-menu .searchresultscontainer .searchresultitemscontainer {
height: 178px;
max-height: 178px;
overflow: auto; }
.search-widget .dropdown-menu .unsearchablecontentcontainer {
border-top: 1px solid #dee2e6;
padding-top: 10px;
font-size: 90%; }
.columns-autoflow-1to1to1 {
column-count: 3; }