mirror of
https://github.com/moodle/moodle.git
synced 2025-08-09 02:46:40 +02:00
Merge branch 'MDL-78885-main' of https://github.com/rezaies/moodle
This commit is contained in:
commit
281fecbd54
72 changed files with 723 additions and 817 deletions
2
user/amd/build/comboboxsearch/user.min.js
vendored
2
user/amd/build/comboboxsearch/user.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -24,7 +24,6 @@ import search_combobox from 'core/comboboxsearch/search_combobox';
|
|||
import {getStrings} from 'core/str';
|
||||
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
||||
import $ from 'jquery';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
export default class UserSearch extends search_combobox {
|
||||
|
||||
|
@ -36,14 +35,19 @@ export default class UserSearch extends search_combobox {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
// Register a small click event onto the document since we need to check if they are clicking off the component.
|
||||
document.addEventListener('click', (e) => {
|
||||
// Since we are handling dropdowns manually, ensure we can close it when clicking off.
|
||||
if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
// Register a couple of events onto the document since we need to check if they are moving off the component.
|
||||
['click', 'focus'].forEach(eventType => {
|
||||
// Since we are handling dropdowns manually, ensure we can close it when moving off.
|
||||
document.addEventListener(eventType, e => {
|
||||
if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
// Register keyboard events.
|
||||
this.component.addEventListener('keydown', this.keyHandler.bind(this));
|
||||
|
||||
// Define our standard lookups.
|
||||
this.selectors = {...this.selectors,
|
||||
courseid: '[data-region="courseid"]',
|
||||
|
@ -51,9 +55,12 @@ export default class UserSearch extends search_combobox {
|
|||
resetPageButton: '[data-action="resetpage"]',
|
||||
};
|
||||
|
||||
const component = document.querySelector(this.componentSelector());
|
||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||
this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||
this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;
|
||||
this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;
|
||||
|
||||
// We need to render some content by default for ARIA purposes.
|
||||
this.renderDefault();
|
||||
}
|
||||
|
||||
static init() {
|
||||
|
@ -78,15 +85,6 @@ export default class UserSearch extends search_combobox {
|
|||
return '.usersearchdropdown';
|
||||
}
|
||||
|
||||
/**
|
||||
* The triggering div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
triggerSelector() {
|
||||
return '.usersearchwidget';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node.
|
||||
*/
|
||||
|
@ -94,11 +92,24 @@ export default class UserSearch extends search_combobox {
|
|||
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
||||
users: this.getMatchedResults().slice(0, 5),
|
||||
hasresults: this.getMatchedResults().length > 0,
|
||||
instance: this.instance,
|
||||
matches: this.getMatchedResults().length,
|
||||
searchterm: this.getSearchTerm(),
|
||||
selectall: this.selectAllResultsLink(),
|
||||
});
|
||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
||||
// Remove aria-activedescendant when the available options change.
|
||||
this.searchInput.removeAttribute('aria-activedescendant');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node by default we want our form to exist.
|
||||
*/
|
||||
async renderDefault() {
|
||||
this.setMatchedResults(await this.filterDataset(await this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
|
||||
await this.renderDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,13 +128,17 @@ export default class UserSearch extends search_combobox {
|
|||
* @returns {Array} The users that match the given criteria.
|
||||
*/
|
||||
async filterDataset(filterableData) {
|
||||
const stringMap = await this.getStringMap();
|
||||
return filterableData.filter((user) => Object.keys(user).some((key) => {
|
||||
if (user[key] === "" || user[key] === null || !stringMap.get(key)) {
|
||||
return false;
|
||||
}
|
||||
return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
}));
|
||||
if (this.getPreppedSearchTerm()) {
|
||||
const stringMap = await this.getStringMap();
|
||||
return filterableData.filter((user) => Object.keys(user).some((key) => {
|
||||
if (user[key] === "" || user[key] === null || !stringMap.get(key)) {
|
||||
return false;
|
||||
}
|
||||
return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,7 +173,6 @@ export default class UserSearch extends search_combobox {
|
|||
);
|
||||
|
||||
user.matchingField = `${escapedMatchingField} (${user.email})`;
|
||||
user.link = this.selectOneLink(user.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -168,22 +182,17 @@ export default class UserSearch extends search_combobox {
|
|||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user interacts with the component.
|
||||
* The handler for when a user changes the value of the component (selects an option from the dropdown).
|
||||
*
|
||||
* @param {MouseEvent} e The triggering event that we are working with.
|
||||
* @param {Event} e The change event.
|
||||
*/
|
||||
clickHandler(e) {
|
||||
super.clickHandler(e).catch(Notification.exception);
|
||||
if (e.target.closest(this.selectors.component)) {
|
||||
// Forcibly prevent BS events so that we can control the open and close.
|
||||
// Really needed because by default input elements cant trigger a dropdown.
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {
|
||||
changeHandler(e) {
|
||||
this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
|
||||
|
||||
if (e.target.value === '0') {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
if (e.target.closest(this.selectors.resetPageButton)) {
|
||||
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
||||
} else {
|
||||
window.location = this.selectOneLink(e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,50 +202,30 @@ export default class UserSearch extends search_combobox {
|
|||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
keyHandler(e) {
|
||||
super.keyHandler(e);
|
||||
|
||||
if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
|
||||
// Switch the key presses to handle keyboard nav.
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
if (
|
||||
this.getSearchTerm() !== ''
|
||||
&& !this.searchDropdown.classList.contains('show')
|
||||
&& e.target.contains(this.combobox)
|
||||
) {
|
||||
this.renderAndShow();
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (document.activeElement === this.getHTMLElements().searchInput) {
|
||||
if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {
|
||||
window.location = this.selectAllResultsLink();
|
||||
}
|
||||
}
|
||||
if (document.activeElement === this.getHTMLElements().clearSearchButton) {
|
||||
this.closeSearch(true);
|
||||
break;
|
||||
}
|
||||
if (e.target.closest(this.selectors.resetPageButton)) {
|
||||
e.stopPropagation();
|
||||
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
||||
break;
|
||||
}
|
||||
if (e.target.closest('.dropdown-item')) {
|
||||
e.preventDefault();
|
||||
window.location = e.target.closest('.dropdown-item').href;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.toggleDropdown();
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
break;
|
||||
case 'Tab':
|
||||
// If the current focus is on clear search, then check if viewall exists then around tab to it.
|
||||
if (e.target.closest(this.selectors.clearSearch)) {
|
||||
if (this.currentViewAll && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.currentViewAll.focus({preventScroll: true});
|
||||
} else {
|
||||
this.closeSearch();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,11 +238,18 @@ export default class UserSearch extends search_combobox {
|
|||
if (on) {
|
||||
this.searchDropdown.classList.add('show');
|
||||
$(this.searchDropdown).show();
|
||||
this.component.setAttribute('aria-expanded', 'true');
|
||||
this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');
|
||||
this.searchInput.focus({preventScroll: true});
|
||||
} else {
|
||||
this.searchDropdown.classList.remove('show');
|
||||
$(this.searchDropdown).hide();
|
||||
this.component.setAttribute('aria-expanded', 'false');
|
||||
|
||||
// As we are manually handling the dropdown, we need to do some housekeeping manually.
|
||||
this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false');
|
||||
this.searchInput.removeAttribute('aria-activedescendant');
|
||||
this.searchDropdown.querySelectorAll('.active[role="option"]').forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,6 +262,7 @@ export default class UserSearch extends search_combobox {
|
|||
|
||||
/**
|
||||
* Build up the view all link that is dedicated to a particular result.
|
||||
* We will call this function when a user interacts with the combobox to redirect them to show their results in the page.
|
||||
*
|
||||
* @param {Number} userID The ID of the user selected.
|
||||
*/
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
* profileimageurl - Link for the users' large profile image.
|
||||
* matchingField - The field in the user object that matched the search criteria.
|
||||
* matchingFieldName - The name of the field that was matched upon for A11y purposes.
|
||||
* link - The link used to redirect upon self to show only this specific user.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
|
@ -30,12 +29,12 @@
|
|||
"fullname": "Foo bar",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
"matchingFieldName": "Fullname"
|
||||
}
|
||||
}}
|
||||
{{<core/local/comboboxsearch/resultitem }}
|
||||
{{$arialabel}}{{#str}}viewresults, core, {{fullname}}{{/str}}{{/arialabel}}
|
||||
{{$arialabel}}{{fullname}}{{/arialabel}}
|
||||
{{$shorttext}}{{fullname}}{{/shorttext}}
|
||||
{{$content}}
|
||||
<span class="d-block px-2 w-25">
|
||||
{{#profileimageurl}}
|
||||
|
|
|
@ -17,35 +17,35 @@
|
|||
Wrapping template for returned result items.
|
||||
|
||||
Context variables required for this template:
|
||||
* instance - The instance ID of the combo box.
|
||||
* users - Our returned users to render.
|
||||
* found - Count of the found users.
|
||||
* total - Total count of users within this report.
|
||||
* selectall - The created link that allows users to select all of the results.
|
||||
* selectall - Whether to show the select all option.
|
||||
* searchterm - The entered text to find these results.
|
||||
* hasusers - Allow the handling where no users exist for the returned search term.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"instance": 25,
|
||||
"users": [
|
||||
{
|
||||
"id": 2,
|
||||
"fullname": "Foo bar",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||
"matchingFieldName": "Fullname"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"fullname": "Bar Foo",
|
||||
"profileimageurl": "http://foo.bar/pluginfile.php/80/user/icon/boost/f1?rev=7631",
|
||||
"matchingField": "Bar <span class=\"font-weight-bold\">Foo</span>",
|
||||
"matchingFieldName": "Fullname",
|
||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
||||
"matchingFieldName": "Fullname"
|
||||
}
|
||||
],
|
||||
"matches": 20,
|
||||
"selectall": "https://foo.bar/grade/report/grader/index.php?id=2&searchvalue=abe",
|
||||
"selectall": true,
|
||||
"searchterm": "Foo",
|
||||
"hasresults": true
|
||||
}
|
||||
|
@ -59,10 +59,15 @@
|
|||
{{/results}}
|
||||
{{$selectall}}
|
||||
{{#selectall}}
|
||||
<li class="w-100 result-row p-1 border-top bottom-0 position-sticky" role="none" id="result-row-{{id}}">
|
||||
<a role="option" class="dropdown-item d-flex small p-3" id="select-all" href="{{{selectall}}}" tabindex="-1">
|
||||
{{#str}}viewallresults, core, {{matches}}{{/str}}
|
||||
</a>
|
||||
<li
|
||||
id="result-row-{{instance}}-0"
|
||||
class="w-100 p-1 border-top bottom-0 position-sticky dropdown-item d-flex small p-3"
|
||||
role="option"
|
||||
{{! The data-short-text attribute is provided so that aria.js would use it rather than the whole content. }}
|
||||
data-short-text="{{searchterm}}"
|
||||
data-value="0"
|
||||
>
|
||||
{{#str}}viewallresults, core, {{matches}}{{/str}}
|
||||
</li>
|
||||
{{/selectall}}
|
||||
{{/selectall}}
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
The user selector trigger element.
|
||||
|
||||
Context variables required for this template:
|
||||
* name - The name of the input element representing the user search combobox.
|
||||
* value - The value of the input element representing the user search combobox.
|
||||
* currentvalue - If the user has already searched, set the value to that.
|
||||
* courseid - The course ID.
|
||||
* group - The group ID.
|
||||
|
@ -27,41 +29,33 @@
|
|||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "input-1",
|
||||
"value": "0",
|
||||
"currentvalue": "bar",
|
||||
"courseid": 2,
|
||||
"group": 25,
|
||||
"resetlink": "grade/report/grader/index.php?id=2"
|
||||
}
|
||||
}}
|
||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}" aria-hidden="true"></span>
|
||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}"></span>
|
||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}"></span>
|
||||
<span class="d-none" data-region="instance" data-instance="{{instance}}"></span>
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}searchusers, core{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}searchusers, core{{/str}}{{/placeholder}}
|
||||
{{$value}}{{currentvalue}}{{/value}}
|
||||
{{$additionalattributes}}
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-controls="user-{{instance}}-result-listbox"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
data-input-element="user-input-{{uniqid}}-{{instance}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{#currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/label}}
|
||||
{{$value}}{{currentvalue}}{{/value}}
|
||||
{{$additionalattributes}}
|
||||
aria-autocomplete="list"
|
||||
data-input-element="user-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
<a class="ml-2 btn btn-link border-0 align-self-center" href="{{resetlink}}" data-action="resetpage" role="link" aria-label="{{#str}}clearsearch, core{{/str}}">
|
||||
{{#str}}clear{{/str}}
|
||||
</a>
|
||||
{{/currentvalue}}
|
||||
{{^currentvalue}}
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchusers, core
|
||||
{{/str}}{{/placeholder}}
|
||||
{{$additionalattributes}}
|
||||
aria-autocomplete="list"
|
||||
data-input-element="user-input-{{uniqid}}"
|
||||
{{/additionalattributes}}
|
||||
{{/ core/search_input_auto }}
|
||||
{{/currentvalue}}
|
||||
<input type="hidden" name="search" id="user-input-{{uniqid}}"/>
|
||||
<input type="hidden" name="{{name}}" value="{{value}}" id="user-input-{{uniqid}}-{{instance}}"/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue