MDL-66890 forumreport_summary: Improved filter close keyboard response
[moodle.git] / mod / forum / report / summary / amd / src / filters.js
CommitLineData
b29de56d
MH
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 * 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 */
24
25import $ from 'jquery';
26import Popper from 'core/popper';
ce8a2d11
JP
27import CustomEvents from 'core/custom_interaction_events';
28import Selectors from 'forumreport_summary/selectors';
a9496531
MH
29import Y from 'core/yui';
30import Ajax from 'core/ajax';
b2aa354d 31import KeyCodes from 'core/key_codes';
b29de56d
MH
32
33export const init = (root) => {
ce8a2d11 34 let jqRoot = $(root);
b29de56d
MH
35
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 });
42
43 // Generic filter handlers.
44
b29de56d 45 // Called to override click event to trigger a proper generate request with filtering.
a9496531
MH
46 const generateWithFilters = (event) => {
47 let newLink = $('#filtersform').attr('action');
b29de56d
MH
48
49 if (event) {
50 event.preventDefault();
51
52 let filterParams = event.target.search.substr(1);
53 newLink += '&' + filterParams;
54 }
55
56 $('#filtersform').attr('action', newLink);
57 $('#filtersform').submit();
58 };
59
60 // Override 'reset table preferences' so it generates with filters.
61 $('.resettable').on("click", "a", function(event) {
62 generateWithFilters(event);
63 });
64
65 // Override table heading sort links so they generate with filters.
66 $('thead').on("click", "a", function(event) {
67 generateWithFilters(event);
68 });
69
70 // Override pagination page links so they generate with filters.
71 $('.pagination').on("click", "a", function(event) {
72 generateWithFilters(event);
73 });
74
cc15134c 75 // Submit report via filter
a9496531 76 const submitWithFilter = (containerelement) => {
b2aa354d
MH
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 });
81
cc15134c
MH
82 // Close the container (eg popover).
83 $(containerelement).addClass('hidden');
84
85 // Submit the filter values and re-generate report.
86 generateWithFilters(false);
87 };
88
a9496531
MH
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);
93
94 popperContent.style.removeProperty("z-index");
95 new Popper(referenceElement, popperContent, {placement: 'bottom'});
96 };
97
b2aa354d 98 // Close the relevant filter.
46fbfb98 99 const closeOpenFilters = (openFilterButton, openFilter) => {
b2aa354d
MH
100 openFilter.classList.add('hidden');
101 openFilter.setAttribute('data-openfilter', 'false');
a9496531 102
b2aa354d
MH
103 openFilterButton.classList.add('btn-primary');
104 openFilterButton.classList.remove('btn-outline-primary');
105 openFilterButton.setAttribute('aria-expanded', false);
a9496531
MH
106 };
107
b29de56d
MH
108 // Groups filter specific handlers.
109
cc15134c 110 // Event handler for clicking select all groups.
ce8a2d11
JP
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 });
117
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 });
b29de56d
MH
125 });
126
127 // Event handler for showing groups filter popover.
b2aa354d 128 jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() {
b29de56d 129 // Create popover.
a9496531 130 let referenceElement = root.querySelector(Selectors.filters.group.trigger),
ce8a2d11 131 popperContent = root.querySelector(Selectors.filters.group.popover);
b29de56d
MH
132
133 new Popper(referenceElement, popperContent, {placement: 'bottom'});
134
ce8a2d11
JP
135 // Show popover.
136 popperContent.classList.remove('hidden');
b2aa354d 137 popperContent.setAttribute('data-openfilter', 'true');
ce8a2d11
JP
138
139 // Change to outlined button.
140 referenceElement.classList.add('btn-outline-primary');
141 referenceElement.classList.remove('btn-primary');
142
143 // Let screen readers know that it's now expanded.
144 referenceElement.setAttribute('aria-expanded', true);
b2aa354d
MH
145
146 // Add listeners to handle closing filter.
147 const closeListener = e => {
46fbfb98
MH
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)) {
b2aa354d
MH
150 closeOpenFilters(referenceElement, popperContent);
151 document.removeEventListener('click', closeListener);
46fbfb98 152 document.removeEventListener('keyup', closeListener);
b2aa354d
MH
153 document.removeEventListener('keyup', escCloseListener);
154 }
155 };
156
157 document.addEventListener('click', closeListener);
46fbfb98 158 document.addEventListener('keyup', closeListener);
b2aa354d
MH
159
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 };
167
168 document.addEventListener('keyup', escCloseListener);
b29de56d
MH
169 });
170
cc15134c 171 // Event handler to click save groups filter.
ce8a2d11 172 jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() {
b2aa354d
MH
173 // Copy the saved values into the form before submitting.
174 let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox);
175
176 popcheckboxes.forEach(function(popcheckbox) {
177 let filtersform = document.forms.filtersform,
178 saveid = popcheckbox.getAttribute('data-saveid');
179
180 filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked;
181 });
182
cc15134c
MH
183 submitWithFilter('#filter-groups-popover');
184 });
a9496531
MH
185
186 // Dates filter specific handlers.
187
188 // Event handler for showing dates filter popover.
b2aa354d 189 jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() {
a9496531
MH
190
191 // Create popover.
192 let referenceElement = root.querySelector(Selectors.filters.date.trigger),
193 popperContent = root.querySelector(Selectors.filters.date.popover);
194
195 new Popper(referenceElement, popperContent, {placement: 'bottom'});
196
197 // Show popover and move focus.
198 popperContent.classList.remove('hidden');
b2aa354d 199 popperContent.setAttribute('data-openfilter', 'true');
a9496531
MH
200 popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus();
201
202 // Change to outlined button.
203 referenceElement.classList.add('btn-outline-primary');
204 referenceElement.classList.remove('btn-primary');
205
206 // Let screen readers know that it's now expanded.
207 referenceElement.setAttribute('aria-expanded', true);
b2aa354d
MH
208
209 // Add listener to handle closing filter.
210 const closeListener = e => {
46fbfb98
MH
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)) {
b2aa354d
MH
213 closeOpenFilters(referenceElement, popperContent);
214 document.removeEventListener('click', closeListener);
46fbfb98 215 document.removeEventListener('keyup', closeListener);
b2aa354d
MH
216 document.removeEventListener('keyup', escCloseListener);
217 }
218 };
219
220 document.addEventListener('click', closeListener);
46fbfb98 221 document.addEventListener('keyup', closeListener);
b2aa354d
MH
222
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 };
230
231 document.addEventListener('keyup', escCloseListener);
a9496531
MH
232 });
233
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;
241
a9496531
MH
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;
248
249 // Submit the filter values and re-generate report.
250 submitWithFilter('#filter-dates-popover');
251 } else {
252 let args = {data: []};
253
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 }
264
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 }
275
276 const request = {
277 methodname: 'core_calendar_get_timestamps',
278 args: args
279 };
280
281 Ajax.call([request])[0].done(function(result) {
282 let fromTimestamp = 0,
283 toTimestamp = 0;
284
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 });
292
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;
303
304 // Submit the filter values and re-generate report.
305 submitWithFilter('#filter-dates-popover');
306 }
307 });
308 }
309 });
310
311 jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() {
312 updateCalendarPosition(Selectors.filters.date.calendariconfrom);
313 });
314
315 jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() {
316 updateCalendarPosition(Selectors.filters.date.calendariconto);
317 });
b29de56d 318};