diff --git a/lib/amd/build/datafilter.min.js b/lib/amd/build/datafilter.min.js
index aca4d46e5c0..98353ea8c49 100644
--- a/lib/amd/build/datafilter.min.js
+++ b/lib/amd/build/datafilter.min.js
@@ -1,3 +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(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$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(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),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,filterJoin,filterOptions){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,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),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];if(filterRow.querySelector(_selectors.default.data.required))return;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;if(!filterRow.querySelector(_selectors.default.data.required))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(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_exports.default}));
+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(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$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(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),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,filterJoin,filterOptions){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;if(filterDataNode.dataset.filterTypeClass)try{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]))}catch(error){_notification.default.exception(error)}this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),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];if(filterRow.querySelector(_selectors.default.data.required))return;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;if(!filterRow.querySelector(_selectors.default.data.required))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(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_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
index 77da59b377a..c51f0a1a000 100644
--- a/lib/amd/build/datafilter.min.js.map
+++ b/lib/amd/build/datafilter.min.js.map
@@ -1 +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 {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 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 a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.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 // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\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 * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\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, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\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 if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\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 if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\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 * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\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\",\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 * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\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","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"}
\ No newline at end of file
+{"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 {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 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 a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.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 // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\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 * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\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\n // Ensure the filter class passed through exists, otherwise the filtering will break.\n try {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n } catch (error) {\n Notification.exception(error);\n }\n\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\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 if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\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 if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\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 * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\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\",\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 * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\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","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","error","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/datetime.min.js b/lib/amd/build/datafilter/filtertypes/datetime.min.js
index 2bab51b17b1..869ce2194bc 100644
--- a/lib/amd/build/datafilter/filtertypes/datetime.min.js
+++ b/lib/amd/build/datafilter/filtertypes/datetime.min.js
@@ -1,3 +1,3 @@
-define("core/datafilter/filtertypes/datetime",["exports","core/datafilter/filtertype","core/datafilter/selectors","core/templates","core/notification","core/str"],(function(_exports,_filtertype,_selectors,_templates,_notification,_str){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,_filtertype=_interopRequireDefault(_filtertype),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification);const MODES_before="before",MODES_after="after",MODES_between="between";class _default extends _filtertype.default{constructor(filterType,rootNode,initialValues){let filterOptions=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{mode:MODES_before};super(filterType,rootNode,initialValues),_defineProperty(this,"SELECTORS",{afterwrapper:".".concat(this.filterType,"-afterwrapper"),beforewrapper:".".concat(this.filterType,"-beforewrapper"),betweenwrapper:".".concat(this.filterType,"-betweenwrapper"),mode:"[data-filterfield=".concat(this.filterType,"-mode]")}),_defineProperty(this,"mode",null),this.addModeSelector(filterOptions.mode)}async addValueSelector(){var _initialValues$,_initialValues$2;let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const specificFilterSet=this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType)),sourceDataNode=this.getSourceDataForFilter(),defaultBefore=sourceDataNode.getElementsByTagName("option")[0].value,defaultAfter=sourceDataNode.getElementsByTagName("option")[1].value,title=sourceDataNode.getAttribute("data-field-title"),labels=await(0,_str.get_strings)([{key:"datetimefilterafter",component:"core",param:{title:title}},{key:"datetimefilterbefore",component:"core",param:{title:title}}]),context={filtertype:this.filterType,afterlabel:labels[0],beforelabel:labels[1],required:sourceDataNode.dataset.required,aftervalue:null!==(_initialValues$=initialValues[0])&&void 0!==_initialValues$?_initialValues$:defaultAfter,beforevalue:null!==(_initialValues$2=initialValues[1])&&void 0!==_initialValues$2?_initialValues$2:defaultBefore,now:defaultBefore},datetimeUi=await _templates.default.renderForPromise("core/datafilter/filtertypes/datetime_selector",context);return _templates.default.replaceNodeContents(specificFilterSet.querySelector(_selectors.default.filter.regions.values),datetimeUi.html,datetimeUi.js)}async addModeSelector(mode){const modeStrings=await(0,_str.get_strings)([{key:"selectdates"},{key:"filterdatebefore",component:"reportbuilder"},{key:"filterdateafter",component:"reportbuilder"},{key:"between"}]),context={label:modeStrings[0],filtertype:this.filterType,modeoptions:[{value:MODES_before,label:modeStrings[1],selected:mode===MODES_before?"selected":""},{value:MODES_after,label:modeStrings[2],selected:mode===MODES_after?"selected":""},{value:MODES_between,label:modeStrings[3],selected:mode===MODES_between?"selected":""}]},modeUi=await _templates.default.renderForPromise("core/datafilter/filtertypes/datetime_mode",context);this.getFilterValueNode().insertAdjacentHTML("beforebegin",modeUi.html);const modeSelect=this.filterRoot.querySelector(this.SELECTORS.mode);modeSelect.addEventListener("change",this.updateFieldVisibility.bind(this)),modeSelect.dispatchEvent(new Event("change"))}updateFieldVisibility(event){const filterValueNode=this.getFilterValueNode(),afterWrapper=filterValueNode.querySelector(this.SELECTORS.afterwrapper),beforeWrapper=filterValueNode.querySelector(this.SELECTORS.beforewrapper),betweenWrapper=filterValueNode.querySelector(this.SELECTORS.betweenwrapper),value=event.target.value;value===MODES_between?betweenWrapper.classList.remove("d-none"):betweenWrapper.classList.add("d-none"),value===MODES_after||value===MODES_between?afterWrapper.classList.remove("d-none"):afterWrapper.classList.add("d-none"),value===MODES_before||value===MODES_between?beforeWrapper.classList.remove("d-none"):beforeWrapper.classList.add("d-none")}get values(){return[this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'1"]')).value,this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'2"]')).value]}get filterOptions(){return[{name:"mode",value:this.filterRoot.querySelector(this.SELECTORS.mode).value}]}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values,filteroptions:this.filterOptions}}validate(){const mode=document.querySelector(this.SELECTORS.mode).value,before=document.querySelector(this.SELECTORS.beforewrapper+" input"),after=document.querySelector(this.SELECTORS.afterwrapper+" input");return after.setCustomValidity(""),!(mode===MODES_between&&after.value>=before.value)||((0,_str.get_strings)([{key:"invaliddatetimebetween",component:"error",param:{before:before.value,after:after.value}}]).then((strings=>(after.setCustomValidity(strings[0]),after.reportValidity(),strings))).catch(_notification.default.exception),!1)}}return _exports.default=_default,_exports.default}));
+define("core/datafilter/filtertypes/datetime",["exports","core/datafilter/filtertype","core/datafilter/selectors","core/templates","core/notification","core/str"],(function(_exports,_filtertype,_selectors,_templates,_notification,_str){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,_filtertype=_interopRequireDefault(_filtertype),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification);const MODES_before="before",MODES_after="after",MODES_between="between";class _default extends _filtertype.default{constructor(filterType,rootNode,initialValues){let filterOptions=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{mode:MODES_before};super(filterType,rootNode,initialValues),_defineProperty(this,"SELECTORS",{afterwrapper:".".concat(this.filterType,"-afterwrapper"),beforewrapper:".".concat(this.filterType,"-beforewrapper"),betweenwrapper:".".concat(this.filterType,"-betweenwrapper"),mode:"[data-filterfield=".concat(this.filterType,"-mode]")}),_defineProperty(this,"mode",null),this.addModeSelector(filterOptions.mode)}async getContext(initialValues){var _initialValues$,_initialValues$2;const sourceDataNode=this.getSourceDataForFilter(),defaultBefore=sourceDataNode.getElementsByTagName("option")[0].value,defaultAfter=sourceDataNode.getElementsByTagName("option")[1].value,title=sourceDataNode.getAttribute("data-field-title"),labels=await(0,_str.get_strings)([{key:"datetimefilterafter",component:"core",param:{title:title}},{key:"datetimefilterbefore",component:"core",param:{title:title}}]);return{filtertype:this.filterType,afterlabel:labels[0],beforelabel:labels[1],required:sourceDataNode.dataset.required,aftervalue:null!==(_initialValues$=initialValues[0])&&void 0!==_initialValues$?_initialValues$:defaultAfter,beforevalue:null!==(_initialValues$2=initialValues[1])&&void 0!==_initialValues$2?_initialValues$2:defaultBefore}}async addValueSelector(){let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const specificFilterSet=this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType)),context=await this.getContext(initialValues),datetimeUi=await _templates.default.renderForPromise("core/datafilter/filtertypes/datetime_selector",context);return _templates.default.replaceNodeContents(specificFilterSet.querySelector(_selectors.default.filter.regions.values),datetimeUi.html,datetimeUi.js)}async addModeSelector(mode){const modeStrings=await(0,_str.get_strings)([{key:"selectdates"},{key:"filterdatebefore",component:"reportbuilder"},{key:"filterdateafter",component:"reportbuilder"},{key:"between"}]),context={label:modeStrings[0],filtertype:this.filterType,modeoptions:[{value:MODES_before,label:modeStrings[1],selected:mode===MODES_before?"selected":""},{value:MODES_after,label:modeStrings[2],selected:mode===MODES_after?"selected":""},{value:MODES_between,label:modeStrings[3],selected:mode===MODES_between?"selected":""}]},modeUi=await _templates.default.renderForPromise("core/datafilter/filtertypes/datetime_mode",context);this.getFilterValueNode().insertAdjacentHTML("beforebegin",modeUi.html);const modeSelect=this.filterRoot.querySelector(this.SELECTORS.mode);modeSelect.addEventListener("change",this.updateFieldVisibility.bind(this)),modeSelect.dispatchEvent(new Event("change"))}updateFieldVisibility(event){const filterValueNode=this.getFilterValueNode(),afterWrapper=filterValueNode.querySelector(this.SELECTORS.afterwrapper),beforeWrapper=filterValueNode.querySelector(this.SELECTORS.beforewrapper),betweenWrapper=filterValueNode.querySelector(this.SELECTORS.betweenwrapper),value=event.target.value;value===MODES_between?betweenWrapper.classList.remove("d-none"):betweenWrapper.classList.add("d-none"),value===MODES_after||value===MODES_between?afterWrapper.classList.remove("d-none"):afterWrapper.classList.add("d-none"),value===MODES_before||value===MODES_between?beforeWrapper.classList.remove("d-none"):beforeWrapper.classList.add("d-none")}get values(){return[this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'1"]')).value,this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'2"]')).value]}get filterOptions(){return[{name:"mode",value:this.filterRoot.querySelector(this.SELECTORS.mode).value}]}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values,filteroptions:this.filterOptions}}validate(){const mode=document.querySelector(this.SELECTORS.mode).value,before=document.querySelector(this.SELECTORS.beforewrapper+" input"),after=document.querySelector(this.SELECTORS.afterwrapper+" input");return after.setCustomValidity(""),!(mode===MODES_between&&after.value>=before.value)||((0,_str.get_strings)([{key:"invaliddatetimebetween",component:"error",param:{before:before.value,after:after.value}}]).then((strings=>(after.setCustomValidity(strings[0]),after.reportValidity(),strings))).catch(_notification.default.exception),!1)}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=datetime.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/datafilter/filtertypes/datetime.min.js.map b/lib/amd/build/datafilter/filtertypes/datetime.min.js.map
index 68397fa7342..8b4f36a1d4c 100644
--- a/lib/amd/build/datafilter/filtertypes/datetime.min.js.map
+++ b/lib/amd/build/datafilter/filtertypes/datetime.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"datetime.min.js","sources":["../../../src/datafilter/filtertypes/datetime.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 for a date/time selector\n *\n * @module core/datafilter/filtertypes/datetime\n * @author Mark Johnson \n * @copyright 2024 Catalyst IT Europe Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport {get_strings as getStrings} from 'core/str';\n\nconst MODES = {\n before: 'before',\n after: 'after',\n between: 'between',\n};\n\nexport default class extends Filter {\n\n SELECTORS = {\n afterwrapper: `.${this.filterType}-afterwrapper`,\n beforewrapper: `.${this.filterType}-beforewrapper`,\n betweenwrapper: `.${this.filterType}-betweenwrapper`,\n mode: `[data-filterfield=${this.filterType}-mode]`,\n };\n\n mode = null;\n\n constructor(filterType, rootNode, initialValues, filterOptions = {mode: MODES.before}) {\n super(filterType, rootNode, initialValues);\n this.addModeSelector(filterOptions.mode);\n }\n\n async addValueSelector(initialValues = []) {\n // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.\n const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n const sourceDataNode = this.getSourceDataForFilter();\n const defaultBefore = sourceDataNode.getElementsByTagName('option')[0].value;\n const defaultAfter = sourceDataNode.getElementsByTagName('option')[1].value;\n const title = sourceDataNode.getAttribute('data-field-title');\n const labels = await getStrings([\n {\n key: 'datetimefilterafter',\n component: 'core',\n param: {title},\n },\n {\n key: 'datetimefilterbefore',\n component: 'core',\n param: {title},\n },\n ]);\n const context = {\n filtertype: this.filterType,\n afterlabel: labels[0],\n beforelabel: labels[1],\n required: sourceDataNode.dataset.required,\n aftervalue: initialValues[0] ?? defaultAfter,\n beforevalue: initialValues[1] ?? defaultBefore,\n now: defaultBefore,\n };\n const datetimeUi = await Templates.renderForPromise('core/datafilter/filtertypes/datetime_selector', context);\n return Templates.replaceNodeContents(\n specificFilterSet.querySelector(Selectors.filter.regions.values),\n datetimeUi.html,\n datetimeUi.js\n );\n }\n async addModeSelector(mode) {\n const modeStrings = await getStrings([\n {key: 'selectdates'},\n {key: 'filterdatebefore', component: 'reportbuilder'},\n {key: 'filterdateafter', component: 'reportbuilder'},\n {key: 'between'}\n ]);\n const context = {\n label: modeStrings[0],\n filtertype: this.filterType,\n modeoptions: [\n {\n value: MODES.before,\n label: modeStrings[1],\n selected: mode === MODES.before ? 'selected' : '',\n },\n {\n value: MODES.after,\n label: modeStrings[2],\n selected: mode === MODES.after ? 'selected' : '',\n },\n {\n value: MODES.between,\n label: modeStrings[3],\n selected: mode === MODES.between ? 'selected' : '',\n },\n ],\n };\n const modeUi = await Templates.renderForPromise('core/datafilter/filtertypes/datetime_mode', context);\n const filterValueNode = this.getFilterValueNode();\n filterValueNode.insertAdjacentHTML('beforebegin', modeUi.html);\n const modeSelect = this.filterRoot.querySelector(this.SELECTORS.mode);\n modeSelect.addEventListener('change', this.updateFieldVisibility.bind(this));\n modeSelect.dispatchEvent(new Event('change')); // Update field visibility based on initial mode.\n }\n\n updateFieldVisibility(event) {\n const filterValueNode = this.getFilterValueNode();\n const afterWrapper = filterValueNode.querySelector(this.SELECTORS.afterwrapper);\n const beforeWrapper = filterValueNode.querySelector(this.SELECTORS.beforewrapper);\n const betweenWrapper = filterValueNode.querySelector(this.SELECTORS.betweenwrapper);\n const value = event.target.value;\n if (value === MODES.between) {\n betweenWrapper.classList.remove('d-none');\n } else {\n betweenWrapper.classList.add('d-none');\n }\n if (value === MODES.after || value === MODES.between) {\n afterWrapper.classList.remove('d-none');\n } else {\n afterWrapper.classList.add('d-none');\n }\n if (value === MODES.before || value === MODES.between) {\n beforeWrapper.classList.remove('d-none');\n } else {\n beforeWrapper.classList.add('d-none');\n }\n }\n\n get values() {\n return [\n this.filterRoot.querySelector(`[data-filterfield=\"${this.name}1\"]`).value,\n this.filterRoot.querySelector(`[data-filterfield=\"${this.name}2\"]`).value,\n ];\n }\n\n get filterOptions() {\n return [\n {name: 'mode', value: this.filterRoot.querySelector(this.SELECTORS.mode).value}\n ];\n }\n\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n filteroptions: this.filterOptions,\n };\n }\n\n validate() {\n const mode = document.querySelector(this.SELECTORS.mode).value;\n const before = document.querySelector(this.SELECTORS.beforewrapper + ' input');\n const after = document.querySelector(this.SELECTORS.afterwrapper + ' input');\n after.setCustomValidity('');\n if (mode === MODES.between) {\n if (after.value >= before.value) {\n getStrings([\n {\n key: 'invaliddatetimebetween',\n component: 'error',\n param: {\n before: before.value,\n after: after.value,\n },\n },\n ]).then((strings) => {\n after.setCustomValidity(strings[0]);\n after.reportValidity();\n return strings;\n }).catch(Notification.exception);\n return false;\n }\n }\n\n return true;\n }\n}"],"names":["MODES","Filter","constructor","filterType","rootNode","initialValues","filterOptions","mode","afterwrapper","this","beforewrapper","betweenwrapper","addModeSelector","specificFilterSet","querySelector","Selectors","filter","byName","sourceDataNode","getSourceDataForFilter","defaultBefore","getElementsByTagName","value","defaultAfter","title","getAttribute","labels","key","component","param","context","filtertype","afterlabel","beforelabel","required","dataset","aftervalue","beforevalue","now","datetimeUi","Templates","renderForPromise","replaceNodeContents","regions","values","html","js","modeStrings","label","modeoptions","selected","modeUi","getFilterValueNode","insertAdjacentHTML","modeSelect","filterRoot","SELECTORS","addEventListener","updateFieldVisibility","bind","dispatchEvent","Event","event","filterValueNode","afterWrapper","beforeWrapper","betweenWrapper","target","classList","remove","add","name","filterValue","jointype","filteroptions","validate","document","before","after","setCustomValidity","then","strings","reportValidity","catch","Notification","exception"],"mappings":"wvBA8BMA,aACM,SADNA,YAEK,QAFLA,cAGO,iCAGgBC,oBAWzBC,YAAYC,WAAYC,SAAUC,mBAAeC,qEAAgB,CAACC,KAAMP,oBAC9DG,WAAYC,SAAUC,gDAVpB,CACRG,wBAAkBC,KAAKN,4BACvBO,yBAAmBD,KAAKN,6BACxBQ,0BAAoBF,KAAKN,8BACzBI,iCAA2BE,KAAKN,mDAG7B,WAIES,gBAAgBN,cAAcC,wEAGhBF,qEAAgB,SAE7BQ,kBAAoBJ,KAAKL,SAASU,cAAcC,mBAAUC,OAAOC,OAAOR,KAAKN,aAC7Ee,eAAiBT,KAAKU,yBACtBC,cAAgBF,eAAeG,qBAAqB,UAAU,GAAGC,MACjEC,aAAeL,eAAeG,qBAAqB,UAAU,GAAGC,MAChEE,MAAQN,eAAeO,aAAa,oBACpCC,aAAe,oBAAW,CAC5B,CACIC,IAAK,sBACLC,UAAW,OACXC,MAAO,CAACL,MAAAA,QAEZ,CACIG,IAAK,uBACLC,UAAW,OACXC,MAAO,CAACL,MAAAA,UAGVM,QAAU,CACZC,WAAYtB,KAAKN,WACjB6B,WAAYN,OAAO,GACnBO,YAAaP,OAAO,GACpBQ,SAAUhB,eAAeiB,QAAQD,SACjCE,mCAAY/B,cAAc,8CAAMkB,aAChCc,qCAAahC,cAAc,gDAAMe,cACjCkB,IAAKlB,eAEHmB,iBAAmBC,mBAAUC,iBAAiB,gDAAiDX,gBAC9FU,mBAAUE,oBACb7B,kBAAkBC,cAAcC,mBAAUC,OAAO2B,QAAQC,QACzDL,WAAWM,KACXN,WAAWO,0BAGGvC,YACZwC,kBAAoB,oBAAW,CACjC,CAACpB,IAAK,eACN,CAACA,IAAK,mBAAoBC,UAAW,iBACrC,CAACD,IAAK,kBAAmBC,UAAW,iBACpC,CAACD,IAAK,aAEJG,QAAU,CACZkB,MAAOD,YAAY,GACnBhB,WAAYtB,KAAKN,WACjB8C,YAAa,CACT,CACI3B,MAAOtB,aACPgD,MAAOD,YAAY,GACnBG,SAAU3C,OAASP,aAAe,WAAa,IAEnD,CACIsB,MAAOtB,YACPgD,MAAOD,YAAY,GACnBG,SAAU3C,OAASP,YAAc,WAAa,IAElD,CACIsB,MAAOtB,cACPgD,MAAOD,YAAY,GACnBG,SAAU3C,OAASP,cAAgB,WAAa,MAItDmD,aAAeX,mBAAUC,iBAAiB,4CAA6CX,SACrErB,KAAK2C,qBACbC,mBAAmB,cAAeF,OAAON,YACnDS,WAAa7C,KAAK8C,WAAWzC,cAAcL,KAAK+C,UAAUjD,MAChE+C,WAAWG,iBAAiB,SAAUhD,KAAKiD,sBAAsBC,KAAKlD,OACtE6C,WAAWM,cAAc,IAAIC,MAAM,WAGvCH,sBAAsBI,aACZC,gBAAkBtD,KAAK2C,qBACvBY,aAAeD,gBAAgBjD,cAAcL,KAAK+C,UAAUhD,cAC5DyD,cAAgBF,gBAAgBjD,cAAcL,KAAK+C,UAAU9C,eAC7DwD,eAAiBH,gBAAgBjD,cAAcL,KAAK+C,UAAU7C,gBAC9DW,MAAQwC,MAAMK,OAAO7C,MACvBA,QAAUtB,cACVkE,eAAeE,UAAUC,OAAO,UAEhCH,eAAeE,UAAUE,IAAI,UAE7BhD,QAAUtB,aAAesB,QAAUtB,cACnCgE,aAAaI,UAAUC,OAAO,UAE9BL,aAAaI,UAAUE,IAAI,UAE3BhD,QAAUtB,cAAgBsB,QAAUtB,cACpCiE,cAAcG,UAAUC,OAAO,UAE/BJ,cAAcG,UAAUE,IAAI,UAIhC1B,mBACO,CACHnC,KAAK8C,WAAWzC,2CAAoCL,KAAK8D,aAAWjD,MACpEb,KAAK8C,WAAWzC,2CAAoCL,KAAK8D,aAAWjD,OAIxEhB,0BACO,CACH,CAACiE,KAAM,OAAQjD,MAAOb,KAAK8C,WAAWzC,cAAcL,KAAK+C,UAAUjD,MAAMe,QAI7EkD,wBACO,CACHD,KAAM9D,KAAK8D,KACXE,SAAUhE,KAAKgE,SACf7B,OAAQnC,KAAKmC,OACb8B,cAAejE,KAAKH,eAI5BqE,iBACUpE,KAAOqE,SAAS9D,cAAcL,KAAK+C,UAAUjD,MAAMe,MACnDuD,OAASD,SAAS9D,cAAcL,KAAK+C,UAAU9C,cAAgB,UAC/DoE,MAAQF,SAAS9D,cAAcL,KAAK+C,UAAUhD,aAAe,iBACnEsE,MAAMC,kBAAkB,MACpBxE,OAASP,eACL8E,MAAMxD,OAASuD,OAAOvD,8BACX,CACP,CACIK,IAAK,yBACLC,UAAW,QACXC,MAAO,CACHgD,OAAQA,OAAOvD,MACfwD,MAAOA,MAAMxD,UAGtB0D,MAAMC,UACLH,MAAMC,kBAAkBE,QAAQ,IAChCH,MAAMI,iBACCD,WACRE,MAAMC,sBAAaC,YACf"}
\ No newline at end of file
+{"version":3,"file":"datetime.min.js","sources":["../../../src/datafilter/filtertypes/datetime.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 for a date/time selector\n *\n * @module core/datafilter/filtertypes/datetime\n * @author Mark Johnson \n * @copyright 2024 Catalyst IT Europe Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport {get_strings as getStrings} from 'core/str';\n\nconst MODES = {\n before: 'before',\n after: 'after',\n between: 'between',\n};\n\nexport default class extends Filter {\n\n SELECTORS = {\n afterwrapper: `.${this.filterType}-afterwrapper`,\n beforewrapper: `.${this.filterType}-beforewrapper`,\n betweenwrapper: `.${this.filterType}-betweenwrapper`,\n mode: `[data-filterfield=${this.filterType}-mode]`,\n };\n\n mode = null;\n\n constructor(filterType, rootNode, initialValues, filterOptions = {mode: MODES.before}) {\n super(filterType, rootNode, initialValues);\n this.addModeSelector(filterOptions.mode);\n }\n\n /**\n * Get the context object to be sent through to the mustache template.\n * This can be overridden by any filters which inherit from datetime to add/exclude data.\n *\n * @param {array} initialValues\n * @returns {Promise<{filtertype: *, afterlabel: *, beforelabel: *, required, aftervalue: *, beforevalue: *}>}\n */\n async getContext(initialValues) {\n const sourceDataNode = this.getSourceDataForFilter();\n const defaultBefore = sourceDataNode.getElementsByTagName('option')[0].value;\n const defaultAfter = sourceDataNode.getElementsByTagName('option')[1].value;\n const title = sourceDataNode.getAttribute('data-field-title');\n const labels = await getStrings([\n {\n key: 'datetimefilterafter',\n component: 'core',\n param: {title},\n },\n {\n key: 'datetimefilterbefore',\n component: 'core',\n param: {title},\n },\n ]);\n return {\n filtertype: this.filterType,\n afterlabel: labels[0],\n beforelabel: labels[1],\n required: sourceDataNode.dataset.required,\n aftervalue: initialValues[0] ?? defaultAfter,\n beforevalue: initialValues[1] ?? defaultBefore,\n };\n }\n\n async addValueSelector(initialValues = []) {\n // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.\n const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n const context = await this.getContext(initialValues);\n const datetimeUi = await Templates.renderForPromise('core/datafilter/filtertypes/datetime_selector', context);\n return Templates.replaceNodeContents(\n specificFilterSet.querySelector(Selectors.filter.regions.values),\n datetimeUi.html,\n datetimeUi.js\n );\n }\n async addModeSelector(mode) {\n const modeStrings = await getStrings([\n {key: 'selectdates'},\n {key: 'filterdatebefore', component: 'reportbuilder'},\n {key: 'filterdateafter', component: 'reportbuilder'},\n {key: 'between'}\n ]);\n const context = {\n label: modeStrings[0],\n filtertype: this.filterType,\n modeoptions: [\n {\n value: MODES.before,\n label: modeStrings[1],\n selected: mode === MODES.before ? 'selected' : '',\n },\n {\n value: MODES.after,\n label: modeStrings[2],\n selected: mode === MODES.after ? 'selected' : '',\n },\n {\n value: MODES.between,\n label: modeStrings[3],\n selected: mode === MODES.between ? 'selected' : '',\n },\n ],\n };\n const modeUi = await Templates.renderForPromise('core/datafilter/filtertypes/datetime_mode', context);\n const filterValueNode = this.getFilterValueNode();\n filterValueNode.insertAdjacentHTML('beforebegin', modeUi.html);\n const modeSelect = this.filterRoot.querySelector(this.SELECTORS.mode);\n modeSelect.addEventListener('change', this.updateFieldVisibility.bind(this));\n modeSelect.dispatchEvent(new Event('change')); // Update field visibility based on initial mode.\n }\n\n updateFieldVisibility(event) {\n const filterValueNode = this.getFilterValueNode();\n const afterWrapper = filterValueNode.querySelector(this.SELECTORS.afterwrapper);\n const beforeWrapper = filterValueNode.querySelector(this.SELECTORS.beforewrapper);\n const betweenWrapper = filterValueNode.querySelector(this.SELECTORS.betweenwrapper);\n const value = event.target.value;\n if (value === MODES.between) {\n betweenWrapper.classList.remove('d-none');\n } else {\n betweenWrapper.classList.add('d-none');\n }\n if (value === MODES.after || value === MODES.between) {\n afterWrapper.classList.remove('d-none');\n } else {\n afterWrapper.classList.add('d-none');\n }\n if (value === MODES.before || value === MODES.between) {\n beforeWrapper.classList.remove('d-none');\n } else {\n beforeWrapper.classList.add('d-none');\n }\n }\n\n get values() {\n return [\n this.filterRoot.querySelector(`[data-filterfield=\"${this.name}1\"]`).value,\n this.filterRoot.querySelector(`[data-filterfield=\"${this.name}2\"]`).value,\n ];\n }\n\n get filterOptions() {\n return [\n {name: 'mode', value: this.filterRoot.querySelector(this.SELECTORS.mode).value}\n ];\n }\n\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n filteroptions: this.filterOptions,\n };\n }\n\n validate() {\n const mode = document.querySelector(this.SELECTORS.mode).value;\n const before = document.querySelector(this.SELECTORS.beforewrapper + ' input');\n const after = document.querySelector(this.SELECTORS.afterwrapper + ' input');\n after.setCustomValidity('');\n if (mode === MODES.between) {\n if (after.value >= before.value) {\n getStrings([\n {\n key: 'invaliddatetimebetween',\n component: 'error',\n param: {\n before: before.value,\n after: after.value,\n },\n },\n ]).then((strings) => {\n after.setCustomValidity(strings[0]);\n after.reportValidity();\n return strings;\n }).catch(Notification.exception);\n return false;\n }\n }\n\n return true;\n }\n}"],"names":["MODES","Filter","constructor","filterType","rootNode","initialValues","filterOptions","mode","afterwrapper","this","beforewrapper","betweenwrapper","addModeSelector","sourceDataNode","getSourceDataForFilter","defaultBefore","getElementsByTagName","value","defaultAfter","title","getAttribute","labels","key","component","param","filtertype","afterlabel","beforelabel","required","dataset","aftervalue","beforevalue","specificFilterSet","querySelector","Selectors","filter","byName","context","getContext","datetimeUi","Templates","renderForPromise","replaceNodeContents","regions","values","html","js","modeStrings","label","modeoptions","selected","modeUi","getFilterValueNode","insertAdjacentHTML","modeSelect","filterRoot","SELECTORS","addEventListener","updateFieldVisibility","bind","dispatchEvent","Event","event","filterValueNode","afterWrapper","beforeWrapper","betweenWrapper","target","classList","remove","add","name","filterValue","jointype","filteroptions","validate","document","before","after","setCustomValidity","then","strings","reportValidity","catch","Notification","exception"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/amd/src/datafilter.js b/lib/amd/src/datafilter.js
index 6a773ba107d..d3fa3562e72 100644
--- a/lib/amd/src/datafilter.js
+++ b/lib/amd/src/datafilter.js
@@ -191,7 +191,14 @@ export default class {
// Instantiate the Filter class.
let Filter = GenericFilter;
if (filterDataNode.dataset.filterTypeClass) {
- Filter = await import(filterDataNode.dataset.filterTypeClass);
+
+ // Ensure the filter class passed through exists, otherwise the filtering will break.
+ try {
+ Filter = await import(filterDataNode.dataset.filterTypeClass);
+ } catch (error) {
+ Notification.exception(error);
+ }
+
}
this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);
diff --git a/lib/amd/src/datafilter/filtertypes/datetime.js b/lib/amd/src/datafilter/filtertypes/datetime.js
index 920d39b1114..503cadae388 100644
--- a/lib/amd/src/datafilter/filtertypes/datetime.js
+++ b/lib/amd/src/datafilter/filtertypes/datetime.js
@@ -50,9 +50,14 @@ export default class extends Filter {
this.addModeSelector(filterOptions.mode);
}
- async addValueSelector(initialValues = []) {
- // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.
- const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));
+ /**
+ * Get the context object to be sent through to the mustache template.
+ * This can be overridden by any filters which inherit from datetime to add/exclude data.
+ *
+ * @param {array} initialValues
+ * @returns {Promise<{filtertype: *, afterlabel: *, beforelabel: *, required, aftervalue: *, beforevalue: *}>}
+ */
+ async getContext(initialValues) {
const sourceDataNode = this.getSourceDataForFilter();
const defaultBefore = sourceDataNode.getElementsByTagName('option')[0].value;
const defaultAfter = sourceDataNode.getElementsByTagName('option')[1].value;
@@ -69,15 +74,20 @@ export default class extends Filter {
param: {title},
},
]);
- const context = {
+ return {
filtertype: this.filterType,
afterlabel: labels[0],
beforelabel: labels[1],
required: sourceDataNode.dataset.required,
aftervalue: initialValues[0] ?? defaultAfter,
beforevalue: initialValues[1] ?? defaultBefore,
- now: defaultBefore,
};
+ }
+
+ async addValueSelector(initialValues = []) {
+ // We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.
+ const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));
+ const context = await this.getContext(initialValues);
const datetimeUi = await Templates.renderForPromise('core/datafilter/filtertypes/datetime_selector', context);
return Templates.replaceNodeContents(
specificFilterSet.querySelector(Selectors.filter.regions.values),
diff --git a/lib/templates/datafilter/filtertypes/datetime_selector.mustache b/lib/templates/datafilter/filtertypes/datetime_selector.mustache
index 5218d9d7755..3fce5192272 100644
--- a/lib/templates/datafilter/filtertypes/datetime_selector.mustache
+++ b/lib/templates/datafilter/filtertypes/datetime_selector.mustache
@@ -50,7 +50,7 @@
value="{{aftervalue}}"
data-filterfield="{{filtertype}}1"
data-field-title="{{afterlabel}}"
- max="{{now}}"
+ {{#max}}max="{{max}}"{{/max}}
>
@@ -66,7 +66,7 @@
value="{{beforevalue}}"
data-filterfield="{{filtertype}}2"
data-field-title="{{beforelabel}}"
- max="{{now}}"
+ {{#max}}max="{{max}}"{{/max}}
>