Merge branch 'MDL-68963-master' of git://github.com/bmbrands/moodle
[moodle.git] / lib / table / amd / src / dynamic.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 to handle dynamic table features.
18  *
19  * @module     core_table/dynamic
20  * @package    core_table
21  * @copyright  2020 Simey Lameze <simey@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 import * as Selectors from 'core_table/local/dynamic/selectors';
25 import Events from './local/dynamic/events';
26 import Pending from 'core/pending';
27 import {addIconToContainer} from 'core/loadingicon';
28 import {fetch as fetchTableData} from 'core_table/local/dynamic/repository';
29 import Notification from 'core/notification';
31 let watching = false;
33 /**
34  * Ensure that a table is a dynamic table.
35  *
36  * @param {HTMLElement} tableRoot
37  * @returns {Bool}
38  */
39 const checkTableIsDynamic = tableRoot => {
40     if (!tableRoot) {
41         // The table is not a dynamic table.
42         throw new Error("The table specified is not a dynamic table and cannot be updated");
43     }
45     if (!tableRoot.matches(Selectors.main.region)) {
46         // The table is not a dynamic table.
47         throw new Error("The table specified is not a dynamic table and cannot be updated");
48     }
50     return true;
51 };
53 /**
54  * Get the filterset data from a known dynamic table.
55  *
56  * @param {HTMLElement} tableRoot
57  * @returns {Object}
58  */
59 const getFiltersetFromTable = tableRoot => {
60     return JSON.parse(tableRoot.dataset.tableFilters);
61 };
63 /**
64  * Update the specified table based on its current values.
65  *
66  * @param {HTMLElement} tableRoot
67  * @param {Bool} resetContent
68  * @returns {Promise}
69  */
70 export const refreshTableContent = (tableRoot, resetContent = false) => {
71     const filterset = getFiltersetFromTable(tableRoot);
72     addIconToContainer(tableRoot);
74     const pendingPromise = new Pending('core_table/dynamic:refreshTableContent');
76     return fetchTableData(
77         tableRoot.dataset.tableComponent,
78         tableRoot.dataset.tableHandler,
79         tableRoot.dataset.tableUniqueid,
80         {
81             sortData: JSON.parse(tableRoot.dataset.tableSortData),
82             joinType: filterset.jointype,
83             filters: filterset.filters,
84             firstinitial: tableRoot.dataset.tableFirstInitial,
85             lastinitial: tableRoot.dataset.tableLastInitial,
86             pageNumber: tableRoot.dataset.tablePageNumber,
87             pageSize: tableRoot.dataset.tablePageSize,
88             hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),
89         },
90         resetContent,
91     )
92     .then(data => {
93         const placeholder = document.createElement('div');
94         placeholder.innerHTML = data.html;
95         tableRoot.replaceWith(...placeholder.childNodes);
97         // Update the tableRoot.
98         return getTableFromId(tableRoot.dataset.tableUniqueid);
99     }).then(tableRoot => {
100         tableRoot.dispatchEvent(new CustomEvent(Events.tableContentRefreshed, {
101             bubbles: true,
102         }));
104         return tableRoot;
105     })
106     .then(tableRoot => {
107         pendingPromise.resolve();
109         return tableRoot;
110     });
111 };
113 export const updateTable = (tableRoot, {
114     sortBy = null,
115     sortOrder = null,
116     filters = null,
117     firstInitial = null,
118     lastInitial = null,
119     pageNumber = null,
120     pageSize = null,
121     hiddenColumns = null,
122 } = {}, refreshContent = true) => {
123     checkTableIsDynamic(tableRoot);
125     const pendingPromise = new Pending('core_table/dynamic:updateTable');
127     // Update sort fields.
128     if (sortBy && sortOrder) {
129         const sortData = JSON.parse(tableRoot.dataset.tableSortData);
130         sortData.unshift({
131             sortby: sortBy,
132             sortorder: parseInt(sortOrder, 10),
133         });
134         tableRoot.dataset.tableSortData = JSON.stringify(sortData);
135     }
137     // Update initials.
138     if (firstInitial !== null) {
139         tableRoot.dataset.tableFirstInitial = firstInitial;
140     }
142     if (lastInitial !== null) {
143         tableRoot.dataset.tableLastInitial = lastInitial;
144     }
146     if (pageNumber !== null) {
147         tableRoot.dataset.tablePageNumber = pageNumber;
148     }
150     if (pageSize !== null) {
151         tableRoot.dataset.tablePageSize = pageSize;
152     }
154     // Update filters.
155     if (filters) {
156         tableRoot.dataset.tableFilters = JSON.stringify(filters);
157     }
159     // Update hidden columns.
160     if (hiddenColumns) {
161         tableRoot.dataset.tableHiddenColumns = JSON.stringify(hiddenColumns);
162     }
164     // Refresh.
165     if (refreshContent) {
166         return refreshTableContent(tableRoot)
167         .then(tableRoot => {
168             pendingPromise.resolve();
169             return tableRoot;
170         });
171     } else {
172         pendingPromise.resolve();
173         return Promise.resolve(tableRoot);
174     }
175 };
177 /**
178  * Get the table dataset for the specified tableRoot, ensuring that the provided table is a dynamic table.
179  *
180  * @param {HTMLElement} tableRoot
181  * @returns {DOMStringMap}
182  */
183 const getTableData = tableRoot => {
184     checkTableIsDynamic(tableRoot);
186     return tableRoot.dataset;
187 };
189 /**
190  * Update the specified table using the new filters.
191  *
192  * @param {HTMLElement} tableRoot
193  * @param {Object} filters
194  * @param {Bool} refreshContent
195  * @returns {Promise}
196  */
197 export const setFilters = (tableRoot, filters, refreshContent = true) =>
198     updateTable(tableRoot, {filters}, refreshContent);
200 /**
201  * Get the filter data for the specified table.
202  *
203  * @param {HTMLElement} tableRoot
204  * @returns {Object}
205  */
206 export const getFilters = tableRoot => {
207     checkTableIsDynamic(tableRoot);
209     return getFiltersetFromTable(tableRoot);
210 };
212 /**
213  * Update the sort order.
214  *
215  * @param {HTMLElement} tableRoot
216  * @param {String} sortBy
217  * @param {Number} sortOrder
218  * @param {Bool} refreshContent
219  * @returns {Promise}
220  */
221 export const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true) =>
222     updateTable(tableRoot, {sortBy, sortOrder}, refreshContent);
224 /**
225  * Set the page number.
226  *
227  * @param {HTMLElement} tableRoot
228  * @param {String} pageNumber
229  * @param {Bool} refreshContent
230  * @returns {Promise}
231  */
232 export const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>
233     updateTable(tableRoot, {pageNumber}, refreshContent);
235 /**
236  * Get the current page number.
237  *
238  * @param {HTMLElement} tableRoot
239  * @returns {Number}
240  */
241 export const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumber;
243 /**
244  * Set the page size.
245  *
246  * @param {HTMLElement} tableRoot
247  * @param {Number} pageSize
248  * @param {Bool} refreshContent
249  * @returns {Promise}
250  */
251 export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
252     updateTable(tableRoot, {pageSize, pageNumber: 1}, refreshContent);
254 /**
255  * Get the current page size.
256  *
257  * @param {HTMLElement} tableRoot
258  * @returns {Number}
259  */
260 export const getPageSize = tableRoot => getTableData(tableRoot).tablePageSize;
262 /**
263  * Update the first initial to show.
264  *
265  * @param {HTMLElement} tableRoot
266  * @param {String} firstInitial
267  * @param {Bool} refreshContent
268  * @returns {Promise}
269  */
270 export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>
271     updateTable(tableRoot, {firstInitial}, refreshContent);
273 /**
274  * Get the current first initial filter.
275  *
276  * @param {HTMLElement} tableRoot
277  * @returns {String}
278  */
279 export const getFirstInitial = tableRoot => getTableData(tableRoot).tableFirstInitial;
281 /**
282  * Update the last initial to show.
283  *
284  * @param {HTMLElement} tableRoot
285  * @param {String} lastInitial
286  * @param {Bool} refreshContent
287  * @returns {Promise}
288  */
289 export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
290     updateTable(tableRoot, {lastInitial}, refreshContent);
292 /**
293  * Get the current last initial filter.
294  *
295  * @param {HTMLElement} tableRoot
296  * @returns {String}
297  */
298 export const getLastInitial = tableRoot => getTableData(tableRoot).tableLastInitial;
300 /**
301  * Hide a column in the participants table.
302  *
303  * @param {HTMLElement} tableRoot
304  * @param {String} columnToHide
305  * @param {Bool} refreshContent
306  */
307 export const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {
308     const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
309     hiddenColumns.push(columnToHide);
311     updateTable(tableRoot, {hiddenColumns}, refreshContent);
312 };
314 /**
315  * Make a hidden column visible in the participants table.
316  *
317  * @param {HTMLElement} tableRoot
318  * @param {String} columnToShow
319  * @param {Bool} refreshContent
320  */
321 export const showColumn = (tableRoot, columnToShow, refreshContent = true) => {
322     let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
323     hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);
325     updateTable(tableRoot, {hiddenColumns}, refreshContent);
326 };
328 /**
329  * Reset table preferences.
330  *
331  * @param {HTMLElement} tableRoot
332  * @returns {Promise}
333  */
334 const resetTablePreferences = tableRoot => refreshTableContent(tableRoot, true);
336 /**
337  * Set up listeners to handle table updates.
338  */
339 export const init = () => {
340     if (watching) {
341         // Already watching.
342         return;
343     }
344     watching = true;
346     document.addEventListener('click', e => {
347         const tableRoot = e.target.closest(Selectors.main.region);
349         if (!tableRoot) {
350             return;
351         }
353         const sortableLink = e.target.closest(Selectors.table.links.sortableColumn);
354         if (sortableLink) {
355             e.preventDefault();
357             setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder)
358             .catch(Notification.exception);
359         }
361         const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);
362         if (firstInitialLink !== null) {
363             e.preventDefault();
365             setFirstInitial(tableRoot, firstInitialLink.dataset.initial).catch(Notification.exception);
366         }
368         const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);
369         if (lastInitialLink !== null) {
370             e.preventDefault();
372             setLastInitial(tableRoot, lastInitialLink.dataset.initial).catch(Notification.exception);
373         }
375         const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);
376         if (pageItem) {
377             e.preventDefault();
379             setPageNumber(tableRoot, pageItem.dataset.pageNumber).catch(Notification.exception);
380         }
382         const hide = e.target.closest(Selectors.table.links.hide);
383         if (hide) {
384             e.preventDefault();
386             hideColumn(tableRoot, hide.dataset.column).catch(Notification.exception);
387         }
389         const show = e.target.closest(Selectors.table.links.show);
390         if (show) {
391             e.preventDefault();
393             showColumn(tableRoot, show.dataset.column).catch(Notification.exception);
394         }
396         const resetTablePreferencesLink = e.target.closest('.resettable a');
397         if (resetTablePreferencesLink) {
398             e.preventDefault();
400             resetTablePreferences(tableRoot).catch(Notification.exception);
401         }
402     });
403 };
405 /**
406  * Fetch the table via its table region id.
407  *
408  * @param {String} tableRegionId
409  * @returns {HTMLElement}
410  */
411 export const getTableFromId = tableRegionId => {
412     const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));
415     if (!tableRoot) {
416         // The table is not a dynamic table.
417         throw new Error("The table specified is not a dynamic table and cannot be updated");
418     }
420     return tableRoot;
421 };
423 export {
424     Events
425 };