| 1 | efrain | 1 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 2 | //
 | 
        
           |  |  | 3 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 4 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 5 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 6 | // (at your option) any later version.
 | 
        
           |  |  | 7 | //
 | 
        
           |  |  | 8 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 11 | // GNU General Public License for more details.
 | 
        
           |  |  | 12 | //
 | 
        
           |  |  | 13 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 14 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 15 |   | 
        
           |  |  | 16 | /**
 | 
        
           |  |  | 17 |  * Data filter management.
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module     core/datafilter
 | 
        
           |  |  | 20 |  * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 21 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  */
 | 
        
           |  |  | 23 |   | 
        
           |  |  | 24 | import CourseFilter from 'core/datafilter/filtertypes/courseid';
 | 
        
           |  |  | 25 | import GenericFilter from 'core/datafilter/filtertype';
 | 
        
           |  |  | 26 | import {getStrings} from 'core/str';
 | 
        
           |  |  | 27 | import Notification from 'core/notification';
 | 
        
           |  |  | 28 | import Pending from 'core/pending';
 | 
        
           |  |  | 29 | import Selectors from 'core/datafilter/selectors';
 | 
        
           |  |  | 30 | import Templates from 'core/templates';
 | 
        
           |  |  | 31 | import CustomEvents from 'core/custom_interaction_events';
 | 
        
           |  |  | 32 | import jQuery from 'jquery';
 | 
        
           |  |  | 33 |   | 
        
           |  |  | 34 | export default class {
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 |     /**
 | 
        
           |  |  | 37 |      * Initialise the filter on the element with the given filterSet and callback.
 | 
        
           |  |  | 38 |      *
 | 
        
           |  |  | 39 |      * @param {HTMLElement} filterSet The filter element.
 | 
        
           |  |  | 40 |      * @param {Function} applyCallback Callback function when updateTableFromFilter
 | 
        
           |  |  | 41 |      */
 | 
        
           |  |  | 42 |     constructor(filterSet, applyCallback) {
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 44 |         this.filterSet = filterSet;
 | 
        
           |  |  | 45 |         this.applyCallback = applyCallback;
 | 
        
           |  |  | 46 |         // Keep a reference to all of the active filters.
 | 
        
           |  |  | 47 |         this.activeFilters = {
 | 
        
           |  |  | 48 |             courseid: new CourseFilter('courseid', filterSet),
 | 
        
           |  |  | 49 |         };
 | 
        
           |  |  | 50 |     }
 | 
        
           |  |  | 51 |   | 
        
           |  |  | 52 |     /**
 | 
        
           |  |  | 53 |      * Initialise event listeners to the filter.
 | 
        
           |  |  | 54 |      */
 | 
        
           |  |  | 55 |     init() {
 | 
        
           |  |  | 56 |         // Add listeners for the main actions.
 | 
        
           |  |  | 57 |         this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
 | 
        
           |  |  | 58 |             if (e.target.closest(Selectors.filterset.actions.addRow)) {
 | 
        
           |  |  | 59 |                 e.preventDefault();
 | 
        
           |  |  | 60 |   | 
        
           |  |  | 61 |                 this.addFilterRow();
 | 
        
           |  |  | 62 |             }
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |             if (e.target.closest(Selectors.filterset.actions.applyFilters)) {
 | 
        
           |  |  | 65 |                 e.preventDefault();
 | 
        
           |  |  | 66 |                 this.updateTableFromFilter();
 | 
        
           |  |  | 67 |             }
 | 
        
           |  |  | 68 |   | 
        
           |  |  | 69 |             if (e.target.closest(Selectors.filterset.actions.resetFilters)) {
 | 
        
           |  |  | 70 |                 e.preventDefault();
 | 
        
           |  |  | 71 |   | 
        
           |  |  | 72 |                 this.removeAllFilters();
 | 
        
           |  |  | 73 |             }
 | 
        
           |  |  | 74 |         });
 | 
        
           |  |  | 75 |   | 
        
           |  |  | 76 |         // Add the listener to remove a single filter.
 | 
        
           |  |  | 77 |         this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {
 | 
        
           |  |  | 78 |             if (e.target.closest(Selectors.filter.actions.remove)) {
 | 
        
           |  |  | 79 |                 e.preventDefault();
 | 
        
           |  |  | 80 |   | 
        
           |  |  | 81 |                 this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);
 | 
        
           |  |  | 82 |             }
 | 
        
           |  |  | 83 |         });
 | 
        
           |  |  | 84 |   | 
        
           |  |  | 85 |         // Add listeners for the filter type selection.
 | 
        
           |  |  | 86 |         let filterRegion = jQuery(this.getFilterRegion());
 | 
        
           |  |  | 87 |         CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);
 | 
        
           |  |  | 88 |         filterRegion.on(CustomEvents.events.accessibleChange, e => {
 | 
        
           |  |  | 89 |             const typeField = e.target.closest(Selectors.filter.fields.type);
 | 
        
           |  |  | 90 |             if (typeField && typeField.value) {
 | 
        
           |  |  | 91 |                 const filter = e.target.closest(Selectors.filter.region);
 | 
        
           |  |  | 92 |   | 
        
           |  |  | 93 |                 this.addFilter(filter, typeField.value);
 | 
        
           |  |  | 94 |             }
 | 
        
           |  |  | 95 |         });
 | 
        
           |  |  | 96 |   | 
        
           |  |  | 97 |         this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {
 | 
        
           |  |  | 98 |             this.filterSet.dataset.filterverb = e.target.value;
 | 
        
           |  |  | 99 |         });
 | 
        
           |  |  | 100 |     }
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 |     /**
 | 
        
           |  |  | 103 |      * Get the filter list region.
 | 
        
           |  |  | 104 |      *
 | 
        
           |  |  | 105 |      * @return {HTMLElement}
 | 
        
           |  |  | 106 |      */
 | 
        
           |  |  | 107 |     getFilterRegion() {
 | 
        
           |  |  | 108 |         return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);
 | 
        
           |  |  | 109 |     }
 | 
        
           |  |  | 110 |   | 
        
           |  |  | 111 |     /**
 | 
        
           |  |  | 112 |      * Add a filter row.
 | 
        
           |  |  | 113 |      *
 | 
        
           |  |  | 114 |      * @param {Object} filterdata Optional, data for adding for row with an existing filter.
 | 
        
           |  |  | 115 |      * @return {Promise}
 | 
        
           |  |  | 116 |      */
 | 
        
           |  |  | 117 |     addFilterRow(filterdata = {}) {
 | 
        
           |  |  | 118 |         const pendingPromise = new Pending('core/datafilter:addFilterRow');
 | 
        
           |  |  | 119 |         const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
 | 
        
           |  |  | 120 |         return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rownum})
 | 
        
           |  |  | 121 |             .then(({html, js}) => {
 | 
        
           |  |  | 122 |                 const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);
 | 
        
           |  |  | 123 |   | 
        
           |  |  | 124 |                 return newContentNodes;
 | 
        
           |  |  | 125 |             })
 | 
        
           |  |  | 126 |             .then(filterRow => {
 | 
        
           |  |  | 127 |                 // Note: This is a nasty hack.
 | 
        
           |  |  | 128 |                 // We should try to find a better way of doing this.
 | 
        
           |  |  | 129 |                 // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
 | 
        
           |  |  | 130 |                 // it in place.
 | 
        
           |  |  | 131 |                 const typeList = this.filterSet.querySelector(Selectors.data.typeList);
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |                 filterRow.forEach(contentNode => {
 | 
        
           |  |  | 134 |                     const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
 | 
        
           |  |  | 135 |   | 
        
           |  |  | 136 |                     if (contentTypeList) {
 | 
        
           |  |  | 137 |                         contentTypeList.innerHTML = typeList.innerHTML;
 | 
        
           |  |  | 138 |                     }
 | 
        
           |  |  | 139 |                 });
 | 
        
           |  |  | 140 |   | 
        
           |  |  | 141 |                 return filterRow;
 | 
        
           |  |  | 142 |             })
 | 
        
           |  |  | 143 |             .then(filterRow => {
 | 
        
           |  |  | 144 |                 this.updateFiltersOptions();
 | 
        
           |  |  | 145 |   | 
        
           |  |  | 146 |                 return filterRow;
 | 
        
           |  |  | 147 |             })
 | 
        
           |  |  | 148 |             .then(result => {
 | 
        
           |  |  | 149 |                 pendingPromise.resolve();
 | 
        
           |  |  | 150 |   | 
        
           |  |  | 151 |                 // If an existing filter is passed in, add it. Otherwise, leave the row empty.
 | 
        
           |  |  | 152 |                 if (filterdata.filtertype) {
 | 
        
           |  |  | 153 |                     result.forEach(filter => {
 | 
        
           |  |  | 154 |                         this.addFilter(filter, filterdata.filtertype, filterdata.values,
 | 
        
           |  |  | 155 |                             filterdata.jointype, filterdata.filteroptions);
 | 
        
           |  |  | 156 |                     });
 | 
        
           |  |  | 157 |                 }
 | 
        
           |  |  | 158 |                 return result;
 | 
        
           |  |  | 159 |             })
 | 
        
           |  |  | 160 |             .catch(Notification.exception);
 | 
        
           |  |  | 161 |     }
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 |     /**
 | 
        
           |  |  | 164 |      * Get the filter data source node fro the specified filter type.
 | 
        
           |  |  | 165 |      *
 | 
        
           |  |  | 166 |      * @param {String} filterType
 | 
        
           |  |  | 167 |      * @return {HTMLElement}
 | 
        
           |  |  | 168 |      */
 | 
        
           |  |  | 169 |     getFilterDataSource(filterType) {
 | 
        
           |  |  | 170 |         const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);
 | 
        
           |  |  | 171 |   | 
        
           |  |  | 172 |         return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));
 | 
        
           |  |  | 173 |     }
 | 
        
           |  |  | 174 |   | 
        
           |  |  | 175 |     /**
 | 
        
           |  |  | 176 |      * Add a filter to the list of active filters, performing any necessary setup.
 | 
        
           |  |  | 177 |      *
 | 
        
           |  |  | 178 |      * @param {HTMLElement} filterRow
 | 
        
           |  |  | 179 |      * @param {String} filterType
 | 
        
           |  |  | 180 |      * @param {Array} initialFilterValues The initially selected values for the filter
 | 
        
           |  |  | 181 |      * @param {String} filterJoin
 | 
        
           |  |  | 182 |      * @param {Object} filterOptions
 | 
        
           |  |  | 183 |      * @returns {Filter}
 | 
        
           |  |  | 184 |      */
 | 
        
           |  |  | 185 |     async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {
 | 
        
           |  |  | 186 |         // Name the filter on the filter row.
 | 
        
           |  |  | 187 |         filterRow.dataset.filterType = filterType;
 | 
        
           |  |  | 188 |   | 
        
           |  |  | 189 |         const filterDataNode = this.getFilterDataSource(filterType);
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |         // Instantiate the Filter class.
 | 
        
           |  |  | 192 |         let Filter = GenericFilter;
 | 
        
           |  |  | 193 |         if (filterDataNode.dataset.filterTypeClass) {
 | 
        
           | 1441 | ariadna | 194 |   | 
        
           |  |  | 195 |             // Ensure the filter class passed through exists, otherwise the filtering will break.
 | 
        
           |  |  | 196 |             try {
 | 
        
           |  |  | 197 |                 Filter = await import(filterDataNode.dataset.filterTypeClass);
 | 
        
           |  |  | 198 |             } catch (error) {
 | 
        
           |  |  | 199 |                 Notification.exception(error);
 | 
        
           |  |  | 200 |             }
 | 
        
           |  |  | 201 |   | 
        
           | 1 | efrain | 202 |         }
 | 
        
           |  |  | 203 |         this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);
 | 
        
           |  |  | 204 |   | 
        
           |  |  | 205 |         // Disable the select.
 | 
        
           |  |  | 206 |         const typeField = filterRow.querySelector(Selectors.filter.fields.type);
 | 
        
           |  |  | 207 |         typeField.value = filterType;
 | 
        
           |  |  | 208 |         typeField.disabled = 'disabled';
 | 
        
           |  |  | 209 |         // Update the join list.
 | 
        
           |  |  | 210 |         this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);
 | 
        
           |  |  | 211 |         const joinField = filterRow.querySelector(Selectors.filter.fields.join);
 | 
        
           |  |  | 212 |         if (!isNaN(filterJoin)) {
 | 
        
           |  |  | 213 |             joinField.value = filterJoin;
 | 
        
           |  |  | 214 |         }
 | 
        
           |  |  | 215 |         // Update the list of available filter types.
 | 
        
           |  |  | 216 |         this.updateFiltersOptions();
 | 
        
           |  |  | 217 |   | 
        
           |  |  | 218 |         return this.activeFilters[filterType];
 | 
        
           |  |  | 219 |     }
 | 
        
           |  |  | 220 |   | 
        
           |  |  | 221 |     /**
 | 
        
           |  |  | 222 |      * Get the registered filter class for the named filter.
 | 
        
           |  |  | 223 |      *
 | 
        
           |  |  | 224 |      * @param {String} name
 | 
        
           |  |  | 225 |      * @return {Object} See the Filter class.
 | 
        
           |  |  | 226 |      */
 | 
        
           |  |  | 227 |     getFilterObject(name) {
 | 
        
           |  |  | 228 |         return this.activeFilters[name];
 | 
        
           |  |  | 229 |     }
 | 
        
           |  |  | 230 |   | 
        
           |  |  | 231 |     /**
 | 
        
           |  |  | 232 |      * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,
 | 
        
           |  |  | 233 |      * that it is replaced instead of being removed.
 | 
        
           |  |  | 234 |      *
 | 
        
           |  |  | 235 |      * @param {HTMLElement} filterRow
 | 
        
           |  |  | 236 |      * @param {Bool} refreshContent Whether to refresh the table content when removing
 | 
        
           |  |  | 237 |      */
 | 
        
           |  |  | 238 |     removeOrReplaceFilterRow(filterRow, refreshContent) {
 | 
        
           |  |  | 239 |         const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
 | 
        
           |  |  | 240 |         if (filterCount === 1) {
 | 
        
           |  |  | 241 |             this.replaceFilterRow(filterRow, refreshContent);
 | 
        
           |  |  | 242 |         } else {
 | 
        
           |  |  | 243 |             this.removeFilterRow(filterRow, refreshContent);
 | 
        
           |  |  | 244 |         }
 | 
        
           |  |  | 245 |     }
 | 
        
           |  |  | 246 |   | 
        
           |  |  | 247 |     /**
 | 
        
           |  |  | 248 |      * Remove the specified filter row and associated class.
 | 
        
           |  |  | 249 |      *
 | 
        
           |  |  | 250 |      * @param {HTMLElement} filterRow
 | 
        
           |  |  | 251 |      * @param {Bool} refreshContent Whether to refresh the table content when removing
 | 
        
           |  |  | 252 |      */
 | 
        
           |  |  | 253 |     async removeFilterRow(filterRow, refreshContent = true) {
 | 
        
           |  |  | 254 |         if (filterRow.querySelector(Selectors.data.required)) {
 | 
        
           |  |  | 255 |             return;
 | 
        
           |  |  | 256 |         }
 | 
        
           |  |  | 257 |         const filterType = filterRow.querySelector(Selectors.filter.fields.type);
 | 
        
           |  |  | 258 |         const hasFilterValue = !!filterType.value;
 | 
        
           |  |  | 259 |   | 
        
           |  |  | 260 |         // Remove the filter object.
 | 
        
           |  |  | 261 |         this.removeFilterObject(filterRow.dataset.filterType);
 | 
        
           |  |  | 262 |   | 
        
           |  |  | 263 |         // Remove the actual filter HTML.
 | 
        
           |  |  | 264 |         filterRow.remove();
 | 
        
           |  |  | 265 |   | 
        
           |  |  | 266 |         // Update the list of available filter types.
 | 
        
           |  |  | 267 |         this.updateFiltersOptions();
 | 
        
           |  |  | 268 |   | 
        
           |  |  | 269 |         if (hasFilterValue && refreshContent) {
 | 
        
           |  |  | 270 |             // Refresh the table if there was any content in this row.
 | 
        
           |  |  | 271 |             this.updateTableFromFilter();
 | 
        
           |  |  | 272 |         }
 | 
        
           |  |  | 273 |   | 
        
           |  |  | 274 |         // Update filter fieldset legends.
 | 
        
           |  |  | 275 |         const filterLegends = await this.getAvailableFilterLegends();
 | 
        
           |  |  | 276 |   | 
        
           |  |  | 277 |         this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
 | 
        
           |  |  | 278 |             filterRow.querySelector('legend').innerText = filterLegends[index];
 | 
        
           |  |  | 279 |         });
 | 
        
           |  |  | 280 |   | 
        
           |  |  | 281 |     }
 | 
        
           |  |  | 282 |   | 
        
           |  |  | 283 |     /**
 | 
        
           |  |  | 284 |      * Replace the specified filter row with a new one.
 | 
        
           |  |  | 285 |      *
 | 
        
           |  |  | 286 |      * @param {HTMLElement} filterRow
 | 
        
           |  |  | 287 |      * @param {Bool} refreshContent Whether to refresh the table content when removing
 | 
        
           |  |  | 288 |      * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
 | 
        
           |  |  | 289 |      * @return {Promise}
 | 
        
           |  |  | 290 |      */
 | 
        
           |  |  | 291 |     replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {
 | 
        
           |  |  | 292 |         if (filterRow.querySelector(Selectors.data.required)) {
 | 
        
           |  |  | 293 |             return;
 | 
        
           |  |  | 294 |         }
 | 
        
           |  |  | 295 |         // Remove the filter object.
 | 
        
           |  |  | 296 |         this.removeFilterObject(filterRow.dataset.filterType);
 | 
        
           |  |  | 297 |   | 
        
           |  |  | 298 |         return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rowNum})
 | 
        
           |  |  | 299 |             .then(({html, js}) => {
 | 
        
           |  |  | 300 |                 const newContentNodes = Templates.replaceNode(filterRow, html, js);
 | 
        
           |  |  | 301 |   | 
        
           |  |  | 302 |                 return newContentNodes;
 | 
        
           |  |  | 303 |             })
 | 
        
           |  |  | 304 |             .then(filterRow => {
 | 
        
           |  |  | 305 |                 // Note: This is a nasty hack.
 | 
        
           |  |  | 306 |                 // We should try to find a better way of doing this.
 | 
        
           |  |  | 307 |                 // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
 | 
        
           |  |  | 308 |                 // it in place.
 | 
        
           |  |  | 309 |                 const typeList = this.filterSet.querySelector(Selectors.data.typeList);
 | 
        
           |  |  | 310 |   | 
        
           |  |  | 311 |                 filterRow.forEach(contentNode => {
 | 
        
           |  |  | 312 |                     const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |                     if (contentTypeList) {
 | 
        
           |  |  | 315 |                         contentTypeList.innerHTML = typeList.innerHTML;
 | 
        
           |  |  | 316 |                     }
 | 
        
           |  |  | 317 |                 });
 | 
        
           |  |  | 318 |   | 
        
           |  |  | 319 |                 return filterRow;
 | 
        
           |  |  | 320 |             })
 | 
        
           |  |  | 321 |             .then(filterRow => {
 | 
        
           |  |  | 322 |                 this.updateFiltersOptions();
 | 
        
           |  |  | 323 |   | 
        
           |  |  | 324 |                 return filterRow;
 | 
        
           |  |  | 325 |             })
 | 
        
           |  |  | 326 |             .then(filterRow => {
 | 
        
           |  |  | 327 |                 // Refresh the table.
 | 
        
           |  |  | 328 |                 if (refreshContent) {
 | 
        
           |  |  | 329 |                     return this.updateTableFromFilter();
 | 
        
           |  |  | 330 |                 } else {
 | 
        
           |  |  | 331 |                     return filterRow;
 | 
        
           |  |  | 332 |                 }
 | 
        
           |  |  | 333 |             })
 | 
        
           |  |  | 334 |             .catch(Notification.exception);
 | 
        
           |  |  | 335 |     }
 | 
        
           |  |  | 336 |   | 
        
           |  |  | 337 |     /**
 | 
        
           |  |  | 338 |      * Remove the Filter Object from the register.
 | 
        
           |  |  | 339 |      *
 | 
        
           |  |  | 340 |      * @param {string} filterName The name of the filter to be removed
 | 
        
           |  |  | 341 |      */
 | 
        
           |  |  | 342 |     removeFilterObject(filterName) {
 | 
        
           |  |  | 343 |         if (filterName) {
 | 
        
           |  |  | 344 |             const filter = this.getFilterObject(filterName);
 | 
        
           |  |  | 345 |             if (filter) {
 | 
        
           |  |  | 346 |                 filter.tearDown();
 | 
        
           |  |  | 347 |   | 
        
           |  |  | 348 |                 // Remove from the list of active filters.
 | 
        
           |  |  | 349 |                 delete this.activeFilters[filterName];
 | 
        
           |  |  | 350 |             }
 | 
        
           |  |  | 351 |         }
 | 
        
           |  |  | 352 |     }
 | 
        
           |  |  | 353 |   | 
        
           |  |  | 354 |     /**
 | 
        
           |  |  | 355 |      * Remove all filters.
 | 
        
           |  |  | 356 |      *
 | 
        
           |  |  | 357 |      * @returns {Promise}
 | 
        
           |  |  | 358 |      */
 | 
        
           |  |  | 359 |     removeAllFilters() {
 | 
        
           |  |  | 360 |         const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
 | 
        
           |  |  | 361 |         filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));
 | 
        
           |  |  | 362 |   | 
        
           |  |  | 363 |         // Refresh the table.
 | 
        
           |  |  | 364 |         return this.updateTableFromFilter();
 | 
        
           |  |  | 365 |     }
 | 
        
           |  |  | 366 |   | 
        
           |  |  | 367 |     /**
 | 
        
           |  |  | 368 |      * Remove any empty filters.
 | 
        
           |  |  | 369 |      */
 | 
        
           |  |  | 370 |     removeEmptyFilters() {
 | 
        
           |  |  | 371 |         const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
 | 
        
           |  |  | 372 |         filters.forEach(filterRow => {
 | 
        
           |  |  | 373 |             const filterType = filterRow.querySelector(Selectors.filter.fields.type);
 | 
        
           |  |  | 374 |             if (!filterType.value) {
 | 
        
           |  |  | 375 |                 this.removeOrReplaceFilterRow(filterRow, false);
 | 
        
           |  |  | 376 |             }
 | 
        
           |  |  | 377 |         });
 | 
        
           |  |  | 378 |     }
 | 
        
           |  |  | 379 |   | 
        
           |  |  | 380 |     /**
 | 
        
           |  |  | 381 |      * Update the list of filter types to filter out those already selected.
 | 
        
           |  |  | 382 |      */
 | 
        
           |  |  | 383 |     updateFiltersOptions() {
 | 
        
           |  |  | 384 |         const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
 | 
        
           |  |  | 385 |         filters.forEach(filterRow => {
 | 
        
           |  |  | 386 |             const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');
 | 
        
           |  |  | 387 |             options.forEach(option => {
 | 
        
           |  |  | 388 |                 if (option.value === filterRow.dataset.filterType) {
 | 
        
           |  |  | 389 |                     option.classList.remove('hidden');
 | 
        
           |  |  | 390 |                     option.disabled = false;
 | 
        
           |  |  | 391 |                 } else if (this.activeFilters[option.value]) {
 | 
        
           |  |  | 392 |                     option.classList.add('hidden');
 | 
        
           |  |  | 393 |                     option.disabled = true;
 | 
        
           |  |  | 394 |                 } else {
 | 
        
           |  |  | 395 |                     option.classList.remove('hidden');
 | 
        
           |  |  | 396 |                     option.disabled = false;
 | 
        
           |  |  | 397 |                 }
 | 
        
           |  |  | 398 |             });
 | 
        
           |  |  | 399 |         });
 | 
        
           |  |  | 400 |   | 
        
           |  |  | 401 |         // Configure the state of the "Add row" button.
 | 
        
           |  |  | 402 |         // This button is disabled when there is a filter row available for each condition.
 | 
        
           |  |  | 403 |         const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);
 | 
        
           |  |  | 404 |         const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);
 | 
        
           |  |  | 405 |         if (filterDataNode.length <= filters.length) {
 | 
        
           |  |  | 406 |             addRowButton.setAttribute('disabled', 'disabled');
 | 
        
           |  |  | 407 |         } else {
 | 
        
           |  |  | 408 |             addRowButton.removeAttribute('disabled');
 | 
        
           |  |  | 409 |         }
 | 
        
           |  |  | 410 |   | 
        
           |  |  | 411 |         if (filters.length === 1) {
 | 
        
           |  |  | 412 |             this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');
 | 
        
           |  |  | 413 |             this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;
 | 
        
           |  |  | 414 |             this.filterSet.dataset.filterverb = 2;
 | 
        
           |  |  | 415 |         } else {
 | 
        
           |  |  | 416 |             this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');
 | 
        
           |  |  | 417 |         }
 | 
        
           |  |  | 418 |     }
 | 
        
           |  |  | 419 |   | 
        
           |  |  | 420 |     /**
 | 
        
           |  |  | 421 |      * Update the Dynamic table based upon the current filter.
 | 
        
           | 1441 | ariadna | 422 |      *
 | 
        
           |  |  | 423 |      * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,
 | 
        
           |  |  | 424 |      *     for example for pagination/sorting.
 | 
        
           | 1 | efrain | 425 |      */
 | 
        
           | 1441 | ariadna | 426 |     updateTableFromFilter(validate = true) {
 | 
        
           | 1 | efrain | 427 |         const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');
 | 
        
           |  |  | 428 |   | 
        
           |  |  | 429 |         const filters = {};
 | 
        
           | 1441 | ariadna | 430 |         let valid = true;
 | 
        
           | 1 | efrain | 431 |         Object.values(this.activeFilters).forEach(filter => {
 | 
        
           | 1441 | ariadna | 432 |             if (validate) {
 | 
        
           |  |  | 433 |                 valid = valid && filter.validate();
 | 
        
           |  |  | 434 |             }
 | 
        
           | 1 | efrain | 435 |             filters[filter.filterValue.name] = filter.filterValue;
 | 
        
           |  |  | 436 |         });
 | 
        
           | 1441 | ariadna | 437 |         if (validate) {
 | 
        
           |  |  | 438 |             valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();
 | 
        
           |  |  | 439 |         }
 | 
        
           |  |  | 440 |         if (this.applyCallback && valid) {
 | 
        
           | 1 | efrain | 441 |             this.applyCallback(filters, pendingPromise);
 | 
        
           | 1441 | ariadna | 442 |         } else {
 | 
        
           |  |  | 443 |             pendingPromise.resolve();
 | 
        
           | 1 | efrain | 444 |         }
 | 
        
           |  |  | 445 |     }
 | 
        
           |  |  | 446 |   | 
        
           |  |  | 447 |     /**
 | 
        
           |  |  | 448 |      * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
 | 
        
           |  |  | 449 |      *
 | 
        
           |  |  | 450 |      * @return {array}
 | 
        
           |  |  | 451 |      */
 | 
        
           |  |  | 452 |     async getAvailableFilterLegends() {
 | 
        
           |  |  | 453 |         const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
 | 
        
           |  |  | 454 |         let requests = [];
 | 
        
           |  |  | 455 |   | 
        
           |  |  | 456 |         [...Array(maxFilters)].forEach((_, rowIndex) => {
 | 
        
           |  |  | 457 |             requests.push({
 | 
        
           |  |  | 458 |                 "key": "filterrowlegend",
 | 
        
           |  |  | 459 |                 "component": "core",
 | 
        
           |  |  | 460 |                 // Add 1 since rows begin at 1 (index begins at zero).
 | 
        
           |  |  | 461 |                 "param": rowIndex + 1
 | 
        
           |  |  | 462 |             });
 | 
        
           |  |  | 463 |         });
 | 
        
           |  |  | 464 |   | 
        
           |  |  | 465 |         const legendStrings = await getStrings(requests)
 | 
        
           |  |  | 466 |             .then(fetchedStrings => {
 | 
        
           |  |  | 467 |                 return fetchedStrings;
 | 
        
           |  |  | 468 |             })
 | 
        
           |  |  | 469 |             .catch(Notification.exception);
 | 
        
           |  |  | 470 |   | 
        
           |  |  | 471 |         return legendStrings;
 | 
        
           |  |  | 472 |     }
 | 
        
           |  |  | 473 |   | 
        
           |  |  | 474 |     /**
 | 
        
           |  |  | 475 |      * Update the list of join types for a filter.
 | 
        
           |  |  | 476 |      *
 | 
        
           |  |  | 477 |      * This will update the list of join types based on the allowed types defined for a filter.
 | 
        
           |  |  | 478 |      * If only one type is allowed, the list will be hidden.
 | 
        
           |  |  | 479 |      *
 | 
        
           |  |  | 480 |      * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.
 | 
        
           |  |  | 481 |      * @param {Element} filterRow The row being updated.
 | 
        
           |  |  | 482 |      */
 | 
        
           |  |  | 483 |     updateJoinList(filterJoinList, filterRow) {
 | 
        
           |  |  | 484 |         const regularJoinList = [0, 1, 2];
 | 
        
           |  |  | 485 |         // If a join list was specified for this filter, find the default join list and disable the options that are not allowed
 | 
        
           |  |  | 486 |         // for this filter.
 | 
        
           |  |  | 487 |         if (filterJoinList.length !== 0) {
 | 
        
           |  |  | 488 |             const joinField = filterRow.querySelector(Selectors.filter.fields.join);
 | 
        
           |  |  | 489 |             // Check each option from the default list, and disable the option in this filter row if it is not allowed
 | 
        
           |  |  | 490 |             // for this filter.
 | 
        
           |  |  | 491 |             regularJoinList.forEach((join) => {
 | 
        
           |  |  | 492 |                 if (!filterJoinList.includes(join)) {
 | 
        
           |  |  | 493 |                     joinField.options[join].classList.add('hidden');
 | 
        
           |  |  | 494 |                     joinField.options[join].disabled = true;
 | 
        
           |  |  | 495 |                 }
 | 
        
           |  |  | 496 |             });
 | 
        
           |  |  | 497 |             // Now remove the disabled options, and hide the select list of there is only one option left.
 | 
        
           |  |  | 498 |             joinField.options.forEach((element, index) => {
 | 
        
           |  |  | 499 |                 if (element.disabled) {
 | 
        
           |  |  | 500 |                     joinField.options[index] = null;
 | 
        
           |  |  | 501 |                 }
 | 
        
           |  |  | 502 |             });
 | 
        
           |  |  | 503 |             if (joinField.options.length === 1) {
 | 
        
           |  |  | 504 |                 joinField.hidden = true;
 | 
        
           |  |  | 505 |             }
 | 
        
           |  |  | 506 |         }
 | 
        
           |  |  | 507 |     }
 | 
        
           |  |  | 508 | }
 |