MDL-66890 forumreport_summary: Improved filter close keyboard response
[moodle.git] / mod / forum / report / summary / amd / src / filters.js
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/>.
16 /**
17  * Module responsible for handling forum summary report filters.
18  *
19  * @module     forumreport_summary/filters
20  * @package    forumreport_summary
21  * @copyright  2019 Michael Hawkins <michaelh@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 import $ from 'jquery';
26 import Popper from 'core/popper';
27 import CustomEvents from 'core/custom_interaction_events';
28 import Selectors from 'forumreport_summary/selectors';
29 import Y from 'core/yui';
30 import Ajax from 'core/ajax';
31 import KeyCodes from 'core/key_codes';
33 export const init = (root) => {
34     let jqRoot = $(root);
36     // Hide loading spinner and show report once page is ready.
37     // This ensures filters can be applied when sorting by columns.
38     $(document).ready(function() {
39         $('.loading-icon').hide();
40         $('#summaryreport').removeClass('hidden');
41     });
43     // Generic filter handlers.
45     // Called to override click event to trigger a proper generate request with filtering.
46     const generateWithFilters = (event) => {
47         let newLink = $('#filtersform').attr('action');
49         if (event) {
50             event.preventDefault();
52             let filterParams = event.target.search.substr(1);
53             newLink += '&' + filterParams;
54         }
56         $('#filtersform').attr('action', newLink);
57         $('#filtersform').submit();
58     };
60     // Override 'reset table preferences' so it generates with filters.
61     $('.resettable').on("click", "a", function(event) {
62         generateWithFilters(event);
63     });
65     // Override table heading sort links so they generate with filters.
66     $('thead').on("click", "a", function(event) {
67         generateWithFilters(event);
68     });
70     // Override pagination page links so they generate with filters.
71     $('.pagination').on("click", "a", function(event) {
72         generateWithFilters(event);
73     });
75     // Submit report via filter
76     const submitWithFilter = (containerelement) => {
77         // Disable the dates filter mform checker to prevent any changes triggering a warning to the user.
78         Y.use('moodle-core-formchangechecker', function() {
79             M.core_formchangechecker.reset_form_dirty_state();
80         });
82         // Close the container (eg popover).
83         $(containerelement).addClass('hidden');
85         // Submit the filter values and re-generate report.
86         generateWithFilters(false);
87     };
89     // Use popper to override date mform calendar position.
90     const updateCalendarPosition = (referenceid) => {
91         let referenceElement = document.querySelector(referenceid),
92             popperContent = document.querySelector(Selectors.filters.date.calendar);
94         popperContent.style.removeProperty("z-index");
95         new Popper(referenceElement, popperContent, {placement: 'bottom'});
96     };
98     // Close the relevant filter.
99     const closeOpenFilters = (openFilterButton, openFilter) => {
100         openFilter.classList.add('hidden');
101         openFilter.setAttribute('data-openfilter', 'false');
103         openFilterButton.classList.add('btn-primary');
104         openFilterButton.classList.remove('btn-outline-primary');
105         openFilterButton.setAttribute('aria-expanded', false);
106     };
108     // Groups filter specific handlers.
110     // Event handler for clicking select all groups.
111     jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.selectall, function() {
112         let deselected = root.querySelectorAll(Selectors.filters.group.checkbox + ':not(:checked)');
113         deselected.forEach(function(checkbox) {
114             checkbox.checked = true;
115         });
116     });
118     // Event handler for clearing filter by clicking option.
119     jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.clear, function() {
120         // Clear checkboxes.
121         let selected = root.querySelectorAll(Selectors.filters.group.checkbox + ':checked');
122         selected.forEach(function(checkbox) {
123             checkbox.checked = false;
124         });
125     });
127     // Event handler for showing groups filter popover.
128     jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() {
129         // Create popover.
130         let referenceElement = root.querySelector(Selectors.filters.group.trigger),
131             popperContent = root.querySelector(Selectors.filters.group.popover);
133         new Popper(referenceElement, popperContent, {placement: 'bottom'});
135         // Show popover.
136         popperContent.classList.remove('hidden');
137         popperContent.setAttribute('data-openfilter', 'true');
139         // Change to outlined button.
140         referenceElement.classList.add('btn-outline-primary');
141         referenceElement.classList.remove('btn-primary');
143         // Let screen readers know that it's now expanded.
144         referenceElement.setAttribute('aria-expanded', true);
146         // Add listeners to handle closing filter.
147         const closeListener = e => {
148             if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') &&
149                     (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {
150                 closeOpenFilters(referenceElement, popperContent);
151                 document.removeEventListener('click', closeListener);
152                 document.removeEventListener('keyup', closeListener);
153                 document.removeEventListener('keyup', escCloseListener);
154             }
155         };
157         document.addEventListener('click', closeListener);
158         document.addEventListener('keyup', closeListener);
160         const escCloseListener = e => {
161             if (e.keyCode === KeyCodes.escape) {
162                 closeOpenFilters(referenceElement, popperContent);
163                 document.removeEventListener('keyup', escCloseListener);
164                 document.removeEventListener('click', closeListener);
165             }
166         };
168         document.addEventListener('keyup', escCloseListener);
169     });
171     // Event handler to click save groups filter.
172     jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() {
173         // Copy the saved values into the form before submitting.
174         let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox);
176         popcheckboxes.forEach(function(popcheckbox) {
177             let filtersform = document.forms.filtersform,
178                 saveid = popcheckbox.getAttribute('data-saveid');
180             filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked;
181         });
183         submitWithFilter('#filter-groups-popover');
184     });
186     // Dates filter specific handlers.
188    // Event handler for showing dates filter popover.
189     jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() {
191         // Create popover.
192         let referenceElement = root.querySelector(Selectors.filters.date.trigger),
193             popperContent = root.querySelector(Selectors.filters.date.popover);
195         new Popper(referenceElement, popperContent, {placement: 'bottom'});
197         // Show popover and move focus.
198         popperContent.classList.remove('hidden');
199         popperContent.setAttribute('data-openfilter', 'true');
200         popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus();
202         // Change to outlined button.
203         referenceElement.classList.add('btn-outline-primary');
204         referenceElement.classList.remove('btn-primary');
206         // Let screen readers know that it's now expanded.
207         referenceElement.setAttribute('aria-expanded', true);
209         // Add listener to handle closing filter.
210         const closeListener = e => {
211             if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') &&
212                     (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {
213                 closeOpenFilters(referenceElement, popperContent);
214                 document.removeEventListener('click', closeListener);
215                 document.removeEventListener('keyup', closeListener);
216                 document.removeEventListener('keyup', escCloseListener);
217             }
218         };
220         document.addEventListener('click', closeListener);
221         document.addEventListener('keyup', closeListener);
223         const escCloseListener = e => {
224             if (e.keyCode === KeyCodes.escape) {
225                 closeOpenFilters(referenceElement, popperContent);
226                 document.removeEventListener('keyup', escCloseListener);
227                 document.removeEventListener('click', closeListener);
228             }
229         };
231         document.addEventListener('keyup', escCloseListener);
232     });
234     // Event handler to save dates filter.
235     jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() {
236         // Populate the hidden form inputs to submit the data.
237         let filtersForm = document.forms.filtersform;
238         const datesPopover = root.querySelector(Selectors.filters.date.popover);
239         const fromEnabled = datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked ? 1 : 0;
240         const toEnabled = datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked ? 1 : 0;
242         if (!fromEnabled && !toEnabled) {
243             // Update the elements in the filter form.
244             filtersForm.elements['datefrom[timestamp]'].value = 0;
245             filtersForm.elements['datefrom[enabled]'].value = fromEnabled;
246             filtersForm.elements['dateto[timestamp]'].value = 0;
247             filtersForm.elements['dateto[enabled]'].value = toEnabled;
249             // Submit the filter values and re-generate report.
250             submitWithFilter('#filter-dates-popover');
251         } else {
252             let args = {data: []};
254             if (fromEnabled) {
255                 args.data.push({
256                     'key': 'from',
257                     'year': datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value,
258                     'month': datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value,
259                     'day': datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value,
260                     'hour': 0,
261                     'minute': 0
262                 });
263             }
265             if (toEnabled) {
266                 args.data.push({
267                     'key': 'to',
268                     'year': datesPopover.querySelector('[name="filterdatetopopover[year]"]').value,
269                     'month': datesPopover.querySelector('[name="filterdatetopopover[month]"]').value,
270                     'day': datesPopover.querySelector('[name="filterdatetopopover[day]"]').value,
271                     'hour': 23,
272                     'minute': 59
273                 });
274             }
276             const request = {
277                 methodname: 'core_calendar_get_timestamps',
278                 args: args
279             };
281             Ajax.call([request])[0].done(function(result) {
282                 let fromTimestamp = 0,
283                     toTimestamp = 0;
285                 result['timestamps'].forEach(function(data){
286                     if (data.key === 'from') {
287                         fromTimestamp = data.timestamp;
288                     } else if (data.key === 'to') {
289                         toTimestamp = data.timestamp;
290                     }
291                 });
293                 // Display an error if the from date is later than the do date.
294                 if (toTimestamp > 0 && fromTimestamp > toTimestamp) {
295                     const warningdiv = document.getElementById('dates-filter-warning');
296                     warningdiv.classList.remove('hidden');
297                     warningdiv.classList.add('d-block');
298                 } else {
299                     filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp;
300                     filtersForm.elements['datefrom[enabled]'].value = fromEnabled;
301                     filtersForm.elements['dateto[timestamp]'].value = toTimestamp;
302                     filtersForm.elements['dateto[enabled]'].value = toEnabled;
304                     // Submit the filter values and re-generate report.
305                     submitWithFilter('#filter-dates-popover');
306                 }
307             });
308         }
309     });
311     jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() {
312         updateCalendarPosition(Selectors.filters.date.calendariconfrom);
313     });
315     jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() {
316         updateCalendarPosition(Selectors.filters.date.calendariconto);
317     });
318 };