Merge branch 'MDL-75837-master-4' of https://github.com/mihailges/moodle

This commit is contained in:
Jake Dallimore
2022-11-14 17:15:31 +08:00
55 changed files with 577 additions and 680 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_grades/searchwidget/selectors","core/notification"],(function(_exports,_utils,Templates,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.focus();const clearSearchButton=widgetContentContainer.querySelector(Selectors.actions.clearSearch);searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{searchInput.value.length>0?clearSearchButton.classList.remove("d-none"):clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),clearSearchButton.addEventListener("click",(async e=>{e.stopPropagation(),searchInput.value="",searchInput.focus(),clearSearchButton.classList.add("d-none"),await renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}))};_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
+3 -3
View File
@@ -1,10 +1,10 @@
define("core_grades/searchwidget/group",["exports","core/pending","core/templates","core/custom_interaction_events","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str"],(function(_exports,_pending,Templates,_custom_interaction_events,Repository,WidgetBase,_str){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
define("core_grades/searchwidget/group",["exports","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","jquery","core_grades/searchwidget/selectors"],(function(_exports,_pending,Templates,Repository,WidgetBase,_jquery,Selectors){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A small modal to search groups within the gradebook.
* A widget to search groups within the gradebook.
*
* @module core_grades/searchwidget/group
* @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.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase);let registered=!1;_exports.init=()=>{if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve(),registered=!0};const registerListenerEvents=()=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();events.forEach((event=>{document.addEventListener(event,(async e=>{const trigger=e.target.closest(".groupwidget");if(trigger){const courseID=trigger.dataset.courseid,actionBaseUrl=trigger.dataset.actionBaseUrl;e.preventDefault();const data=await Repository.groupFetch(courseID,actionBaseUrl).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;WidgetBase.init(bodyPromise,data.groups,searchGroups(),(0,_str.get_string)("selectagroup","core"))}}))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/group/groupsearch_body",[]))},searchGroups=()=>()=>(groups,searchTerm)=>{if(""===searchTerm)return groups;searchTerm=searchTerm.toLowerCase();const searchResults=[];return groups.forEach((group=>{group.name.toLowerCase().includes(searchTerm)&&searchResults.push(group)})),searchResults}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);let registered=!1;_exports.init=()=>{if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve(),registered=!0};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("group"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("group")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,actionBaseUrl=e.relatedTarget.dataset.actionBaseUrl;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.groupFetch(courseID,actionBaseUrl).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.groups,searchGroups())})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/group/groupsearch_body",[])),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("group")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchGroups=()=>()=>(groups,searchTerm)=>{if(""===searchTerm)return groups;searchTerm=searchTerm.toLowerCase();const searchResults=[];return groups.forEach((group=>{group.name.toLowerCase().includes(searchTerm)&&searchResults.push(group)})),searchResults}}));
//# sourceMappingURL=group.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("core_grades/searchwidget/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={regions:{searchResults:'[data-region="search-results-container-widget"]',unsearchableContent:'[data-region="unsearchable-content-container-widget"]'},actions:{search:'[data-action="search"]',clearSearch:'[data-action="clearsearch"]'},elements:{getSearchWidgetSelector:searchtype=>'.search-widget[data-searchtype="'.concat(searchtype,'"]'),getSearchWidgetDropdownSelector:searchtype=>'.search-widget[data-searchtype="'.concat(searchtype,'"] .dropdown-menu')}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../../src/searchwidget/selectors.js"],"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 * Selectors for the search widget.\n *\n * @module core_grades/searchwidget/selectors\n * @copyright 2022 Mihail Geshoski <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n regions: {\n searchResults: '[data-region=\"search-results-container-widget\"]',\n unsearchableContent: '[data-region=\"unsearchable-content-container-widget\"]',\n },\n actions: {\n search: '[data-action=\"search\"]',\n clearSearch: '[data-action=\"clearsearch\"]',\n },\n elements: {\n getSearchWidgetSelector: searchtype => `.search-widget[data-searchtype=\"${searchtype}\"]`,\n getSearchWidgetDropdownSelector: searchtype => `.search-widget[data-searchtype=\"${searchtype}\"] .dropdown-menu`,\n },\n};\n"],"names":["regions","searchResults","unsearchableContent","actions","search","clearSearch","elements","getSearchWidgetSelector","searchtype","getSearchWidgetDropdownSelector"],"mappings":"oLAuBe,CACXA,QAAS,CACLC,cAAe,kDACfC,oBAAqB,yDAEzBC,QAAS,CACLC,OAAQ,yBACRC,YAAa,+BAEjBC,SAAU,CACNC,wBAAyBC,sDAAiDA,iBAC1EC,gCAAiCD,sDAAiDA"}
+77 -70
View File
@@ -14,105 +14,112 @@
// 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);
// We want to focus on the first known user interable element within the dropdown.
searchInput.focus();
const clearSearchButton = widgetContentContainer.querySelector(Selectors.actions.clearSearch);
renderSearchResults(searchResultsContainer, data);
// The search input is triggered.
searchInput.addEventListener('input', debounce(async() => {
// If search query is present display the 'clear search' button, otherwise hide it.
if (searchInput.value.length > 0) {
clearSearchButton.classList.remove('d-none');
} else {
clearSearchButton.classList.add('d-none');
}
// Display the search results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
}, 300));
if (unsearchableContent) {
const unsearchableContentContainer = body.querySelector(
'[data-region="unsearchable-content-container-widget"]');
unsearchableContentContainer.innerHTML += unsearchableContent;
}
// Clear search is triggered.
clearSearchButton.addEventListener('click', async(e) => {
e.stopPropagation();
// Clear the entered search query in the search bar.
searchInput.value = "";
searchInput.focus();
clearSearchButton.classList.add('d-none');
// 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();
// Display all results.
await renderSearchResults(
searchResultsContainer,
debounceCallee(
searchInput.value,
data,
searchFunc()
)
);
});
};
/**
* 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 +140,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 +156,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;
+36 -39
View File
@@ -14,7 +14,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A small modal to search groups within the gradebook.
* A widget to search groups within the gradebook.
*
* @module core_grades/searchwidget/group
* @copyright 2022 Mathew May <mathew.solutions>
@@ -23,10 +23,10 @@
import Pending from 'core/pending';
import * as Templates from 'core/templates';
import CustomEvents from "core/custom_interaction_events";
import * as Repository from 'core_grades/searchwidget/repository';
import * as WidgetBase from 'core_grades/searchwidget/basewidget';
import {get_string as getString} from 'core/str';
import $ from 'jquery';
import * as Selectors from 'core_grades/searchwidget/selectors';
/**
* Whether the event listener has already been registered for this module.
@@ -58,51 +58,48 @@ export const init = () => {
* @method registerListenerEvents
*/
const registerListenerEvents = () => {
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();
const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('group'));
// Register events.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
const trigger = e.target.closest('.groupwidget');
if (trigger) {
const courseID = trigger.dataset.courseid;
const actionBaseUrl = trigger.dataset.actionBaseUrl;
e.preventDefault();
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
$(Selectors.elements.getSearchWidgetSelector('group')).on('show.bs.dropdown', async(e) => {
const courseID = e.relatedTarget.dataset.courseid;
const actionBaseUrl = e.relatedTarget.dataset.actionBaseUrl;
// Display a loading icon in the dropdown menu container until the body promise is resolved.
await WidgetBase.showLoader(dropdownMenuContainer);
// If an error occurs while fetching the data, display the error within the modal.
const data = await Repository.groupFetch(courseID, actionBaseUrl).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
WidgetBase.init(
bodyPromise,
data.groups,
searchGroups(),
getString('selectagroup', 'core')
);
}
// If an error occurs while fetching the data, display the error within the dropdown menu.
const data = await Repository.groupFetch(courseID, actionBaseUrl).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
await WidgetBase.init(
dropdownMenuContainer,
bodyPromise,
data.groups,
searchGroups(),
);
});
// Resolvers for passed functions in the modal creation.
// Resolvers for passed functions in the dropdown creation.
bodyPromiseResolver(Templates.render(
'core_grades/searchwidget/group/groupsearch_body',
[]
));
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
$(Selectors.elements.getSearchWidgetSelector('group')).on('hide.bs.dropdown', () => {
// Reset the state once the groups menu dropdown is closed.
dropdownMenuContainer.innerHTML = '';
});
};
/**
+37
View File
@@ -0,0 +1,37 @@
// 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/>.
/**
* Selectors for the search widget.
*
* @module core_grades/searchwidget/selectors
* @copyright 2022 Mihail Geshoski <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
regions: {
searchResults: '[data-region="search-results-container-widget"]',
unsearchableContent: '[data-region="unsearchable-content-container-widget"]',
},
actions: {
search: '[data-action="search"]',
clearSearch: '[data-action="clearsearch"]',
},
elements: {
getSearchWidgetSelector: searchtype => `.search-widget[data-searchtype="${searchtype}"]`,
getSearchWidgetDropdownSelector: searchtype => `.search-widget[data-searchtype="${searchtype}"] .dropdown-menu`,
},
};
@@ -105,6 +105,7 @@ class get_enrolled_users_for_search_widget extends external_api {
$userpicture->size = 1;
$user->profileimage = $userpicture->get_url($PAGE)->out(false);
$user->email = $guiuser->email;
$user->active = false; // @TODO MDL-76246
$users[] = $user;
}
@@ -151,6 +152,7 @@ class get_enrolled_users_for_search_widget extends external_api {
core_user::get_property_type('email'),
'An email address - allow email as root@localhost',
VALUE_OPTIONAL),
'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED)
];
return new external_single_structure($userfields);
}
@@ -116,8 +116,10 @@ class get_groups_for_search_widget extends external_api {
'group' => $group->id
]);
return (object) [
'id' => $group->id,
'name' => $group->name,
'url' => $url->out(false),
'active' => false // @TODO MDL-76246
];
}, $groupsmenu);
}
@@ -147,8 +149,10 @@ class get_groups_for_search_widget extends external_api {
*/
public static function group_description(): external_description {
$groupfields = [
'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED),
'url' => new external_value(PARAM_URL, 'The link that applies the group action', VALUE_REQUIRED),
'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED),
'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED)
];
return new external_single_structure($groupfields);
}
+3 -3
View File
@@ -1,10 +1,10 @@
define("gradereport_singleview/grade",["exports","core/pending","core/templates","core/custom_interaction_events","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str"],(function(_exports,_pending,Templates,_custom_interaction_events,Repository,WidgetBase,_str){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
define("gradereport_singleview/grade",["exports","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","jquery","core_grades/searchwidget/selectors"],(function(_exports,_pending,Templates,Repository,WidgetBase,_jquery,Selectors){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A small modal to search users within the gradebook.
* A small modal to search grade items within the gradebook.
*
* @module gradereport_singleview
* @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.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();events.forEach((event=>{document.addEventListener(event,(async e=>{const trigger=e.target.closest(".gradewidget");if(trigger){const courseID=trigger.dataset.courseid;e.preventDefault();const data=await Repository.gradeitemFetch(courseID).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[]||void 0===data)return;WidgetBase.init(bodyPromise,data.gradeitems,searchGradeitems(),(0,_str.get_string)("selectagrade","gradereport_singleview"))}}))})),bodyPromiseResolver(Templates.render("gradereport_singleview/gradesearch_body",[]))},searchGradeitems=()=>()=>(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((module=>{module.name.toLowerCase().includes(searchTerm)&&searchResults.push(module)})),searchResults}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("grade"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("grade")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.gradeitemFetch(courseID).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&(await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.gradeitems,searchGradeitems()),bodyPromiseResolver(Templates.render("gradereport_singleview/gradesearch_body",[])))})),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("grade")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchGradeitems=()=>()=>(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((module=>{module.name.toLowerCase().includes(searchTerm)&&searchResults.push(module)})),searchResults}}));
//# sourceMappingURL=grade.min.js.map
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,10 +1,10 @@
define("gradereport_singleview/user",["exports","core/pending","core/templates","core/custom_interaction_events","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str","core/url"],(function(_exports,_pending,Templates,_custom_interaction_events,Repository,WidgetBase,_str,_url){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
define("gradereport_singleview/user",["exports","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/url","jquery","core_grades/searchwidget/selectors"],(function(_exports,_pending,Templates,Repository,WidgetBase,_url,_jquery,Selectors){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A small modal to search users within the gradebook.
*
* @module gradereport_singleview/user
* @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.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();events.forEach((event=>{document.addEventListener(event,(async e=>{const trigger=e.target.closest(".userwidget");if(trigger){const courseID=trigger.dataset.courseid,groupId=trigger.dataset.groupid;e.preventDefault();const actionBaseUrl=_url.default.relativeUrl("/grade/report/singleview/index.php",{item:"user"},!1),data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;WidgetBase.init(bodyPromise,data.users,searchUsers(),(0,_str.get_string)("selectauser","grades"))}}))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",[]))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("user"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,groupId=e.relatedTarget.dataset.groupid,actionBaseUrl=_url.default.relativeUrl("/grade/report/singleview/index.php",{item:"user"},!1);await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));data!==[]&&(await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.users,searchUsers()),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",[])))})),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}}));
//# sourceMappingURL=user.min.js.map
File diff suppressed because one or more lines are too long
+38 -42
View File
@@ -14,7 +14,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A small modal to search users within the gradebook.
* A small modal to search grade items within the gradebook.
*
* @module gradereport_singleview
* @copyright 2022 Mathew May <mathew.solutions>
@@ -23,10 +23,10 @@
import Pending from 'core/pending';
import * as Templates from 'core/templates';
import CustomEvents from "core/custom_interaction_events";
import * as Repository from 'core_grades/searchwidget/repository';
import * as WidgetBase from 'core_grades/searchwidget/basewidget';
import {get_string as getString} from 'core/str';
import $ from 'jquery';
import * as Selectors from 'core_grades/searchwidget/selectors';
/**
* Our entry point into starting to build the search widget.
@@ -46,50 +46,46 @@ export const init = () => {
* @method registerListenerEvents
*/
const registerListenerEvents = () => {
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();
const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('grade'));
// Register events.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
const trigger = e.target.closest('.gradewidget');
if (trigger) {
const courseID = trigger.dataset.courseid;
e.preventDefault();
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
$(Selectors.elements.getSearchWidgetSelector('grade')).on('show.bs.dropdown', async(e) => {
const courseID = e.relatedTarget.dataset.courseid;
// Display a loading icon in the dropdown menu container until the body promise is resolved.
await WidgetBase.showLoader(dropdownMenuContainer);
// If an error occurs while fetching the data, display the error within the modal.
const data = await Repository.gradeitemFetch(courseID).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === [] || data === undefined) {
return;
}
WidgetBase.init(
bodyPromise,
data.gradeitems,
searchGradeitems(),
getString('selectagrade', 'gradereport_singleview')
);
}
// If an error occurs while fetching the data, display the error within the modal.
const data = await Repository.gradeitemFetch(courseID).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
await WidgetBase.init(
dropdownMenuContainer,
bodyPromise,
data.gradeitems,
searchGradeitems()
);
// Resolvers for passed functions in the modal creation.
bodyPromiseResolver(Templates.render('gradereport_singleview/gradesearch_body', []));
});
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
$(Selectors.elements.getSearchWidgetSelector('grade')).on('hide.bs.dropdown', () => {
// Reset the state once the grade item menu dropdown is closed.
dropdownMenuContainer.innerHTML = '';
});
// Resolvers for passed functions in the modal creation.
bodyPromiseResolver(Templates.render(
'gradereport_singleview/gradesearch_body',
[]
));
};
/**
+39 -43
View File
@@ -23,11 +23,11 @@
import Pending from 'core/pending';
import * as Templates from 'core/templates';
import CustomEvents from "core/custom_interaction_events";
import * as Repository from 'core_grades/searchwidget/repository';
import * as WidgetBase from 'core_grades/searchwidget/basewidget';
import {get_string as getString} from 'core/str';
import Url from 'core/url';
import $ from 'jquery';
import * as Selectors from 'core_grades/searchwidget/selectors';
/**
* Our entry point into starting to build the search widget.
@@ -47,52 +47,48 @@ export const init = () => {
* @method registerListenerEvents
*/
const registerListenerEvents = () => {
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();
const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user'));
// Register events.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
const trigger = e.target.closest('.userwidget');
if (trigger) {
const courseID = trigger.dataset.courseid;
const groupId = trigger.dataset.groupid;
e.preventDefault();
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
$(Selectors.elements.getSearchWidgetSelector('user')).on('show.bs.dropdown', async(e) => {
const courseID = e.relatedTarget.dataset.courseid;
const groupId = e.relatedTarget.dataset.groupid;
const actionBaseUrl = Url.relativeUrl('/grade/report/singleview/index.php', {item: 'user'}, false);
// Display a loading icon in the dropdown menu container until the body promise is resolved.
await WidgetBase.showLoader(dropdownMenuContainer);
const actionBaseUrl = Url.relativeUrl('/grade/report/singleview/index.php', {item: 'user'}, false);
// If an error occurs while fetching the data, display the error within the modal.
const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
WidgetBase.init(
bodyPromise,
data.users,
searchUsers(),
getString('selectauser', 'grades')
);
}
// If an error occurs while fetching the data, display the error within the dropdown menu.
const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
await WidgetBase.init(
dropdownMenuContainer,
bodyPromise,
data.users,
searchUsers()
);
// Resolvers for passed functions in the dropdown menu creation.
bodyPromiseResolver(Templates.render('core_grades/searchwidget/user/usersearch_body', []));
});
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
$(Selectors.elements.getSearchWidgetSelector('user')).on('hide.bs.dropdown', () => {
// Reset the state once the user menu dropdown is closed.
dropdownMenuContainer.innerHTML = '';
});
// Resolvers for passed functions in the modal creation.
bodyPromiseResolver(Templates.render(
'core_grades/searchwidget/user/usersearch_body',
[]
));
};
/**
+4 -2
View File
@@ -85,7 +85,7 @@ class singleview extends core_course_external {
$gradeitems = array_map(function ($gradeitem) use ($PAGE, $USER, $params) {
$item = new \stdClass();
$item->gid = $gradeitem->id;
$item->id = $gradeitem->id;
$url = new moodle_url('/grade/report/singleview/index.php', [
'id' => $params['courseid'],
'itemid' => $gradeitem->id,
@@ -94,6 +94,7 @@ class singleview extends core_course_external {
);
$item->name = $gradeitem->get_name(true);
$item->url = $url->out(false);
$item->active = false; // @TODO MDL-76246
return $item;
}, $gradeableitems);
@@ -114,13 +115,14 @@ class singleview extends core_course_external {
return new external_single_structure([
'gradeitems' => new external_multiple_structure(
new external_single_structure([
'gid' => new external_value(PARAM_INT, 'ID of the grade item', VALUE_OPTIONAL),
'id' => new external_value(PARAM_INT, 'ID of the grade item', VALUE_OPTIONAL),
'url' => new external_value(
PARAM_URL,
'The link to the grade report',
VALUE_OPTIONAL
),
'name' => new external_value(PARAM_TEXT, 'The full name of the grade item', VALUE_OPTIONAL),
'active' => new external_value(PARAM_BOOL, 'Are we currently on this item?', VALUE_REQUIRED)
])
),
'warnings' => new external_warnings(),
+1 -1
View File
@@ -61,7 +61,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
}
$this->page->requires->js_call_amd('gradereport_singleview/user', 'init');
return $this->render_from_template('gradereport_singleview/user_selector', $data);
return $this->render_from_template('core_grades/user_selector', $data);
}
/**
+1 -1
View File
@@ -147,7 +147,7 @@
overflow: auto;
}
.path-grade-report-singleview .user-selector .userinitials {
.path-grade-report-singleview .search-widget[data-searchtype="user"] .userinitials {
width: 40px;
height: 40px;
}
@@ -29,8 +29,8 @@
}
}
}}
<div class="grade-selector d-flex">
<div role="button" class="gradewidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-searchtype="grade" tabindex="0" aria-label="{{#str}} selectagrade, gradereport_singleview {{/str}}">
<div class="search-widget dropdown d-flex" data-searchtype="grade">
<button aria-expanded="false" data-toggle="dropdown" class="btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" aria-label="{{#str}} selectagrade, gradereport_singleview {{/str}}">
<div class="align-items-center d-flex">
{{#selectedoption}}
<div class="d-block pr-3">
@@ -48,5 +48,7 @@
</div>
{{/selectedoption}}
</div>
</button>
<div class="dropdown-menu wide">
</div>
</div>
@@ -1,71 +0,0 @@
{{!
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 gradereport_singleview/user_selector
The user selector trigger element.
Context variables required for this template:
* courseid - The course ID.
* groupid - The group ID.
* selectedoption - (optional) Object containing information about the selected option.
* image - The image corresponding to the selected option.
* text - The text of the selected option.
* additionaltext - (optional) Additional text displayed below the selected option (e.g. the user email)
Example context (json):
{
"courseid": "2",
"groupid": "2",
"selectedoption": {
"image": "<img src=\"http://example.com/pluginfile.php/14/user/icon/boost/f2\" class=\"userpicture\" width=\"40\" alt=\"\">",
"text": "John Doe",
"additionaltext": "[email protected]"
}
}
}}
<div class="user-selector d-flex">
<div role="button" class="userwidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-groupid="{{groupid}}" data-searchtype="user" tabindex="0" aria-label="{{#str}} selctauser, core {{/str}}">
<div class="align-items-center d-flex">
{{#selectedoption}}
<div class="selected-option-img d-block pr-2">
{{#image}}
{{{image}}}
{{/image}}
{{^image}}
<span class="userinitials"></span>
{{/image}}
</div>
<div class="selected-option-info d-block pr-3">
<span class="selected-option-text p-0 font-weight-bold">
{{text}}
</span>
{{#additionaltext}}
<span class="d-block small">
{{additionaltext}}
</span>
{{/additionaltext}}
</div>
{{/selectedoption}}
{{^selectedoption}}
<div class="d-block pr-2">
<span class="userinitials"></span>
</div>
<div class="user-info d-block pr-3">
{{#str}} selctauser, core {{/str}}
</div>
{{/selectedoption}}
</div>
</div>
</div>
@@ -27,7 +27,7 @@
}
}}
<div class="zero-state mx-auto w-50 text-center my-6">
<img src="{{imglink}}" alt="{{#str}}pluginname, gradereport_singleview{{/str}}" role="presentation" class="my-5">
<img src="{{imglink}}" alt="{{#str}}pluginname, gradereport_singleview{{/str}}" aria-hidden="true" class="my-5">
<h3>{{#str}}pluginname, gradereport_singleview{{/str}}</h3>
<p>{{#str}}viewsingleuserorgradeitem, gradereport_singleview{{/str}}</p>
<div class="justify-content-center d-flex">
@@ -25,7 +25,7 @@
}
}}
<div class="zero-state mx-auto w-50 text-center my-6">
<img src="{{imglink}}" alt="{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}" role="presentation" class="my-5">
<img src="{{imglink}}" alt="{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}" aria-hidden="true" class="my-5">
<h3>{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}</h3>
<p>{{#str}}singleviewdescription, gradereport_singleview{{/str}}</p>
</div>
@@ -25,7 +25,7 @@
}
}}
<div class="zero-state mx-auto w-50 text-center my-6">
<img src="{{imglink}}" alt="{{#str}}viewsingleuser, gradereport_singleview{{/str}}" role="presentation" class="my-5">
<img src="{{imglink}}" alt="{{#str}}viewsingleuser, gradereport_singleview{{/str}}" aria-hidden="true" class="my-5">
<h3>{{#str}}viewsingleuser, gradereport_singleview{{/str}}</h3>
<p>{{#str}}singleviewdescription, gradereport_singleview{{/str}}</p>
</div>
@@ -22,10 +22,10 @@ Feature: Given we have opted to search for a grade item, Lets find and search th
Scenario: A teacher can search for and find a grade item to view
Given I navigate to "View > Single view" in the course gradebook
And I click on "Grade items" "link" in the ".page-toggler" "css_element"
When I click on ".gradewidget" "css_element"
Then I confirm "Test assignment one" in "Select a grade item" search within the gradebook widget exists
And I confirm "Test assignment two" in "Select a grade item" search within the gradebook widget exists
When I click on ".search-widget[data-searchtype='grade']" "css_element"
Then I confirm "Test assignment one" in "grade" search within the gradebook widget exists
And I confirm "Test assignment two" in "grade" search within the gradebook widget exists
And I set the field "searchinput" to "two"
And I wait "1" seconds
And I confirm "Test assignment two" in "Select a grade item" search within the gradebook widget exists
And I confirm "Test assignment one" in "Select a grade item" search within the gradebook widget does not exist
And I confirm "Test assignment two" in "grade" search within the gradebook widget exists
And I confirm "Test assignment one" in "grade" search within the gradebook widget does not exist
@@ -21,9 +21,10 @@ Feature: Given we land on the index page, select what type of report we wish to
Scenario: I switch between the two report types within singleview
Given I navigate to "View > Single view" in the course gradebook
And I click on "Grade items" "link" in the ".page-toggler" "css_element"
When I click on ".gradewidget" "css_element"
Then I wait until "Select a grade item" "dialogue" exists
And I click on "Close" "button" in the "Select a grade item" "dialogue"
And I click on "Users" "link" in the ".page-toggler" "css_element"
And I click on ".userwidget" "css_element"
And I wait until "Select a user" "dialogue" exists
And ".search-widget[data-searchtype='user']" "css_element" should not exist
And ".search-widget[data-searchtype='grade']" "css_element" should exist
And I confirm "assign" in "grade" search within the gradebook widget exists
When I click on "Users" "link" in the ".page-toggler" "css_element"
Then ".search-widget[data-searchtype='grade']" "css_element" should not exist
And ".search-widget[data-searchtype='user']" "css_element" should exist
And I confirm "student1" in "user" search within the gradebook widget exists
@@ -59,7 +59,7 @@ Feature: We can use Single view
Scenario: I can update grades, add feedback and exclude grades.
Given I navigate to "View > Single view" in the course gradebook
And I click on "Users" "link"
And I click on "Users" "link" in the ".page-toggler" "css_element"
And I click on "Student" in the "user" search widget
And I turn editing mode on
And I set the field "Override for Test assignment one" to "1"
@@ -99,7 +99,7 @@ Feature: We can use Single view
And I log in as "teacher2"
And I am on "Course 1" course homepage
Given I navigate to "View > Single view" in the course gradebook
And I click on "Users" "link"
And I click on "Users" "link" in the ".page-toggler" "css_element"
And I click on "Student" in the "user" search widget
And I turn editing mode on
And the "Exclude for Test assignment one" "checkbox" should be disabled
@@ -23,12 +23,12 @@ Feature: Within the singleview report, a teacher can search for users.
Given I navigate to "View > Single view" in the course gradebook
When I click on "Users" "link" in the ".page-toggler" "css_element"
And I wait until the page is ready
And I click on ".userwidget" "css_element"
Then I confirm "Student 1" in "Select a user" search within the gradebook widget exists
And I confirm "Student 2" in "Select a user" search within the gradebook widget exists
And I confirm "Student 32" in "Select a user" search within the gradebook widget exists
And I click on ".search-widget[data-searchtype='user']" "css_element"
Then I confirm "Student 1" in "user" search within the gradebook widget exists
And I confirm "Student 2" in "user" search within the gradebook widget exists
And I confirm "Student 32" in "user" search within the gradebook widget exists
And I set the field "searchinput" to "2"
And I wait "1" seconds
And I confirm "Student 2" in "Select a user" search within the gradebook widget exists
And I confirm "Student 32" in "Select a user" search within the gradebook widget exists
And I confirm "Student 1" in "Select a user" search within the gradebook widget does not exist
And I confirm "Student 2" in "user" search within the gradebook widget exists
And I confirm "Student 32" in "user" search within the gradebook widget exists
And I confirm "Student 1" in "user" search within the gradebook widget does not exist
+3 -3
View File
@@ -1,10 +1,10 @@
define("gradereport_user/user",["exports","core/pending","core/templates","core/custom_interaction_events","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str","core/url"],(function(_exports,_pending,Templates,_custom_interaction_events,Repository,WidgetBase,_str,_url){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
define("gradereport_user/user",["exports","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str","core/url","jquery","core_grades/searchwidget/selectors"],(function(_exports,_pending,Templates,Repository,WidgetBase,_str,_url,_jquery,Selectors){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A small modal to search users within the gradebook.
* A widget to search users within the gradebook.
*
* @module gradereport_user/user
* @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.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();events.forEach((event=>{document.addEventListener(event,(async e=>{const trigger=e.target.closest(".userwidget");if(trigger){const courseID=trigger.dataset.courseid,groupId=trigger.dataset.groupid;e.preventDefault();const actionBaseUrl=_url.default.relativeUrl("/grade/report/user/index.php",{},!1),data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("core_grades/searchwidget/searchitem",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});WidgetBase.init(bodyPromise,data.users,searchUsers(),(0,_str.get_string)("selectauser","grades"),allUsersOption)}}))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("user"));(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,groupId=e.relatedTarget.dataset.groupid,actionBaseUrl=_url.default.relativeUrl("/grade/report/user/index.php",{},!1);await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.userFetch(courseID,actionBaseUrl,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("gradereport_user/all_users_item",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.users,searchUsers(),allUsersOption),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0}))})),(0,_jquery.default)(Selectors.elements.getSearchWidgetSelector("user")).on("hide.bs.dropdown",(()=>{dropdownMenuContainer.innerHTML=""}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}}));
//# sourceMappingURL=user.min.js.map
File diff suppressed because one or more lines are too long
+51 -53
View File
@@ -14,7 +14,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A small modal to search users within the gradebook.
* A widget to search users within the gradebook.
*
* @module gradereport_user/user
* @copyright 2022 Mathew May <mathew.solutions>
@@ -23,11 +23,12 @@
import Pending from 'core/pending';
import * as Templates from 'core/templates';
import CustomEvents from "core/custom_interaction_events";
import * as Repository from 'core_grades/searchwidget/repository';
import * as WidgetBase from 'core_grades/searchwidget/basewidget';
import {get_string as getString} from 'core/str';
import Url from 'core/url';
import $ from 'jquery';
import * as Selectors from 'core_grades/searchwidget/selectors';
/**
* Our entry point into starting to build the search widget.
@@ -47,62 +48,59 @@ export const init = () => {
* @method registerListenerEvents
*/
const registerListenerEvents = () => {
const events = [
'click',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
CustomEvents.define(document, events);
let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();
const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user'));
// Register events.
events.forEach((event) => {
document.addEventListener(event, async(e) => {
const trigger = e.target.closest('.userwidget');
if (trigger) {
const courseID = trigger.dataset.courseid;
const groupId = trigger.dataset.groupid;
e.preventDefault();
// Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
$(Selectors.elements.getSearchWidgetSelector('user')).on('show.bs.dropdown', async(e) => {
const courseID = e.relatedTarget.dataset.courseid;
const groupId = e.relatedTarget.dataset.groupid;
const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false);
// Display a loading icon in the dropdown menu container until the body promise is resolved.
await WidgetBase.showLoader(dropdownMenuContainer);
const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false);
// If an error occurs while fetching the data, display the error within the modal.
const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
// The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.
const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);
const allUsersOption = await Templates.render('core_grades/searchwidget/searchitem', {
id: 0,
name: allUsersOptionName,
url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),
});
WidgetBase.init(
bodyPromise,
data.users,
searchUsers(),
getString('selectauser', 'grades'),
allUsersOption
);
}
// If an error occurs while fetching the data, display the error within the dropdown menu.
const data = await Repository.userFetch(courseID, actionBaseUrl, groupId).catch(async(e) => {
const errorTemplateData = {
'errormessage': e.message
};
bodyPromiseResolver(
await Templates.render('core_grades/searchwidget/error', errorTemplateData)
);
});
// Early return if there is no module data.
if (data === []) {
return;
}
// The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.
const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);
const allUsersOption = await Templates.render('gradereport_user/all_users_item', {
id: 0,
name: allUsersOptionName,
url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),
});
await WidgetBase.init(
dropdownMenuContainer,
bodyPromise,
data.users,
searchUsers(),
allUsersOption
);
// Resolvers for passed functions in the dropdown menu creation.
bodyPromiseResolver(Templates.render(
'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}
));
});
// Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
$(Selectors.elements.getSearchWidgetSelector('user')).on('hide.bs.dropdown', () => {
// Reset the state once the user menu dropdown is closed.
dropdownMenuContainer.innerHTML = '';
});
// Resolvers for passed functions in the modal creation.
bodyPromiseResolver(Templates.render(
'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}
));
};
/**
@@ -30,7 +30,7 @@ $string['gotonextreport'] = 'Go to next user report';
$string['gotopreviousreport'] = 'Go to previous user report';
$string['pluginname'] = 'User report';
$string['user:view'] = 'View your own grade report';
$string['userreportdesc'] = 'View a user\'s grades, any feedback and the course total.';
$string['userreports'] = 'User reports';
$string['privacy:metadata:preference:gradereport_user_view_user'] = 'Whether to view report as current user or another user in the gradebook reports';
$string['selectuser'] = 'Select a user to view their grades';
$string['selectuserinstructions'] = 'By selecting a user you can view the grades by activity';
$string['tablesummary'] = 'The table is arranged as a list of graded items including categories of graded items. When items are in a category they will be indicated as such.';
+1 -1
View File
@@ -127,7 +127,7 @@ class gradereport_user_renderer extends plugin_renderer_base {
}
$this->page->requires->js_call_amd('gradereport_user/user', 'init');
return $this->render_from_template('gradereport_user/user_selector', $data);
return $this->render_from_template('core_grades/user_selector', $data);
}
/**
+1 -1
View File
@@ -148,7 +148,7 @@
align-items: center;
}
.path-grade-report-user .user-selector .userinitials {
.path-grade-report-user .search-widget[data-searchtype="user"] .userinitials {
width: 40px;
height: 40px;
}
@@ -0,0 +1,33 @@
{{!
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 gradereport_user/all_users_item
Search result line items.
Example context (json):
{
"id": "0",
"name": "All users(10)",
"url": "http://foo.bar/gradereport/?userid=0&id=2"
}
}}
<a role="menuitem" id="item-{{id}}" href="{{url}}" class="dropdown-item d-flex px-2 py-1" tabindex="0">
<span class="pull-left result-cell text-truncate mr-2">
{{name}}
</span>
</a>
@@ -25,7 +25,7 @@
}
}}
<div class="zero-state mx-auto w-50 text-center my-6">
<img src="{{imglink}}" alt="{{#str}}selectuser, gradereport_user{{/str}}" role="presentation" class="my-5">
<h3>{{#str}}selectuser, gradereport_user{{/str}}</h3>
<p>{{#str}}selectuserinstructions, gradereport_user{{/str}}</p>
<img src="{{imglink}}" alt="{{#str}}userreports, gradereport_user{{/str}}" aria-hidden="true" class="my-5">
<h3>{{#str}}userreports, gradereport_user{{/str}}</h3>
<p>{{#str}}userreportdesc, gradereport_user{{/str}}</p>
</div>
@@ -28,50 +28,38 @@ Feature: Group searching functionality within the user report.
Scenario: A teacher can see the 'group' search widget only when group mode is enabled in the course
Given I navigate to "View > User report" in the course gradebook
And ".groupwidget" "css_element" should exist
And ".search-widget[data-searchtype='group']" "css_element" should exist
And I am on "Course 1" course homepage
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| id_groupmode | No groups |
And I press "Save and display"
When I navigate to "View > User report" in the course gradebook
Then ".groupwidget" "css_element" should not exist
Then ".search-widget[data-searchtype='group']" "css_element" should not exist
Scenario: A teacher can search for and find a group to find a user in
Given I navigate to "View > User report" in the course gradebook
And I click on ".groupwidget" "css_element"
And I wait until "Select a group" "dialogue" exists
And I should see "Tutor group" in the "Select a group" "dialogue"
And I should see "Marker group" in the "Select a group" "dialogue"
And I click on ".search-widget[data-searchtype='group']" "css_element"
And I confirm "Tutor group" in "group" search within the gradebook widget exists
And I confirm "Marker group" in "group" search within the gradebook widget exists
When I set the field "searchinput" to "tutor"
And I wait "1" seconds
Then I should see "Tutor group" in the "Select a group" "dialogue"
And I should not see "Marker group" in the "Select a group" "dialogue"
Then I confirm "Tutor group" in "group" search within the gradebook widget exists
And I confirm "Marker group" in "group" search within the gradebook widget does not exist
Scenario: A teacher can only see the group members in the 'user' search widget after selecting a group option
Given I navigate to "View > User report" in the course gradebook
# Confirm that all users are initially displayed in the 'user' search widget.
And I click on ".userwidget" "css_element"
And I wait until "Select a user" "dialogue" exists
And I should see "Student 1" in the "Select a user" "dialogue"
And I should see "Student 2" in the "Select a user" "dialogue"
And I click on "Close" "button" in the "Select a user" "dialogue"
And I confirm "Student 1" in "user" search within the gradebook widget exists
And I confirm "Student 2" in "user" search within the gradebook widget exists
# Select a particular group from the 'group' search widget.
When I click on "Default group" in the "group" search widget
# Confirm that only users which are members of the selected group are displayed in the 'user' search widget.
And I click on ".userwidget" "css_element"
And I wait until "Select a user" "dialogue" exists
Then I should see "Student 1" in the "Select a user" "dialogue"
And I should not see "Student 2" in the "Select a user" "dialogue"
And I click on "Close" "button" in the "Select a user" "dialogue"
And I confirm "Student 1" in "user" search within the gradebook widget exists
And I confirm "Student 2" in "user" search within the gradebook widget does not exist
And I click on "Tutor group" in the "group" search widget
And I click on ".userwidget" "css_element"
And I wait until "Select a user" "dialogue" exists
And I should not see "Student 1" in the "Select a user" "dialogue"
And I should not see "Student 2" in the "Select a user" "dialogue"
And I click on "Close" "button" in the "Select a user" "dialogue"
And I confirm "Student 1" in "user" search within the gradebook widget does not exist
And I confirm "Student 2" in "user" search within the gradebook widget does not exist
And I click on "All participants" in the "group" search widget
And I click on ".userwidget" "css_element"
And I wait until "Select a user" "dialogue" exists
And I should see "Student 1" in the "Select a user" "dialogue"
And I should see "Student 2" in the "Select a user" "dialogue"
And I confirm "Student 1" in "user" search within the gradebook widget exists
And I confirm "Student 2" in "user" search within the gradebook widget exists
@@ -21,12 +21,12 @@ Feature: Within the User report, a teacher can search for users.
Scenario: A teacher can search for and find a user to view
Given I navigate to "View > User report" in the course gradebook
When I click on ".userwidget" "css_element"
Then I confirm "Student 1" in "Select a user" search within the gradebook widget exists
And I confirm "Student 2" in "Select a user" search within the gradebook widget exists
And I confirm "Student 32" in "Select a user" search within the gradebook widget exists
When I click on ".search-widget[data-searchtype='user']" "css_element"
Then I confirm "Student 1" in "user" search within the gradebook widget exists
And I confirm "Student 2" in "user" search within the gradebook widget exists
And I confirm "Student 32" in "user" search within the gradebook widget exists
And I set the field "searchinput" to "2"
And I wait "1" seconds
And I confirm "Student 2" in "Select a user" search within the gradebook widget exists
And I confirm "Student 32" in "Select a user" search within the gradebook widget exists
And I confirm "Student 1" in "Select a user" search within the gradebook widget does not exist
And I confirm "Student 2" in "user" search within the gradebook widget exists
And I confirm "Student 32" in "user" search within the gradebook widget exists
And I confirm "Student 1" in "user" search within the gradebook widget does not exist
+4 -2
View File
@@ -33,8 +33,8 @@
"selectedgroup": "Group 1"
}
}}
<div class="group-selector d-flex">
<div role="button" class="groupwidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-action-base-url="{{groupactionbaseurl}}" data-searchtype="group" tabindex="0">
<div class="search-widget d-flex dropdown" data-searchtype="group">
<button aria-expanded="false" data-toggle="dropdown" class="btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-action-base-url="{{groupactionbaseurl}}">
<div class="align-items-center d-flex">
<div class="d-block pr-3">
<span class="d-block small">
@@ -45,5 +45,7 @@
</span>
</div>
</div>
</button>
<div class="dropdown-menu narrow">
</div>
</div>
@@ -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>
@@ -24,25 +24,26 @@
"id": "1",
"name": "Quiz 1",
"url": "http://foo.bar/gradereport/?userid=25",
"fullname": "Cameron Greeve"
"fullname": "Cameron Greeve",
"active": true
}
}}
<div class="w-100 result-row" role="row" id="result-row-{{id}}">
<li class="w-100 result-row" role="none" 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">
<a role="menuitem" id="item-{{id}}" href="{{url}}" class="dropdown-item d-flex px-2 py-1" tabindex="-1"">
<span class="pull-left result-cell text-truncate 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">
<a role="menuitem" id="user-{{id}}" href="{{url}}" class="dropdown-item d-flex px-2 py-1 align-items-center" tabindex="-1">
<span class="w-50 pull-left text-truncate 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 class="w-50 pull-left text-truncate small email">
{{email}}
</span>
</a>
{{/name}}
</div>
</li>
@@ -34,13 +34,13 @@
]
}
}}
<div class="searchresultitemscontainer-wrapper">
<div class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2" role="listbox" data-region="search-result-items-container">
<div class="searchresultitemscontainer-wrapper dropdown">
<ul class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2 list-group" role="menu" data-region="search-result-items-container" tabindex="0">
{{#searchresults}}
{{>core_grades/searchwidget/searchitem}}
{{/searchresults}}
{{^searchresults}}
<p>{{#str}} resultsfound, core, 0 {{/str}}</p>
{{/searchresults}}
</div>
</ul>
</div>
@@ -32,5 +32,5 @@
<div class="searchresultscontainer" data-region="search-results-container-widget" aria-live="polite"></div>
{{#displayunsearchablecontent}}
<div class="unsearchablecontentcontainer" data-region="unsearchable-content-container-widget" aria-live="polite"></div>
<div class="unsearchablecontentcontainer" data-region="unsearchable-content-container-widget" role="menu" aria-live="polite"></div>
{{/displayunsearchablecontent}}
@@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template gradereport_user/user_selector
@template core_grades/user_selector
The user selector trigger element.
@@ -38,8 +38,8 @@
}
}
}}
<div class="user-selector d-flex">
<div role="button" class="userwidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-groupid="{{groupid}}" data-searchtype="user" tabindex="0" aria-label="{{#str}} selectauser, core_grades {{/str}}">
<div class="search-widget dropdown d-flex" data-searchtype="user">
<button aria-expanded="false" data-toggle="dropdown" class="btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-groupid="{{groupid}}" aria-label="{{#str}} selectauser, core_grades {{/str}}">
<div class="align-items-center d-flex">
{{#selectedoption}}
<div class="selected-option-img d-block pr-2">
@@ -70,5 +70,7 @@
</div>
{{/selectedoption}}
</div>
</button>
<div class="dropdown-menu wide">
</div>
</div>
+37 -29
View File
@@ -426,38 +426,61 @@ class behat_grade extends behat_base {
* Confirm if a value is within the search widget within the gradebook.
*
* Examples:
* - I confirm "User1" in "User" search within the gradebook widget exists
* - I confirm "User" in "user" search within the gradebook widget exists
* - I confirm "Group" in "group" search within the gradebook widget exists
* - I confirm "Grade item" in "grade" search within the gradebook widget exists
*
* @Given /^I confirm "(?P<needle>(?:[^"]|\\")*)" in "(?P<haystack>(?:[^"]|\\")*)" search within the gradebook widget exists$/
* @param string $needle The value to search for.
* @param string $haystack The selector to use within the zero state.
* @param string $haystack The type of the search widget.
*/
public function i_confirm_in_search_within_the_gradebook_widget_exists($needle, $haystack) {
$this->execute("behat_general::wait_until_exists", [$haystack, "dialogue"]);
$this->execute("behat_general::assert_element_contains_text", [$needle, $haystack, "dialogue"]);
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
// Make sure that the dropdown menu is visible.
$node = $this->find("css_element", "{$triggercssselector} .dropdown-menu");
if (!$node->isVisible()) {
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
}
$this->execute("behat_general::wait_until_the_page_is_ready");
$this->execute("behat_general::assert_element_contains_text",
[$needle, "{$triggercssselector} .dropdown-menu", "css_element"]);
}
/**
* Confirm if a value is not within the search widget within the gradebook.
*
* Examples:
* - I confirm "User1" in "User" search within the gradebook widget does not exist
* - I confirm "User" in "user" search within the gradebook widget does not exist
* - I confirm "Group" in "group" search within the gradebook widget does not exist
* - I confirm "Grade item" in "grade" search within the gradebook widget does not exist
*
* @Given /^I confirm "(?P<needle>(?:[^"]|\\")*)" in "(?P<haystack>(?:[^"]|\\")*)" search within the gradebook widget does not exist$/
* @param string $needle The value to search for.
* @param string $haystack The selector to use within the zero state.
* @param string $haystack The type of the search widget.
*/
public function i_confirm_in_search_within_the_gradebook_widget_does_not_exist($needle, $haystack) {
$this->execute("behat_general::wait_until_exists", [$haystack, "dialogue"]);
$this->execute("behat_general::assert_element_not_contains_text", [$needle, $haystack, "dialogue"]);
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
// Make sure that the dropdown menu is visible.
$node = $this->find("css_element", "{$triggercssselector} .dropdown-menu");
if (!$node->isVisible()) {
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
}
$this->execute("behat_general::wait_until_the_page_is_ready");
$this->execute("behat_general::assert_element_not_contains_text",
[$needle, "{$triggercssselector} .dropdown-menu", "css_element"]);
}
/**
* Clicks on an option from the specified search widget in the current gradebook page.
*
* Examples:
* - I click on "Student1" in the "user" search widget
* - I click on "Group1" in the "group" search widget
* - I click on "Student" in the "user" search widget
* - I click on "Group" in the "group" search widget
* - I click on "Grade item" in the "grade" search widget
*
* @Given /^I click on "(?P<needle>(?:[^"]|\\")*)" in the "(?P<haystack>(?:[^"]|\\")*)" search widget$/
* @param string $needle The value to search for.
@@ -466,26 +489,11 @@ class behat_grade extends behat_base {
public function i_click_on_in_search_widget(string $needle, string $haystack) {
$this->execute("behat_general::wait_until_the_page_is_ready");
$triggercssselector = '';
$dialoguetitle = '';
switch ($haystack) {
case 'user':
$triggercssselector = '.userwidget';
$dialoguetitle = 'Select a user';
break;
case 'grade':
$triggercssselector = '.gradewidget';
$dialoguetitle = 'Select a grade item';
break;
case 'group':
$triggercssselector = '.groupwidget';
$dialoguetitle = 'Select a group';
break;
}
$triggercssselector = ".search-widget[data-searchtype='{$haystack}']";
$this->execute("behat_general::i_click_on", [$triggercssselector, "css_element"]);
$this->execute("behat_general::wait_until_exists", [$dialoguetitle, "dialogue"]);
$this->execute('behat_general::i_click_on_in_the', [$needle, "link", $dialoguetitle, 'dialogue']);
$this->execute("behat_general::wait_until_the_page_is_ready");
$this->execute('behat_general::i_click_on_in_the',
[$needle, "link", "{$triggercssselector} .dropdown-menu", "css_element"]);
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
define("core/aria",["exports","./local/aria/aria-hidden","./local/aria/aria-combobox"],(function(_exports,_ariaHidden,_ariaCombobox){Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"comboBox",{enumerable:!0,get:function(){return _ariaCombobox.comboBox}}),Object.defineProperty(_exports,"hide",{enumerable:!0,get:function(){return _ariaHidden.hide}}),Object.defineProperty(_exports,"hideSiblings",{enumerable:!0,get:function(){return _ariaHidden.hideSiblings}}),Object.defineProperty(_exports,"unhide",{enumerable:!0,get:function(){return _ariaHidden.unhide}}),Object.defineProperty(_exports,"unhideSiblings",{enumerable:!0,get:function(){return _ariaHidden.unhideSiblings}})}));
define("core/aria",["exports","./local/aria/aria-hidden"],(function(_exports,_ariaHidden){Object.defineProperty(_exports,"__esModule",{value:!0}),Object.defineProperty(_exports,"hide",{enumerable:!0,get:function(){return _ariaHidden.hide}}),Object.defineProperty(_exports,"hideSiblings",{enumerable:!0,get:function(){return _ariaHidden.hideSiblings}}),Object.defineProperty(_exports,"unhide",{enumerable:!0,get:function(){return _ariaHidden.unhide}}),Object.defineProperty(_exports,"unhideSiblings",{enumerable:!0,get:function(){return _ariaHidden.unhideSiblings}})}));
//# sourceMappingURL=aria.min.js.map
-3
View File
@@ -1,3 +0,0 @@
define("core/local/aria/aria-combobox",["exports","core/key_codes"],(function(_exports,_key_codes){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.comboBox=void 0;_exports.comboBox=comboInput=>{registerEventListeners(comboInput)};const registerEventListeners=comboInput=>{document.addEventListener("keydown",(e=>{if(e.target===comboInput){let next=null;const comboResultArea=document.querySelector('[data-region="search-result-items-container"]'),resultRows=Array.from(comboResultArea.querySelectorAll('[role="row"]')),resultCells=Array.from(comboResultArea.querySelectorAll('[role="gridcell"]')),activeResultRow=comboResultArea.querySelector('.active[role="row"]'),activeResultCell=comboResultArea.querySelector('.focused-cell[role="gridcell"]');switch(e.keyCode){case _key_codes.arrowUp:if(null===activeResultRow)next=setFirstActiveRow(next,resultRows,comboInput,resultRows.length-1);else for(let i=0;i<resultRows.length;i++)if(resultRows[i].id===activeResultRow.id){next=resultRows[i-1];break}break;case _key_codes.arrowDown:if(null===activeResultRow)next=setFirstActiveRow(next,resultRows,comboInput,0);else for(let i=0;i<resultRows.length-1;i++)if(resultRows[i].id===activeResultRow.id){next=resultRows[i+1];break}break;case _key_codes.home:next=resultRows[0];break;case _key_codes.end:next=resultRows[resultRows.length-1];break;case _key_codes.enter||_key_codes.space:window.location=activeResultCell.href;break;case _key_codes.arrowLeft:if(null===activeResultRow)next=setFirstActiveRow(next,resultRows,comboInput,0);else for(let i=0;i<resultCells.length;i++)if(resultCells[i].id===activeResultCell.id){if(void 0===resultCells[i-1]){resultCells[i].classList.remove("focused-cell"),resultCells[resultCells.length-1].classList.add("focused-cell");break}resultCells[i].classList.remove("focused-cell"),resultCells[i-1].classList.add("focused-cell");break}break;case _key_codes.arrowRight:if(null===activeResultRow)next=setFirstActiveRow(next,resultRows,comboInput,0);else for(let i=0;i<resultCells.length-1;i++){if(resultCells[i].id===activeResultCell.id){resultCells[i].classList.remove("focused-cell"),resultCells[i+1].classList.add("focused-cell");break}if(void 0===resultCells[i+2]){resultCells[i+1].classList.remove("focused-cell"),resultCells[0].classList.add("focused-cell");break}}break;default:window.console.log("nothing to see here!")}nextHandler(next,e,activeResultRow,comboInput)}}))},setFirstActiveRow=(next,resultRows,comboInput,val)=>((next=resultRows[val]).setAttribute("aria-selected","true"),next.classList.add("active"),comboInput.setAttribute("aria-activedescendant",next.id),next.querySelector(".result-cell").classList.add("focused-cell"),next),nextHandler=(next,e,activeResultRow,comboInput)=>{next&&(e.preventDefault(),null!==activeResultRow&&(activeResultRow.classList.remove("active"),activeResultRow.querySelector(".result-cell").classList.remove("focused-cell")),next.classList.add("active"),next.querySelector(".result-cell").classList.add("focused-cell"),comboInput.setAttribute("aria-activedescendant",next.id))}}));
//# sourceMappingURL=aria-combobox.min.js.map
File diff suppressed because one or more lines are too long
-3
View File
@@ -27,6 +27,3 @@ export {
hideSiblings,
unhideSiblings,
} from './local/aria/aria-hidden';
export {
comboBox
} from './local/aria/aria-combobox';
-180
View File
@@ -1,180 +0,0 @@
// 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/>.
import {end, arrowLeft, arrowRight, arrowUp, arrowDown, home, enter, space} from 'core/key_codes';
/**
* ARIA helpers related to the combobox role.
*
* @module core/local/aria/aria-combobox.
* @copyright 2022 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Our entry point into adding accessibility handling for comboboxes.
*
* @param {Element} comboInput The combobox area to add aria handling to.
*/
export const comboBox = (comboInput) => {
registerEventListeners(comboInput);
};
/**
* Event management of the provided combobox.
*
* @param {Element} comboInput The combobox area to add aria handling to.
*/
const registerEventListeners = (comboInput) => {
document.addEventListener('keydown', (e) => {
if (e.target === comboInput) {
let next = null;
const comboResultArea = document.querySelector('[data-region="search-result-items-container"]');
const resultRows = Array.from(comboResultArea.querySelectorAll('[role="row"]'));
const resultCells = Array.from(comboResultArea.querySelectorAll('[role="gridcell"]'));
const activeResultRow = comboResultArea.querySelector('.active[role="row"]');
const activeResultCell = comboResultArea.querySelector('.focused-cell[role="gridcell"]');
switch (e.keyCode) {
case arrowUp: {
// TODO: Handle the wrapping.
if (activeResultRow === null) {
next = setFirstActiveRow(next, resultRows, comboInput, resultRows.length - 1);
} else {
for (let i = 0; i < resultRows.length; i++) {
if (resultRows[i].id === activeResultRow.id) {
next = resultRows[i - 1];
break;
}
}
}
break;
}
case arrowDown: {
if (activeResultRow === null) {
next = setFirstActiveRow(next, resultRows, comboInput, 0);
} else {
for (let i = 0; i < resultRows.length - 1; i++) {
if (resultRows[i].id === activeResultRow.id) {
next = resultRows[i + 1];
break;
}
}
}
break;
}
case home: {
next = resultRows[0];
break;
}
case end: {
next = resultRows[resultRows.length - 1];
break;
}
case enter || space: {
// Redirect the user to the appropriate link.
// TODO: Space does not work, special handler on the cell itself?
window.location = activeResultCell.href;
break;
}
case arrowLeft: {
if (activeResultRow === null) {
next = setFirstActiveRow(next, resultRows, comboInput, 0);
} else {
for (let i = 0; i < resultCells.length; i++) {
if (resultCells[i].id === activeResultCell.id) {
if (resultCells[i - 1] === undefined) {
resultCells[i].classList.remove('focused-cell');
resultCells[resultCells.length - 1].classList.add('focused-cell');
break;
} else {
resultCells[i].classList.remove('focused-cell');
resultCells[i - 1].classList.add('focused-cell');
break;
}
}
}
}
break;
}
case arrowRight: {
if (activeResultRow === null) {
next = setFirstActiveRow(next, resultRows, comboInput, 0);
} else {
for (let i = 0; i < resultCells.length - 1; i++) {
if (resultCells[i].id === activeResultCell.id) {
resultCells[i].classList.remove('focused-cell');
resultCells[i + 1].classList.add('focused-cell');
break;
}
if (resultCells[i + 2] === undefined) {
resultCells[i + 1].classList.remove('focused-cell');
resultCells[0].classList.add('focused-cell');
break;
}
}
}
break;
}
default: {
window.console.log('nothing to see here!');
break;
}
}
// Variable next is set if we do want to act on the keypress.
nextHandler(next, e, activeResultRow, comboInput);
}
});
};
/**
* With search, we can't automatically set aria elements in the results field, so we do it here.
*
* @param {Element} next
* @param {Array} resultRows
* @param {Element} comboInput
* @param {Number} val
* @returns {Element}
*/
const setFirstActiveRow = (next, resultRows, comboInput, val) => {
// Set first option as active.
next = resultRows[val];
next.setAttribute('aria-selected', 'true');
next.classList.add('active');
comboInput.setAttribute('aria-activedescendant', next.id);
next.querySelector('.result-cell').classList.add('focused-cell');
return next;
};
/**
* Given we have a value to next set active, handle some of the basic handling.
*
* @param {Element} next
* @param {Event} e
* @param {Element} activeResultRow
* @param {Element} comboInput
*/
const nextHandler = (next, e, activeResultRow, comboInput) => {
if (next) {
e.preventDefault();
if (activeResultRow !== null) {
activeResultRow.classList.remove('active');
activeResultRow.querySelector('.result-cell').classList.remove('focused-cell');
}
next.classList.add('active');
// Find whatever the first result cell is to add the class.
next.querySelector('.result-cell').classList.add('focused-cell');
comboInput.setAttribute('aria-activedescendant', next.id);
}
};
+1
View File
@@ -43,6 +43,7 @@
</label>
<input
type="text"
role="searchbox"
data-region="input"
data-action="search"
id="searchinput"
+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; }