diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt
index bcdde824c63..eb39eddda20 100644
--- a/lang/en/deprecated.txt
+++ b/lang/en/deprecated.txt
@@ -57,3 +57,17 @@ notflagged,core_question
cannotswitcheditmodeon,core_error
multilangforceold,core_admin
nopermissionmove,core_question
+addcondition,core_user
+adverbfor_and,core_user
+adverbfor_andnot,core_user
+adverbfor_or,core_user
+applyfilters,core_user
+clearfilterrow,core_user
+clearfilters,core_user
+filtersetmatchdescription,core_user
+filterrowlegend,core_user
+filtertype,core_user
+match,core_user
+matchofthefollowing,core_user
+placeholdertypeorselect,core_user
+selectfiltertype,core_user
diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index 97e7d7e4a77..c767cc1a21b 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -52,6 +52,7 @@ $string['addactivitytosection'] = 'Add an activity to section \'{$a}\'';
$string['addadmin'] = 'Add admin';
$string['addblock'] = 'Add a block';
$string['addcomment'] = 'Add a comment...';
+$string['addcondition'] = 'Add condition';
$string['addcountertousername'] = 'Create user by adding number to username';
$string['addcreator'] = 'Add course creator';
$string['adddots'] = 'Add...';
@@ -153,6 +154,7 @@ $string['answer'] = 'Answer';
$string['any'] = 'Any';
$string['approve'] = 'Approve';
$string['appearance'] = 'Appearance';
+$string['applyfilters'] = 'Apply filters';
$string['areyousure'] = 'Are you sure?';
$string['areyousuretorestorethis'] = 'Do you want to continue?';
$string['areyousuretorestorethisinfo'] = 'Later in this process you will have a choice of adding this backup to an existing course or creating a completely new course.';
@@ -255,6 +257,8 @@ $string['changesmadereallygoaway'] = 'You have made changes. Are you sure you wa
$string['city'] = 'City/town';
$string['cleaningtempdata'] = 'Cleaning temp data';
$string['clear'] = 'Clear';
+$string['clearfilterrow'] = 'Remove filter row';
+$string['clearfilters'] = 'Clear filters';
$string['clearsearch'] = 'Clear search input';
$string['clickhelpiconformoreinfo'] = '... continues ... Click on the help icon to read the full article';
$string['clickhere'] = 'Click here ...';
@@ -880,6 +884,7 @@ $string['filesfolders'] = 'Files/folders';
$string['fileuploadwithcontent'] = 'File uploads should not include the content parameter';
$string['filloutallfields'] = 'Please fill out all fields in this form';
$string['filter'] = 'Filter';
+$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
$string['findmorecourses'] = 'Find more courses...';
$string['first'] = 'First';
$string['firstaccess'] = 'First access';
@@ -1070,7 +1075,9 @@ $string['courseenddateenabled_desc'] = 'This setting determines if the course en
$string['eventcontentviewed'] = 'Content viewed';
$string['filter'] = 'Filter';
$string['filteroption'] = '{$a->criteria}: {$a->value}';
+$string['filterrowlegend'] = 'Filter {$a}';
$string['filters'] = 'Filters';
+$string['filtertype'] = 'Filter type';
$string['icon'] = 'Icon';
$string['idnumber'] = 'ID number';
$string['idnumbercourse'] = 'Course ID number';
@@ -1244,7 +1251,9 @@ $string['manageroles'] = 'Roles and permissions';
$string['markallread'] = 'Mark all as read';
$string['markedthistopic'] = 'This topic is highlighted as the current topic';
$string['markthistopic'] = 'Highlight this topic as the current topic';
+$string['match'] = 'Match';
$string['matchingsearchandrole'] = 'Matching \'{$a->search}\' and {$a->role}';
+$string['matchofthefollowing'] = 'of the following:';
$string['maxareabytesreached'] = 'The file (or the total size of several files) is larger than the space remaining in this area.';
$string['maxsectionslimit'] = 'Cannot create new section as it would exceed the maximum number of sections allowed for this course ({$a}).';
$string['maxfilesize'] = 'Maximum size for new files: {$a}';
@@ -1575,6 +1584,9 @@ $string['olduserdirectory'] = 'This is the OLD users directory, and is no longer
$string['opendrawerblocks'] = 'Open block drawer';
$string['opendrawerindex'] = 'Open course index';
$string['opensinnewwindow'] = 'Opens in new window';
+$string['operator_and'] = 'and';
+$string['operator_andnot'] = 'and';
+$string['operator_or'] = 'or';
$string['optional'] = 'optional';
$string['options'] = 'options';
$string['order'] = 'Order';
@@ -1639,6 +1651,7 @@ $string['phone2'] = 'Mobile phone';
$string['phpinfo'] = 'PHP info';
$string['pictureof'] = 'Picture of {$a}';
$string['pictureofuser'] = 'User picture';
+$string['placeholdertypeorselect'] = 'Type or select...';
$string['pleaseclose'] = 'Please close this window now.';
$string['pleasesearchmore'] = 'Please search some more';
$string['pleaseusesearch'] = 'Please use the search';
@@ -1923,6 +1936,7 @@ $string['selectedfile'] = 'Selected file';
$string['selectedcategories'] = 'Selected categories';
$string['selectednowmove'] = '{$a} files selected for moving. Now go into the destination folder and press \'Move files to here\'';
$string['selectfiles'] = 'Select files';
+$string['selectfiltertype'] = 'Select';
$string['selectitem'] = 'Select \'{$a}\'';
$string['selectmoduletoviewhelp'] = 'Select an activity or resource to view its help.
diff --git a/lang/en/user.php b/lang/en/user.php
index a09182eb0cc..1513e95485b 100644
--- a/lang/en/user.php
+++ b/lang/en/user.php
@@ -22,23 +22,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['addcondition'] = 'Add condition';
-$string['adverbfor_and'] = 'and';
-$string['adverbfor_andnot'] = 'and';
-$string['adverbfor_or'] = 'or';
-$string['applyfilters'] = 'Apply filters';
-$string['clearfilterrow'] = 'Remove filter row';
-$string['clearfilters'] = 'Clear filters';
-$string['clearfilterselection'] = 'Remove "{$a}" from filter';
$string['countparticipantsfound'] = '{$a} participants found';
-$string['filterrowlegend'] = 'Filter {$a}';
-$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
-$string['filtertype'] = 'Filter type';
-$string['match'] = 'Match';
-$string['matchofthefollowing'] = 'of the following:';
$string['moodlenetprofile'] = 'MoodleNet profile ID';
$string['moodlenetprofile_help'] = 'Your MoodleNet profile ID links your MoodleNet profile with this site.';
-$string['placeholdertypeorselect'] = 'Type or select...';
$string['placeholdertype'] = 'Type...';
$string['privacy:courserequestpath'] = 'Requested courses';
$string['privacy:descriptionpath'] = 'Profile description';
@@ -146,7 +132,6 @@ $string['privacy:profileimagespath'] = 'Profile images';
$string['privacy:privatefilespath'] = 'Private files';
$string['privacy:sessionpath'] = 'Session data';
$string['filterbykeyword'] = 'Keyword';
-$string['selectfiltertype'] = 'Select';
$string['supportmessagesent'] = 'Your message has been sent.';
$string['supportmessagesentforloggedoutuser'] = 'Be careful with this message. The sender was not logged in, so their identity has not been confirmed.';
$string['supportmessagenotsent'] = "Unfortunately your message could not be sent.";
@@ -154,3 +139,20 @@ $string['supportmessagealternative'] = 'Instead you can email {$a}.';
$string['target:upcomingactivitiesdue'] = 'Upcoming activities due';
$string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.';
$string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.';
+
+// Deprecated since Moodle 4.1.
+$string['addcondition'] = 'Add condition';
+$string['adverbfor_and'] = 'and';
+$string['adverbfor_andnot'] = 'and';
+$string['adverbfor_or'] = 'or';
+$string['applyfilters'] = 'Apply filters';
+$string['clearfilterrow'] = 'Remove filter row';
+$string['clearfilters'] = 'Clear filters';
+$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
+$string['filterrowlegend'] = 'Filter {$a}';
+$string['filtertype'] = 'Filter type';
+$string['match'] = 'Match';
+$string['matchofthefollowing'] = 'of the following:';
+$string['selectfiltertype'] = 'Select';
+$string['placeholdertypeorselect'] = 'Type or select...';
+$string['clearfilterselection'] = 'Remove "{$a}" from filter';
diff --git a/lib/amd/build/datafilter.min.js b/lib/amd/build/datafilter.min.js
new file mode 100644
index 00000000000..9a0ed4abd7f
--- /dev/null
+++ b/lib/amd/build/datafilter.min.js
@@ -0,0 +1,3 @@
+define("core/datafilter",["exports","core/datafilter/filtertypes/courseid","core/datafilter/filtertype","core/str","core/notification","core/pending","core/datafilter/selectors","core/templates","core/custom_interaction_events","jquery"],(function(_exports,_courseid,_filtertype,_str,_notification,_pending,_selectors,_templates,_custom_interaction_events,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_courseid=_interopRequireDefault(_courseid),_filtertype=_interopRequireDefault(_filtertype),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}return _exports.default=class{constructor(filterSet,applyCallback){this.filterSet=filterSet,this.applyCallback=applyCallback,this.activeFilters={courseid:new _courseid.default("courseid",filterSet)}}init(){this.filterSet.querySelector(_selectors.default.filterset.region).addEventListener("click",(e=>{e.target.closest(_selectors.default.filterset.actions.addRow)&&(e.preventDefault(),this.addFilterRow()),e.target.closest(_selectors.default.filterset.actions.applyFilters)&&(e.preventDefault(),this.updateTableFromFilter()),e.target.closest(_selectors.default.filterset.actions.resetFilters)&&(e.preventDefault(),this.removeAllFilters())})),this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist).addEventListener("click",(e=>{e.target.closest(_selectors.default.filter.actions.remove)&&(e.preventDefault(),this.removeOrReplaceFilterRow(e.target.closest(_selectors.default.filter.region),!0))}));let filterRegion=(0,_jquery.default)(this.getFilterRegion());_custom_interaction_events.default.define(filterRegion,[_custom_interaction_events.default.events.accessibleChange]),filterRegion.on(_custom_interaction_events.default.events.accessibleChange,(e=>{const typeField=e.target.closest(_selectors.default.filter.fields.type);if(typeField&&typeField.value){const filter=e.target.closest(_selectors.default.filter.region);this.addFilter(filter,typeField.value)}})),this.filterSet.querySelector(_selectors.default.filterset.fields.join).addEventListener("change",(e=>{this.filterSet.dataset.filterverb=e.target.value}))}getFilterRegion(){return this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist)}addFilterRow(){const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=1+this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length;return _templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rownum}).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.appendNodeContents(this.getFilterRegion(),html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((result=>(pendingPromise.resolve(),result))).catch(_notification.default.exception)}getFilterDataSource(filterType){return this.filterSet.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(filterType))}async addFilter(filterRow,filterType,initialFilterValues){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;filterDataNode.dataset.filterTypeClass&&(Filter=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filterDataNode.dataset.filterTypeClass],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filterDataNode.dataset.filterTypeClass)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filterDataNode.dataset.filterTypeClass]))),this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);return typeField.value=filterType,typeField.disabled="disabled",this.updateFiltersOptions(),this.activeFilters[filterType]}getFilterObject(name){return this.activeFilters[name]}removeOrReplaceFilterRow(filterRow,refreshContent){1===this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length?this.replaceFilterRow(filterRow,refreshContent):this.removeFilterRow(filterRow,refreshContent)}async removeFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];const hasFilterValue=!!filterRow.querySelector(_selectors.default.filter.fields.type).value;this.removeFilterObject(filterRow.dataset.filterType),filterRow.remove(),this.updateFiltersOptions(),hasFilterValue&&refreshContent&&this.updateTableFromFilter();const filterLegends=await this.getAvailableFilterLegends();this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach(((filterRow,index)=>{filterRow.querySelector("legend").innerText=filterLegends[index]}))}replaceFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],rowNum=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;return this.removeFilterObject(filterRow.dataset.filterType),_templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rowNum}).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNode(filterRow,html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((filterRow=>refreshContent?this.updateTableFromFilter():filterRow)).catch(_notification.default.exception)}removeFilterObject(filterName){if(filterName){const filter=this.getFilterObject(filterName);filter&&(filter.tearDown(),delete this.activeFilters[filterName])}}removeAllFilters(){return this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>this.removeOrReplaceFilterRow(filterRow,!1))),this.updateTableFromFilter()}removeEmptyFilters(){this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>{filterRow.querySelector(_selectors.default.filter.fields.type).value||this.removeOrReplaceFilterRow(filterRow,!1)}))}updateFiltersOptions(){const filters=this.getFilterRegion().querySelectorAll(_selectors.default.filter.region);filters.forEach((filterRow=>{filterRow.querySelectorAll(_selectors.default.filter.fields.type+" option").forEach((option=>{option.value===filterRow.dataset.filterType?(option.classList.remove("hidden"),option.disabled=!1):this.activeFilters[option.value]?(option.classList.add("hidden"),option.disabled=!0):(option.classList.remove("hidden"),option.disabled=!1)}))}));const addRowButton=this.filterSet.querySelector(_selectors.default.filterset.actions.addRow);this.filterSet.querySelectorAll(_selectors.default.data.fields.all).length<=filters.length?addRowButton.setAttribute("disabled","disabled"):addRowButton.removeAttribute("disabled"),1===filters.length?(this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.add("hidden"),this.filterSet.querySelector(_selectors.default.filterset.fields.join).value=2,this.filterSet.dataset.filterverb=2):this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.remove("hidden")}updateTableFromFilter(){const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};Object.values(this.activeFilters).forEach((filter=>{filters[filter.filterValue.name]=filter.filterValue})),this.applyCallback&&this.applyCallback(filters,pendingPromise)}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core_user",param:rowIndex+1})}));return await(0,_str.get_strings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}},_exports.default}));
+
+//# sourceMappingURL=datafilter.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter.min.js.map b/lib/amd/build/datafilter.min.js.map
new file mode 100644
index 00000000000..4d7ee506b60
--- /dev/null
+++ b/lib/amd/build/datafilter.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"datafilter.min.js","sources":["../src/datafilter.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 .\n\n/**\n * Data filter management.\n *\n * @module core/datafilter\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from 'core/datafilter/filtertypes/courseid';\nimport GenericFilter from 'core/datafilter/filtertype';\nimport {get_strings as getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport CustomEvents from 'core/custom_interaction_events';\nimport jQuery from 'jquery';\n\nexport default class {\n\n /**\n * Initialise the filter on the element with the given filterSet and callback.\n *\n * @param {HTMLElement} filterSet The filter element.\n * @param {Function} applyCallback Callback function when updateTableFromFilter\n */\n constructor(filterSet, applyCallback) {\n\n this.filterSet = filterSet;\n this.applyCallback = applyCallback;\n // Keep a reference to all of the active filters.\n this.activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n }\n\n /**\n * Initialise event listeners to the filter.\n */\n init() {\n // Add listeners for the main actions.\n this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n this.addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n\n this.updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n this.removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);\n }\n });\n\n // Add listeners for the filter type selection.\n let filterRegion = jQuery(this.getFilterRegion());\n CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);\n filterRegion.on(CustomEvents.events.accessibleChange, e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n this.addFilter(filter, typeField.value);\n }\n });\n\n this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n this.filterSet.dataset.filterverb = e.target.value;\n });\n }\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n getFilterRegion() {\n return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);\n }\n\n /**\n * Add an unselected filter row.\n *\n * @return {Promise}\n */\n addFilterRow() {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(result => {\n pendingPromise.resolve();\n\n return result;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n getFilterDataSource(filterType) {\n const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n }\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues) {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = this.getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n return this.activeFilters[filterType];\n }\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n getFilterObject(name) {\n return this.activeFilters[name];\n }\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n removeOrReplaceFilterRow(filterRow, refreshContent) {\n const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n if (filterCount === 1) {\n this.replaceFilterRow(filterRow, refreshContent);\n } else {\n this.removeFilterRow(filterRow, refreshContent);\n }\n }\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n async removeFilterRow(filterRow, refreshContent = true) {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n const hasFilterValue = !!filterType.value;\n\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n if (hasFilterValue && refreshContent) {\n // Refresh the table if there was any content in this row.\n this.updateTableFromFilter();\n }\n\n // Update filter fieldset legends.\n const filterLegends = await this.getAvailableFilterLegends();\n\n this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n }\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n if (refreshContent) {\n return this.updateTableFromFilter();\n } else {\n return filterRow;\n }\n })\n .catch(Notification.exception);\n }\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n removeFilterObject(filterName) {\n if (filterName) {\n const filter = this.getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete this.activeFilters[filterName];\n }\n }\n }\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n removeAllFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));\n\n // Refresh the table.\n return this.updateTableFromFilter();\n }\n\n /**\n * Remove any empty filters.\n */\n removeEmptyFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n this.removeOrReplaceFilterRow(filterRow, false);\n }\n });\n }\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n updateFiltersOptions() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (this.activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;\n this.filterSet.dataset.filterverb = 2;\n } else {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n }\n\n /**\n * Update the Dynamic table based upon the current filter.\n */\n updateTableFromFilter() {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n Object.values(this.activeFilters).forEach(filter => {\n filters[filter.filterValue.name] = filter.filterValue;\n });\n\n if (this.applyCallback) {\n this.applyCallback(filters, pendingPromise);\n }\n }\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n async getAvailableFilterLegends() {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core_user\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n }\n\n}\n"],"names":["constructor","filterSet","applyCallback","activeFilters","courseid","CourseFilter","init","querySelector","Selectors","filterset","region","addEventListener","e","target","closest","actions","addRow","preventDefault","addFilterRow","applyFilters","updateTableFromFilter","resetFilters","removeAllFilters","regions","filterlist","filter","remove","removeOrReplaceFilterRow","filterRegion","this","getFilterRegion","define","CustomEvents","events","accessibleChange","on","typeField","fields","type","value","addFilter","join","dataset","filterverb","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterDataNode","Filter","GenericFilter","filterTypeClass","disabled","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","Object","values","filterValue","maxFilters","document","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings"],"mappings":"2kCAyCIA,YAAYC,UAAWC,oBAEdD,UAAYA,eACZC,cAAgBA,mBAEhBC,cAAgB,CACjBC,SAAU,IAAIC,kBAAa,WAAYJ,YAO/CK,YAESL,UAAUM,cAAcC,mBAAUC,UAAUC,QAAQC,iBAAiB,SAASC,IAC3EA,EAAEC,OAAOC,QAAQN,mBAAUC,UAAUM,QAAQC,UAC7CJ,EAAEK,sBAEGC,gBAGLN,EAAEC,OAAOC,QAAQN,mBAAUC,UAAUM,QAAQI,gBAC7CP,EAAEK,sBAEGG,yBAGLR,EAAEC,OAAOC,QAAQN,mBAAUC,UAAUM,QAAQM,gBAC7CT,EAAEK,sBAEGK,4BAKRrB,UAAUM,cAAcC,mBAAUC,UAAUc,QAAQC,YAAYb,iBAAiB,SAASC,IACvFA,EAAEC,OAAOC,QAAQN,mBAAUiB,OAAOV,QAAQW,UAC1Cd,EAAEK,sBAEGU,yBAAyBf,EAAEC,OAAOC,QAAQN,mBAAUiB,OAAOf,SAAS,WAK7EkB,cAAe,mBAAOC,KAAKC,sDAClBC,OAAOH,aAAc,CAACI,mCAAaC,OAAOC,mBACvDN,aAAaO,GAAGH,mCAAaC,OAAOC,kBAAkBtB,UAC5CwB,UAAYxB,EAAEC,OAAOC,QAAQN,mBAAUiB,OAAOY,OAAOC,SACvDF,WAAaA,UAAUG,MAAO,OACxBd,OAASb,EAAEC,OAAOC,QAAQN,mBAAUiB,OAAOf,aAE5C8B,UAAUf,OAAQW,UAAUG,gBAIpCtC,UAAUM,cAAcC,mBAAUC,UAAU4B,OAAOI,MAAM9B,iBAAiB,UAAUC,SAChFX,UAAUyC,QAAQC,WAAa/B,EAAEC,OAAO0B,SASrDT,yBACWD,KAAK5B,UAAUM,cAAcC,mBAAUC,UAAUc,QAAQC,YAQpEN,qBACU0B,eAAiB,IAAIC,iBAAQ,gCAC7BC,OAAS,EAAIjB,KAAKC,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QAAQsC,cAC7EC,mBAAUC,iBAAiB,6BAA8B,WAAcJ,SACzEK,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACcL,mBAAUM,mBAAmB1B,KAAKC,kBAAmBuB,KAAMC,OAItFH,MAAKK,kBAKIC,SAAW5B,KAAK5B,UAAUM,cAAcC,mBAAUkD,KAAKD,iBAE7DD,UAAUG,SAAQC,oBACRC,gBAAkBD,YAAYrD,cAAcC,mBAAUiB,OAAOY,OAAOC,MAEtEuB,kBACAA,gBAAgBC,UAAYL,SAASK,cAItCN,aAEVL,MAAKK,iBACGO,uBAEEP,aAEVL,MAAKa,SACFpB,eAAeqB,UAERD,UAEVE,MAAMC,sBAAaC,WAS5BC,oBAAoBC,mBACOzC,KAAK5B,UAAUM,cAAcC,mBAAUC,UAAUc,QAAQgD,YAE1DhE,cAAcC,mBAAUkD,KAAKrB,OAAOmC,OAAOF,6BAWrDd,UAAWc,WAAYG,qBAEnCjB,UAAUd,QAAQ4B,WAAaA,iBAEzBI,eAAiB7C,KAAKwC,oBAAoBC,gBAG5CK,OAASC,oBACTF,eAAehC,QAAQmC,kBACvBF,6NAAsBD,eAAehC,QAAQmC,2SAAvBH,eAAehC,QAA5B,2EAAagC,eAAehC,QAAQmC,yBAE5C1E,cAAcmE,YAAc,IAAIK,OAAOL,WAAYzC,KAAK5B,UAAWwE,2BAGlErC,UAAYoB,UAAUjD,cAAcC,mBAAUiB,OAAOY,OAAOC,aAClEF,UAAUG,MAAQ+B,WAClBlC,UAAU0C,SAAW,gBAGhBf,uBAEElC,KAAK1B,cAAcmE,YAS9BS,gBAAgBC,aACLnD,KAAK1B,cAAc6E,MAU9BrD,yBAAyB6B,UAAWyB,gBAEZ,IADApD,KAAKC,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QAAQsC,YAE5EkC,iBAAiB1B,UAAWyB,qBAE5BE,gBAAgB3B,UAAWyB,sCAUlBzB,eAAWyB,gFAEvBG,iBADa5B,UAAUjD,cAAcC,mBAAUiB,OAAOY,OAAOC,MAC/BC,WAG/B8C,mBAAmB7B,UAAUd,QAAQ4B,YAG1Cd,UAAU9B,cAGLqC,uBAEDqB,gBAAkBH,qBAEb7D,8BAIHkE,oBAAsBzD,KAAK0D,iCAE5BzD,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QAAQiD,SAAQ,CAACH,UAAWgC,SACjFhC,UAAUjD,cAAc,UAAUkF,UAAYH,cAAcE,UAapEN,iBAAiB1B,eAAWyB,0EAAuBS,8DAAS,cAEnDL,mBAAmB7B,UAAUd,QAAQ4B,YAEnCrB,mBAAUC,iBAAiB,6BAA8B,WAAcwC,SACzEvC,MAAKwC,YAACtC,KAACA,KAADC,GAAOA,iBACcL,mBAAU2C,YAAYpC,UAAWH,KAAMC,OAIlEH,MAAKK,kBAKIC,SAAW5B,KAAK5B,UAAUM,cAAcC,mBAAUkD,KAAKD,iBAE7DD,UAAUG,SAAQC,oBACRC,gBAAkBD,YAAYrD,cAAcC,mBAAUiB,OAAOY,OAAOC,MAEtEuB,kBACAA,gBAAgBC,UAAYL,SAASK,cAItCN,aAEVL,MAAKK,iBACGO,uBAEEP,aAEVL,MAAKK,WAEEyB,eACOpD,KAAKT,wBAELoC,YAGdU,MAAMC,sBAAaC,WAQ5BiB,mBAAmBQ,eACXA,WAAY,OACNpE,OAASI,KAAKkD,gBAAgBc,YAChCpE,SACAA,OAAOqE,kBAGAjE,KAAK1B,cAAc0F,cAUtCvE,0BACoBO,KAAKC,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QACjEiD,SAAQH,WAAa3B,KAAKF,yBAAyB6B,WAAW,KAG/D3B,KAAKT,wBAMhB2E,qBACoBlE,KAAKC,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QACjEiD,SAAQH,YACOA,UAAUjD,cAAcC,mBAAUiB,OAAOY,OAAOC,MACnDC,YACPZ,yBAAyB6B,WAAW,MAQrDO,6BACUiC,QAAUnE,KAAKC,kBAAkBiB,iBAAiBvC,mBAAUiB,OAAOf,QACzEsF,QAAQrC,SAAQH,YACIA,UAAUT,iBAAiBvC,mBAAUiB,OAAOY,OAAOC,KAAO,WAClEqB,SAAQsC,SACRA,OAAO1D,QAAUiB,UAAUd,QAAQ4B,YACnC2B,OAAOC,UAAUxE,OAAO,UACxBuE,OAAOnB,UAAW,GACXjD,KAAK1B,cAAc8F,OAAO1D,QACjC0D,OAAOC,UAAUC,IAAI,UACrBF,OAAOnB,UAAW,IAElBmB,OAAOC,UAAUxE,OAAO,UACxBuE,OAAOnB,UAAW,eAOxBsB,aAAevE,KAAK5B,UAAUM,cAAcC,mBAAUC,UAAUM,QAAQC,QACvDa,KAAK5B,UAAU8C,iBAAiBvC,mBAAUkD,KAAKrB,OAAOgE,KAC1DrD,QAAUgD,QAAQhD,OACjCoD,aAAaE,aAAa,WAAY,YAEtCF,aAAaG,gBAAgB,YAGV,IAAnBP,QAAQhD,aACH/C,UAAUM,cAAcC,mBAAUC,UAAUc,QAAQiF,aAAaN,UAAUC,IAAI,eAC/ElG,UAAUM,cAAcC,mBAAUC,UAAU4B,OAAOI,MAAMF,MAAQ,OACjEtC,UAAUyC,QAAQC,WAAa,QAE/B1C,UAAUM,cAAcC,mBAAUC,UAAUc,QAAQiF,aAAaN,UAAUxE,OAAO,UAO/FN,8BACUwB,eAAiB,IAAIC,iBAAQ,yCAE7BmD,QAAU,GAChBS,OAAOC,OAAO7E,KAAK1B,eAAewD,SAAQlC,SACtCuE,QAAQvE,OAAOkF,YAAY3B,MAAQvD,OAAOkF,eAG1C9E,KAAK3B,oBACAA,cAAc8F,QAASpD,wDAU1BgE,WAAaC,SAAStG,cAAcC,mBAAUkD,KAAKoD,gBAAgB9D,OAAS,MAC9E+D,SAAW,OAEXC,MAAMJ,aAAajD,SAAQ,CAACsD,EAAGC,YAC/BH,SAASI,KAAK,KACH,4BACM,kBAEJD,SAAW,oBAIA,oBAAWH,UAClC5D,MAAKiE,gBACKA,iBAEVlD,MAAMC,sBAAaC"}
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertype.min.js b/lib/amd/build/datafilter/filtertype.min.js
new file mode 100644
index 00000000000..84b94559f41
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertype.min.js
@@ -0,0 +1,10 @@
+define("core/datafilter/filtertype",["exports","core/form-autocomplete","core/datafilter/selectors","core/str"],(function(_exports,_formAutocomplete,_selectors,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Base Filter class for a filter type in the filter UI.
+ *
+ * @module core/datafilter/filtertype
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_formAutocomplete=_interopRequireDefault(_formAutocomplete),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(filterType,rootNode,initialValues){this.filterType=filterType,this.rootNode=rootNode,this.addValueSelector(initialValues)}tearDown(){}get placeholder(){return(0,_str.get_string)("placeholdertypeorselect","core")}get showSuggestions(){return!0}async addValueSelector(){let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const filterValueNode=this.getFilterValueNode(),sourceDataNode=this.getSourceDataForFilter();if(!sourceDataNode)return;filterValueNode.innerHTML=sourceDataNode.outerHTML;const dataSource=filterValueNode.querySelector("select");dataSource.id="filter-value-"+dataSource.getAttribute("data-field-name");const filterValueLabel=document.createElement("label");filterValueLabel.setAttribute("for",dataSource.id),filterValueLabel.classList.add("sr-only"),filterValueLabel.innerText=dataSource.getAttribute("data-field-title"),filterValueNode.appendChild(filterValueLabel),initialValues.forEach((filterValue=>{let selectedOption=dataSource.querySelector('option[value="'.concat(filterValue,'"]'));selectedOption?selectedOption.selected=!0:this.showSuggestions||(selectedOption=document.createElement("option"),selectedOption.value=filterValue,selectedOption.innerHTML=filterValue,selectedOption.selected=!0,dataSource.append(selectedOption))})),_formAutocomplete.default.enhance(dataSource,"1"==dataSource.dataset.allowCustom,null,await this.placeholder,!1,this.showSuggestions,null,!dataSource.multiple,{items:"core/datafilter/autocomplete_selection_items",layout:"core/datafilter/autocomplete_layout",selection:"core/datafilter/autocomplete_selection"})}get filterRoot(){return this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType))}getSourceDataForFilter(){return this.rootNode.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(this.filterType))}getFilterValueNode(){return this.filterRoot.querySelector(_selectors.default.filter.regions.values)}get name(){return this.filterType}get jointype(){return parseInt(this.filterRoot.querySelector(_selectors.default.filter.fields.join).value,10)}get rawValues(){const filterValueSelect=this.getFilterValueNode().querySelector("select");return Object.values((select=filterValueSelect,select.querySelectorAll(":checked"))).map((option=>option.value));var select}get values(){return this.rawValues.map((option=>parseInt(option,10)))}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values}}},_exports.default}));
+
+//# sourceMappingURL=filtertype.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertype.min.js.map b/lib/amd/build/datafilter/filtertype.min.js.map
new file mode 100644
index 00000000000..a8b5c6eccf7
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertype.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"filtertype.min.js","sources":["../../src/datafilter/filtertype.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 .\n\n/**\n * Base Filter class for a filter type in the filter UI.\n *\n * @module core/datafilter/filtertype\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from 'core/datafilter/selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n * @param {Array} initialValues The initial values for the selector\n */\n constructor(filterType, rootNode, initialValues) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector(initialValues);\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertypeorselect', 'core');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return true;\n }\n\n /**\n * Add the value selector to the filter row.\n *\n * @param {Array} initialValues\n */\n async addValueSelector(initialValues = []) {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n const sourceDataNode = this.getSourceDataForFilter();\n if (!sourceDataNode) {\n return;\n }\n filterValueNode.innerHTML = sourceDataNode.outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n // Set an ID for this filter value element.\n dataSource.id = 'filter-value-' + dataSource.getAttribute('data-field-name');\n\n // Create a hidden label for the filter value.\n const filterValueLabel = document.createElement('label');\n filterValueLabel.setAttribute('for', dataSource.id);\n filterValueLabel.classList.add('sr-only');\n filterValueLabel.innerText = dataSource.getAttribute('data-field-title');\n\n // Append this label to the filter value container.\n filterValueNode.appendChild(filterValueLabel);\n\n // If there are any initial values then attempt to apply them.\n initialValues.forEach(filterValue => {\n let selectedOption = dataSource.querySelector(`option[value=\"${filterValue}\"]`);\n if (selectedOption) {\n selectedOption.selected = true;\n } else if (!this.showSuggestions) {\n selectedOption = document.createElement('option');\n selectedOption.value = filterValue;\n selectedOption.innerHTML = filterValue;\n selectedOption.selected = true;\n\n dataSource.append(selectedOption);\n }\n });\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await this.placeholder,\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n this.showSuggestions,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple,\n\n // Template overrides.\n {\n items: 'core/datafilter/autocomplete_selection_items',\n layout: 'core/datafilter/autocomplete_layout',\n selection: 'core/datafilter/autocomplete_selection',\n }\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return parseInt(this.filterRoot.querySelector(Selectors.filter.fields.join).value, 10);\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"names":["constructor","filterType","rootNode","initialValues","addValueSelector","tearDown","placeholder","showSuggestions","filterValueNode","this","getFilterValueNode","sourceDataNode","getSourceDataForFilter","innerHTML","outerHTML","dataSource","querySelector","id","getAttribute","filterValueLabel","document","createElement","setAttribute","classList","add","innerText","appendChild","forEach","filterValue","selectedOption","selected","value","append","enhance","dataset","allowCustom","multiple","items","layout","selection","filterRoot","Selectors","filter","byName","filterset","regions","datasource","data","fields","values","name","jointype","parseInt","join","rawValues","filterValueSelect","Object","select","querySelectorAll","map","option"],"mappings":";;;;;;;6NA+CIA,YAAYC,WAAYC,SAAUC,oBACzBF,WAAaA,gBACbC,SAAWA,cAEXE,iBAAiBD,eAM1BE,YASIC,yBACO,mBAAU,0BAA2B,QAQ5CC,6BACO,+BAQYJ,qEAAgB,SAC7BK,gBAAkBC,KAAKC,qBAGvBC,eAAiBF,KAAKG,6BACvBD,sBAGLH,gBAAgBK,UAAYF,eAAeG,gBAErCC,WAAaP,gBAAgBQ,cAAc,UAGjDD,WAAWE,GAAK,gBAAkBF,WAAWG,aAAa,yBAGpDC,iBAAmBC,SAASC,cAAc,SAChDF,iBAAiBG,aAAa,MAAOP,WAAWE,IAChDE,iBAAiBI,UAAUC,IAAI,WAC/BL,iBAAiBM,UAAYV,WAAWG,aAAa,oBAGrDV,gBAAgBkB,YAAYP,kBAG5BhB,cAAcwB,SAAQC,kBACdC,eAAiBd,WAAWC,sCAA+BY,mBAC3DC,eACAA,eAAeC,UAAW,EAClBrB,KAAKF,kBACbsB,eAAiBT,SAASC,cAAc,UACxCQ,eAAeE,MAAQH,YACvBC,eAAehB,UAAYe,YAC3BC,eAAeC,UAAW,EAE1Bf,WAAWiB,OAAOH,8CAIbI,QAETlB,WAGkC,KAAlCA,WAAWmB,QAAQC,YAGnB,WAGM1B,KAAKH,aAGX,EAGAG,KAAKF,gBAGL,MAGCQ,WAAWqB,SAGZ,CACIC,MAAO,+CACPC,OAAQ,sCACRC,UAAW,2CAUnBC,wBACO/B,KAAKP,SAASc,cAAcyB,mBAAUC,OAAOC,OAAOlC,KAAKR,aAQpEW,gCAC2BH,KAAKP,SAASc,cAAcyB,mBAAUG,UAAUC,QAAQC,YAEzD9B,cAAcyB,mBAAUM,KAAKC,OAAOL,OAAOlC,KAAKR,aAQ1ES,4BACWD,KAAK+B,WAAWxB,cAAcyB,mBAAUC,OAAOG,QAAQI,QAQ9DC,kBACOzC,KAAKR,WAQZkD,sBACOC,SAAS3C,KAAK+B,WAAWxB,cAAcyB,mBAAUC,OAAOM,OAAOK,MAAMtB,MAAO,IAQnFuB,sBAEMC,kBADkB9C,KAAKC,qBACaM,cAAc,iBAEjDwC,OAAOP,QAnLMQ,OAmLqBF,kBAlLtCE,OAAOC,iBAAiB,cAkLkCC,KAAIC,QAAUA,OAAO7B,QAnL9D0B,IAAAA,OA2LpBR,oBACOxC,KAAK6C,UAAUK,KAAIC,QAAUR,SAASQ,OAAQ,MAQrDhC,wBACO,CACHsB,KAAMzC,KAAKyC,KACXC,SAAU1C,KAAK0C,SACfF,OAAQxC,KAAKwC"}
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/country.min.js b/lib/amd/build/datafilter/filtertypes/country.min.js
new file mode 100644
index 00000000000..0d6ab38f632
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/country.min.js
@@ -0,0 +1,10 @@
+define("core/datafilter/filtertypes/country",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj;
+/**
+ * Country filter
+ *
+ * @module core/datafilter/filtertypes/country
+ * @copyright 2021 Paul Holden
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{get values(){return this.rawValues}}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=country.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/country.min.js.map b/lib/amd/build/datafilter/filtertypes/country.min.js.map
new file mode 100644
index 00000000000..3b840b2314b
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/country.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"country.min.js","sources":["../../../src/datafilter/filtertypes/country.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 .\n\n/**\n * Country filter\n *\n * @module core/datafilter/filtertypes/country\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\n\nexport default class extends Filter {\n\n /**\n * For country the final value is an array of country code strings\n *\n * @return {Object}\n */\n get values() {\n return this.rawValues;\n }\n}\n"],"names":["Filter","values","this","rawValues"],"mappings":";;;;;;;4KAyB6BA,oBAOrBC,oBACOC,KAAKC"}
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/courseid.min.js b/lib/amd/build/datafilter/filtertypes/courseid.min.js
new file mode 100644
index 00000000000..134ec8ed9d2
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/courseid.min.js
@@ -0,0 +1,10 @@
+define("core/datafilter/filtertypes/courseid",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj;
+/**
+ * Course ID filter.
+ *
+ * @module core/datafilter/filtertypes/courseid
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{constructor(filterType,filterSet){super(filterType,filterSet)}async addValueSelector(){}get filterValue(){return{name:this.name,jointype:1,values:[parseInt(this.rootNode.dataset.tableCourseId,10)]}}}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=courseid.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/courseid.min.js.map b/lib/amd/build/datafilter/filtertypes/courseid.min.js.map
new file mode 100644
index 00000000000..4a5f87ad3fd
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/courseid.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"courseid.min.js","sources":["../../../src/datafilter/filtertypes/courseid.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 .\n\n/**\n * Course ID filter.\n *\n * @module core/datafilter/filtertypes/courseid\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from 'core/datafilter/filtertype';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n async addValueSelector() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: 1,\n values: [parseInt(this.rootNode.dataset.tableCourseId, 10)],\n };\n }\n}\n"],"names":["Filter","constructor","filterType","filterSet","filterValue","name","this","jointype","values","parseInt","rootNode","dataset","tableCourseId"],"mappings":";;;;;;;4KAwB6BA,oBACzBC,YAAYC,WAAYC,iBACdD,WAAYC,qCAYlBC,wBACO,CACHC,KAAMC,KAAKD,KACXE,SAAU,EACVC,OAAQ,CAACC,SAASH,KAAKI,SAASC,QAAQC,cAAe"}
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/keyword.min.js b/lib/amd/build/datafilter/filtertypes/keyword.min.js
new file mode 100644
index 00000000000..bfc2fb23040
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/keyword.min.js
@@ -0,0 +1,10 @@
+define("core/datafilter/filtertypes/keyword",["exports","core/datafilter/filtertype","core/str"],(function(_exports,_filtertype,_str){var obj;
+/**
+ * Keyword filter.
+ *
+ * @module core/datafilter/filtertypes/keyword
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{get values(){return this.rawValues}get placeholder(){return(0,_str.get_string)("placeholdertype","core_user")}get showSuggestions(){return!1}}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=keyword.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/keyword.min.js.map b/lib/amd/build/datafilter/filtertypes/keyword.min.js.map
new file mode 100644
index 00000000000..14f1d92d58b
--- /dev/null
+++ b/lib/amd/build/datafilter/filtertypes/keyword.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"keyword.min.js","sources":["../../../src/datafilter/filtertypes/keyword.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 .\n\n/**\n * Keyword filter.\n *\n * @module core/datafilter/filtertypes/keyword\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from 'core/datafilter/filtertype';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"names":["Filter","values","this","rawValues","placeholder","showSuggestions"],"mappings":";;;;;;;4KAyB6BA,oBAMrBC,oBACOC,KAAKC,UAQZC,yBACO,mBAAU,kBAAmB,aAQpCC,6BACO"}
\ No newline at end of file
diff --git a/user/amd/build/local/participantsfilter/selectors.min.js b/lib/amd/build/datafilter/selectors.min.js
similarity index 86%
rename from user/amd/build/local/participantsfilter/selectors.min.js
rename to lib/amd/build/datafilter/selectors.min.js
index 5214c5f69af..335229b97a2 100644
--- a/user/amd/build/local/participantsfilter/selectors.min.js
+++ b/lib/amd/build/datafilter/selectors.min.js
@@ -1,8 +1,8 @@
-define("core_user/local/participantsfilter/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
+define("core/datafilter/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* Module containing the selectors for user filters.
*
- * @module core_user/local/user_filter/selectors
+ * @module core/datafilter/selectors
* @copyright 2020 Michael Hawkins
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
diff --git a/lib/amd/build/datafilter/selectors.min.js.map b/lib/amd/build/datafilter/selectors.min.js.map
new file mode 100644
index 00000000000..1cb34ad6cf8
--- /dev/null
+++ b/lib/amd/build/datafilter/selectors.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"selectors.min.js","sources":["../../src/datafilter/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 .\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core/datafilter/selectors\n * @copyright 2020 Michael Hawkins \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n },\n};\n"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList","typeListSelect"],"mappings":";;;;;;;;MAuBMA,gBAAkBC,sCAAiCA,aACnDC,gBAAkBC,sCAAiCA,aACnDC,eAAiBC,oCAA+BA,yBAEvC,CACXC,OAAQ,CACJL,OAAQD,gBAAgB,UACxBO,QAAS,CACLC,OAAQN,gBAAgB,WAE5BO,OAAQ,CACJC,KAAMN,eAAe,QACrBO,KAAMP,eAAe,SAEzBQ,QAAS,CACLC,OAAQb,gBAAgB,UAE5Bc,OAAQC,gBAAWf,gBAAgB,wCAA+Be,YAEtEC,UAAW,CACPf,OAAQD,gBAAgB,WACxBO,QAAS,CACLU,OAAQf,gBAAgB,OACxBgB,aAAchB,gBAAgB,SAC9BiB,aAAcjB,gBAAgB,UAElCU,QAAS,CACLQ,YAAapB,gBAAgB,eAC7BqB,WAAYrB,gBAAgB,WAC5BsB,WAAYtB,gBAAgB,mBAEhCS,OAAQ,CACJC,eAASV,gBAAgB,2BAAkBI,eAAe,WAGlEmB,KAAM,CACFd,OAAQ,CACJK,OAAQC,kCAA6BA,WACrCS,cAAQxB,gBAAgB,yCAE5ByB,SAAUzB,gBAAgB,kBAC1B0B,+BAAyB1B,gBAAgB"}
\ No newline at end of file
diff --git a/lib/amd/src/datafilter.js b/lib/amd/src/datafilter.js
new file mode 100644
index 00000000000..b9ec8a6fa89
--- /dev/null
+++ b/lib/amd/src/datafilter.js
@@ -0,0 +1,436 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Data filter management.
+ *
+ * @module core/datafilter
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import CourseFilter from 'core/datafilter/filtertypes/courseid';
+import GenericFilter from 'core/datafilter/filtertype';
+import {get_strings as getStrings} from 'core/str';
+import Notification from 'core/notification';
+import Pending from 'core/pending';
+import Selectors from 'core/datafilter/selectors';
+import Templates from 'core/templates';
+import CustomEvents from 'core/custom_interaction_events';
+import jQuery from 'jquery';
+
+export default class {
+
+ /**
+ * Initialise the filter on the element with the given filterSet and callback.
+ *
+ * @param {HTMLElement} filterSet The filter element.
+ * @param {Function} applyCallback Callback function when updateTableFromFilter
+ */
+ constructor(filterSet, applyCallback) {
+
+ this.filterSet = filterSet;
+ this.applyCallback = applyCallback;
+ // Keep a reference to all of the active filters.
+ this.activeFilters = {
+ courseid: new CourseFilter('courseid', filterSet),
+ };
+ }
+
+ /**
+ * Initialise event listeners to the filter.
+ */
+ init() {
+ // Add listeners for the main actions.
+ this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
+ if (e.target.closest(Selectors.filterset.actions.addRow)) {
+ e.preventDefault();
+
+ this.addFilterRow();
+ }
+
+ if (e.target.closest(Selectors.filterset.actions.applyFilters)) {
+ e.preventDefault();
+
+ this.updateTableFromFilter();
+ }
+
+ if (e.target.closest(Selectors.filterset.actions.resetFilters)) {
+ e.preventDefault();
+
+ this.removeAllFilters();
+ }
+ });
+
+ // Add the listener to remove a single filter.
+ this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {
+ if (e.target.closest(Selectors.filter.actions.remove)) {
+ e.preventDefault();
+
+ this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);
+ }
+ });
+
+ // Add listeners for the filter type selection.
+ let filterRegion = jQuery(this.getFilterRegion());
+ CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);
+ filterRegion.on(CustomEvents.events.accessibleChange, e => {
+ const typeField = e.target.closest(Selectors.filter.fields.type);
+ if (typeField && typeField.value) {
+ const filter = e.target.closest(Selectors.filter.region);
+
+ this.addFilter(filter, typeField.value);
+ }
+ });
+
+ this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {
+ this.filterSet.dataset.filterverb = e.target.value;
+ });
+ }
+
+ /**
+ * Get the filter list region.
+ *
+ * @return {HTMLElement}
+ */
+ getFilterRegion() {
+ return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);
+ }
+
+ /**
+ * Add an unselected filter row.
+ *
+ * @return {Promise}
+ */
+ addFilterRow() {
+ const pendingPromise = new Pending('core/datafilter:addFilterRow');
+ const rownum = 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
+ return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rownum})
+ .then(({html, js}) => {
+ const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);
+
+ return newContentNodes;
+ })
+ .then(filterRow => {
+ // Note: This is a nasty hack.
+ // We should try to find a better way of doing this.
+ // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
+ // it in place.
+ const typeList = this.filterSet.querySelector(Selectors.data.typeList);
+
+ filterRow.forEach(contentNode => {
+ const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
+
+ if (contentTypeList) {
+ contentTypeList.innerHTML = typeList.innerHTML;
+ }
+ });
+
+ return filterRow;
+ })
+ .then(filterRow => {
+ this.updateFiltersOptions();
+
+ return filterRow;
+ })
+ .then(result => {
+ pendingPromise.resolve();
+
+ return result;
+ })
+ .catch(Notification.exception);
+ }
+
+ /**
+ * Get the filter data source node fro the specified filter type.
+ *
+ * @param {String} filterType
+ * @return {HTMLElement}
+ */
+ getFilterDataSource(filterType) {
+ const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);
+
+ return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));
+ }
+
+ /**
+ * Add a filter to the list of active filters, performing any necessary setup.
+ *
+ * @param {HTMLElement} filterRow
+ * @param {String} filterType
+ * @param {Array} initialFilterValues The initially selected values for the filter
+ * @returns {Filter}
+ */
+ async addFilter(filterRow, filterType, initialFilterValues) {
+ // Name the filter on the filter row.
+ filterRow.dataset.filterType = filterType;
+
+ const filterDataNode = this.getFilterDataSource(filterType);
+
+ // Instantiate the Filter class.
+ let Filter = GenericFilter;
+ if (filterDataNode.dataset.filterTypeClass) {
+ Filter = await import(filterDataNode.dataset.filterTypeClass);
+ }
+ this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues);
+
+ // Disable the select.
+ const typeField = filterRow.querySelector(Selectors.filter.fields.type);
+ typeField.value = filterType;
+ typeField.disabled = 'disabled';
+
+ // Update the list of available filter types.
+ this.updateFiltersOptions();
+
+ return this.activeFilters[filterType];
+ }
+
+ /**
+ * Get the registered filter class for the named filter.
+ *
+ * @param {String} name
+ * @return {Object} See the Filter class.
+ */
+ getFilterObject(name) {
+ return this.activeFilters[name];
+ }
+
+ /**
+ * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,
+ * that it is replaced instead of being removed.
+ *
+ * @param {HTMLElement} filterRow
+ * @param {Bool} refreshContent Whether to refresh the table content when removing
+ */
+ removeOrReplaceFilterRow(filterRow, refreshContent) {
+ const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
+ if (filterCount === 1) {
+ this.replaceFilterRow(filterRow, refreshContent);
+ } else {
+ this.removeFilterRow(filterRow, refreshContent);
+ }
+ }
+
+ /**
+ * Remove the specified filter row and associated class.
+ *
+ * @param {HTMLElement} filterRow
+ * @param {Bool} refreshContent Whether to refresh the table content when removing
+ */
+ async removeFilterRow(filterRow, refreshContent = true) {
+ const filterType = filterRow.querySelector(Selectors.filter.fields.type);
+ const hasFilterValue = !!filterType.value;
+
+ // Remove the filter object.
+ this.removeFilterObject(filterRow.dataset.filterType);
+
+ // Remove the actual filter HTML.
+ filterRow.remove();
+
+ // Update the list of available filter types.
+ this.updateFiltersOptions();
+
+ if (hasFilterValue && refreshContent) {
+ // Refresh the table if there was any content in this row.
+ this.updateTableFromFilter();
+ }
+
+ // Update filter fieldset legends.
+ const filterLegends = await this.getAvailableFilterLegends();
+
+ this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
+ filterRow.querySelector('legend').innerText = filterLegends[index];
+ });
+
+ }
+
+ /**
+ * Replace the specified filter row with a new one.
+ *
+ * @param {HTMLElement} filterRow
+ * @param {Bool} refreshContent Whether to refresh the table content when removing
+ * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
+ * @return {Promise}
+ */
+ replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {
+ // Remove the filter object.
+ this.removeFilterObject(filterRow.dataset.filterType);
+
+ return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rowNum})
+ .then(({html, js}) => {
+ const newContentNodes = Templates.replaceNode(filterRow, html, js);
+
+ return newContentNodes;
+ })
+ .then(filterRow => {
+ // Note: This is a nasty hack.
+ // We should try to find a better way of doing this.
+ // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
+ // it in place.
+ const typeList = this.filterSet.querySelector(Selectors.data.typeList);
+
+ filterRow.forEach(contentNode => {
+ const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
+
+ if (contentTypeList) {
+ contentTypeList.innerHTML = typeList.innerHTML;
+ }
+ });
+
+ return filterRow;
+ })
+ .then(filterRow => {
+ this.updateFiltersOptions();
+
+ return filterRow;
+ })
+ .then(filterRow => {
+ // Refresh the table.
+ if (refreshContent) {
+ return this.updateTableFromFilter();
+ } else {
+ return filterRow;
+ }
+ })
+ .catch(Notification.exception);
+ }
+
+ /**
+ * Remove the Filter Object from the register.
+ *
+ * @param {string} filterName The name of the filter to be removed
+ */
+ removeFilterObject(filterName) {
+ if (filterName) {
+ const filter = this.getFilterObject(filterName);
+ if (filter) {
+ filter.tearDown();
+
+ // Remove from the list of active filters.
+ delete this.activeFilters[filterName];
+ }
+ }
+ }
+
+ /**
+ * Remove all filters.
+ *
+ * @returns {Promise}
+ */
+ removeAllFilters() {
+ const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
+ filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));
+
+ // Refresh the table.
+ return this.updateTableFromFilter();
+ }
+
+ /**
+ * Remove any empty filters.
+ */
+ removeEmptyFilters() {
+ const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
+ filters.forEach(filterRow => {
+ const filterType = filterRow.querySelector(Selectors.filter.fields.type);
+ if (!filterType.value) {
+ this.removeOrReplaceFilterRow(filterRow, false);
+ }
+ });
+ }
+
+ /**
+ * Update the list of filter types to filter out those already selected.
+ */
+ updateFiltersOptions() {
+ const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
+ filters.forEach(filterRow => {
+ const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');
+ options.forEach(option => {
+ if (option.value === filterRow.dataset.filterType) {
+ option.classList.remove('hidden');
+ option.disabled = false;
+ } else if (this.activeFilters[option.value]) {
+ option.classList.add('hidden');
+ option.disabled = true;
+ } else {
+ option.classList.remove('hidden');
+ option.disabled = false;
+ }
+ });
+ });
+
+ // Configure the state of the "Add row" button.
+ // This button is disabled when there is a filter row available for each condition.
+ const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);
+ const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);
+ if (filterDataNode.length <= filters.length) {
+ addRowButton.setAttribute('disabled', 'disabled');
+ } else {
+ addRowButton.removeAttribute('disabled');
+ }
+
+ if (filters.length === 1) {
+ this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');
+ this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;
+ this.filterSet.dataset.filterverb = 2;
+ } else {
+ this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');
+ }
+ }
+
+ /**
+ * Update the Dynamic table based upon the current filter.
+ */
+ updateTableFromFilter() {
+ const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');
+
+ const filters = {};
+ Object.values(this.activeFilters).forEach(filter => {
+ filters[filter.filterValue.name] = filter.filterValue;
+ });
+
+ if (this.applyCallback) {
+ this.applyCallback(filters, pendingPromise);
+ }
+ }
+
+ /**
+ * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
+ *
+ * @return {array}
+ */
+ async getAvailableFilterLegends() {
+ const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
+ let requests = [];
+
+ [...Array(maxFilters)].forEach((_, rowIndex) => {
+ requests.push({
+ "key": "filterrowlegend",
+ "component": "core_user",
+ // Add 1 since rows begin at 1 (index begins at zero).
+ "param": rowIndex + 1
+ });
+ });
+
+ const legendStrings = await getStrings(requests)
+ .then(fetchedStrings => {
+ return fetchedStrings;
+ })
+ .catch(Notification.exception);
+
+ return legendStrings;
+ }
+
+}
diff --git a/user/amd/src/local/participantsfilter/filter.js b/lib/amd/src/datafilter/filtertype.js
similarity index 93%
rename from user/amd/src/local/participantsfilter/filter.js
rename to lib/amd/src/datafilter/filtertype.js
index 7e485d8ae65..c424518e9cd 100644
--- a/user/amd/src/local/participantsfilter/filter.js
+++ b/lib/amd/src/datafilter/filtertype.js
@@ -14,14 +14,14 @@
// along with Moodle. If not, see .
/**
- * Base Filter class for a filter type in the participants filter UI.
+ * Base Filter class for a filter type in the filter UI.
*
- * @module core_user/local/participantsfilter/filter
+ * @module core/datafilter/filtertype
* @copyright 2020 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Autocomplete from 'core/form-autocomplete';
-import Selectors from './selectors';
+import Selectors from 'core/datafilter/selectors';
import {get_string as getString} from 'core/str';
/**
@@ -65,7 +65,7 @@ export default class {
* @return {Promise} Resolving to a String
*/
get placeholder() {
- return getString('placeholdertypeorselect', 'core_user');
+ return getString('placeholdertypeorselect', 'core');
}
/**
@@ -148,9 +148,9 @@ export default class {
// Template overrides.
{
- items: 'core_user/local/participantsfilter/autocomplete_selection_items',
- layout: 'core_user/local/participantsfilter/autocomplete_layout',
- selection: 'core_user/local/participantsfilter/autocomplete_selection',
+ items: 'core/datafilter/autocomplete_selection_items',
+ layout: 'core/datafilter/autocomplete_layout',
+ selection: 'core/datafilter/autocomplete_selection',
}
);
}
diff --git a/user/amd/src/local/participantsfilter/filtertypes/country.js b/lib/amd/src/datafilter/filtertypes/country.js
similarity index 91%
rename from user/amd/src/local/participantsfilter/filtertypes/country.js
rename to lib/amd/src/datafilter/filtertypes/country.js
index 82bb15a5108..6022fa46ea6 100644
--- a/user/amd/src/local/participantsfilter/filtertypes/country.js
+++ b/lib/amd/src/datafilter/filtertypes/country.js
@@ -16,12 +16,12 @@
/**
* Country filter
*
- * @module core_user/local/participantsfilter/filtertypes/country
+ * @module core/datafilter/filtertypes/country
* @copyright 2021 Paul Holden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import Filter from '../filter';
+import Filter from 'core/datafilter/filtertype';
export default class extends Filter {
diff --git a/user/amd/src/local/participantsfilter/filtertypes/courseid.js b/lib/amd/src/datafilter/filtertypes/courseid.js
similarity index 92%
rename from user/amd/src/local/participantsfilter/filtertypes/courseid.js
rename to lib/amd/src/datafilter/filtertypes/courseid.js
index eeb78bf35ca..33ec9465c57 100644
--- a/user/amd/src/local/participantsfilter/filtertypes/courseid.js
+++ b/lib/amd/src/datafilter/filtertypes/courseid.js
@@ -16,11 +16,11 @@
/**
* Course ID filter.
*
- * @module core_user/local/participantsfilter/filtertypes/courseid
+ * @module core/datafilter/filtertypes/courseid
* @copyright 2020 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import Filter from '../filter';
+import Filter from 'core/datafilter/filtertype';
export default class extends Filter {
constructor(filterType, filterSet) {
diff --git a/user/amd/src/local/participantsfilter/filtertypes/keyword.js b/lib/amd/src/datafilter/filtertypes/keyword.js
similarity index 93%
rename from user/amd/src/local/participantsfilter/filtertypes/keyword.js
rename to lib/amd/src/datafilter/filtertypes/keyword.js
index 5d3a92373d0..729a9b2a1d7 100644
--- a/user/amd/src/local/participantsfilter/filtertypes/keyword.js
+++ b/lib/amd/src/datafilter/filtertypes/keyword.js
@@ -16,11 +16,11 @@
/**
* Keyword filter.
*
- * @module core_user/local/participantsfilter/filtertypes/keyword
+ * @module core/datafilter/filtertypes/keyword
* @copyright 2020 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-import Filter from '../filter';
+import Filter from 'core/datafilter/filtertype';
import {get_string as getString} from 'core/str';
export default class extends Filter {
diff --git a/user/amd/src/local/participantsfilter/selectors.js b/lib/amd/src/datafilter/selectors.js
similarity index 97%
rename from user/amd/src/local/participantsfilter/selectors.js
rename to lib/amd/src/datafilter/selectors.js
index 2da7c94657e..84adc81d2cc 100644
--- a/user/amd/src/local/participantsfilter/selectors.js
+++ b/lib/amd/src/datafilter/selectors.js
@@ -16,7 +16,7 @@
/**
* Module containing the selectors for user filters.
*
- * @module core_user/local/user_filter/selectors
+ * @module core/datafilter/selectors
* @copyright 2020 Michael Hawkins
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
diff --git a/lib/classes/output/datafilter.php b/lib/classes/output/datafilter.php
new file mode 100644
index 00000000000..35c1ff27a52
--- /dev/null
+++ b/lib/classes/output/datafilter.php
@@ -0,0 +1,101 @@
+.
+
+namespace core\output;
+
+use context;
+use renderable;
+use stdClass;
+use templatable;
+
+/**
+ * The filter renderable class.
+ *
+ * @package core
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Tomo Tsuyuki
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class datafilter implements renderable, templatable {
+
+ /** @var context $context The context where the filters are being rendered. */
+ protected $context;
+
+ /** @var string $tableregionid Container of the table to be updated by this filter, is used to retrieve the table */
+ protected $tableregionid;
+
+ /** @var stdClass $course The course shown */
+ protected $course;
+
+ /**
+ * Filter constructor.
+ *
+ * @param context $context The context where the filters are being rendered
+ * @param string|null $tableregionid Container of the table which will be updated by this filter
+ */
+ public function __construct(context $context, ?string $tableregionid = null) {
+ $this->context = $context;
+ $this->tableregionid = $tableregionid;
+
+ if ($context instanceof \context_course) {
+ $this->course = get_course($context->instanceid);
+ }
+ }
+
+ /**
+ * Get data for all filter types.
+ *
+ * @return array
+ */
+ abstract protected function get_filtertypes(): array;
+
+ /**
+ * Get a standardised filter object.
+ *
+ * @param string $name
+ * @param string $title
+ * @param bool $custom
+ * @param bool $multiple
+ * @param string|null $filterclass
+ * @param array $values
+ * @param bool $allowempty
+ * @return stdClass|null
+ */
+ protected function get_filter_object(
+ string $name,
+ string $title,
+ bool $custom,
+ bool $multiple,
+ ?string $filterclass,
+ array $values,
+ bool $allowempty = false
+ ): ?stdClass {
+
+ if (!$allowempty && empty($values)) {
+ // Do not show empty filters.
+ return null;
+ }
+
+ return (object) [
+ 'name' => $name,
+ 'title' => $title,
+ 'allowcustom' => $custom,
+ 'allowmultiple' => $multiple,
+ 'filtertypeclass' => $filterclass,
+ 'values' => $values,
+ ];
+ }
+}
diff --git a/user/templates/local/participantsfilter/autocomplete_layout.mustache b/lib/templates/datafilter/autocomplete_layout.mustache
similarity index 94%
rename from user/templates/local/participantsfilter/autocomplete_layout.mustache
rename to lib/templates/datafilter/autocomplete_layout.mustache
index 4c9665f6645..89cf5ba73d8 100644
--- a/user/templates/local/participantsfilter/autocomplete_layout.mustache
+++ b/lib/templates/datafilter/autocomplete_layout.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/autocomplete_layout
+ @template core/datafilter/autocomplete_layout
Moodle template for the layout of autocomplete elements.
diff --git a/user/templates/local/participantsfilter/autocomplete_selection.mustache b/lib/templates/datafilter/autocomplete_selection.mustache
similarity index 96%
rename from user/templates/local/participantsfilter/autocomplete_selection.mustache
rename to lib/templates/datafilter/autocomplete_selection.mustache
index c365c33e892..789742050fc 100644
--- a/user/templates/local/participantsfilter/autocomplete_selection.mustache
+++ b/lib/templates/datafilter/autocomplete_selection.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/autocomplete_selection
+ @template core/datafilter/autocomplete_selection
Moodle template for the wrapper of currently selected items in an autocomplate form element.
diff --git a/user/templates/local/participantsfilter/autocomplete_selection_items.mustache b/lib/templates/datafilter/autocomplete_selection_items.mustache
similarity index 92%
rename from user/templates/local/participantsfilter/autocomplete_selection_items.mustache
rename to lib/templates/datafilter/autocomplete_selection_items.mustache
index 4faad8cc1ce..89b3c3d4514 100644
--- a/user/templates/local/participantsfilter/autocomplete_selection_items.mustache
+++ b/lib/templates/datafilter/autocomplete_selection_items.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/autocomplete_selection_items
+ @template core/datafilter/autocomplete_selection_items
Moodle template for the currently selected items in an autocomplete form element.
@@ -43,7 +43,7 @@
{{#items}}
- {{label}}
+ {{{label}}}
{{/items}}
{{^items}}
diff --git a/lib/templates/datafilter/filter.mustache b/lib/templates/datafilter/filter.mustache
new file mode 100644
index 00000000000..55b4b0e3faf
--- /dev/null
+++ b/lib/templates/datafilter/filter.mustache
@@ -0,0 +1,63 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/datafilter/filter
+
+ Template for the form containing one or more filter rows.
+
+ Example context (json):
+ {
+ "filtertypes": [
+ {
+ "name": "status",
+ "title": "Status",
+ "values": [
+ {
+ "value": 1,
+ "title": "Active"
+ },
+ {
+ "value": 0,
+ "title": "Suspended"
+ }
+ ]
+ }
+ ]
+ }
+}}
+
+
+ {{> core/datafilter/filter_match }}
+
+
+ {{> core/datafilter/filter_row }}
+
+
+
+
+
+
+
+
+
+ {{> core/datafilter/filter_types }}
+
+
+
+
diff --git a/lib/templates/datafilter/filter_match.mustache b/lib/templates/datafilter/filter_match.mustache
new file mode 100644
index 00000000000..17b8b620de2
--- /dev/null
+++ b/lib/templates/datafilter/filter_match.mustache
@@ -0,0 +1,55 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/datafilter/filter_match
+
+ Template for the form containing one or more filter rows.
+
+ Example context (json):
+ {
+ "filtertypes": [
+ {
+ "name": "status",
+ "title": "Status",
+ "values": [
+ {
+ "value": 1,
+ "title": "Active"
+ },
+ {
+ "value": 0,
+ "title": "Suspended"
+ }
+ ]
+ }
+ ]
+ }
+}}
+
+
+
+
+ {{#str}}matchofthefollowing{{/str}}
+
+
diff --git a/user/templates/local/participantsfilter/filterrow.mustache b/lib/templates/datafilter/filter_row.mustache
similarity index 66%
rename from user/templates/local/participantsfilter/filterrow.mustache
rename to lib/templates/datafilter/filter_row.mustache
index 8dd86a69c0b..f7ea7e03f08 100644
--- a/user/templates/local/participantsfilter/filterrow.mustache
+++ b/lib/templates/datafilter/filter_row.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/filterrow
+ @template core/datafilter/filter_row
Template for use by each filter condition.
@@ -35,20 +35,20 @@
}}
diff --git a/user/templates/local/participantsfilter/filtertype.mustache b/lib/templates/datafilter/filter_type.mustache
similarity index 90%
rename from user/templates/local/participantsfilter/filtertype.mustache
rename to lib/templates/datafilter/filter_type.mustache
index f38eb931786..c841d5d0b95 100644
--- a/user/templates/local/participantsfilter/filtertype.mustache
+++ b/lib/templates/datafilter/filter_type.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/filtertype
+ @template core/datafilter/filter_type
Filter type data, not shown to users but used as a source of data for form autocompletion.
@@ -33,8 +33,8 @@
"name": "status",
"title": "Enrolment Status",
"allowcustom": "0",
- "allowmultiple" false,
- "filtertypeclass": "core_user/local/participantsfilter/filtertypes/courseid",
+ "allowmultiple": false,
+ "filtertypeclass": "core/filter_types/courseid",
"values": [
{
"value": "0",
diff --git a/user/templates/local/participantsfilter/filtertypes.mustache b/lib/templates/datafilter/filter_types.mustache
similarity index 90%
rename from user/templates/local/participantsfilter/filtertypes.mustache
rename to lib/templates/datafilter/filter_types.mustache
index b8aea7c8789..f666a36075f 100644
--- a/user/templates/local/participantsfilter/filtertypes.mustache
+++ b/lib/templates/datafilter/filter_types.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_user/local/participantsfilter/filtertypes
+ @template core/datafilter/filter_types
Placeholder to fetch all filter types.
@@ -51,12 +51,12 @@
}}