mirror of
https://github.com/moodle/moodle.git
synced 2025-08-08 10:26: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
grade/amd/build/comboboxsearch/grade.min.js
vendored
2
grade/amd/build/comboboxsearch/grade.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
|
@ -40,6 +40,38 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
};
|
};
|
||||||
const component = document.querySelector(this.componentSelector());
|
const component = document.querySelector(this.componentSelector());
|
||||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||||
|
this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;
|
||||||
|
|
||||||
|
const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);
|
||||||
|
searchValueElement.addEventListener('change', () => {
|
||||||
|
this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
|
||||||
|
|
||||||
|
const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);
|
||||||
|
if (valueElement.value !== searchValueElement.value) {
|
||||||
|
valueElement.value = searchValueElement.value;
|
||||||
|
valueElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchValueElement.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$component.on('hide.bs.dropdown', () => {
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
|
|
||||||
|
const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role="listbox"]`);
|
||||||
|
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
|
||||||
|
option.classList.remove('active');
|
||||||
|
});
|
||||||
|
listbox.scrollTop = 0;
|
||||||
|
|
||||||
|
// Use setTimeout to make sure the following code is executed after the click event is handled.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.searchInput.value !== '') {
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.renderDefault();
|
this.renderDefault();
|
||||||
}
|
}
|
||||||
|
@ -66,25 +98,19 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
return '.gradesearchdropdown';
|
return '.gradesearchdropdown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The triggering div that contains the searching widget.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
triggerSelector() {
|
|
||||||
return '.gradesearchwidget';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the content then replace the node.
|
* Build the content then replace the node.
|
||||||
*/
|
*/
|
||||||
async renderDropdown() {
|
async renderDropdown() {
|
||||||
const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {
|
const {html, js} = await renderForPromise('core/local/comboboxsearch/resultset', {
|
||||||
|
instance: this.instance,
|
||||||
results: this.getMatchedResults(),
|
results: this.getMatchedResults(),
|
||||||
hasresults: this.getMatchedResults().length > 0,
|
hasresults: this.getMatchedResults().length > 0,
|
||||||
searchterm: this.getSearchTerm(),
|
searchterm: this.getSearchTerm(),
|
||||||
});
|
});
|
||||||
replaceNodeContents(this.selectors.placeholder, html, js);
|
replaceNodeContents(this.selectors.placeholder, html, js);
|
||||||
|
// Remove aria-activedescendant when the available options change.
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,11 +124,6 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
|
|
||||||
this.updateNodes();
|
this.updateNodes();
|
||||||
this.registerInputEvents();
|
this.registerInputEvents();
|
||||||
|
|
||||||
// Add a small BS listener so that we can set the focus correctly on open.
|
|
||||||
this.$component.on('shown.bs.dropdown', () => {
|
|
||||||
this.searchInput.focus({preventScroll: true});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,7 +163,6 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
return {
|
return {
|
||||||
id: grade.id,
|
id: grade.id,
|
||||||
name: grade.name,
|
name: grade.name,
|
||||||
link: this.selectOneLink(grade.id),
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -174,46 +194,25 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
* @param {MouseEvent} e The triggering event that we are working with.
|
* @param {MouseEvent} e The triggering event that we are working with.
|
||||||
*/
|
*/
|
||||||
async clickHandler(e) {
|
async clickHandler(e) {
|
||||||
if (e.target.closest(this.selectors.dropdown)) {
|
if (e.target.closest(this.selectors.clearSearch)) {
|
||||||
// Forcibly prevent BS events so that we can control the open and close.
|
e.stopPropagation();
|
||||||
// Really needed because by default input elements cant trigger a dropdown.
|
// Clear the entered search query in the search bar.
|
||||||
e.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
this.clearSearchButton.addEventListener('click', async() => {
|
|
||||||
this.searchInput.value = '';
|
this.searchInput.value = '';
|
||||||
this.setSearchTerms(this.searchInput.value);
|
this.setSearchTerms(this.searchInput.value);
|
||||||
|
this.searchInput.focus();
|
||||||
|
this.clearSearchButton.classList.add('d-none');
|
||||||
|
// Display results.
|
||||||
await this.filterrenderpipe();
|
await this.filterrenderpipe();
|
||||||
});
|
|
||||||
// Prevent normal key presses activating this.
|
|
||||||
if (e.target.closest('.dropdown-item') && e.button === 0) {
|
|
||||||
window.location = e.target.closest('.dropdown-item').href;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handler for when a user presses a key within the component.
|
* The handler for when a user changes the value of the component (selects an option from the dropdown).
|
||||||
*
|
*
|
||||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
* @param {Event} e The change event.
|
||||||
*/
|
*/
|
||||||
keyHandler(e) {
|
changeHandler(e) {
|
||||||
super.keyHandler(e);
|
window.location = this.selectOneLink(e.target.value);
|
||||||
// Switch the key presses to handle keyboard nav.
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Tab':
|
|
||||||
if (e.target.closest(this.selectors.input)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.clearSearchButton.focus({preventScroll: true});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
if (document.activeElement.getAttribute('role') === 'option') {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.searchInput.focus({preventScroll: true});
|
|
||||||
} else if (e.target.closest(this.selectors.input)) {
|
|
||||||
const trigger = this.component.querySelector(this.selectors.trigger);
|
|
||||||
trigger.focus({preventScroll: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,6 +235,7 @@ export default class GradeItemSearch extends search_combobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* 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} gradeID The ID of the grade item selected.
|
* @param {Number} gradeID The ID of the grade item selected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -45,12 +45,18 @@ class core_grades_renderer extends plugin_renderer_base {
|
||||||
* Renders the group selector trigger element.
|
* Renders the group selector trigger element.
|
||||||
*
|
*
|
||||||
* @param object $course The course object.
|
* @param object $course The course object.
|
||||||
* @param string|null $groupactionbaseurl The base URL for the group action.
|
* @param string|null $groupactionbaseurl This parameter has been deprecated since 4.4 and should not be used anymore.
|
||||||
* @return string|null The raw HTML to render.
|
* @return string|null The raw HTML to render.
|
||||||
*/
|
*/
|
||||||
public function group_selector(object $course, ?string $groupactionbaseurl = null): ?string {
|
public function group_selector(object $course, ?string $groupactionbaseurl = null): ?string {
|
||||||
global $USER;
|
global $USER;
|
||||||
|
|
||||||
|
if ($groupactionbaseurl !== null) {
|
||||||
|
debugging(
|
||||||
|
'The $groupactionbaseurl argument has been deprecated. Please remove it from your method calls.',
|
||||||
|
DEBUG_DEVELOPER,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Make sure that group mode is enabled.
|
// Make sure that group mode is enabled.
|
||||||
if (!$groupmode = $course->groupmode) {
|
if (!$groupmode = $course->groupmode) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -59,17 +65,12 @@ class core_grades_renderer extends plugin_renderer_base {
|
||||||
$sbody = $this->render_from_template('core_group/comboboxsearch/searchbody', [
|
$sbody = $this->render_from_template('core_group/comboboxsearch/searchbody', [
|
||||||
'courseid' => $course->id,
|
'courseid' => $course->id,
|
||||||
'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS),
|
'currentvalue' => optional_param('groupsearchvalue', '', PARAM_NOTAGS),
|
||||||
|
'instance' => rand(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') :
|
$label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') : get_string('selectgroupsseparate');
|
||||||
get_string('selectgroupsseparate');
|
|
||||||
|
|
||||||
$data = [
|
$buttondata = ['label' => $label];
|
||||||
'name' => 'group',
|
|
||||||
'label' => $label,
|
|
||||||
'courseid' => $course->id,
|
|
||||||
'groupactionbaseurl' => $groupactionbaseurl
|
|
||||||
];
|
|
||||||
|
|
||||||
$context = context_course::instance($course->id);
|
$context = context_course::instance($course->id);
|
||||||
|
|
||||||
|
@ -80,22 +81,27 @@ class core_grades_renderer extends plugin_renderer_base {
|
||||||
}
|
}
|
||||||
|
|
||||||
$activegroup = groups_get_course_group($course, true, $allowedgroups);
|
$activegroup = groups_get_course_group($course, true, $allowedgroups);
|
||||||
$data['group'] = $activegroup;
|
$buttondata['group'] = $activegroup;
|
||||||
|
|
||||||
if ($activegroup) {
|
if ($activegroup) {
|
||||||
$group = groups_get_group($activegroup);
|
$group = groups_get_group($activegroup);
|
||||||
$data['selectedgroup'] = format_string($group->name, true, ['context' => $context]);
|
$buttondata['selectedgroup'] = format_string($group->name, true, ['context' => $context]);
|
||||||
} else if ($activegroup === 0) {
|
} else if ($activegroup === 0) {
|
||||||
$data['selectedgroup'] = get_string('allparticipants');
|
$buttondata['selectedgroup'] = get_string('allparticipants');
|
||||||
}
|
}
|
||||||
|
|
||||||
$groupdropdown = new comboboxsearch(
|
$groupdropdown = new comboboxsearch(
|
||||||
false,
|
false,
|
||||||
$this->render_from_template('core_group/comboboxsearch/group_selector', $data),
|
$this->render_from_template('core_group/comboboxsearch/group_selector', $buttondata),
|
||||||
$sbody,
|
$sbody,
|
||||||
'group-search',
|
'group-search',
|
||||||
'groupsearchwidget',
|
'groupsearchwidget',
|
||||||
'groupsearchdropdown overflow-auto w-100',
|
'groupsearchdropdown overflow-auto',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
$label,
|
||||||
|
'group',
|
||||||
|
$activegroup
|
||||||
);
|
);
|
||||||
return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this));
|
return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this));
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the grader report.\n *\n * @module gradereport_grader/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"2WAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
|
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the grader report.\n *\n * @module gradereport_grader/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"2WAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the grader report.\n *\n * @module gradereport_grader/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport * as Repository from 'gradereport_grader/local/user/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(courseID).then((r) => r.users);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","User","UserSearch","constructor","fetchDataset","Repository","userFetch","then","r","users","selectAllResultsLink","Url","relativeUrl","id","gpr_search","this","getSearchTerm","selectOneLink","userID","gpr_userid"],"mappings":";;;;;;;q0BA2BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,eAEhDC,aAAaC,cAE9BC,2CAKW,IAAIF,KAQfG,sBACWC,WAAWC,UAAUV,UAAUW,MAAMC,GAAMA,EAAEC,QAQxDC,8BACWC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,kBAClB,GASPC,cAAcC,eACHP,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,gBACjBG,WAAYD,SACb"}
|
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the grader report.\n *\n * @module gradereport_grader/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport * as Repository from 'gradereport_grader/local/user/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(courseID).then((r) => r.users);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","User","UserSearch","constructor","fetchDataset","Repository","userFetch","then","r","users","selectAllResultsLink","Url","relativeUrl","id","gpr_search","this","getSearchTerm","selectOneLink","userID","gpr_userid"],"mappings":";;;;;;;q0BA2BMA,oBACS,eADTA,mBAEQ,2BAGRC,SADYC,SAASC,cAAcH,qBACdG,cAAcH,oBAAoBI,QAAQC,eAEhDC,aAAaC,cAE9BC,2CAKW,IAAIF,KAQfG,sBACWC,WAAWC,UAAUV,UAAUW,MAAMC,GAAMA,EAAEC,QAQxDC,8BACWC,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,kBAClB,GASPC,cAAcC,eACHP,aAAIC,YAAY,iCAAkC,CACrDC,GAAIjB,SACJkB,WAAYC,KAAKC,gBACjBG,WAAYD,SACb"}
|
|
@ -56,6 +56,7 @@ const selectors = {
|
||||||
count: '[data-collapse="count"]',
|
count: '[data-collapse="count"]',
|
||||||
placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
|
placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
|
||||||
fullDropdown: '.collapsecolumndropdown',
|
fullDropdown: '.collapsecolumndropdown',
|
||||||
|
searchResultContainer: '.searchresultitemscontainer',
|
||||||
};
|
};
|
||||||
|
|
||||||
const countIndicator = document.querySelector(selectors.count);
|
const countIndicator = document.querySelector(selectors.count);
|
||||||
|
@ -98,6 +99,19 @@ export default class ColumnSearch extends search_combobox {
|
||||||
document.querySelector('.gradereport-grader-table').classList.remove('d-none');
|
document.querySelector('.gradereport-grader-table').classList.remove('d-none');
|
||||||
}, 10);
|
}, 10);
|
||||||
}).then(() => pendingPromise.resolve()).catch(Notification.exception);
|
}).then(() => pendingPromise.resolve()).catch(Notification.exception);
|
||||||
|
|
||||||
|
this.$component.on('hide.bs.dropdown', () => {
|
||||||
|
const searchResultContainer = this.component.querySelector(selectors.searchResultContainer);
|
||||||
|
searchResultContainer.scrollTop = 0;
|
||||||
|
|
||||||
|
// Use setTimeout to make sure the following code is executed after the click event is handled.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.searchInput.value !== '') {
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,15 +132,6 @@ export default class ColumnSearch extends search_combobox {
|
||||||
return '.searchresultitemscontainer';
|
return '.searchresultitemscontainer';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The triggering div that contains the searching widget.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
triggerSelector() {
|
|
||||||
return '.collapsecolumn';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the dataset that we will be searching upon.
|
* Return the dataset that we will be searching upon.
|
||||||
*
|
*
|
||||||
|
@ -218,25 +223,6 @@ export default class ColumnSearch extends search_combobox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler for when a user presses a key within the component.
|
|
||||||
*
|
|
||||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
|
||||||
*/
|
|
||||||
async keyHandler(e) {
|
|
||||||
super.keyHandler(e);
|
|
||||||
|
|
||||||
// Switch the key presses to handle keyboard nav.
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Tab':
|
|
||||||
if (e.target.closest(this.selectors.input)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.clearSearchButton.focus({preventScroll: true});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle any keyboard inputs.
|
* Handle any keyboard inputs.
|
||||||
*/
|
*/
|
||||||
|
@ -492,6 +478,7 @@ export default class ColumnSearch extends search_combobox {
|
||||||
// Update the collapsed button pill.
|
// Update the collapsed button pill.
|
||||||
this.countUpdate();
|
this.countUpdate();
|
||||||
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {
|
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {
|
||||||
|
'instance': this.instance,
|
||||||
'results': this.getMatchedResults(),
|
'results': this.getMatchedResults(),
|
||||||
'userid': this.userID,
|
'userid': this.userID,
|
||||||
});
|
});
|
||||||
|
@ -505,6 +492,7 @@ export default class ColumnSearch extends search_combobox {
|
||||||
// Add a small BS listener so that we can set the focus correctly on open.
|
// Add a small BS listener so that we can set the focus correctly on open.
|
||||||
this.$component.on('shown.bs.dropdown', () => {
|
this.$component.on('shown.bs.dropdown', () => {
|
||||||
this.searchInput.focus({preventScroll: true});
|
this.searchInput.focus({preventScroll: true});
|
||||||
|
this.selectallEnable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,14 +500,26 @@ export default class ColumnSearch extends search_combobox {
|
||||||
* Build the content then replace the node.
|
* Build the content then replace the node.
|
||||||
*/
|
*/
|
||||||
async renderDropdown() {
|
async renderDropdown() {
|
||||||
const form = this.component.querySelector(selectors.formDropdown);
|
|
||||||
const selectall = form.querySelector('[data-action="selectall"]');
|
|
||||||
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {
|
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {
|
||||||
|
instance: this.instance,
|
||||||
'results': this.getMatchedResults(),
|
'results': this.getMatchedResults(),
|
||||||
'searchTerm': this.getSearchTerm(),
|
'searchTerm': this.getSearchTerm(),
|
||||||
});
|
});
|
||||||
selectall.disabled = this.getMatchedResults().length === 0;
|
|
||||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
||||||
|
this.selectallEnable();
|
||||||
|
// Reset the expand button to be disabled as we have re-rendered the dropdown.
|
||||||
|
const form = this.component.querySelector(selectors.formDropdown);
|
||||||
|
const expandButton = form.querySelector(`[data-action="${selectors.formItems.save}"`);
|
||||||
|
expandButton.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given we render the dropdown, Determine if we want to enable the select all checkbox.
|
||||||
|
*/
|
||||||
|
selectallEnable() {
|
||||||
|
const form = this.component.querySelector(selectors.formDropdown);
|
||||||
|
const selectall = form.querySelector('[data-action="selectall"]');
|
||||||
|
selectall.disabled = this.getMatchedResults().length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default class Group extends GroupSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* Build up the link that is dedicated to a particular result.
|
||||||
*
|
*
|
||||||
* @param {Number} groupID The ID of the group selected.
|
* @param {Number} groupID The ID of the group selected.
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default class User extends UserSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* Build up the link that is dedicated to a particular result.
|
||||||
*
|
*
|
||||||
* @param {Number} userID The ID of the user selected.
|
* @param {Number} userID The ID of the user selected.
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
|
|
|
@ -32,6 +32,9 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
/** @var string $usersearch The content that the current user is looking for. */
|
/** @var string $usersearch The content that the current user is looking for. */
|
||||||
protected string $usersearch = '';
|
protected string $usersearch = '';
|
||||||
|
|
||||||
|
/** @var int $userid The ID of the user that the current user is looking for. */
|
||||||
|
protected int $userid = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class constructor.
|
* The class constructor.
|
||||||
*
|
*
|
||||||
|
@ -40,7 +43,13 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
public function __construct(\context_course $context) {
|
public function __construct(\context_course $context) {
|
||||||
parent::__construct($context);
|
parent::__construct($context);
|
||||||
|
|
||||||
|
$this->userid = optional_param('gpr_userid', 0, PARAM_INT);
|
||||||
$this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS);
|
$this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS);
|
||||||
|
|
||||||
|
if ($this->userid) {
|
||||||
|
$user = \core_user::get_user($this->userid);
|
||||||
|
$this->usersearch = fullname($user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,6 +89,10 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
$this->context,
|
$this->context,
|
||||||
'/grade/report/grader/index.php'
|
'/grade/report/grader/index.php'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$firstnameinitial = $SESSION->gradereport["filterfirstname-{$this->context->id}"] ?? '';
|
||||||
|
$lastnameinitial = $SESSION->gradereport["filtersurname-{$this->context->id}"] ?? '';
|
||||||
|
|
||||||
$initialselector = new comboboxsearch(
|
$initialselector = new comboboxsearch(
|
||||||
false,
|
false,
|
||||||
$initialscontent->buttoncontent,
|
$initialscontent->buttoncontent,
|
||||||
|
@ -88,6 +101,13 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
'initialswidget',
|
'initialswidget',
|
||||||
'initialsdropdown',
|
'initialsdropdown',
|
||||||
$initialscontent->buttonheader,
|
$initialscontent->buttonheader,
|
||||||
|
true,
|
||||||
|
get_string('filterbyname', 'core_grades'),
|
||||||
|
'nameinitials',
|
||||||
|
json_encode([
|
||||||
|
'first' => $firstnameinitial,
|
||||||
|
'last' => $lastnameinitial,
|
||||||
|
])
|
||||||
);
|
);
|
||||||
$data['initialselector'] = $initialselector->export_for_template($output);
|
$data['initialselector'] = $initialselector->export_for_template($output);
|
||||||
$data['groupselector'] = $gradesrenderer->group_selector($course);
|
$data['groupselector'] = $gradesrenderer->group_selector($course);
|
||||||
|
@ -96,14 +116,20 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
$searchinput = $OUTPUT->render_from_template('core_user/comboboxsearch/user_selector', [
|
$searchinput = $OUTPUT->render_from_template('core_user/comboboxsearch/user_selector', [
|
||||||
'currentvalue' => $this->usersearch,
|
'currentvalue' => $this->usersearch,
|
||||||
'courseid' => $courseid,
|
'courseid' => $courseid,
|
||||||
|
'instance' => rand(),
|
||||||
'resetlink' => $resetlink->out(false),
|
'resetlink' => $resetlink->out(false),
|
||||||
'group' => 0,
|
'group' => 0,
|
||||||
|
'name' => 'usersearch',
|
||||||
|
'value' => json_encode([
|
||||||
|
'userid' => $this->userid,
|
||||||
|
'search' => $this->usersearch,
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
$searchdropdown = new comboboxsearch(
|
$searchdropdown = new comboboxsearch(
|
||||||
true,
|
true,
|
||||||
$searchinput,
|
$searchinput,
|
||||||
null,
|
null,
|
||||||
'user-search dropdown d-flex',
|
'user-search d-flex',
|
||||||
null,
|
null,
|
||||||
'usersearchdropdown overflow-auto',
|
'usersearchdropdown overflow-auto',
|
||||||
null,
|
null,
|
||||||
|
@ -123,6 +149,8 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
'collapsecolumndropdown p-3 flex-column ' . $collapsemenudirection,
|
'collapsecolumndropdown p-3 flex-column ' . $collapsemenudirection,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
|
get_string('aria:dropdowncolumns', 'gradereport_grader'),
|
||||||
|
'collapsedcolumns'
|
||||||
);
|
);
|
||||||
$data['collapsedcolumns'] = [
|
$data['collapsedcolumns'] = [
|
||||||
'classes' => 'd-none',
|
'classes' => 'd-none',
|
||||||
|
@ -135,10 +163,12 @@ class action_bar extends \core_grades\output\action_bar {
|
||||||
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
|
$allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($SESSION->gradereport["filterfirstname-{$this->context->id}"]) ||
|
if (
|
||||||
!empty($SESSION->gradereport["filterlastname-{$this->context->id}"]) ||
|
$firstnameinitial ||
|
||||||
|
$lastnameinitial ||
|
||||||
groups_get_course_group($course, true, $allowedgroups) ||
|
groups_get_course_group($course, true, $allowedgroups) ||
|
||||||
$this->usersearch) {
|
$this->usersearch
|
||||||
|
) {
|
||||||
$reset = new moodle_url('/grade/report/grader/index.php', [
|
$reset = new moodle_url('/grade/report/grader/index.php', [
|
||||||
'id' => $courseid,
|
'id' => $courseid,
|
||||||
'group' => 0,
|
'group' => 0,
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"instance": 25,
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"name": "42",
|
"name": "42",
|
||||||
|
@ -39,24 +40,18 @@
|
||||||
{{$placeholder}}{{#str}}
|
{{$placeholder}}{{#str}}
|
||||||
searchcollapsedcolumns, core_grades
|
searchcollapsedcolumns, core_grades
|
||||||
{{/str}}{{/placeholder}}
|
{{/str}}{{/placeholder}}
|
||||||
{{$additionalattributes}}
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded="true"
|
|
||||||
aria-controls="collapse-listbox"
|
|
||||||
aria-autocomplete="list"
|
|
||||||
data-input-element="collapse-input-{{uniqid}}"
|
|
||||||
{{/additionalattributes}}
|
|
||||||
{{/ core/search_input_auto }}
|
{{/ core/search_input_auto }}
|
||||||
<input type="hidden" name="search" id="collapse-input-{{uniqid}}"/>
|
|
||||||
|
|
||||||
<form class="columnsdropdownform flex-column h-100">
|
<form class="columnsdropdownform flex-column h-100">
|
||||||
<ul id="collapse-listbox" class="searchresultitemscontainer overflow-auto py-2 px-1 list-group mx-0 text-truncate" role="listbox" data-region="search-result-items-container" tabindex="-1" aria-label="{{#cleanstr}} aria:dropdowncolumns, gradereport_grader {{/cleanstr}}" aria-busy="true">
|
<fieldset>
|
||||||
{{>gradereport_grader/collapse/collapseresults}}
|
<legend class="sr-only">{{#str}} aria:dropdowncolumns, gradereport_grader {{/str}}</legend>
|
||||||
</ul>
|
<ul id="collapse-{{instance}}-listbox" class="searchresultitemscontainer overflow-auto py-2 px-1 list-group mx-0 text-truncate" data-region="search-result-items-container">
|
||||||
|
{{>gradereport_grader/collapse/collapseresults}}
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
<div class="d-flex mt-2">
|
<div class="d-flex mt-2">
|
||||||
<div class="d-flex align-items-center form-check">
|
<div class="d-flex align-items-center form-check">
|
||||||
<label id="check-all" for="check-all-input" class="selected-option-info text-truncate form-check-label d-block" tabindex="-1">
|
<label id="check-all" class="selected-option-info text-truncate form-check-label d-block">
|
||||||
<input disabled id="check-all-input" class="form-check-input" type="checkbox" data-action="selectall" aria-label="{{#str}}selectall, core{{/str}}">
|
<input disabled class="form-check-input" type="checkbox" data-action="selectall">
|
||||||
{{#str}}selectall, core{{/str}}
|
{{#str}}selectall, core{{/str}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
"category": "Hitchhikers grade category"
|
"category": "Hitchhikers grade category"
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<li class="w-100 result-row form-check mb-1" role="none">
|
<li class="w-100 result-row form-check mb-1">
|
||||||
<input id="check-{{name}}-input" class="form-check-input" role="option" data-collapse="{{name}}" type="checkbox" value="" aria-labelledby="check-{{name}}">
|
<label class="selected-option-info d-block pr-3 text-truncate form-check-label">
|
||||||
<label id="check-{{name}}" class="selected-option-info d-block pr-3 text-truncate form-check-label" tabindex="-1">
|
<input class="form-check-input" data-collapse="{{name}}" type="checkbox" value="">
|
||||||
<span class="selected-option-text w-100 p-0">
|
<span class="selected-option-text w-100 p-0">
|
||||||
{{{displayName}}}
|
{{{displayName}}}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -110,9 +110,9 @@ Feature: Within the grader report, test that we can collapse columns
|
||||||
And I click on "Collapsed columns" "combobox"
|
And I click on "Collapsed columns" "combobox"
|
||||||
# This is checking that the column name search dropdown exists.
|
# This is checking that the column name search dropdown exists.
|
||||||
When I wait until "Search collapsed columns" "field" exists
|
When I wait until "Search collapsed columns" "field" exists
|
||||||
And I click on "Test assignment one" "option_role" in the "form" "gradereport_grader > collapse search"
|
And I click on "Test assignment one" "checkbox" in the "form" "gradereport_grader > collapse search"
|
||||||
And I click on "Test assignment three" "option_role" in the "form" "gradereport_grader > collapse search"
|
And I click on "Test assignment three" "checkbox" in the "form" "gradereport_grader > collapse search"
|
||||||
And I click on "Phone" "option_role" in the "form" "gradereport_grader > collapse search"
|
And I click on "Phone" "checkbox" in the "form" "gradereport_grader > collapse search"
|
||||||
And I click on "Expand" "button" in the "form" "gradereport_grader > collapse search"
|
And I click on "Expand" "button" in the "form" "gradereport_grader > collapse search"
|
||||||
And "Test assignment one" "link" in the "First name / Last name" "table_row" should be visible
|
And "Test assignment one" "link" in the "First name / Last name" "table_row" should be visible
|
||||||
And "Test assignment three" "link" in the "First name / Last name" "table_row" should be visible
|
And "Test assignment three" "link" in the "First name / Last name" "table_row" should be visible
|
||||||
|
@ -181,41 +181,18 @@ Feature: Within the grader report, test that we can collapse columns
|
||||||
And I choose "Collapse" in the open action menu
|
And I choose "Collapse" in the open action menu
|
||||||
# Basic tests for the page.
|
# Basic tests for the page.
|
||||||
When I click on "Collapsed columns" "combobox"
|
When I click on "Collapsed columns" "combobox"
|
||||||
And the page should meet accessibility standards
|
|
||||||
And the page should meet "wcag131, wcag141, wcag412" accessibility standards
|
|
||||||
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
||||||
# Move onto general keyboard navigation testing.
|
# Move onto general keyboard navigation testing.
|
||||||
Then the focused element is "Search collapsed columns" "field"
|
Then the focused element is "Search collapsed columns" "field"
|
||||||
And I press the down key
|
|
||||||
And the focused element is "Email address" "option_role"
|
|
||||||
And I press the end key
|
|
||||||
And the focused element is "Country" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "Email address" "option_role"
|
|
||||||
And I press the up key
|
|
||||||
And the focused element is "Country" "option_role"
|
|
||||||
And I press the down key
|
|
||||||
And the focused element is "Email address" "option_role"
|
|
||||||
And I press the end key
|
|
||||||
And I press the tab key
|
|
||||||
And the focused element is "Select all" "checkbox"
|
|
||||||
And I press the escape key
|
And I press the escape key
|
||||||
And the focused element is "Collapsed columns" "combobox"
|
And the focused element is "Collapsed columns" "combobox"
|
||||||
And I click on "Collapsed columns" "combobox"
|
And I click on "Collapsed columns" "combobox"
|
||||||
Then I set the field "Search collapsed columns" to "Goodmeme"
|
|
||||||
And I wait until "No results for \"Goodmeme\"" "text" exists
|
|
||||||
And I press the down key
|
|
||||||
And the focused element is "Search collapsed columns" "field"
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search collapsed columns" to "phone"
|
And I set the field "Search collapsed columns" to "phone"
|
||||||
And I wait until "Mobile phone" "option_role" exists
|
And I wait until "Mobile phone" "checkbox" exists
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
And the focused element is "Clear search input" "button" in the ".dropdown-menu.show" "css_element"
|
And the focused element is "Clear search input" "button" in the ".dropdown-menu.show" "css_element"
|
||||||
And I press the tab key
|
And I press the escape key
|
||||||
And I press the tab key
|
|
||||||
And I press the tab key
|
|
||||||
And I press the tab key
|
|
||||||
And the focused element is "Close" "button" in the ".dropdown-menu.show" "css_element"
|
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
# The course grade category menu.
|
# The course grade category menu.
|
||||||
And the focused element is "Cell actions" "button"
|
And the focused element is "Cell actions" "button"
|
||||||
|
@ -306,6 +283,6 @@ Feature: Within the grader report, test that we can collapse columns
|
||||||
And I wait until "Collapsed columns" "combobox" exists
|
And I wait until "Collapsed columns" "combobox" exists
|
||||||
When I click on "Collapsed columns" "combobox"
|
When I click on "Collapsed columns" "combobox"
|
||||||
And I click on "Select all" "checkbox"
|
And I click on "Select all" "checkbox"
|
||||||
And I click on "Email" "option_role" in the "form" "gradereport_grader > collapse search"
|
And I click on "Email" "checkbox" in the "form" "gradereport_grader > collapse search"
|
||||||
# The select all option should now be unchecked, Checking the form or option role is iffy with behat so we use the id.
|
# The select all option should now be unchecked, Checking the form or option role is iffy with behat so we use the id.
|
||||||
Then "input#check-all-input:not([checked=checked])" "css_element" should exist
|
Then the field "Select all" matches value ""
|
||||||
|
|
|
@ -80,17 +80,14 @@ Feature: Group searching functionality within the grader report.
|
||||||
And I click on "Search groups" "field"
|
And I click on "Search groups" "field"
|
||||||
And I wait until "Default group" "option_role" exists
|
And I wait until "Default group" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the end key
|
|
||||||
And the focused element is "Tutor group" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the up key
|
|
||||||
And the focused element is "Tutor group" "option_role"
|
|
||||||
And I press the down key
|
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the escape key
|
|
||||||
And the focused element is "Search groups" "field"
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "All participants" "option_role"
|
||||||
|
And I press the up key
|
||||||
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Tutor group" "option_role"
|
||||||
|
And I press the down key
|
||||||
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "All participants" "option_role"
|
||||||
Then I set the field "Search groups" to "Goodmeme"
|
Then I set the field "Search groups" to "Goodmeme"
|
||||||
And I wait until "Tutor group" "option_role" does not exist
|
And I wait until "Tutor group" "option_role" does not exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
|
@ -101,7 +98,8 @@ Feature: Group searching functionality within the grader report.
|
||||||
And I set the field "Search groups" to "Tutor"
|
And I set the field "Search groups" to "Tutor"
|
||||||
And I wait until "All participants" "option_role" does not exist
|
And I wait until "All participants" "option_role" does not exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Tutor group" "option_role"
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Tutor group" "option_role"
|
||||||
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search groups" to "Marker"
|
And I set the field "Search groups" to "Marker"
|
||||||
|
|
|
@ -120,6 +120,7 @@ Feature: Within the grader report, test that we can open our generic filter drop
|
||||||
# Click off the drop down
|
# Click off the drop down
|
||||||
And I click on "Filter by name" "combobox"
|
And I click on "Filter by name" "combobox"
|
||||||
And "input[data-action=save]" "css_element" should be visible
|
And "input[data-action=save]" "css_element" should be visible
|
||||||
|
And I change window size to "large"
|
||||||
And I click on user profile field menu "fullname"
|
And I click on user profile field menu "fullname"
|
||||||
And "input[data-action=save]" "css_element" should not be visible
|
And "input[data-action=save]" "css_element" should not be visible
|
||||||
|
|
||||||
|
|
|
@ -156,13 +156,12 @@ Feature: Within the grader report, test that we can search for users
|
||||||
| Dummy User |
|
| Dummy User |
|
||||||
|
|
||||||
Scenario: A teacher can quickly tell that a search is active on the current table
|
Scenario: A teacher can quickly tell that a search is active on the current table
|
||||||
Given I click on "Turtle" in the "user" search widget
|
When I click on "Turtle" in the "user" search widget
|
||||||
# The search input remains in the field on reload this is in keeping with other search implementations.
|
# The search input should contain the name of the user we have selected, so that it is clear that the result pertains to a specific user.
|
||||||
When the field "Search users" matches value "Turtle"
|
Then the field "Search users" matches value "Turtle Manatee"
|
||||||
And I wait until "View all results (1)" "link" does not exist
|
|
||||||
# Test if we can then further retain the turtle result set and further filter from there.
|
# Test if we can then further retain the turtle result set and further filter from there.
|
||||||
Then I set the field "Search users" to "Turtle plagiarism"
|
And I set the field "Search users" to "Turtle plagiarism"
|
||||||
And "Turtle Manatee" "list_item" should not exist
|
And "Turtle Manatee" "list_item" should not be visible
|
||||||
And I should see "No results for \"Turtle plagiarism\""
|
And I should see "No results for \"Turtle plagiarism\""
|
||||||
|
|
||||||
Scenario: A teacher can search for values besides the users' name
|
Scenario: A teacher can search for values besides the users' name
|
||||||
|
@ -244,15 +243,11 @@ Feature: Within the grader report, test that we can search for users
|
||||||
And the page should meet "wcag131, wcag141, wcag412" accessibility standards
|
And the page should meet "wcag131, wcag141, wcag412" accessibility standards
|
||||||
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the end key
|
|
||||||
And the focused element is "View all results (5)" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "Student 1" "option_role"
|
|
||||||
And I press the up key
|
And I press the up key
|
||||||
And the focused element is "View all results (5)" "option_role"
|
And ".active" "css_element" should exist in the "View all results (5)" "option_role"
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the escape key
|
And I press the escape key
|
||||||
And the focused element is "Search users" "field"
|
And the focused element is "Search users" "field"
|
||||||
Then I set the field "Search users" to "Goodmeme"
|
Then I set the field "Search users" to "Goodmeme"
|
||||||
|
@ -263,20 +258,20 @@ Feature: Within the grader report, test that we can search for users
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
And I wait until "Turtle Manatee" "option_role" exists
|
And I wait until "Turtle Manatee" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
And I wait until "View all results (5)" "option_role" exists
|
And I click on "Search users" "field"
|
||||||
|
And I wait until "Turtle Manatee" "option_role" exists
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
And the focused element is "Clear search input" "button"
|
And the focused element is "Clear search input" "button"
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
And the focused element is "View all results (5)" "option_role"
|
|
||||||
And I press the tab key
|
|
||||||
And ".groupsearchwidget" "css_element" should exist
|
And ".groupsearchwidget" "css_element" should exist
|
||||||
# Ensure we can interact with the input & clear search options with the keyboard.
|
# Ensure we can interact with the input & clear search options with the keyboard.
|
||||||
# Space & Enter have the same handling for triggering the two functionalities.
|
# Space & Enter have the same handling for triggering the two functionalities.
|
||||||
And I set the field "Search users" to "User"
|
And I set the field "Search users" to "User"
|
||||||
|
And I press the up key
|
||||||
And I press the enter key
|
And I press the enter key
|
||||||
And I wait to be redirected
|
And I wait to be redirected
|
||||||
And the following should exist in the "user-grades" table:
|
And the following should exist in the "user-grades" table:
|
||||||
|
@ -289,15 +284,6 @@ Feature: Within the grader report, test that we can search for users
|
||||||
| Teacher 1 |
|
| Teacher 1 |
|
||||||
| Student 1 |
|
| Student 1 |
|
||||||
| Turtle Manatee |
|
| Turtle Manatee |
|
||||||
# Sometimes with behat we get unattached nodes causing spurious failures.
|
|
||||||
And I wait "1" seconds
|
|
||||||
And I set the field "Search users" to "ABC"
|
|
||||||
And I wait until "Turtle Manatee" "option_role" exists
|
|
||||||
And I press the tab key
|
|
||||||
And the focused element is "Clear search input" "button"
|
|
||||||
And I press the enter key
|
|
||||||
And I wait until the page is ready
|
|
||||||
And I confirm "Turtle Manatee" in "user" search within the gradebook widget does not exist
|
|
||||||
|
|
||||||
Scenario: Once a teacher searches, it'll apply the currently set filters and inform the teacher as such
|
Scenario: Once a teacher searches, it'll apply the currently set filters and inform the teacher as such
|
||||||
# Set up a basic filtering case.
|
# Set up a basic filtering case.
|
||||||
|
@ -357,7 +343,7 @@ Feature: Within the grader report, test that we can search for users
|
||||||
And the field "perpage" matches value "20"
|
And the field "perpage" matches value "20"
|
||||||
When I set the field "Search users" to "42"
|
When I set the field "Search users" to "42"
|
||||||
# One of the users' phone numbers also matches.
|
# One of the users' phone numbers also matches.
|
||||||
And I wait until "View all results (2)" "link" exists
|
And I wait until "View all results (2)" "option_role" exists
|
||||||
Then I confirm "Student s42" in "user" search within the gradebook widget exists
|
Then I confirm "Student s42" in "user" search within the gradebook widget exists
|
||||||
|
|
||||||
Scenario: As a teacher I save grades using search and pagination
|
Scenario: As a teacher I save grades using search and pagination
|
||||||
|
@ -378,13 +364,13 @@ Feature: Within the grader report, test that we can search for users
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
# Search for a single user on second page and save grades.
|
# Search for a single user on second page and save grades.
|
||||||
When I set the field "Search users" to "test32"
|
When I set the field "Search users" to "test32"
|
||||||
And I wait until "View all results (1)" "link" exists
|
And I wait until "View all results (1)" "option_role" exists
|
||||||
And I click on "Student test32" "option_role"
|
And I click on "Student test32" "option_role"
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
And I give the grade "80.00" to the user "Student test32" for the grade item "Test assignment one"
|
And I give the grade "80.00" to the user "Student test32" for the grade item "Test assignment one"
|
||||||
And I press "Save changes"
|
And I press "Save changes"
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
Then the field "Search users" matches value "test32"
|
Then the field "Search users" matches value "Student test32"
|
||||||
And the following should exist in the "user-grades" table:
|
And the following should exist in the "user-grades" table:
|
||||||
| -1- |
|
| -1- |
|
||||||
| Student test32 |
|
| Student test32 |
|
||||||
|
@ -394,7 +380,7 @@ Feature: Within the grader report, test that we can search for users
|
||||||
And I give the grade "70.00" to the user "Student test31" for the grade item "Test assignment one"
|
And I give the grade "70.00" to the user "Student test31" for the grade item "Test assignment one"
|
||||||
And I press "Save changes"
|
And I press "Save changes"
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
Then the field "Search users" matches value "test3"
|
Then the field "Search users" matches value "Student test31"
|
||||||
And the following should exist in the "user-grades" table:
|
And the following should exist in the "user-grades" table:
|
||||||
| -1- |
|
| -1- |
|
||||||
| Student test31 |
|
| Student test31 |
|
||||||
|
@ -423,7 +409,7 @@ Feature: Within the grader report, test that we can search for users
|
||||||
| Student test32 |
|
| Student test32 |
|
||||||
# Search for multiple users on second page and save grades.
|
# Search for multiple users on second page and save grades.
|
||||||
And I set the field "Search users" to "test3"
|
And I set the field "Search users" to "test3"
|
||||||
And I wait until "View all results (11)" "link" exists
|
And I wait until "View all results (11)" "option_role" exists
|
||||||
And I click on "View all results (11)" "option_role"
|
And I click on "View all results (11)" "option_role"
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
And I give the grade "10.00" to the user "Student test32" for the grade item "Test assignment one"
|
And I give the grade "10.00" to the user "Student test32" for the grade item "Test assignment one"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the singleview report.\n *\n * @module gradereport_singleview/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n item = null;\n\n /**\n * Construct the class.\n *\n * @param {string} item The page type we are currently on.\n */\n constructor(item) {\n super();\n this.item = item;\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} item The page type we are currently on.\n * @returns {Group}\n */\n static init(item) {\n return new Group(item);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/singleview/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n item: this.item\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","item","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"mhBAyBqBA,cAAcC,eAW/BC,YAAYC,kFAPL,WASEA,KAAOA,UAGPC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,qBASjEH,aACD,IAAIH,MAAMG,MASrBU,cAAcC,gBACHC,aAAIC,YAAY,qCAAsC,CACzDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,QACPX,KAAME,KAAKF,OACZ"}
|
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the singleview report.\n *\n * @module gradereport_singleview/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n item = null;\n\n /**\n * Construct the class.\n *\n * @param {string} item The page type we are currently on.\n */\n constructor(item) {\n super();\n this.item = item;\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} item The page type we are currently on.\n * @returns {Group}\n */\n static init(item) {\n return new Group(item);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/singleview/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n item: this.item\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","item","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"mhBAyBqBA,cAAcC,eAW/BC,YAAYC,kFAPL,WASEA,KAAOA,UAGPC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,qBASjEH,aACD,IAAIH,MAAMG,MASrBU,cAAcC,gBACHC,aAAIC,YAAY,qCAAsC,CACzDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,QACPX,KAAME,KAAKF,OACZ"}
|
|
@ -5,6 +5,6 @@ define("gradereport_singleview/user",["exports","core_user/comboboxsearch/user",
|
||||||
* @module gradereport_singleview/user
|
* @module gradereport_singleview/user
|
||||||
* @copyright 2023 Mathew May <mathew.solutions>
|
* @copyright 2023 Mathew May <mathew.solutions>
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class User extends _user.default{constructor(){super()}static init(){return new User}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}selectAllResultsLink(){return null}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/singleview/index.php",{id:this.courseID,searchvalue:this.getSearchTerm(),item:"user",userid:userID},!1)}fetchDataset(){const gts="string"==typeof this.groupID&&""===this.groupID?0:this.groupID;return Repository.userFetch(this.courseID,gts).then((r=>r.users))}}return _exports.default=User,_exports.default}));
|
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class User extends _user.default{constructor(){super()}static init(){return new User}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{instance:this.instance,users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.searchInput.removeAttribute("aria-activedescendant")}selectAllResultsLink(){return null}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/singleview/index.php",{id:this.courseID,searchvalue:this.getSearchTerm(),item:"user",userid:userID},!1)}fetchDataset(){const gts="string"==typeof this.groupID&&""===this.groupID?0:this.groupID;return Repository.userFetch(this.courseID,gts).then((r=>r.users))}}return _exports.default=User,_exports.default}));
|
||||||
|
|
||||||
//# sourceMappingURL=user.min.js.map
|
//# sourceMappingURL=user.min.js.map
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the singleview report.\n *\n * @module gradereport_singleview/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Stub out default required function unused here.\n * @returns {null}\n */\n selectAllResultsLink() {\n return null;\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/singleview/index.php', {\n id: this.courseID,\n searchvalue: this.getSearchTerm(),\n item: 'user',\n userid: userID,\n }, false);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n // Small typing checks as sometimes groups don't exist therefore the element returns a empty string.\n const gts = typeof (this.groupID) === \"string\" && this.groupID === '' ? 0 : this.groupID;\n return Repository.userFetch(this.courseID, gts).then((r) => r.users);\n }\n}\n"],"names":["User","UserSearch","constructor","html","js","users","this","getMatchedResults","slice","hasresults","length","searchterm","getSearchTerm","getHTMLElements","searchDropdown","selectAllResultsLink","selectOneLink","userID","Url","relativeUrl","id","courseID","searchvalue","item","userid","fetchDataset","gts","groupID","Repository","userFetch","then","r"],"mappings":";;;;;;;q0BA2BqBA,aAAaC,cAE9BC,2CAKW,IAAIF,kCAOLG,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOC,KAAKC,oBAAoBC,MAAM,EAAG,GACzCC,WAAYH,KAAKC,oBAAoBG,OAAS,EAC9CC,WAAYL,KAAKM,qDAEDN,KAAKO,kBAAkBC,eAAgBX,KAAMC,IAOrEW,8BACW,KASXC,cAAcC,eACHC,aAAIC,YAAY,qCAAsC,CACzDC,GAAId,KAAKe,SACTC,YAAahB,KAAKM,gBAClBW,KAAM,OACNC,OAAQP,SACT,GAQPQ,qBAEUC,IAAgC,iBAAlBpB,KAAKqB,SAA0C,KAAjBrB,KAAKqB,QAAiB,EAAIrB,KAAKqB,eAC1EC,WAAWC,UAAUvB,KAAKe,SAAUK,KAAKI,MAAMC,GAAMA,EAAE1B"}
|
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the singleview report.\n *\n * @module gradereport_singleview/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n instance: this.instance,\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Stub out default required function unused here.\n * @returns {null}\n */\n selectAllResultsLink() {\n return null;\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/singleview/index.php', {\n id: this.courseID,\n searchvalue: this.getSearchTerm(),\n item: 'user',\n userid: userID,\n }, false);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n // Small typing checks as sometimes groups don't exist therefore the element returns a empty string.\n const gts = typeof (this.groupID) === \"string\" && this.groupID === '' ? 0 : this.groupID;\n return Repository.userFetch(this.courseID, gts).then((r) => r.users);\n }\n}\n"],"names":["User","UserSearch","constructor","html","js","instance","this","users","getMatchedResults","slice","hasresults","length","searchterm","getSearchTerm","getHTMLElements","searchDropdown","searchInput","removeAttribute","selectAllResultsLink","selectOneLink","userID","Url","relativeUrl","id","courseID","searchvalue","item","userid","fetchDataset","gts","groupID","Repository","userFetch","then","r"],"mappings":";;;;;;;q0BA2BqBA,aAAaC,cAE9BC,2CAKW,IAAIF,kCAOLG,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,SAAUC,KAAKD,SACfE,MAAOD,KAAKE,oBAAoBC,MAAM,EAAG,GACzCC,WAAYJ,KAAKE,oBAAoBG,OAAS,EAC9CC,WAAYN,KAAKO,qDAEDP,KAAKQ,kBAAkBC,eAAgBZ,KAAMC,SAE5DY,YAAYC,gBAAgB,yBAOrCC,8BACW,KASXC,cAAcC,eACHC,aAAIC,YAAY,qCAAsC,CACzDC,GAAIjB,KAAKkB,SACTC,YAAanB,KAAKO,gBAClBa,KAAM,OACNC,OAAQP,SACT,GAQPQ,qBAEUC,IAAgC,iBAAlBvB,KAAKwB,SAA0C,KAAjBxB,KAAKwB,QAAiB,EAAIxB,KAAKwB,eAC1EC,WAAWC,UAAU1B,KAAKkB,SAAUK,KAAKI,MAAMC,GAAMA,EAAE3B"}
|
|
@ -57,7 +57,7 @@ export default class Group extends GroupSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* Build up the link that is dedicated to a particular result.
|
||||||
*
|
*
|
||||||
* @param {Number} groupID The ID of the group selected.
|
* @param {Number} groupID The ID of the group selected.
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
|
|
|
@ -40,11 +40,14 @@ export default class User extends UserSearch {
|
||||||
*/
|
*/
|
||||||
async renderDropdown() {
|
async renderDropdown() {
|
||||||
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
||||||
|
instance: this.instance,
|
||||||
users: this.getMatchedResults().slice(0, 5),
|
users: this.getMatchedResults().slice(0, 5),
|
||||||
hasresults: this.getMatchedResults().length > 0,
|
hasresults: this.getMatchedResults().length > 0,
|
||||||
searchterm: this.getSearchTerm(),
|
searchterm: this.getSearchTerm(),
|
||||||
});
|
});
|
||||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
||||||
|
// Remove aria-activedescendant when the available options change.
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -131,7 +131,7 @@ class singleview extends grade_report {
|
||||||
|
|
||||||
protected function setup_groups() {
|
protected function setup_groups() {
|
||||||
parent::setup_groups();
|
parent::setup_groups();
|
||||||
$this->group_selector = static::groups_course_menu($this->course, $this->pbarurl);
|
$this->group_selector = static::groups_course_menu($this->course);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,19 +139,13 @@ class singleview extends grade_report {
|
||||||
* so all reports would automatically use it.
|
* so all reports would automatically use it.
|
||||||
*
|
*
|
||||||
* @param stdClass $course
|
* @param stdClass $course
|
||||||
* @param moodle_url $urlroot
|
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected static function groups_course_menu(stdClass $course, moodle_url $urlroot) {
|
protected static function groups_course_menu(stdClass $course) {
|
||||||
global $PAGE;
|
global $PAGE;
|
||||||
|
|
||||||
$renderer = $PAGE->get_renderer('core_grades');
|
$renderer = $PAGE->get_renderer('core_grades');
|
||||||
$params = $urlroot->params();
|
return $renderer->group_selector($course);
|
||||||
if ($params['item'] == 'user') {
|
|
||||||
$params['item'] = 'user_select';
|
|
||||||
$urlroot->params($params);
|
|
||||||
}
|
|
||||||
return $renderer->group_selector($course, $urlroot->out());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -46,18 +46,29 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
|
||||||
*/
|
*/
|
||||||
public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string {
|
public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string {
|
||||||
$resetlink = new moodle_url('/grade/report/singleview/index.php', ['id' => $course->id, 'group' => $groupid ?? 0]);
|
$resetlink = new moodle_url('/grade/report/singleview/index.php', ['id' => $course->id, 'group' => $groupid ?? 0]);
|
||||||
|
$submitteduserid = optional_param('userid', '', PARAM_INT);
|
||||||
|
|
||||||
|
if ($submitteduserid) {
|
||||||
|
$user = core_user::get_user($submitteduserid);
|
||||||
|
$currentvalue = fullname($user);
|
||||||
|
} else {
|
||||||
|
$currentvalue = '';
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'currentvalue' => optional_param('searchvalue', '', PARAM_NOTAGS),
|
'currentvalue' => $currentvalue,
|
||||||
'courseid' => $course->id,
|
'courseid' => $course->id,
|
||||||
|
'instance' => rand(),
|
||||||
'group' => $groupid ?? 0,
|
'group' => $groupid ?? 0,
|
||||||
'resetlink' => $resetlink->out(false),
|
'resetlink' => $resetlink->out(false),
|
||||||
'userid' => $userid ?? 0
|
'name' => 'userid',
|
||||||
|
'value' => $submitteduserid ?? '',
|
||||||
];
|
];
|
||||||
$dropdown = new comboboxsearch(
|
$dropdown = new comboboxsearch(
|
||||||
true,
|
true,
|
||||||
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
|
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
|
||||||
null,
|
null,
|
||||||
'user-search dropdown d-flex',
|
'user-search d-flex',
|
||||||
null,
|
null,
|
||||||
'usersearchdropdown overflow-auto',
|
'usersearchdropdown overflow-auto',
|
||||||
null,
|
null,
|
||||||
|
@ -78,6 +89,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
|
||||||
$data = [
|
$data = [
|
||||||
'name' => 'itemid',
|
'name' => 'itemid',
|
||||||
'courseid' => $course->id,
|
'courseid' => $course->id,
|
||||||
|
'instance' => rand(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// If a particular grade item option is selected (not in zero state).
|
// If a particular grade item option is selected (not in zero state).
|
||||||
|
@ -92,6 +104,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
|
||||||
$sbody = $this->render_from_template('core/local/comboboxsearch/searchbody', [
|
$sbody = $this->render_from_template('core/local/comboboxsearch/searchbody', [
|
||||||
'courseid' => $course->id,
|
'courseid' => $course->id,
|
||||||
'currentvalue' => optional_param('gradesearchvalue', '', PARAM_NOTAGS),
|
'currentvalue' => optional_param('gradesearchvalue', '', PARAM_NOTAGS),
|
||||||
|
'instance' => $data['instance'],
|
||||||
]);
|
]);
|
||||||
$dropdown = new comboboxsearch(
|
$dropdown = new comboboxsearch(
|
||||||
false,
|
false,
|
||||||
|
@ -99,7 +112,12 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
|
||||||
$sbody,
|
$sbody,
|
||||||
'grade-search h-100',
|
'grade-search h-100',
|
||||||
'gradesearchwidget h-100',
|
'gradesearchwidget h-100',
|
||||||
'gradesearchdropdown overflow-auto w-100',
|
'gradesearchdropdown overflow-auto',
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
get_string('selectagrade', 'gradereport_singleview'),
|
||||||
|
'itemid',
|
||||||
|
$gradeitemid
|
||||||
);
|
);
|
||||||
return $this->render_from_template($dropdown->get_template(), $dropdown->export_for_template($this));
|
return $this->render_from_template($dropdown->get_template(), $dropdown->export_for_template($this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
* itemid - The value of the grade item selector element (id of the preselected grade item)
|
* itemid - The value of the grade item selector element (id of the preselected grade item)
|
||||||
|
* instance - The instance ID of the combo box.
|
||||||
* courseid - The course ID.
|
* courseid - The course ID.
|
||||||
* selectedoption - (optional) Object containing information about the selected option.
|
* selectedoption - (optional) Object containing information about the selected option.
|
||||||
* text - The text of the selected option.
|
* text - The text of the selected option.
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
"itemid": "21",
|
"itemid": "21",
|
||||||
|
"instance": "25",
|
||||||
"courseid": "2",
|
"courseid": "2",
|
||||||
"selectedoption": {
|
"selectedoption": {
|
||||||
"text": "Grade item 1"
|
"text": "Grade item 1"
|
||||||
|
@ -34,7 +36,7 @@
|
||||||
<div class="align-items-center d-flex">
|
<div class="align-items-center d-flex">
|
||||||
{{#selectedoption}}
|
{{#selectedoption}}
|
||||||
<div class="d-block pr-3 text-truncate">
|
<div class="d-block pr-3 text-truncate">
|
||||||
<span class="d-block small">
|
<span class="d-block small" aria-hidden="true">
|
||||||
{{#str}} selectagrade, gradereport_singleview {{/str}}
|
{{#str}} selectagrade, gradereport_singleview {{/str}}
|
||||||
</span>
|
</span>
|
||||||
<span class="p-0 font-weight-bold">
|
<span class="p-0 font-weight-bold">
|
||||||
|
@ -48,5 +50,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{/selectedoption}}
|
{{/selectedoption}}
|
||||||
</div>
|
</div>
|
||||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}"></span>
|
||||||
<span class="d-none" data-region="itemid" data-itemid="{{itemid}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="itemid" data-itemid="{{itemid}}"></span>
|
||||||
|
<span class="d-none" data-region="instance" data-instance="{{instance}}"></span>
|
||||||
|
|
|
@ -47,17 +47,14 @@ Feature: Given we have opted to search for a grade item, Lets find and search th
|
||||||
And I click on "Search items" "field"
|
And I click on "Search items" "field"
|
||||||
And I wait until "Test assignment one" "option_role" exists
|
And I wait until "Test assignment one" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Test assignment one" "option_role"
|
|
||||||
And I press the end key
|
|
||||||
And the focused element is "Course total" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "Test assignment one" "option_role"
|
|
||||||
And I press the up key
|
|
||||||
And the focused element is "Course total" "option_role"
|
|
||||||
And I press the down key
|
|
||||||
And the focused element is "Test assignment one" "option_role"
|
|
||||||
And I press the escape key
|
|
||||||
And the focused element is "Search items" "field"
|
And the focused element is "Search items" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Test assignment one" "option_role"
|
||||||
|
And I press the up key
|
||||||
|
And the focused element is "Search items" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Course total" "option_role"
|
||||||
|
And I press the down key
|
||||||
|
And the focused element is "Search items" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Test assignment one" "option_role"
|
||||||
Then I set the field "Search items" to "Goodmeme"
|
Then I set the field "Search items" to "Goodmeme"
|
||||||
And I wait until "Test assignment one" "option_role" does not exist
|
And I wait until "Test assignment one" "option_role" does not exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
|
|
|
@ -150,9 +150,9 @@ Feature: We can use Single view
|
||||||
Given I click on user menu "Grainne Beauchamp"
|
Given I click on user menu "Grainne Beauchamp"
|
||||||
And I choose "Single view for this user" in the open action menu
|
And I choose "Single view for this user" in the open action menu
|
||||||
Then I should see "Gronya,Beecham"
|
Then I should see "Gronya,Beecham"
|
||||||
And I follow "Nee,Chumlee"
|
And I click on "Nee,Chumlee" "link" in the ".stickyfooter" "css_element"
|
||||||
Then I should see "Nee,Chumlee"
|
Then I should see "Nee,Chumlee"
|
||||||
And I follow "Gronya,Beecham"
|
And I click on "Gronya,Beecham" "link" in the ".stickyfooter" "css_element"
|
||||||
Then I should see "Gronya,Beecham"
|
Then I should see "Gronya,Beecham"
|
||||||
And I open the action menu in "Test assignment four" "table_row"
|
And I open the action menu in "Test assignment four" "table_row"
|
||||||
And I choose "Show all grades" in the open action menu
|
And I choose "Show all grades" in the open action menu
|
||||||
|
|
|
@ -104,11 +104,12 @@ Feature: Within the singleview report, a teacher can search for users.
|
||||||
Given I click on "Turtle" in the "user" search widget
|
Given I click on "Turtle" in the "user" search widget
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
# The search input remains in the field on reload this is in keeping with other search implementations.
|
# The search input remains in the field on reload this is in keeping with other search implementations.
|
||||||
When the field "Search users" matches value "Turtle"
|
When the field "Search users" matches value "Turtle Manatee"
|
||||||
And "Turtle Manatee" "option_role" should not exist
|
# The users get preloaded for accessibility reasons.
|
||||||
|
And "Turtle Manatee" "option_role" should exist
|
||||||
# Test if we can then further retain the turtle result set and further filter from there.
|
# Test if we can then further retain the turtle result set and further filter from there.
|
||||||
Then I set the field "Search users" to "Turtle plagiarism"
|
Then I set the field "Search users" to "Turtle plagiarism"
|
||||||
And "Turtle Manatee" "list_item" should not exist
|
And I wait until "Turtle Manatee" "option_role" does not exist
|
||||||
And I should see "No results for \"Turtle plagiarism\""
|
And I should see "No results for \"Turtle plagiarism\""
|
||||||
|
|
||||||
Scenario: A teacher can search for values besides the users' name
|
Scenario: A teacher can search for values besides the users' name
|
||||||
|
@ -186,15 +187,11 @@ Feature: Within the singleview report, a teacher can search for users.
|
||||||
# Move onto general keyboard navigation testing.
|
# Move onto general keyboard navigation testing.
|
||||||
When I wait until "Turtle Manatee" "option_role" exists
|
When I wait until "Turtle Manatee" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the end key
|
|
||||||
And the focused element is "Dummy User" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "Student 1" "option_role"
|
|
||||||
And I press the up key
|
And I press the up key
|
||||||
And the focused element is "Dummy User" "option_role"
|
And ".active" "css_element" should exist in the "Dummy User" "option_role"
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the escape key
|
And I press the escape key
|
||||||
And the focused element is "Search users" "field"
|
And the focused element is "Search users" "field"
|
||||||
Then I set the field "Search users" to "Goodmeme"
|
Then I set the field "Search users" to "Goodmeme"
|
||||||
|
@ -206,7 +203,7 @@ Feature: Within the singleview report, a teacher can search for users.
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
And I wait until "Turtle Manatee" "option_role" exists
|
And I wait until "Turtle Manatee" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the user report.\n *\n * @module gradereport_user/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"yWAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,+BAAgC,CACnDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
|
{"version":3,"file":"group.min.js","sources":["../src/group.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 * Allow the user to search for groups within the user report.\n *\n * @module gradereport_user/group\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"yWAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,+BAAgC,CACnDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"}
|
2
grade/report/user/amd/build/user.min.js
vendored
2
grade/report/user/amd/build/user.min.js
vendored
|
@ -5,6 +5,6 @@ define("gradereport_user/user",["exports","core_user/comboboxsearch/user","core/
|
||||||
* @module gradereport_user/user
|
* @module gradereport_user/user
|
||||||
* @copyright 2023 Mathew May <mathew.solutions>
|
* @copyright 2023 Mathew May <mathew.solutions>
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class User extends _user.default{constructor(){super()}static init(){return new User}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,matches:this.getDatasetSize(),searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,userid:0,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}fetchDataset(){const gts="string"==typeof this.groupID&&""===this.groupID?0:this.groupID;return Repository.userFetch(this.courseID,gts).then((r=>r.users))}}return _exports.default=User,_exports.default}));
|
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class User extends _user.default{constructor(){super()}static init(){return new User}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,instance:this.instance,matches:this.getDatasetSize(),searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.searchInput.removeAttribute("aria-activedescendant")}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,userid:0,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}fetchDataset(){const gts="string"==typeof this.groupID&&""===this.groupID?0:this.groupID;return Repository.userFetch(this.courseID,gts).then((r=>r.users))}}return _exports.default=User,_exports.default}));
|
||||||
|
|
||||||
//# sourceMappingURL=user.min.js.map
|
//# sourceMappingURL=user.min.js.map
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the user report.\n *\n * @module gradereport_user/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n matches: this.getDatasetSize(),\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n userid: 0,\n searchvalue: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n // Small typing checks as sometimes groups don't exist therefore the element returns a empty string.\n const gts = typeof (this.groupID) === \"string\" && this.groupID === '' ? 0 : this.groupID;\n return Repository.userFetch(this.courseID, gts).then((r) => r.users);\n }\n}\n"],"names":["User","UserSearch","constructor","html","js","users","this","getMatchedResults","slice","hasresults","length","matches","getDatasetSize","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","Url","relativeUrl","id","courseID","userid","searchvalue","selectOneLink","userID","fetchDataset","gts","groupID","Repository","userFetch","then","r"],"mappings":";;;;;;;q0BA2BqBA,aAAaC,cAE9BC,2CAKW,IAAIF,kCAOLG,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOC,KAAKC,oBAAoBC,MAAM,EAAG,GACzCC,WAAYH,KAAKC,oBAAoBG,OAAS,EAC9CC,QAASL,KAAKM,iBACdC,WAAYP,KAAKQ,gBACjBC,UAAWT,KAAKU,4DAEAV,KAAKW,kBAAkBC,eAAgBf,KAAMC,IAQrEY,8BACWG,aAAIC,YAAY,+BAAgC,CACnDC,GAAIf,KAAKgB,SACTC,OAAQ,EACRC,YAAalB,KAAKQ,kBACnB,GASPW,cAAcC,eACHP,aAAIC,YAAY,+BAAgC,CACnDC,GAAIf,KAAKgB,SACTE,YAAalB,KAAKQ,gBAClBS,OAAQG,SACT,GAQPC,qBAEUC,IAAgC,iBAAlBtB,KAAKuB,SAA0C,KAAjBvB,KAAKuB,QAAiB,EAAIvB,KAAKuB,eAC1EC,WAAWC,UAAUzB,KAAKgB,SAAUM,KAAKI,MAAMC,GAAMA,EAAE5B"}
|
{"version":3,"file":"user.min.js","sources":["../src/user.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 * Allow the user to search for learners within the user report.\n *\n * @module gradereport_user/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n matches: this.getDatasetSize(),\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n userid: 0,\n searchvalue: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n // Small typing checks as sometimes groups don't exist therefore the element returns a empty string.\n const gts = typeof (this.groupID) === \"string\" && this.groupID === '' ? 0 : this.groupID;\n return Repository.userFetch(this.courseID, gts).then((r) => r.users);\n }\n}\n"],"names":["User","UserSearch","constructor","html","js","users","this","getMatchedResults","slice","hasresults","length","instance","matches","getDatasetSize","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","searchInput","removeAttribute","Url","relativeUrl","id","courseID","userid","searchvalue","selectOneLink","userID","fetchDataset","gts","groupID","Repository","userFetch","then","r"],"mappings":";;;;;;;q0BA2BqBA,aAAaC,cAE9BC,2CAKW,IAAIF,kCAOLG,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOC,KAAKC,oBAAoBC,MAAM,EAAG,GACzCC,WAAYH,KAAKC,oBAAoBG,OAAS,EAC9CC,SAAUL,KAAKK,SACfC,QAASN,KAAKO,iBACdC,WAAYR,KAAKS,gBACjBC,UAAWV,KAAKW,4DAEAX,KAAKY,kBAAkBC,eAAgBhB,KAAMC,SAE5DgB,YAAYC,gBAAgB,yBAQrCJ,8BACWK,aAAIC,YAAY,+BAAgC,CACnDC,GAAIlB,KAAKmB,SACTC,OAAQ,EACRC,YAAarB,KAAKS,kBACnB,GASPa,cAAcC,eACHP,aAAIC,YAAY,+BAAgC,CACnDC,GAAIlB,KAAKmB,SACTE,YAAarB,KAAKS,gBAClBW,OAAQG,SACT,GAQPC,qBAEUC,IAAgC,iBAAlBzB,KAAK0B,SAA0C,KAAjB1B,KAAK0B,QAAiB,EAAI1B,KAAK0B,eAC1EC,WAAWC,UAAU5B,KAAKmB,SAAUM,KAAKI,MAAMC,GAAMA,EAAE/B"}
|
|
@ -43,7 +43,7 @@ export default class Group extends GroupSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* Build up the link that is dedicated to a particular result.
|
||||||
*
|
*
|
||||||
* @param {Number} groupID The ID of the group selected.
|
* @param {Number} groupID The ID of the group selected.
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
|
|
|
@ -42,11 +42,14 @@ export default class User extends UserSearch {
|
||||||
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
||||||
users: this.getMatchedResults().slice(0, 5),
|
users: this.getMatchedResults().slice(0, 5),
|
||||||
hasresults: this.getMatchedResults().length > 0,
|
hasresults: this.getMatchedResults().length > 0,
|
||||||
|
instance: this.instance,
|
||||||
matches: this.getDatasetSize(),
|
matches: this.getDatasetSize(),
|
||||||
searchterm: this.getSearchTerm(),
|
searchterm: this.getSearchTerm(),
|
||||||
selectall: this.selectAllResultsLink(),
|
selectall: this.selectAllResultsLink(),
|
||||||
});
|
});
|
||||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
||||||
|
// Remove aria-activedescendant when the available options change.
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +66,7 @@ export default class User extends UserSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* Build up the link that is dedicated to a particular result.
|
||||||
*
|
*
|
||||||
* @param {Number} userID The ID of the user selected.
|
* @param {Number} userID The ID of the user selected.
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
|
|
|
@ -93,19 +93,30 @@ class gradereport_user_renderer extends plugin_renderer_base {
|
||||||
*/
|
*/
|
||||||
public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string {
|
public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string {
|
||||||
$resetlink = new moodle_url('/grade/report/user/index.php', ['id' => $course->id, 'group' => 0]);
|
$resetlink = new moodle_url('/grade/report/user/index.php', ['id' => $course->id, 'group' => 0]);
|
||||||
|
$submitteduserid = optional_param('userid', '', PARAM_INT);
|
||||||
|
|
||||||
|
if ($submitteduserid) {
|
||||||
|
$user = core_user::get_user($submitteduserid);
|
||||||
|
$currentvalue = fullname($user);
|
||||||
|
} else {
|
||||||
|
$currentvalue = '';
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'currentvalue' => optional_param('searchvalue', '', PARAM_NOTAGS),
|
'currentvalue' => $currentvalue,
|
||||||
|
'instance' => rand(),
|
||||||
'resetlink' => $resetlink->out(false),
|
'resetlink' => $resetlink->out(false),
|
||||||
'name' => 'userid',
|
'name' => 'userid',
|
||||||
|
'value' => $submitteduserid ?? '',
|
||||||
'courseid' => $course->id,
|
'courseid' => $course->id,
|
||||||
'groupid' => $groupid ?? 0,
|
'group' => $groupid ?? 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
$searchdropdown = new comboboxsearch(
|
$searchdropdown = new comboboxsearch(
|
||||||
true,
|
true,
|
||||||
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
|
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
|
||||||
null,
|
null,
|
||||||
'user-search dropdown d-flex',
|
'user-search d-flex',
|
||||||
null,
|
null,
|
||||||
'usersearchdropdown overflow-auto',
|
'usersearchdropdown overflow-auto',
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -82,17 +82,14 @@ Feature: Group searching functionality within the user report.
|
||||||
And I click on "Search groups" "field"
|
And I click on "Search groups" "field"
|
||||||
And I wait until "Default group" "option_role" exists
|
And I wait until "Default group" "option_role" exists
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the end key
|
|
||||||
And the focused element is "Tutor group" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the up key
|
|
||||||
And the focused element is "Tutor group" "option_role"
|
|
||||||
And I press the down key
|
|
||||||
And the focused element is "All participants" "option_role"
|
|
||||||
And I press the escape key
|
|
||||||
And the focused element is "Search groups" "field"
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "All participants" "option_role"
|
||||||
|
And I press the up key
|
||||||
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Tutor group" "option_role"
|
||||||
|
And I press the down key
|
||||||
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "All participants" "option_role"
|
||||||
Then I set the field "Search groups" to "Goodmeme"
|
Then I set the field "Search groups" to "Goodmeme"
|
||||||
And I wait until "Tutor group" "option_role" does not exist
|
And I wait until "Tutor group" "option_role" does not exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
|
@ -103,7 +100,8 @@ Feature: Group searching functionality within the user report.
|
||||||
And I set the field "Search groups" to "Tutor"
|
And I set the field "Search groups" to "Tutor"
|
||||||
And I wait until "All participants" "option_role" does not exist
|
And I wait until "All participants" "option_role" does not exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Tutor group" "option_role"
|
And the focused element is "Search groups" "field"
|
||||||
|
And ".active" "css_element" should exist in the "Tutor group" "option_role"
|
||||||
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search groups" to "Marker"
|
And I set the field "Search groups" to "Marker"
|
||||||
|
|
|
@ -107,8 +107,7 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
And "Student 1" "heading" should exist
|
And "Student 1" "heading" should exist
|
||||||
And "Turtle Manatee" "heading" should exist
|
And "Turtle Manatee" "heading" should exist
|
||||||
And "Teacher 1" "heading" should not exist
|
And "Teacher 1" "heading" should not exist
|
||||||
And I click on "Clear" "link" in the ".user-search" "css_element"
|
And "Clear" "link" should not exist in the ".user-search" "css_element"
|
||||||
And I wait until the page is ready
|
|
||||||
And "Dummy User" "heading" should exist
|
And "Dummy User" "heading" should exist
|
||||||
And "User Example" "heading" should exist
|
And "User Example" "heading" should exist
|
||||||
And "User Test" "heading" should exist
|
And "User Test" "heading" should exist
|
||||||
|
@ -122,13 +121,13 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
And I should see "No results for \"a\""
|
And I should see "No results for \"a\""
|
||||||
|
|
||||||
Scenario: A teacher can quickly tell that a search is active on the current table
|
Scenario: A teacher can quickly tell that a search is active on the current table
|
||||||
Given I click on "Turtle" in the "user" search widget
|
When I click on "Turtle" in the "user" search widget
|
||||||
# The search input remains in the field on reload this is in keeping with other search implementations.
|
# The search input should contain the name of the user we have selected, so that it is clear that the result pertains to a specific user.
|
||||||
When the field "Search users" matches value "Turtle"
|
Then the field "Search users" matches value "Turtle Manatee"
|
||||||
And I wait until "View all results (5)" "link" does not exist
|
And I wait until "View all results (5)" "link" does not exist
|
||||||
# Test if we can then further retain the turtle result set and further filter from there.
|
# Test if we can then further retain the turtle result set and further filter from there.
|
||||||
Then I set the field "Search users" to "Turtle plagiarism"
|
And I set the field "Search users" to "Turtle plagiarism"
|
||||||
And "Turtle Manatee" "list_item" should not exist
|
And I wait until "Turtle Manatee" "list_item" does not exist
|
||||||
And I should see "No results for \"Turtle plagiarism\""
|
And I should see "No results for \"Turtle plagiarism\""
|
||||||
|
|
||||||
Scenario: A teacher can search for values besides the users' name
|
Scenario: A teacher can search for values besides the users' name
|
||||||
|
@ -187,6 +186,7 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
And I confirm "User Example" in "user" search within the gradebook widget exists
|
And I confirm "User Example" in "user" search within the gradebook widget exists
|
||||||
And I confirm "User Test" in "user" search within the gradebook widget exists
|
And I confirm "User Test" in "user" search within the gradebook widget exists
|
||||||
And I confirm "Student 1" in "user" search within the gradebook widget exists
|
And I confirm "Student 1" in "user" search within the gradebook widget exists
|
||||||
|
And I press the up key
|
||||||
And I press the enter key
|
And I press the enter key
|
||||||
And I wait until the page is ready
|
And I wait until the page is ready
|
||||||
And "Student 1" "heading" should exist
|
And "Student 1" "heading" should exist
|
||||||
|
@ -200,21 +200,15 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
Scenario: A teacher can set focus and search using the input are with a keyboard
|
Scenario: A teacher can set focus and search using the input are with a keyboard
|
||||||
Given I set the field "Search users" to "ABC"
|
Given I set the field "Search users" to "ABC"
|
||||||
# Basic tests for the page.
|
# Basic tests for the page.
|
||||||
And the page should meet accessibility standards
|
|
||||||
And the page should meet "wcag131, wcag141, wcag412" accessibility standards
|
|
||||||
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
||||||
# Move onto general keyboard navigation testing.
|
# Move onto general keyboard navigation testing.
|
||||||
When "Turtle Manatee" "option_role" should exist
|
When "Turtle Manatee" "option_role" should exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the end key
|
|
||||||
And the focused element is "View all results (5)" "option_role"
|
|
||||||
And I press the home key
|
|
||||||
And the focused element is "Student 1" "option_role"
|
|
||||||
And I press the up key
|
And I press the up key
|
||||||
And the focused element is "View all results (5)" "option_role"
|
And ".active" "css_element" should exist in the "View all results (5)" "option_role"
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
And I press the escape key
|
And I press the escape key
|
||||||
And the focused element is "Search users" "field"
|
And the focused element is "Search users" "field"
|
||||||
Then I set the field "Search users" to "Goodmeme"
|
Then I set the field "Search users" to "Goodmeme"
|
||||||
|
@ -225,16 +219,15 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
And "Turtle Manatee" "option_role" should exist
|
And "Turtle Manatee" "option_role" should exist
|
||||||
And I press the down key
|
And I press the down key
|
||||||
And the focused element is "Student 1" "option_role"
|
And ".active" "css_element" should exist in the "Student 1" "option_role"
|
||||||
|
|
||||||
# Lets check the tabbing order.
|
# Lets check the tabbing order.
|
||||||
And I set the field "Search users" to "ABC"
|
And I set the field "Search users" to "ABC"
|
||||||
And "View all results (5)" "option_role" should exist
|
And I wait until "Clear search input" "button" exists
|
||||||
|
And I click on "Search users" "field"
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
And the focused element is "Clear search input" "button" in the ".user-search" "css_element"
|
And the focused element is "Clear search input" "button" in the ".user-search" "css_element"
|
||||||
And I press the tab key
|
And I press the tab key
|
||||||
And the focused element is "View all results (5)" "option_role"
|
|
||||||
And I press the tab key
|
|
||||||
And ".groupsearchwidget" "css_element" should exist
|
And ".groupsearchwidget" "css_element" should exist
|
||||||
# Ensure we can interact with the input & clear search options with the keyboard.
|
# Ensure we can interact with the input & clear search options with the keyboard.
|
||||||
# Space & Enter have the same handling for triggering the two functionalities.
|
# Space & Enter have the same handling for triggering the two functionalities.
|
||||||
|
@ -248,12 +241,3 @@ Feature: Within the User report, a teacher can search for users.
|
||||||
And "User Test" "heading" should not exist
|
And "User Test" "heading" should not exist
|
||||||
And "Teacher 1" "heading" should not exist
|
And "Teacher 1" "heading" should not exist
|
||||||
And "Turtle Manatee" "heading" should not exist
|
And "Turtle Manatee" "heading" should not exist
|
||||||
# Sometimes with behat we get unattached nodes causing spurious failures.
|
|
||||||
And I wait "1" seconds
|
|
||||||
And I set the field "Search users" to "ABC"
|
|
||||||
And "Turtle Manatee" "option_role" should exist
|
|
||||||
And I press the tab key
|
|
||||||
And the focused element is "Clear search input" "button" in the ".user-search" "css_element"
|
|
||||||
And I press the enter key
|
|
||||||
And I wait until the page is ready
|
|
||||||
And I confirm "Turtle Manatee" in "user" search within the gradebook widget does not exist
|
|
||||||
|
|
2
group/amd/build/comboboxsearch/group.min.js
vendored
2
group/amd/build/comboboxsearch/group.min.js
vendored
|
@ -1,3 +1,3 @@
|
||||||
define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents(),this.$component.on("shown.bs.dropdown",(()=>{this.searchInput.focus({preventScroll:!0})}))}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,link:this.selectOneLink(group.id),groupimageurl:group.groupimageurl}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){switch(super.keyHandler(e),e.key){case"Tab":e.target.closest(this.selectors.input)&&(e.preventDefault(),this.clearSearchButton.focus({preventScroll:!0}));break;case"Escape":if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default}));
|
define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default}));
|
||||||
|
|
||||||
//# sourceMappingURL=group.min.js.map
|
//# sourceMappingURL=group.min.js.map
|
File diff suppressed because one or more lines are too long
|
@ -39,6 +39,40 @@ export default class GroupSearch extends search_combobox {
|
||||||
};
|
};
|
||||||
const component = document.querySelector(this.componentSelector());
|
const component = document.querySelector(this.componentSelector());
|
||||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||||
|
// Override the instance since the body is built outside the constructor for the combobox.
|
||||||
|
this.instance = component.querySelector(this.selectors.instance).dataset.instance;
|
||||||
|
|
||||||
|
const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);
|
||||||
|
searchValueElement.addEventListener('change', () => {
|
||||||
|
this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
|
||||||
|
|
||||||
|
const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);
|
||||||
|
if (valueElement.value !== searchValueElement.value) {
|
||||||
|
valueElement.value = searchValueElement.value;
|
||||||
|
valueElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchValueElement.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$component.on('hide.bs.dropdown', () => {
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
|
|
||||||
|
const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role="listbox"]`);
|
||||||
|
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
|
||||||
|
option.classList.remove('active');
|
||||||
|
});
|
||||||
|
listbox.scrollTop = 0;
|
||||||
|
|
||||||
|
// Use setTimeout to make sure the following code is executed after the click event is handled.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.searchInput.value !== '') {
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.renderDefault().catch(Notification.exception);
|
this.renderDefault().catch(Notification.exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,15 +98,6 @@ export default class GroupSearch extends search_combobox {
|
||||||
return '.groupsearchdropdown';
|
return '.groupsearchdropdown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The triggering div that contains the searching widget.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
triggerSelector() {
|
|
||||||
return '.groupsearchwidget';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the content then replace the node.
|
* Build the content then replace the node.
|
||||||
*/
|
*/
|
||||||
|
@ -80,9 +105,12 @@ export default class GroupSearch extends search_combobox {
|
||||||
const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {
|
const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {
|
||||||
groups: this.getMatchedResults(),
|
groups: this.getMatchedResults(),
|
||||||
hasresults: this.getMatchedResults().length > 0,
|
hasresults: this.getMatchedResults().length > 0,
|
||||||
|
instance: this.instance,
|
||||||
searchterm: this.getSearchTerm(),
|
searchterm: this.getSearchTerm(),
|
||||||
});
|
});
|
||||||
replaceNodeContents(this.selectors.placeholder, html, js);
|
replaceNodeContents(this.selectors.placeholder, html, js);
|
||||||
|
// Remove aria-activedescendant when the available options change.
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,12 +123,6 @@ export default class GroupSearch extends search_combobox {
|
||||||
await this.renderDropdown();
|
await this.renderDropdown();
|
||||||
|
|
||||||
this.updateNodes();
|
this.updateNodes();
|
||||||
this.registerInputEvents();
|
|
||||||
|
|
||||||
// Add a small BS listener so that we can set the focus correctly on open.
|
|
||||||
this.$component.on('shown.bs.dropdown', () => {
|
|
||||||
this.searchInput.focus({preventScroll: true});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,7 +162,6 @@ export default class GroupSearch extends search_combobox {
|
||||||
return {
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
link: this.selectOneLink(group.id),
|
|
||||||
groupimageurl: group.groupimageurl,
|
groupimageurl: group.groupimageurl,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
@ -148,14 +169,41 @@ export default class GroupSearch extends search_combobox {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle any keyboard inputs.
|
* The handler for when a user interacts with the component.
|
||||||
|
*
|
||||||
|
* @param {MouseEvent} e The triggering event that we are working with.
|
||||||
*/
|
*/
|
||||||
registerInputEvents() {
|
async clickHandler(e) {
|
||||||
|
if (e.target.closest(this.selectors.clearSearch)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Clear the entered search query in the search bar.
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.setSearchTerms(this.searchInput.value);
|
||||||
|
this.searchInput.focus();
|
||||||
|
this.clearSearchButton.classList.add('d-none');
|
||||||
|
// Display results.
|
||||||
|
await this.filterrenderpipe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler for when a user changes the value of the component (selects an option from the dropdown).
|
||||||
|
*
|
||||||
|
* @param {Event} e The change event.
|
||||||
|
*/
|
||||||
|
changeHandler(e) {
|
||||||
|
window.location = this.selectOneLink(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the input event listener for the text input area.
|
||||||
|
*/
|
||||||
|
registerInputHandlers() {
|
||||||
// Register & handle the text input.
|
// Register & handle the text input.
|
||||||
this.searchInput.addEventListener('input', debounce(async() => {
|
this.searchInput.addEventListener('input', debounce(async() => {
|
||||||
this.setSearchTerms(this.searchInput.value);
|
this.setSearchTerms(this.searchInput.value);
|
||||||
// We can also require a set amount of input before search.
|
// We can also require a set amount of input before search.
|
||||||
if (this.searchInput.value === '') {
|
if (this.getSearchTerm() === '') {
|
||||||
// Hide the "clear" search button in the search bar.
|
// Hide the "clear" search button in the search bar.
|
||||||
this.clearSearchButton.classList.add('d-none');
|
this.clearSearchButton.classList.add('d-none');
|
||||||
} else {
|
} else {
|
||||||
|
@ -167,75 +215,9 @@ export default class GroupSearch extends search_combobox {
|
||||||
}, 300));
|
}, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler for when a user interacts with the component.
|
|
||||||
*
|
|
||||||
* @param {MouseEvent} e The triggering event that we are working with.
|
|
||||||
*/
|
|
||||||
async clickHandler(e) {
|
|
||||||
if (e.target.closest(this.selectors.dropdown)) {
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
this.clearSearchButton.addEventListener('click', async() => {
|
|
||||||
this.searchInput.value = '';
|
|
||||||
this.setSearchTerms(this.searchInput.value);
|
|
||||||
await this.filterrenderpipe();
|
|
||||||
});
|
|
||||||
// Prevent normal key presses activating this.
|
|
||||||
if (e.target.closest('.dropdown-item') && e.button === 0) {
|
|
||||||
window.location = e.target.closest('.dropdown-item').href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler for when a user presses a key within the component.
|
|
||||||
*
|
|
||||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
|
||||||
*/
|
|
||||||
keyHandler(e) {
|
|
||||||
super.keyHandler(e);
|
|
||||||
// Switch the key presses to handle keyboard nav.
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Tab':
|
|
||||||
if (e.target.closest(this.selectors.input)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.clearSearchButton.focus({preventScroll: true});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
if (document.activeElement.getAttribute('role') === 'option') {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.searchInput.focus({preventScroll: true});
|
|
||||||
} else if (e.target.closest(this.selectors.input)) {
|
|
||||||
const trigger = this.component.querySelector(this.selectors.trigger);
|
|
||||||
trigger.focus({preventScroll: true});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override the input event listener for the text input area.
|
|
||||||
*/
|
|
||||||
registerInputHandlers() {
|
|
||||||
// Register & handle the text input.
|
|
||||||
this.searchInput.addEventListener('input', debounce(() => {
|
|
||||||
this.setSearchTerms(this.searchInput.value);
|
|
||||||
// We can also require a set amount of input before search.
|
|
||||||
if (this.getSearchTerm() === '') {
|
|
||||||
// Hide the "clear" search button in the search bar.
|
|
||||||
this.clearSearchButton.classList.add('d-none');
|
|
||||||
} else {
|
|
||||||
// Display the "clear" search button in the search bar.
|
|
||||||
this.clearSearchButton.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
}, 300));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build up the view all link that is dedicated to a particular result.
|
* 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} groupID The ID of the group selected.
|
* @param {Number} groupID The ID of the group selected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -31,10 +31,10 @@
|
||||||
"selectedgroup": "Group 1"
|
"selectedgroup": "Group 1"
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="groupid" data-groupid="{{group}}"></span>
|
||||||
<div class="align-items-center d-flex">
|
<div class="align-items-center d-flex">
|
||||||
<div class="d-block pr-3 text-truncate">
|
<div class="d-block pr-3 text-truncate">
|
||||||
<span class="d-block small">
|
<span class="d-block small" aria-hidden="true">
|
||||||
{{label}}
|
{{label}}
|
||||||
</span>
|
</span>
|
||||||
<span class="p-0 font-weight-bold">
|
<span class="p-0 font-weight-bold">
|
||||||
|
|
|
@ -19,14 +19,12 @@
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
* id - Group system ID.
|
* id - Group system ID.
|
||||||
* name - Groups' name.
|
* name - Groups' name.
|
||||||
* link - The link used to redirect upon self to show only this specific group.
|
|
||||||
* groupimageurl - The link of the groups picture.
|
* groupimageurl - The link of the groups picture.
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "Foo bar",
|
"name": "Foo bar",
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2",
|
|
||||||
"groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
"groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
* groups - Our returned groups to render.
|
* groups - Our returned groups to render.
|
||||||
|
* instance - The instance ID of the combo box.
|
||||||
* searchterm - The entered text to find these results.
|
* searchterm - The entered text to find these results.
|
||||||
* hasgroups - Allow the handling where no users exist for the returned search term.
|
* hasgroups - Allow the handling where no users exist for the returned search term.
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"instance": 25,
|
||||||
"searchterm": "Foo",
|
"searchterm": "Foo",
|
||||||
"hasresults": true
|
"hasresults": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,42 +29,27 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<div class="flex-column h-100 w-100">
|
<div class="flex-column h-100 w-100">
|
||||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}"></span>
|
||||||
<span class="d-none" data-region="groupid" data-groupid="{{groupid}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="groupid" data-groupid="{{groupid}}"></span>
|
||||||
|
<span class="d-none" data-region="instance" data-instance="{{instance}}"></span>
|
||||||
|
|
||||||
{{#currentvalue}}
|
{{< core/search_input_auto }}
|
||||||
{{< core/search_input_auto }}
|
{{$label}}{{#str}}
|
||||||
{{$label}}{{#str}}
|
searchgroups, core
|
||||||
searchgroups, core
|
{{/str}}{{/label}}
|
||||||
{{/str}}{{/label}}
|
{{$placeholder}}{{#str}}
|
||||||
{{$value}}{{currentvalue}}{{/value}}
|
searchgroups, core
|
||||||
{{$additionalattributes}}
|
{{/str}}{{/placeholder}}
|
||||||
role="combobox"
|
{{$value}}{{currentvalue}}{{/value}}
|
||||||
aria-expanded="true"
|
{{$additionalattributes}}
|
||||||
aria-controls="groups-result-listbox"
|
role="combobox"
|
||||||
aria-autocomplete="list"
|
aria-expanded="true"
|
||||||
data-input-element="input-{{uniqid}}"
|
aria-controls="groups-{{instance}}-result-listbox"
|
||||||
{{/additionalattributes}}
|
aria-autocomplete="list"
|
||||||
{{/ core/search_input_auto }}
|
data-input-element="input-{{uniqid}}-{{instance}}"
|
||||||
{{/currentvalue}}
|
{{/additionalattributes}}
|
||||||
{{^currentvalue}}
|
{{/ core/search_input_auto }}
|
||||||
{{< core/search_input_auto }}
|
<input type="hidden" name="search" id="input-{{uniqid}}-{{instance}}"/>
|
||||||
{{$label}}{{#str}}
|
|
||||||
searchgroups, core
|
|
||||||
{{/str}}{{/label}}
|
|
||||||
{{$placeholder}}{{#str}}
|
|
||||||
searchgroups, core
|
|
||||||
{{/str}}{{/placeholder}}
|
|
||||||
{{$additionalattributes}}
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded="true"
|
|
||||||
aria-controls="groups-result-listbox"
|
|
||||||
aria-autocomplete="list"
|
|
||||||
data-input-element="input-{{uniqid}}"
|
|
||||||
{{/additionalattributes}}
|
|
||||||
{{/ core/search_input_auto }}
|
|
||||||
{{/currentvalue}}
|
|
||||||
<input type="hidden" name="search" id="input-{{uniqid}}"/>
|
|
||||||
|
|
||||||
<div data-region="searchplaceholder"></div>
|
<div data-region="searchplaceholder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,3 +115,6 @@ gradeitemadvanced,core_grades
|
||||||
gradeitemadvanced_help,core_grades
|
gradeitemadvanced_help,core_grades
|
||||||
to,core
|
to,core
|
||||||
from,core
|
from,core
|
||||||
|
aria-toggledropdown,core_grades
|
||||||
|
aria:dropdowngrades,core_grades
|
||||||
|
viewresults,core
|
||||||
|
|
|
@ -889,9 +889,6 @@ $string['xml'] = 'XML';
|
||||||
$string['yes'] = 'Yes';
|
$string['yes'] = 'Yes';
|
||||||
$string['yourgrade'] = 'Your grade';
|
$string['yourgrade'] = 'Your grade';
|
||||||
|
|
||||||
$string['aria-toggledropdown'] = 'Toggle the following dropdown';
|
|
||||||
$string['aria:dropdowngrades'] = 'Grade items found';
|
|
||||||
|
|
||||||
// Deprecated since Moodle 4.2.
|
// Deprecated since Moodle 4.2.
|
||||||
$string['showanalysisicon'] = 'Show grade analysis icon';
|
$string['showanalysisicon'] = 'Show grade analysis icon';
|
||||||
$string['showanalysisicon_desc'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
|
$string['showanalysisicon_desc'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
|
||||||
|
@ -918,3 +915,5 @@ $string['studentsperpage_help'] = 'This setting determines the number of student
|
||||||
$string['grade'] = 'Grade';
|
$string['grade'] = 'Grade';
|
||||||
$string['gradeitemadvanced'] = 'Advanced grade item options';
|
$string['gradeitemadvanced'] = 'Advanced grade item options';
|
||||||
$string['gradeitemadvanced_help'] = 'Select all elements that should be displayed as advanced when editing grade items.';
|
$string['gradeitemadvanced_help'] = 'Select all elements that should be displayed as advanced when editing grade items.';
|
||||||
|
$string['aria-toggledropdown'] = 'Toggle the following dropdown';
|
||||||
|
$string['aria:dropdowngrades'] = 'Grade items found';
|
||||||
|
|
|
@ -2384,7 +2384,6 @@ $string['viewallresults'] = 'View all results ({$a})';
|
||||||
$string['viewmore'] = 'View more';
|
$string['viewmore'] = 'View more';
|
||||||
$string['viewallsubcategories'] = 'View all subcategories';
|
$string['viewallsubcategories'] = 'View all subcategories';
|
||||||
$string['viewfileinpopup'] = 'View file in a popup window';
|
$string['viewfileinpopup'] = 'View file in a popup window';
|
||||||
$string['viewresults'] = 'View results for {$a}';
|
|
||||||
$string['viewprofile'] = 'View profile';
|
$string['viewprofile'] = 'View profile';
|
||||||
$string['views'] = 'Views';
|
$string['views'] = 'Views';
|
||||||
$string['viewsolution'] = 'view solution';
|
$string['viewsolution'] = 'view solution';
|
||||||
|
@ -2504,3 +2503,4 @@ $string['updatingain'] = 'Updating {$a->what} in {$a->in}';
|
||||||
$string['summaryof'] = 'Summary of {$a}';
|
$string['summaryof'] = 'Summary of {$a}';
|
||||||
$string['from'] = 'From';
|
$string['from'] = 'From';
|
||||||
$string['to'] = 'To';
|
$string['to'] = 'To';
|
||||||
|
$string['viewresults'] = 'View results for {$a}';
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
lib/amd/build/local/aria/selectors.min.js
vendored
2
lib/amd/build/local/aria/selectors.min.js
vendored
|
@ -1,3 +1,3 @@
|
||||||
define("core/local/aria/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={aria:{hidden:"[aria-hidden]"},elements:{focusable:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',focusableToUnhide:"[data-aria-hidden-tab-index]"}},_exports.default}));
|
define("core/local/aria/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={aria:{hidden:"[aria-hidden]"},elements:{focusable:'input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),a[href]:not([disabled]):not([tabindex^="-"]),button:not([disabled]):not([tabindex^="-"]),textarea:not([disabled]):not([tabindex^="-"]),select:not([disabled]):not([tabindex^="-"]),[tabindex]:not([disabled]):not([tabindex^="-"])',focusableToUnhide:"[data-aria-hidden-tab-index]"}},_exports.default}));
|
||||||
|
|
||||||
//# sourceMappingURL=selectors.min.js.map
|
//# sourceMappingURL=selectors.min.js.map
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"selectors.min.js","sources":["../../../src/local/aria/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 used for ARIA.\n *\n * @module core/local/aria/selectors\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n aria: {\n hidden: '[aria-hidden]',\n },\n elements: {\n focusable: 'input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]',\n focusableToUnhide: '[data-aria-hidden-tab-index]',\n },\n};\n"],"names":["aria","hidden","elements","focusable","focusableToUnhide"],"mappings":"2KAsBe,CACXA,KAAM,CACFC,OAAQ,iBAEZC,SAAU,CACNC,UAAW,4EACXC,kBAAmB"}
|
{"version":3,"file":"selectors.min.js","sources":["../../../src/local/aria/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 used for ARIA.\n *\n * @module core/local/aria/selectors\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n aria: {\n hidden: '[aria-hidden]',\n },\n elements: {\n focusable: 'input:not([type=\"hidden\"]):not([disabled]):not([tabindex^=\"-\"]),' +\n 'a[href]:not([disabled]):not([tabindex^=\"-\"]),' +\n 'button:not([disabled]):not([tabindex^=\"-\"]),' +\n 'textarea:not([disabled]):not([tabindex^=\"-\"]),' +\n 'select:not([disabled]):not([tabindex^=\"-\"]),' +\n '[tabindex]:not([disabled]):not([tabindex^=\"-\"])',\n focusableToUnhide: '[data-aria-hidden-tab-index]',\n },\n};\n"],"names":["aria","hidden","elements","focusable","focusableToUnhide"],"mappings":"2KAsBe,CACXA,KAAM,CACFC,OAAQ,iBAEZC,SAAU,CACNC,UAAW,qSAMXC,kBAAmB"}
|
|
@ -14,7 +14,6 @@
|
||||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import CustomEvents from "core/custom_interaction_events";
|
|
||||||
import {debounce} from 'core/utils';
|
import {debounce} from 'core/utils';
|
||||||
import Pending from 'core/pending';
|
import Pending from 'core/pending';
|
||||||
|
|
||||||
|
@ -25,25 +24,19 @@ import Pending from 'core/pending';
|
||||||
* @copyright 2023 Mathew May <mathew.solutions>
|
* @copyright 2023 Mathew May <mathew.solutions>
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/
|
*/
|
||||||
// Reused variables for the class.
|
|
||||||
const events = [
|
|
||||||
'keydown',
|
|
||||||
CustomEvents.events.activate,
|
|
||||||
CustomEvents.events.keyboardActivate
|
|
||||||
];
|
|
||||||
const UP = -1;
|
|
||||||
const DOWN = 1;
|
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
// Define our standard lookups.
|
// Define our standard lookups.
|
||||||
selectors = {
|
selectors = {
|
||||||
component: this.componentSelector(),
|
component: this.componentSelector(),
|
||||||
trigger: this.triggerSelector(),
|
toggle: '[data-toggle="dropdown"]',
|
||||||
|
instance: '[data-region="instance"]',
|
||||||
input: '[data-action="search"]',
|
input: '[data-action="search"]',
|
||||||
clearSearch: '[data-action="clearsearch"]',
|
clearSearch: '[data-action="clearsearch"]',
|
||||||
dropdown: this.dropdownSelector(),
|
dropdown: this.dropdownSelector(),
|
||||||
resultitems: '[role="option"]',
|
resultitems: '[role="option"]',
|
||||||
viewall: '#select-all',
|
viewall: '#select-all',
|
||||||
|
combobox: '[role="combobox"]',
|
||||||
};
|
};
|
||||||
|
|
||||||
// The results from the called filter function.
|
// The results from the called filter function.
|
||||||
|
@ -70,9 +63,12 @@ export default class {
|
||||||
|
|
||||||
// DOM nodes that persist.
|
// DOM nodes that persist.
|
||||||
component = document.querySelector(this.selectors.component);
|
component = document.querySelector(this.selectors.component);
|
||||||
|
instance = this.component.dataset.instance;
|
||||||
|
toggle = this.component.querySelector(this.selectors.toggle);
|
||||||
searchInput = this.component.querySelector(this.selectors.input);
|
searchInput = this.component.querySelector(this.selectors.input);
|
||||||
searchDropdown = this.component.querySelector(this.selectors.dropdown);
|
searchDropdown = this.component.querySelector(this.selectors.dropdown);
|
||||||
clearSearchButton = this.component.querySelector(this.selectors.clearSearch);
|
clearSearchButton = this.component.querySelector(this.selectors.clearSearch);
|
||||||
|
combobox = this.component.querySelector(this.selectors.combobox);
|
||||||
$component = $(this.component);
|
$component = $(this.component);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -80,10 +76,18 @@ export default class {
|
||||||
this.setSearchTerms(this.searchInput?.value ?? '');
|
this.setSearchTerms(this.searchInput?.value ?? '');
|
||||||
// Begin handling the base search component.
|
// Begin handling the base search component.
|
||||||
this.registerClickHandlers();
|
this.registerClickHandlers();
|
||||||
this.registerKeyHandlers();
|
|
||||||
// Conditionally set up the input handler since we don't know exactly how we were called.
|
// Conditionally set up the input handler since we don't know exactly how we were called.
|
||||||
|
// If the combobox is rendered later, then you'll need to call this.registerInputHandlers() manually.
|
||||||
|
// An example of this is the collapse columns in the gradebook.
|
||||||
if (this.searchInput !== null) {
|
if (this.searchInput !== null) {
|
||||||
this.registerInputHandlers();
|
this.registerInputHandlers();
|
||||||
|
this.registerChangeHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a search term, show the clear button.
|
||||||
|
if (this.getSearchTerm() !== '') {
|
||||||
|
this.clearSearchButton.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,9 +136,10 @@ export default class {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub out a required function.
|
* Stub out a required function.
|
||||||
|
* @deprecated since Moodle 4.4
|
||||||
*/
|
*/
|
||||||
triggerSelector() {
|
triggerSelector() {
|
||||||
throw new Error(`triggerSelector() must be implemented in ${this.constructor.name}`);
|
window.console.warning('triggerSelector() is deprecated. Consider using this.selectors.toggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,9 +233,9 @@ export default class {
|
||||||
*/
|
*/
|
||||||
closeSearch(clear = false) {
|
closeSearch(clear = false) {
|
||||||
this.toggleDropdown();
|
this.toggleDropdown();
|
||||||
// Hide the "clear" search button search bar.
|
|
||||||
this.clearSearchButton.classList.add('d-none');
|
|
||||||
if (clear) {
|
if (clear) {
|
||||||
|
// Hide the "clear" search button search bar.
|
||||||
|
this.clearSearchButton.classList.add('d-none');
|
||||||
// Clear the entered search query in the search bar and hide the search results container.
|
// Clear the entered search query in the search bar and hide the search results container.
|
||||||
this.setSearchTerms('');
|
this.setSearchTerms('');
|
||||||
this.searchInput.value = "";
|
this.searchInput.value = "";
|
||||||
|
@ -254,13 +259,10 @@ export default class {
|
||||||
* @param {Boolean} on Flag to toggle hiding or showing values.
|
* @param {Boolean} on Flag to toggle hiding or showing values.
|
||||||
*/
|
*/
|
||||||
toggleDropdown(on = false) {
|
toggleDropdown(on = false) {
|
||||||
this.$component.dropdown('toggle');
|
|
||||||
if (on) {
|
if (on) {
|
||||||
this.searchDropdown.classList.add('show');
|
$(this.toggle).dropdown('show');
|
||||||
$(this.searchDropdown).show();
|
|
||||||
} else {
|
} else {
|
||||||
this.searchDropdown.classList.remove('show');
|
$(this.toggle).dropdown('hide');
|
||||||
$(this.searchDropdown).hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,15 +287,11 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register key event listeners.
|
* Register change event listeners.
|
||||||
*/
|
*/
|
||||||
registerKeyHandlers() {
|
registerChangeHandlers() {
|
||||||
CustomEvents.define(document, events);
|
const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);
|
||||||
|
valueElement.addEventListener('change', this.changeHandler.bind(this));
|
||||||
// Register click events.
|
|
||||||
events.forEach((event) => {
|
|
||||||
this.component.addEventListener(event, this.keyHandler.bind(this));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,22 +306,17 @@ export default class {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setSearchTerms(this.searchInput.value);
|
this.setSearchTerms(this.searchInput.value);
|
||||||
// We can also require a set amount of input before search.
|
|
||||||
|
const pendingPromise = new Pending();
|
||||||
if (this.getSearchTerm() === '') {
|
if (this.getSearchTerm() === '') {
|
||||||
this.toggleDropdown();
|
this.toggleDropdown();
|
||||||
// Hide the "clear" search button in the search bar.
|
|
||||||
this.clearSearchButton.classList.add('d-none');
|
this.clearSearchButton.classList.add('d-none');
|
||||||
|
await this.filterrenderpipe();
|
||||||
} else {
|
} else {
|
||||||
const pendingPromise = new Pending();
|
this.clearSearchButton.classList.remove('d-none');
|
||||||
await this.renderAndShow().then(() => {
|
await this.renderAndShow();
|
||||||
// Display the "clear" search button in the search bar.
|
|
||||||
this.clearSearchButton.classList.remove('d-none');
|
|
||||||
return;
|
|
||||||
}).then(() => {
|
|
||||||
pendingPromise.resolve();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
pendingPromise.resolve();
|
||||||
}, 300, {pending: true}));
|
}, 300, {pending: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,48 +347,6 @@ export default class {
|
||||||
this.toggleDropdown(true);
|
this.toggleDropdown(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current focus either on the preceding or next result item.
|
|
||||||
*
|
|
||||||
* @param {Number} direction Is the user moving up or down the resultset?
|
|
||||||
* @param {KeyboardEvent} e The JS event from the event handler.
|
|
||||||
*/
|
|
||||||
keyUpDown(direction, e) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Stop Bootstrap from being clever.
|
|
||||||
e.stopPropagation();
|
|
||||||
// Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.
|
|
||||||
if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {
|
|
||||||
if (direction === UP) {
|
|
||||||
this.moveToLastNode();
|
|
||||||
} else {
|
|
||||||
this.moveToFirstNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const index = this.resultNodes.indexOf(this.currentNode);
|
|
||||||
if (this.currentNode) {
|
|
||||||
if (direction === UP) {
|
|
||||||
if (index === 0) {
|
|
||||||
this.moveToLastNode();
|
|
||||||
} else {
|
|
||||||
this.moveToNode(index - 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (index + 1 >= this.resultNodes.length) {
|
|
||||||
this.moveToFirstNode();
|
|
||||||
} else {
|
|
||||||
this.moveToNode(index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (direction === UP) {
|
|
||||||
this.moveToLastNode();
|
|
||||||
} else {
|
|
||||||
this.moveToFirstNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handler for when a user interacts with the component.
|
* The handler for when a user interacts with the component.
|
||||||
*
|
*
|
||||||
|
@ -403,90 +354,30 @@ export default class {
|
||||||
*/
|
*/
|
||||||
async clickHandler(e) {
|
async clickHandler(e) {
|
||||||
this.updateNodes();
|
this.updateNodes();
|
||||||
|
|
||||||
// Prevent normal key presses activating this.
|
|
||||||
if (e.target.closest('.dropdown-item') && e.button === 0) {
|
|
||||||
window.location = e.target.closest('.dropdown-item').href;
|
|
||||||
}
|
|
||||||
// The "clear search" button is triggered.
|
// The "clear search" button is triggered.
|
||||||
if (e.target.closest(this.selectors.clearSearch) && e.button === 0) {
|
if (e.target.closest(this.selectors.clearSearch)) {
|
||||||
this.closeSearch(true);
|
this.closeSearch(true);
|
||||||
this.searchInput.focus({preventScroll: true});
|
this.searchInput.focus();
|
||||||
|
// Remove aria-activedescendant when the available options change.
|
||||||
|
this.searchInput.removeAttribute('aria-activedescendant');
|
||||||
}
|
}
|
||||||
// User may have accidentally clicked off the dropdown and wants to reopen it.
|
// User may have accidentally clicked off the dropdown and wants to reopen it.
|
||||||
if (e.target.closest(this.selectors.input) && this.getSearchTerm() !== '' && e.button === 0) {
|
if (
|
||||||
|
this.getSearchTerm() !== ''
|
||||||
|
&& !this.getHTMLElements().searchDropdown.classList.contains('show')
|
||||||
|
&& e.target.closest(this.selectors.input)
|
||||||
|
) {
|
||||||
await this.renderAndShow();
|
await this.renderAndShow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handler for when a user presses a key within the component.
|
* The handler for when a user changes the value of the component (selects an option from the dropdown).
|
||||||
*
|
*
|
||||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
* @param {Event} e The change event.
|
||||||
*/
|
*/
|
||||||
keyHandler(e) {
|
// eslint-disable-next-line no-unused-vars
|
||||||
this.updateNodes();
|
changeHandler(e) {
|
||||||
// Switch the key presses to handle keyboard nav.
|
// Components may override this method to do something.
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowUp':
|
|
||||||
this.keyUpDown(UP, e);
|
|
||||||
break;
|
|
||||||
case 'ArrowDown':
|
|
||||||
this.keyUpDown(DOWN, e);
|
|
||||||
break;
|
|
||||||
case 'Home':
|
|
||||||
e.preventDefault();
|
|
||||||
this.moveToFirstNode();
|
|
||||||
break;
|
|
||||||
case 'End':
|
|
||||||
e.preventDefault();
|
|
||||||
this.moveToLastNode();
|
|
||||||
break;
|
|
||||||
case 'Tab':
|
|
||||||
// If the current focus is on the view all link, then close the widget then set focus on the next tertiary nav item.
|
|
||||||
if (e.target.closest(this.selectors.viewall)) {
|
|
||||||
this.closeSearch();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set focus on a given node after parsed through the calling functions.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} node The node to set focus upon.
|
|
||||||
*/
|
|
||||||
selectNode = (node) => {
|
|
||||||
node.focus({preventScroll: true});
|
|
||||||
this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the focus on the first node within the array.
|
|
||||||
*/
|
|
||||||
moveToFirstNode = () => {
|
|
||||||
if (this.resultNodes.length > 0) {
|
|
||||||
this.selectNode(this.resultNodes[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the focus to the final node within the array.
|
|
||||||
*/
|
|
||||||
moveToLastNode = () => {
|
|
||||||
if (this.resultNodes.length > 0) {
|
|
||||||
this.selectNode(this.resultNodes[this.resultNodes.length - 1]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set focus on any given specified node within the node array.
|
|
||||||
*
|
|
||||||
* @param {Number} index Which item within the array to set focus upon.
|
|
||||||
*/
|
|
||||||
moveToNode = (index) => {
|
|
||||||
if (this.resultNodes.length > 0) {
|
|
||||||
this.selectNode(this.resultNodes[index]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,12 @@ export default {
|
||||||
hidden: '[aria-hidden]',
|
hidden: '[aria-hidden]',
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
focusable: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
|
focusable: 'input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),' +
|
||||||
|
'a[href]:not([disabled]):not([tabindex^="-"]),' +
|
||||||
|
'button:not([disabled]):not([tabindex^="-"]),' +
|
||||||
|
'textarea:not([disabled]):not([tabindex^="-"]),' +
|
||||||
|
'select:not([disabled]):not([tabindex^="-"]),' +
|
||||||
|
'[tabindex]:not([disabled]):not([tabindex^="-"])',
|
||||||
focusableToUnhide: '[data-aria-hidden-tab-index]',
|
focusableToUnhide: '[data-aria-hidden-tab-index]',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -80,6 +80,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||||
'group_message' => 'group_message',
|
'group_message' => 'group_message',
|
||||||
'autocomplete' => 'autocomplete',
|
'autocomplete' => 'autocomplete',
|
||||||
'iframe' => 'iframe',
|
'iframe' => 'iframe',
|
||||||
|
'option_role' => 'option_role',
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -30,7 +30,9 @@ use templatable;
|
||||||
*/
|
*/
|
||||||
class comboboxsearch implements renderable, templatable {
|
class comboboxsearch implements renderable, templatable {
|
||||||
|
|
||||||
/** @var bool $renderlater Should the dropdown render straightaway? */
|
/** @var bool $renderlater Should the dropdown render straightaway? We sometimes need to output the component without all of the
|
||||||
|
* data and leave the rendering of any defaults and actual data to the caller. We will give you a basic placeholder that can
|
||||||
|
* then be easily replaced.*/
|
||||||
protected $renderlater;
|
protected $renderlater;
|
||||||
|
|
||||||
/** @var string $buttoncontent What is the content of the "Button" that users will always see. */
|
/** @var string $buttoncontent What is the content of the "Button" that users will always see. */
|
||||||
|
@ -54,18 +56,30 @@ class comboboxsearch implements renderable, templatable {
|
||||||
/** @var boolean $usesbutton Whether to provide a A11y button. */
|
/** @var boolean $usesbutton Whether to provide a A11y button. */
|
||||||
protected $usesbutton;
|
protected $usesbutton;
|
||||||
|
|
||||||
|
/** @var null|string $label The label of the combobox. */
|
||||||
|
protected $label;
|
||||||
|
|
||||||
|
/** @var null|string $name The name of the input element representing the combobox. */
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/** @var null|string $value The value of the input element representing the combobox. */
|
||||||
|
protected $value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class constructor.
|
* The class constructor.
|
||||||
*
|
*
|
||||||
* @param bool $renderlater How we figure out if we should render the template instantly.
|
* @param bool $renderlater How we figure out if we should render the template instantly.
|
||||||
* @param string $buttoncontent What gets placed in the button.
|
* @param string $buttoncontent What gets placed in the button.
|
||||||
* @param ?string $dropdowncontent What can be placed in the dropdown if we are rendering now.
|
* @param ?string $dropdowncontent What will be placed in the dropdown if we are rendering now.
|
||||||
* @param ?string $parentclasses The classes that can be added that the bootstrap events are attached to.
|
* @param ?string $parentclasses The classes that can be added that the bootstrap events are attached to.
|
||||||
* @param ?string $buttonclasses Any special classes that may be needed.
|
* @param ?string $buttonclasses Any special classes that may be needed.
|
||||||
* @param ?string $dropdownclasses Any special classes that may be needed.
|
* @param ?string $dropdownclasses Any special classes that may be needed.
|
||||||
* @param ?string $buttonheader If the button item in the tertiary nav needs an extra top header for context.
|
* @param ?string $buttonheader Sometimes we want extra context for a button before it is shown, basically a pseudo header.
|
||||||
* @param bool $usebutton If we want the mustache to add the button roles for us or do we have another aria role node?
|
* @param ?bool $usebutton If we want the mustache to add the button roles for us or do we have another aria role node?
|
||||||
* @throws moodle_exception If the implementor incorrectly call this module.
|
* @param ?string $label The label of the combobox.
|
||||||
|
* @param ?string $name The name of the input element representing the combobox.
|
||||||
|
* @param ?string $value The value of the input element representing the combobox.
|
||||||
|
* @throws moodle_exception If the implementor incorrectly calls this module.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
bool $renderlater,
|
bool $renderlater,
|
||||||
|
@ -75,7 +89,10 @@ class comboboxsearch implements renderable, templatable {
|
||||||
?string $buttonclasses = null,
|
?string $buttonclasses = null,
|
||||||
?string $dropdownclasses = null,
|
?string $dropdownclasses = null,
|
||||||
?string $buttonheader = null,
|
?string $buttonheader = null,
|
||||||
?bool $usebutton = true
|
?bool $usebutton = true,
|
||||||
|
?string $label = null,
|
||||||
|
?string $name = null,
|
||||||
|
?string $value = null
|
||||||
) {
|
) {
|
||||||
// Ensure implementors cant request to render the content now and not provide us any to show.
|
// Ensure implementors cant request to render the content now and not provide us any to show.
|
||||||
if (!$renderlater && empty($dropdowncontent)) {
|
if (!$renderlater && empty($dropdowncontent)) {
|
||||||
|
@ -87,6 +104,20 @@ class comboboxsearch implements renderable, templatable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($usebutton && !$label) {
|
||||||
|
debugging(
|
||||||
|
'You have requested to use the button but have not provided a label for the combobox.',
|
||||||
|
DEBUG_DEVELOPER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($usebutton && !$name) {
|
||||||
|
debugging(
|
||||||
|
'You have requested to use the button but have not provided a name for the input element.',
|
||||||
|
DEBUG_DEVELOPER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->renderlater = $renderlater;
|
$this->renderlater = $renderlater;
|
||||||
$this->buttoncontent = $buttoncontent;
|
$this->buttoncontent = $buttoncontent;
|
||||||
$this->dropdowncontent = $dropdowncontent;
|
$this->dropdowncontent = $dropdowncontent;
|
||||||
|
@ -95,6 +126,9 @@ class comboboxsearch implements renderable, templatable {
|
||||||
$this->dropdownclasses = $dropdownclasses;
|
$this->dropdownclasses = $dropdownclasses;
|
||||||
$this->buttonheader = $buttonheader;
|
$this->buttonheader = $buttonheader;
|
||||||
$this->usesbutton = $usebutton;
|
$this->usesbutton = $usebutton;
|
||||||
|
$this->label = $label;
|
||||||
|
$this->name = $name;
|
||||||
|
$this->value = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,7 +139,6 @@ class comboboxsearch implements renderable, templatable {
|
||||||
*/
|
*/
|
||||||
public function export_for_template(renderer_base $output): array {
|
public function export_for_template(renderer_base $output): array {
|
||||||
return [
|
return [
|
||||||
'rtl' => right_to_left(),
|
|
||||||
'renderlater' => $this->renderlater,
|
'renderlater' => $this->renderlater,
|
||||||
'buttoncontent' => $this->buttoncontent ,
|
'buttoncontent' => $this->buttoncontent ,
|
||||||
'dropdowncontent' => $this->dropdowncontent,
|
'dropdowncontent' => $this->dropdowncontent,
|
||||||
|
@ -115,6 +148,9 @@ class comboboxsearch implements renderable, templatable {
|
||||||
'buttonheader' => $this->buttonheader,
|
'buttonheader' => $this->buttonheader,
|
||||||
'usebutton' => $this->usesbutton,
|
'usebutton' => $this->usesbutton,
|
||||||
'instance' => rand(), // Template uniqid is per render out so sometimes these conflict.
|
'instance' => rand(), // Template uniqid is per render out so sometimes these conflict.
|
||||||
|
'label' => $this->label,
|
||||||
|
'name' => $this->name,
|
||||||
|
'value' => $this->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
Combobox search selector dropdown.
|
Combobox search selector dropdown.
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
* rtl - Is this dropdown being used in a RTL case? if so, we need to ensure it drops down in the right place.
|
* label - The label for the the combobox.
|
||||||
|
* name - The name of the input element representing the combobox.
|
||||||
|
* value - The value of the input element representing the combobox.
|
||||||
* renderlater - This determines if we show a placeholder whilst fetching content to replace within the placeholder region
|
* renderlater - This determines if we show a placeholder whilst fetching content to replace within the placeholder region
|
||||||
|
* buttonheader - The header to be shown above the button
|
||||||
* buttoncontent - The string to be shown to users to trigger the dropdown
|
* buttoncontent - The string to be shown to users to trigger the dropdown
|
||||||
* usebutton - If we want to use a button to trigger the dropdown, or just the dropdown itself
|
* usebutton - If we want to use a button to trigger the dropdown, or just the dropdown itself
|
||||||
* dropdowncontent - If rendering now, The content within the dropdown
|
* dropdowncontent - If rendering now, The content within the dropdown
|
||||||
|
@ -29,10 +32,13 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
"rtl": false,
|
"label": "Example searchable combobox",
|
||||||
|
"name": "input-1",
|
||||||
|
"value": "0",
|
||||||
"renderlater": false,
|
"renderlater": false,
|
||||||
|
"buttonheader": "Example:",
|
||||||
"usebutton": true,
|
"usebutton": true,
|
||||||
"buttoncontent": "Example dropdown button",
|
"buttoncontent": "Dropdown button",
|
||||||
"dropdowncontent": "Some body content to render right now",
|
"dropdowncontent": "Some body content to render right now",
|
||||||
"parentclasses": "my-dropdown",
|
"parentclasses": "my-dropdown",
|
||||||
"buttonclasses": "my-button",
|
"buttonclasses": "my-button",
|
||||||
|
@ -43,17 +49,7 @@
|
||||||
{{#buttonheader}}
|
{{#buttonheader}}
|
||||||
<small>{{.}}</small>
|
<small>{{.}}</small>
|
||||||
{{/buttonheader}}
|
{{/buttonheader}}
|
||||||
<div class="{{#parentclasses}}{{.}}{{/parentclasses}}"
|
<div class="{{#parentclasses}}{{.}}{{/parentclasses}} dropdown" data-instance="{{instance}}">
|
||||||
{{^usebutton}}
|
|
||||||
tabindex="-1"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
role="combobox"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-controls="dialog-{{instance}}-{{uniqid}}"
|
|
||||||
aria-label="{{#cleanstr}} aria-toggledropdown, core_grades {{/cleanstr}}"
|
|
||||||
data-input-element="input-{{uniqid}}"
|
|
||||||
{{/usebutton}}>
|
|
||||||
|
|
||||||
{{#usebutton}}
|
{{#usebutton}}
|
||||||
<div tabindex="0"
|
<div tabindex="0"
|
||||||
|
@ -62,20 +58,23 @@
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-controls="dialog-{{instance}}-{{uniqid}}"
|
aria-controls="dialog-{{instance}}-{{uniqid}}"
|
||||||
class="{{#buttonclasses}}{{.}}{{/buttonclasses}} btn dropdown-toggle keep-open d-flex text-left align-items-center p-0 font-weight-bold"
|
class="{{#buttonclasses}}{{.}}{{/buttonclasses}} btn dropdown-toggle d-flex text-left align-items-center p-0 font-weight-bold"
|
||||||
aria-label="{{#cleanstr}} aria-toggledropdown, core_grades {{/cleanstr}}"
|
aria-label="{{label}}"
|
||||||
data-input-element="input-{{uniqid}}">
|
data-input-element="input-{{instance}}-{{uniqid}}">
|
||||||
{{{buttoncontent}}}
|
{{{buttoncontent}}}
|
||||||
</div>
|
</div>
|
||||||
|
<input type="hidden" name="{{name}}" value="{{value}}" id="input-{{instance}}-{{uniqid}}"/>
|
||||||
{{/usebutton}}
|
{{/usebutton}}
|
||||||
|
|
||||||
{{^usebutton}}{{{buttoncontent}}}{{/usebutton}}
|
{{^usebutton}}{{{buttoncontent}}}{{/usebutton}}
|
||||||
|
|
||||||
<div class="{{#dropdownclasses}}{{.}}{{/dropdownclasses}} dropdown-menu {{#rtl}}dropdown-menu-right{{/rtl}}"
|
<div class="{{#dropdownclasses}}{{.}}{{/dropdownclasses}} dropdown-menu"
|
||||||
id="dialog-{{instance}}-{{uniqid}}"
|
id="dialog-{{instance}}-{{uniqid}}"
|
||||||
role="dialog"
|
{{#usebutton}}
|
||||||
aria-modal="true"
|
role="dialog"
|
||||||
aria-label="{{#cleanstr}} selectagroup, core {{/cleanstr}}"
|
aria-modal="true"
|
||||||
|
aria-label="{{label}}"
|
||||||
|
{{/usebutton}}
|
||||||
>
|
>
|
||||||
<div class="w-100 p-3" data-region="placeholder">
|
<div class="w-100 p-3" data-region="placeholder">
|
||||||
{{#renderlater}}
|
{{#renderlater}}
|
||||||
|
|
|
@ -18,22 +18,31 @@
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
* id - Result item system ID.
|
* id - Result item system ID.
|
||||||
|
* instance - The instance ID of the combo box.
|
||||||
* name - Result item human readable name.
|
* name - Result item human readable name.
|
||||||
* link - The link used to redirect upon self to show only this specific result item.
|
|
||||||
|
The shorttext pragma can be used to provide a short text for the result item.
|
||||||
|
This is especially useful when the result item content contains html tags.
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "Foo bar",
|
"instance": 25,
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
"name": "Foo bar"
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<li class="w-100 result-row" role="none" id="result-row-{{id}}">
|
<li
|
||||||
<a role="option" id="result-{{id}}" href="{{{link}}}" class="dropdown-item d-flex px-0 py-1 align-items-center" tabindex="-1" aria-label="{{$arialabel}}{{#str}}viewresults, core, {{name}}{{/str}}{{/arialabel}}">
|
id="result-{{instance}}-{{id}}"
|
||||||
{{$content}}
|
class="w-100 dropdown-item d-flex px-0 align-items-center"
|
||||||
<span class="d-block w-100 px-2 text-truncate">
|
role="option"
|
||||||
{{name}}
|
{{! aria.js would use the data-short-text attribute if it is provided. }}
|
||||||
</span>
|
data-short-text="{{$shorttext}}{{/shorttext}}"
|
||||||
{{/content}}
|
data-value="{{id}}"
|
||||||
</a>
|
aria-label="{{$arialabel}}{{name}}{{/arialabel}}"
|
||||||
|
>
|
||||||
|
{{$content}}
|
||||||
|
<span class="d-block w-100 px-2 text-truncate">
|
||||||
|
{{name}}
|
||||||
|
</span>
|
||||||
|
{{/content}}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -17,23 +17,22 @@
|
||||||
Wrapping template for returned result items.
|
Wrapping template for returned result items.
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
|
* instance - The instance ID of the combo box.
|
||||||
* results - Our returned results to render.
|
* results - Our returned results to render.
|
||||||
* searchterm - The entered text to find these results.
|
* searchterm - The entered text to find these results.
|
||||||
* hasresult - Allow the handling where no results exist for the returned search term.
|
* hasresult - Allow the handling where no results exist for the returned search term.
|
||||||
* noresults - Our fall through case if nothing matches.
|
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"instance": 25,
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "Foo bar",
|
"name": "Foo bar"
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Bar Foo",
|
"name": "Bar Foo"
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"searchterm": "Foo",
|
"searchterm": "Foo",
|
||||||
|
@ -41,17 +40,22 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<div class="d-flex flex-column mh-100 h-100">
|
<div class="d-flex flex-column mh-100 h-100">
|
||||||
{{#hasresults}}
|
<ul
|
||||||
<ul id="{{$listid}}list{{/listid}}-result-listbox" class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2 list-group h-100 mx-0 {{$listclasses}}{{/listclasses}}" role="listbox" data-region="search-result-items-container" tabindex="-1" aria-label="{{#cleanstr}} aria:dropdowngrades, core_grades {{/cleanstr}}">
|
id="{{$listid}}list{{/listid}}-{{instance}}-result-listbox"
|
||||||
|
class="searchresultitemscontainer d-flex flex-column mw-100 position-relative py-2 list-group h-100 mx-0 {{$listclasses}}{{/listclasses}}"
|
||||||
|
role="listbox"
|
||||||
|
data-region="search-result-items-container"
|
||||||
|
>
|
||||||
|
{{#hasresults}}
|
||||||
{{$results}}
|
{{$results}}
|
||||||
{{#results}}
|
{{#results}}
|
||||||
{{>core/local/comboboxsearch/resultitem}}
|
{{>core/local/comboboxsearch/resultitem}}
|
||||||
{{/results}}
|
{{/results}}
|
||||||
{{/results}}
|
{{/results}}
|
||||||
{{$selectall}}{{/selectall}}
|
{{$selectall}}{{/selectall}}
|
||||||
</ul>
|
{{/hasresults}}
|
||||||
{{/hasresults}}
|
</ul>
|
||||||
{{^hasresults}}
|
{{^hasresults}}
|
||||||
<span class="small d-block px-4 my-4">{{#str}} noresultsfor, core, {{searchterm}}{{/str}}</span>
|
<span class="small d-block px-4 mt-2 mb-4">{{#str}} noresultsfor, core, {{searchterm}}{{/str}}</span>
|
||||||
{{/hasresults}}
|
{{/hasresults}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,39 +28,22 @@
|
||||||
}}
|
}}
|
||||||
<div class="flex-column h-100 w-100">
|
<div class="flex-column h-100 w-100">
|
||||||
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}" aria-hidden="true"></span>
|
||||||
|
{{< core/search_input_auto }}
|
||||||
{{#currentvalue}}
|
{{$label}}{{#str}}
|
||||||
{{< core/search_input_auto }}
|
searchitems, core
|
||||||
{{$label}}{{#str}}
|
{{/str}}{{/label}}
|
||||||
searchitems, core
|
{{$placeholder}}{{#str}}
|
||||||
{{/str}}{{/label}}
|
searchitems, core
|
||||||
{{$value}}{{currentvalue}}{{/value}}
|
{{/str}}{{/placeholder}}
|
||||||
{{$additionalattributes}}
|
{{$value}}{{currentvalue}}{{/value}}
|
||||||
role="combobox"
|
{{$additionalattributes}}
|
||||||
aria-expanded="true"
|
role="combobox"
|
||||||
aria-controls="list-result-listbox"
|
aria-expanded="true"
|
||||||
aria-autocomplete="list"
|
aria-controls="list-{{instance}}-result-listbox"
|
||||||
data-input-element="result-input-{{uniqid}}"
|
aria-autocomplete="list"
|
||||||
{{/additionalattributes}}
|
data-input-element="result-input-{{uniqid}}-{{instance}}"
|
||||||
{{/ core/search_input_auto }}
|
{{/additionalattributes}}
|
||||||
{{/currentvalue}}
|
{{/ core/search_input_auto }}
|
||||||
{{^currentvalue}}
|
<input type="hidden" name="search" id="result-input-{{uniqid}}-{{instance}}"/>
|
||||||
{{< core/search_input_auto }}
|
|
||||||
{{$label}}{{#str}}
|
|
||||||
searchitems, core
|
|
||||||
{{/str}}{{/label}}
|
|
||||||
{{$placeholder}}{{#str}}
|
|
||||||
searchitems, core
|
|
||||||
{{/str}}{{/placeholder}}
|
|
||||||
{{$additionalattributes}}
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded="true"
|
|
||||||
aria-controls="list-result-listbox"
|
|
||||||
aria-autocomplete="list"
|
|
||||||
data-input-element="result-input-{{uniqid}}"
|
|
||||||
{{/additionalattributes}}
|
|
||||||
{{/ core/search_input_auto }}
|
|
||||||
{{/currentvalue}}
|
|
||||||
<input type="hidden" name="search" id="result-input-{{uniqid}}"/>
|
|
||||||
<div data-region="searchplaceholder"></div>
|
<div data-region="searchplaceholder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -99,6 +99,7 @@ information provided here is intended especially for developers.
|
||||||
to bool if the original functionality is desired.
|
to bool if the original functionality is desired.
|
||||||
* core\hook\manager::phpunit_get_instance() now sets self::$instance to the mocked instance if the optional $persist argument is
|
* core\hook\manager::phpunit_get_instance() now sets self::$instance to the mocked instance if the optional $persist argument is
|
||||||
true, so future calls to ::get_instance() will return it.
|
true, so future calls to ::get_instance() will return it.
|
||||||
|
* The triggerSelector method in the `core/comboboxsearch/search_combobox` JS module is deprecated. It was not used.
|
||||||
|
|
||||||
=== 4.3 ===
|
=== 4.3 ===
|
||||||
|
|
||||||
|
|
4
theme/boost/amd/build/aria.min.js
vendored
4
theme/boost/amd/build/aria.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
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Pending from 'core/pending';
|
import Pending from 'core/pending';
|
||||||
|
import * as FocusLockManager from 'core/local/aria/focuslock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop downs from bootstrap don't support keyboard accessibility by default.
|
* Drop downs from bootstrap don't support keyboard accessibility by default.
|
||||||
|
@ -78,13 +79,10 @@ const dropdownFix = () => {
|
||||||
const menu = e.target.parentElement.querySelector('[role="menu"]');
|
const menu = e.target.parentElement.querySelector('[role="menu"]');
|
||||||
let menuItems = false;
|
let menuItems = false;
|
||||||
let foundMenuItem = false;
|
let foundMenuItem = false;
|
||||||
let textInput = false;
|
|
||||||
|
|
||||||
if (menu) {
|
if (menu) {
|
||||||
menuItems = menu.querySelectorAll('[role="menuitem"]');
|
menuItems = menu.querySelectorAll('[role="menuitem"]');
|
||||||
textInput = e.target.parentElement.querySelector('[data-action="search"]');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuItems && menuItems.length > 0) {
|
if (menuItems && menuItems.length > 0) {
|
||||||
// Up key opens the menu at the end.
|
// Up key opens the menu at the end.
|
||||||
if (trigger === 'ArrowUp') {
|
if (trigger === 'ArrowUp') {
|
||||||
|
@ -101,10 +99,7 @@ const dropdownFix = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textInput) {
|
if (foundMenuItem) {
|
||||||
shiftFocus(textInput);
|
|
||||||
}
|
|
||||||
if (foundMenuItem && textInput === null) {
|
|
||||||
shiftFocus(foundMenuItem);
|
shiftFocus(foundMenuItem);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -198,10 +193,26 @@ const dropdownFix = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.dropdown').on('shown.bs.dropdown', e => {
|
||||||
|
const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`);
|
||||||
|
if (dialog) {
|
||||||
|
// Use setTimeout to make sure the dialog is positioned correctly to prevent random scrolling.
|
||||||
|
setTimeout(() => {
|
||||||
|
FocusLockManager.trapFocus(dialog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('.dropdown').on('hidden.bs.dropdown', e => {
|
$('.dropdown').on('hidden.bs.dropdown', e => {
|
||||||
|
const dialog = e.target.querySelector(`#${e.relatedTarget.getAttribute('aria-controls')}[role="dialog"]`);
|
||||||
|
if (dialog) {
|
||||||
|
FocusLockManager.untrapFocus();
|
||||||
|
}
|
||||||
|
|
||||||
// We need to focus on the menu trigger.
|
// We need to focus on the menu trigger.
|
||||||
const trigger = e.target.querySelector('[data-toggle="dropdown"]');
|
const trigger = e.target.querySelector('[data-toggle="dropdown"]');
|
||||||
const focused = document.activeElement != document.body ? document.activeElement : null;
|
// If it's a click event, then no element is focused because the clicked element is inside a closed dropdown.
|
||||||
|
const focused = e.clickEvent?.target || (document.activeElement !== document.body ? document.activeElement : null);
|
||||||
if (trigger && focused && e.target.contains(focused)) {
|
if (trigger && focused && e.target.contains(focused)) {
|
||||||
shiftFocus(trigger, () => {
|
shiftFocus(trigger, () => {
|
||||||
if (document.activeElement === document.body) {
|
if (document.activeElement === document.body) {
|
||||||
|
@ -297,9 +308,9 @@ const comboboxFix = () => {
|
||||||
if (editable && !next) {
|
if (editable && !next) {
|
||||||
next = options[options.length - 1];
|
next = options[options.length - 1];
|
||||||
}
|
}
|
||||||
} else if (trigger == 'Home') {
|
} else if (trigger == 'Home' && !editable) {
|
||||||
next = options[0];
|
next = options[0];
|
||||||
} else if (trigger == 'End') {
|
} else if (trigger == 'End' && !editable) {
|
||||||
next = options[options.length - 1];
|
next = options[options.length - 1];
|
||||||
} else if ((trigger == ' ' && !editable) || trigger == 'Enter') {
|
} else if ((trigger == ' ' && !editable) || trigger == 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -338,7 +349,6 @@ const comboboxFix = () => {
|
||||||
const listbox = option.closest('[role="listbox"]');
|
const listbox = option.closest('[role="listbox"]');
|
||||||
const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`);
|
const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`);
|
||||||
if (combobox) {
|
if (combobox) {
|
||||||
combobox.focus();
|
|
||||||
selectOption(combobox, option);
|
selectOption(combobox, option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -368,9 +378,9 @@ const comboboxFix = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (combobox.hasAttribute('value')) {
|
if (combobox.hasAttribute('value')) {
|
||||||
combobox.value = option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
|
combobox.value = option.dataset.shortText || option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
|
||||||
} else {
|
} else {
|
||||||
combobox.textContent = option.textContent;
|
combobox.textContent = option.dataset.shortText || option.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (combobox.dataset.inputElement) {
|
if (combobox.dataset.inputElement) {
|
||||||
|
|
|
@ -3121,14 +3121,16 @@ blockquote {
|
||||||
.usersearchdropdown,
|
.usersearchdropdown,
|
||||||
.gradesearchdropdown,
|
.gradesearchdropdown,
|
||||||
.groupsearchdropdown {
|
.groupsearchdropdown {
|
||||||
max-width: 350px;
|
&.dropdown-menu {
|
||||||
.searchresultitemscontainer {
|
width: 350px;
|
||||||
max-height: 170px;
|
.searchresultitemscontainer {
|
||||||
overflow: auto;
|
max-height: 170px;
|
||||||
/* stylelint-disable declaration-no-important */
|
overflow: auto;
|
||||||
img {
|
/* stylelint-disable declaration-no-important */
|
||||||
height: 48px !important;
|
img {
|
||||||
width: 48px !important;
|
height: 48px !important;
|
||||||
|
width: 48px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26026,21 +26026,21 @@ blockquote {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Combobox search dropdowns */
|
/* Combobox search dropdowns */
|
||||||
.usersearchdropdown,
|
.usersearchdropdown.dropdown-menu,
|
||||||
.gradesearchdropdown,
|
.gradesearchdropdown.dropdown-menu,
|
||||||
.groupsearchdropdown {
|
.groupsearchdropdown.dropdown-menu {
|
||||||
max-width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
.usersearchdropdown .searchresultitemscontainer,
|
.usersearchdropdown.dropdown-menu .searchresultitemscontainer,
|
||||||
.gradesearchdropdown .searchresultitemscontainer,
|
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer,
|
||||||
.groupsearchdropdown .searchresultitemscontainer {
|
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer {
|
||||||
max-height: 170px;
|
max-height: 170px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
/* stylelint-disable declaration-no-important */
|
/* stylelint-disable declaration-no-important */
|
||||||
}
|
}
|
||||||
.usersearchdropdown .searchresultitemscontainer img,
|
.usersearchdropdown.dropdown-menu .searchresultitemscontainer img,
|
||||||
.gradesearchdropdown .searchresultitemscontainer img,
|
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img,
|
||||||
.groupsearchdropdown .searchresultitemscontainer img {
|
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img {
|
||||||
height: 48px !important;
|
height: 48px !important;
|
||||||
width: 48px !important;
|
width: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26026,21 +26026,21 @@ blockquote {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Combobox search dropdowns */
|
/* Combobox search dropdowns */
|
||||||
.usersearchdropdown,
|
.usersearchdropdown.dropdown-menu,
|
||||||
.gradesearchdropdown,
|
.gradesearchdropdown.dropdown-menu,
|
||||||
.groupsearchdropdown {
|
.groupsearchdropdown.dropdown-menu {
|
||||||
max-width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
.usersearchdropdown .searchresultitemscontainer,
|
.usersearchdropdown.dropdown-menu .searchresultitemscontainer,
|
||||||
.gradesearchdropdown .searchresultitemscontainer,
|
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer,
|
||||||
.groupsearchdropdown .searchresultitemscontainer {
|
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer {
|
||||||
max-height: 170px;
|
max-height: 170px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
/* stylelint-disable declaration-no-important */
|
/* stylelint-disable declaration-no-important */
|
||||||
}
|
}
|
||||||
.usersearchdropdown .searchresultitemscontainer img,
|
.usersearchdropdown.dropdown-menu .searchresultitemscontainer img,
|
||||||
.gradesearchdropdown .searchresultitemscontainer img,
|
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img,
|
||||||
.groupsearchdropdown .searchresultitemscontainer img {
|
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img {
|
||||||
height: 48px !important;
|
height: 48px !important;
|
||||||
width: 48px !important;
|
width: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
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 {getStrings} from 'core/str';
|
||||||
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
import {renderForPromise, replaceNodeContents} from 'core/templates';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Notification from 'core/notification';
|
|
||||||
|
|
||||||
export default class UserSearch extends search_combobox {
|
export default class UserSearch extends search_combobox {
|
||||||
|
|
||||||
|
@ -36,14 +35,19 @@ export default class UserSearch extends search_combobox {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
// Register a small click event onto the document since we need to check if they are clicking off the component.
|
// Register a couple of events onto the document since we need to check if they are moving off the component.
|
||||||
document.addEventListener('click', (e) => {
|
['click', 'focus'].forEach(eventType => {
|
||||||
// Since we are handling dropdowns manually, ensure we can close it when clicking off.
|
// Since we are handling dropdowns manually, ensure we can close it when moving off.
|
||||||
if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {
|
document.addEventListener(eventType, e => {
|
||||||
this.toggleDropdown();
|
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.
|
// Define our standard lookups.
|
||||||
this.selectors = {...this.selectors,
|
this.selectors = {...this.selectors,
|
||||||
courseid: '[data-region="courseid"]',
|
courseid: '[data-region="courseid"]',
|
||||||
|
@ -51,9 +55,12 @@ export default class UserSearch extends search_combobox {
|
||||||
resetPageButton: '[data-action="resetpage"]',
|
resetPageButton: '[data-action="resetpage"]',
|
||||||
};
|
};
|
||||||
|
|
||||||
const component = document.querySelector(this.componentSelector());
|
this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;
|
||||||
this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;
|
|
||||||
this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;
|
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() {
|
static init() {
|
||||||
|
@ -78,15 +85,6 @@ export default class UserSearch extends search_combobox {
|
||||||
return '.usersearchdropdown';
|
return '.usersearchdropdown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The triggering div that contains the searching widget.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
triggerSelector() {
|
|
||||||
return '.usersearchwidget';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the content then replace the node.
|
* 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', {
|
const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {
|
||||||
users: this.getMatchedResults().slice(0, 5),
|
users: this.getMatchedResults().slice(0, 5),
|
||||||
hasresults: this.getMatchedResults().length > 0,
|
hasresults: this.getMatchedResults().length > 0,
|
||||||
|
instance: this.instance,
|
||||||
matches: this.getMatchedResults().length,
|
matches: this.getMatchedResults().length,
|
||||||
searchterm: this.getSearchTerm(),
|
searchterm: this.getSearchTerm(),
|
||||||
selectall: this.selectAllResultsLink(),
|
selectall: this.selectAllResultsLink(),
|
||||||
});
|
});
|
||||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
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.
|
* @returns {Array} The users that match the given criteria.
|
||||||
*/
|
*/
|
||||||
async filterDataset(filterableData) {
|
async filterDataset(filterableData) {
|
||||||
const stringMap = await this.getStringMap();
|
if (this.getPreppedSearchTerm()) {
|
||||||
return filterableData.filter((user) => Object.keys(user).some((key) => {
|
const stringMap = await this.getStringMap();
|
||||||
if (user[key] === "" || user[key] === null || !stringMap.get(key)) {
|
return filterableData.filter((user) => Object.keys(user).some((key) => {
|
||||||
return false;
|
if (user[key] === "" || user[key] === null || !stringMap.get(key)) {
|
||||||
}
|
return false;
|
||||||
return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
}
|
||||||
}));
|
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.matchingField = `${escapedMatchingField} (${user.email})`;
|
||||||
user.link = this.selectOneLink(user.id);
|
|
||||||
break;
|
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) {
|
changeHandler(e) {
|
||||||
super.clickHandler(e).catch(Notification.exception);
|
this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.
|
||||||
if (e.target.closest(this.selectors.component)) {
|
|
||||||
// Forcibly prevent BS events so that we can control the open and close.
|
if (e.target.value === '0') {
|
||||||
// Really needed because by default input elements cant trigger a dropdown.
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {
|
|
||||||
window.location = this.selectAllResultsLink();
|
window.location = this.selectAllResultsLink();
|
||||||
}
|
} else {
|
||||||
if (e.target.closest(this.selectors.resetPageButton)) {
|
window.location = this.selectOneLink(e.target.value);
|
||||||
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,50 +202,30 @@ export default class UserSearch extends search_combobox {
|
||||||
* @param {KeyboardEvent} e The triggering event that we are working with.
|
* @param {KeyboardEvent} e The triggering event that we are working with.
|
||||||
*/
|
*/
|
||||||
keyHandler(e) {
|
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 the key presses to handle keyboard nav.
|
||||||
switch (e.key) {
|
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 'Enter':
|
||||||
case ' ':
|
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)) {
|
if (e.target.closest(this.selectors.resetPageButton)) {
|
||||||
|
e.stopPropagation();
|
||||||
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
window.location = e.target.closest(this.selectors.resetPageButton).href;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (e.target.closest('.dropdown-item')) {
|
|
||||||
e.preventDefault();
|
|
||||||
window.location = e.target.closest('.dropdown-item').href;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this.toggleDropdown();
|
this.toggleDropdown();
|
||||||
this.searchInput.focus({preventScroll: true});
|
this.searchInput.focus({preventScroll: true});
|
||||||
break;
|
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) {
|
if (on) {
|
||||||
this.searchDropdown.classList.add('show');
|
this.searchDropdown.classList.add('show');
|
||||||
$(this.searchDropdown).show();
|
$(this.searchDropdown).show();
|
||||||
this.component.setAttribute('aria-expanded', 'true');
|
this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');
|
||||||
|
this.searchInput.focus({preventScroll: true});
|
||||||
} else {
|
} else {
|
||||||
this.searchDropdown.classList.remove('show');
|
this.searchDropdown.classList.remove('show');
|
||||||
$(this.searchDropdown).hide();
|
$(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.
|
* 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.
|
* @param {Number} userID The ID of the user selected.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
* profileimageurl - Link for the users' large profile image.
|
* profileimageurl - Link for the users' large profile image.
|
||||||
* matchingField - The field in the user object that matched the search criteria.
|
* 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.
|
* 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):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
@ -30,12 +29,12 @@
|
||||||
"fullname": "Foo bar",
|
"fullname": "Foo bar",
|
||||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||||
"matchingFieldName": "Fullname",
|
"matchingFieldName": "Fullname"
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{{<core/local/comboboxsearch/resultitem }}
|
{{<core/local/comboboxsearch/resultitem }}
|
||||||
{{$arialabel}}{{#str}}viewresults, core, {{fullname}}{{/str}}{{/arialabel}}
|
{{$arialabel}}{{fullname}}{{/arialabel}}
|
||||||
|
{{$shorttext}}{{fullname}}{{/shorttext}}
|
||||||
{{$content}}
|
{{$content}}
|
||||||
<span class="d-block px-2 w-25">
|
<span class="d-block px-2 w-25">
|
||||||
{{#profileimageurl}}
|
{{#profileimageurl}}
|
||||||
|
|
|
@ -17,35 +17,35 @@
|
||||||
Wrapping template for returned result items.
|
Wrapping template for returned result items.
|
||||||
|
|
||||||
Context variables required for this template:
|
Context variables required for this template:
|
||||||
|
* instance - The instance ID of the combo box.
|
||||||
* users - Our returned users to render.
|
* users - Our returned users to render.
|
||||||
* found - Count of the found users.
|
* found - Count of the found users.
|
||||||
* total - Total count of users within this report.
|
* 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.
|
* searchterm - The entered text to find these results.
|
||||||
* hasusers - Allow the handling where no users exist for the returned search term.
|
* hasusers - Allow the handling where no users exist for the returned search term.
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"instance": 25,
|
||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"fullname": "Foo bar",
|
"fullname": "Foo bar",
|
||||||
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
"profileimageurl": "http://foo.bar/pluginfile.php/79/user/icon/boost/f1?rev=7630",
|
||||||
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
"matchingField": "<span class=\"font-weight-bold\">Foo</span> bar",
|
||||||
"matchingFieldName": "Fullname",
|
"matchingFieldName": "Fullname"
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"fullname": "Bar Foo",
|
"fullname": "Bar Foo",
|
||||||
"profileimageurl": "http://foo.bar/pluginfile.php/80/user/icon/boost/f1?rev=7631",
|
"profileimageurl": "http://foo.bar/pluginfile.php/80/user/icon/boost/f1?rev=7631",
|
||||||
"matchingField": "Bar <span class=\"font-weight-bold\">Foo</span>",
|
"matchingField": "Bar <span class=\"font-weight-bold\">Foo</span>",
|
||||||
"matchingFieldName": "Fullname",
|
"matchingFieldName": "Fullname"
|
||||||
"link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"matches": 20,
|
"matches": 20,
|
||||||
"selectall": "https://foo.bar/grade/report/grader/index.php?id=2&searchvalue=abe",
|
"selectall": true,
|
||||||
"searchterm": "Foo",
|
"searchterm": "Foo",
|
||||||
"hasresults": true
|
"hasresults": true
|
||||||
}
|
}
|
||||||
|
@ -59,10 +59,15 @@
|
||||||
{{/results}}
|
{{/results}}
|
||||||
{{$selectall}}
|
{{$selectall}}
|
||||||
{{#selectall}}
|
{{#selectall}}
|
||||||
<li class="w-100 result-row p-1 border-top bottom-0 position-sticky" role="none" id="result-row-{{id}}">
|
<li
|
||||||
<a role="option" class="dropdown-item d-flex small p-3" id="select-all" href="{{{selectall}}}" tabindex="-1">
|
id="result-row-{{instance}}-0"
|
||||||
{{#str}}viewallresults, core, {{matches}}{{/str}}
|
class="w-100 p-1 border-top bottom-0 position-sticky dropdown-item d-flex small p-3"
|
||||||
</a>
|
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>
|
</li>
|
||||||
{{/selectall}}
|
{{/selectall}}
|
||||||
{{/selectall}}
|
{{/selectall}}
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
The user selector trigger element.
|
The user selector trigger element.
|
||||||
|
|
||||||
Context variables required for this template:
|
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.
|
* currentvalue - If the user has already searched, set the value to that.
|
||||||
* courseid - The course ID.
|
* courseid - The course ID.
|
||||||
* group - The group ID.
|
* group - The group ID.
|
||||||
|
@ -27,41 +29,33 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"name": "input-1",
|
||||||
|
"value": "0",
|
||||||
"currentvalue": "bar",
|
"currentvalue": "bar",
|
||||||
"courseid": 2,
|
"courseid": 2,
|
||||||
"group": 25,
|
"group": 25,
|
||||||
"resetlink": "grade/report/grader/index.php?id=2"
|
"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="courseid" data-courseid="{{courseid}}"></span>
|
||||||
<span class="d-none" data-region="groupid" data-groupid="{{group}}" aria-hidden="true"></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}}
|
{{#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}}">
|
<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}}
|
{{#str}}clear{{/str}}
|
||||||
</a>
|
</a>
|
||||||
{{/currentvalue}}
|
{{/currentvalue}}
|
||||||
{{^currentvalue}}
|
<input type="hidden" name="{{name}}" value="{{value}}" id="user-input-{{uniqid}}-{{instance}}"/>
|
||||||
{{< 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}}"/>
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue