MDL-68612 core_table: Pending checks for dyanmic updates
[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';
30 let watching = false;
32 /**
33  * Ensure that a table is a dynamic table.
34  *
35  * @param {HTMLElement} tableRoot
36  * @returns {Bool}
37  */
38 const checkTableIsDynamic = tableRoot => {
39     if (!tableRoot) {
40         // The table is not a dynamic table.
41         throw new Error("The table specified is not a dynamic table and cannot be updated");
42     }
44     if (!tableRoot.matches(Selectors.main.region)) {
45         // The table is not a dynamic table.
46         throw new Error("The table specified is not a dynamic table and cannot be updated");
47     }
49     return true;
50 };
52 /**
53  * Get the filterset data from a known dynamic table.
54  *
55  * @param {HTMLElement} tableRoot
56  * @returns {Object}
57  */
58 const getFiltersetFromTable = tableRoot => {
59     return JSON.parse(tableRoot.dataset.tableFilters);
60 };
62 /**
63  * Update the specified table based on its current values.
64  *
65  * @param {HTMLElement} tableRoot
66  * @param {Bool} resetContent
67  * @returns {Promise}
68  */
69 export const refreshTableContent = (tableRoot, resetContent = false) => {
70     const filterset = getFiltersetFromTable(tableRoot);
71     addIconToContainer(tableRoot);
73     const pendingPromise = new Pending('core_table/dynamic:refreshTableContent');
75     return fetchTableData(
76         tableRoot.dataset.tableComponent,
77         tableRoot.dataset.tableHandler,
78         tableRoot.dataset.tableUniqueid,
79         {
80             sortData: JSON.parse(tableRoot.dataset.tableSortData),
81             joinType: filterset.jointype,
82             filters: filterset.filters,
83             firstinitial: tableRoot.dataset.tableFirstInitial,
84             lastinitial: tableRoot.dataset.tableLastInitial,
85             pageNumber: tableRoot.dataset.tablePageNumber,
86             pageSize: tableRoot.dataset.tablePageSize,
87             hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),
88         },
89         resetContent,
90     )
91     .then(data => {
92         const placeholder = document.createElement('div');
93         placeholder.innerHTML = data.html;
94         tableRoot.replaceWith(...placeholder.childNodes);
96         // Update the tableRoot.
97         return getTableFromId(tableRoot.dataset.tableUniqueid);
98     }).then(tableRoot => {
99         tableRoot.dispatchEvent(new CustomEvent(Events.tableContentRefreshed, {
100             bubbles: true,
101         }));
103         return tableRoot;
104     })
105     .then(tableRoot => {
106         pendingPromise.resolve();
108         return tableRoot;
109     });
110 };
112 export const updateTable = (tableRoot, {
113     sortBy = null,
114     sortOrder = null,
115     filters = null,
116     firstInitial = null,
117     lastInitial = null,
118     pageNumber = null,
119     pageSize = null,
120     hiddenColumns = null,
121 } = {}, refreshContent = true) => {
122     checkTableIsDynamic(tableRoot);
124     const pendingPromise = new Pending('core_table/dynamic:updateTable');
126     // Update sort fields.
127     if (sortBy && sortOrder) {
128         const sortData = JSON.parse(tableRoot.dataset.tableSortData);
129         sortData.unshift({
130             sortby: sortBy,
131             sortorder: parseInt(sortOrder, 10),
132         });
133         tableRoot.dataset.tableSortData = JSON.stringify(sortData);
134     }
136     // Update initials.
137     if (firstInitial !== null) {
138         tableRoot.dataset.tableFirstInitial = firstInitial;
139     }
141     if (lastInitial !== null) {
142         tableRoot.dataset.tableLastInitial = lastInitial;
143     }
145     if (pageNumber !== null) {
146         tableRoot.dataset.tablePageNumber = pageNumber;
147     }
149     if (pageSize !== null) {
150         tableRoot.dataset.tablePageSize = pageSize;
151     }
153     // Update filters.
154     if (filters) {
155         tableRoot.dataset.tableFilters = JSON.stringify(filters);
156     }
158     // Update hidden columns.
159     if (hiddenColumns) {
160         tableRoot.dataset.tableHiddenColumns = JSON.stringify(hiddenColumns);
161     }
163     // Refresh.
164     if (refreshContent) {
165         return refreshTableContent(tableRoot)
166         .then(tableRoot => {
167             pendingPromise.resolve();
168             return tableRoot;
169         });
170     } else {
171         pendingPromise.resolve();
172         return Promise.resolve(tableRoot);
173     }
174 };
176 /**
177  * Get the table dataset for the specified tableRoot, ensuring that the provided table is a dynamic table.
178  *
179  * @param {HTMLElement} tableRoot
180  * @returns {DOMStringMap}
181  */
182 const getTableData = tableRoot => {
183     checkTableIsDynamic(tableRoot);
185     return tableRoot.dataset;
186 };
188 /**
189  * Update the specified table using the new filters.
190  *
191  * @param {HTMLElement} tableRoot
192  * @param {Object} filters
193  * @param {Bool} refreshContent
194  * @returns {Promise}
195  */
196 export const setFilters = (tableRoot, filters, refreshContent = true) =>
197     updateTable(tableRoot, {filters}, refreshContent);
199 /**
200  * Get the filter data for the specified table.
201  *
202  * @param {HTMLElement} tableRoot
203  * @returns {Object}
204  */
205 export const getFilters = tableRoot => {
206     checkTableIsDynamic(tableRoot);
208     return getFiltersetFromTable(tableRoot);
209 };
211 /**
212  * Update the sort order.
213  *
214  * @param {HTMLElement} tableRoot
215  * @param {String} sortBy
216  * @param {Number} sortOrder
217  * @param {Bool} refreshContent
218  * @returns {Promise}
219  */
220 export const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true) =>
221     updateTable(tableRoot, {sortBy, sortOrder}, refreshContent);
223 /**
224  * Set the page number.
225  *
226  * @param {HTMLElement} tableRoot
227  * @param {String} pageNumber
228  * @param {Bool} refreshContent
229  * @returns {Promise}
230  */
231 export const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>
232     updateTable(tableRoot, {pageNumber}, refreshContent);
234 /**
235  * Get the current page number.
236  *
237  * @param {HTMLElement} tableRoot
238  * @returns {Number}
239  */
240 export const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumber;
242 /**
243  * Set the page size.
244  *
245  * @param {HTMLElement} tableRoot
246  * @param {Number} pageSize
247  * @param {Bool} refreshContent
248  * @returns {Promise}
249  */
250 export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
251     updateTable(tableRoot, {pageSize, pageNumber: 0}, refreshContent);
253 /**
254  * Get the current page size.
255  *
256  * @param {HTMLElement} tableRoot
257  * @returns {Number}
258  */
259 export const getPageSize = tableRoot => getTableData(tableRoot).tablePageSize;
261 /**
262  * Update the first initial to show.
263  *
264  * @param {HTMLElement} tableRoot
265  * @param {String} firstInitial
266  * @param {Bool} refreshContent
267  * @returns {Promise}
268  */
269 export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>
270     updateTable(tableRoot, {firstInitial}, refreshContent);
272 /**
273  * Get the current first initial filter.
274  *
275  * @param {HTMLElement} tableRoot
276  * @returns {String}
277  */
278 export const getFirstInitial = tableRoot => getTableData(tableRoot).tableFirstInitial;
280 /**
281  * Update the last initial to show.
282  *
283  * @param {HTMLElement} tableRoot
284  * @param {String} lastInitial
285  * @param {Bool} refreshContent
286  * @returns {Promise}
287  */
288 export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
289     updateTable(tableRoot, {lastInitial}, refreshContent);
291 /**
292  * Get the current last initial filter.
293  *
294  * @param {HTMLElement} tableRoot
295  * @returns {String}
296  */
297 export const getLastInitial = tableRoot => getTableData(tableRoot).tableLastInitial;
299 /**
300  * Hide a column in the participants table.
301  *
302  * @param {HTMLElement} tableRoot
303  * @param {String} columnToHide
304  * @param {Bool} refreshContent
305  */
306 export const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {
307     const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
308     hiddenColumns.push(columnToHide);
310     updateTable(tableRoot, {hiddenColumns}, refreshContent);
311 };
313 /**
314  * Make a hidden column visible in the participants table.
315  *
316  * @param {HTMLElement} tableRoot
317  * @param {String} columnToShow
318  * @param {Bool} refreshContent
319  */
320 export const showColumn = (tableRoot, columnToShow, refreshContent = true) => {
321     let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
322     hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);
324     updateTable(tableRoot, {hiddenColumns}, refreshContent);
325 };
327 /**
328  * Reset table preferences.
329  *
330  * @param {HTMLElement} tableRoot
331  * @returns {Promise}
332  */
333 const resetTablePreferences = tableRoot => refreshTableContent(tableRoot, true);
335 /**
336  * Set up listeners to handle table updates.
337  */
338 export const init = () => {
339     if (watching) {
340         // Already watching.
341         return;
342     }
343     watching = true;
345     document.addEventListener('click', e => {
346         const tableRoot = e.target.closest(Selectors.main.region);
348         if (!tableRoot) {
349             return;
350         }
352         const sortableLink = e.target.closest(Selectors.table.links.sortableColumn);
353         if (sortableLink) {
354             e.preventDefault();
356             setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder);
357         }
359         const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);
360         if (firstInitialLink !== null) {
361             e.preventDefault();
363             setFirstInitial(tableRoot, firstInitialLink.dataset.initial);
364         }
366         const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);
367         if (lastInitialLink !== null) {
368             e.preventDefault();
370             setLastInitial(tableRoot, lastInitialLink.dataset.initial);
371         }
373         const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);
374         if (pageItem) {
375             e.preventDefault();
377             setPageNumber(tableRoot, pageItem.dataset.pageNumber);
378         }
380         const hide = e.target.closest(Selectors.table.links.hide);
381         if (hide) {
382             e.preventDefault();
384             hideColumn(tableRoot, hide.dataset.column);
385         }
387         const show = e.target.closest(Selectors.table.links.show);
388         if (show) {
389             e.preventDefault();
391             showColumn(tableRoot, show.dataset.column);
392         }
394         const resetTablePreferencesLink = e.target.closest('.resettable a');
395         if (resetTablePreferencesLink) {
396             e.preventDefault();
398             resetTablePreferences(tableRoot);
399         }
400     });
401 };
403 /**
404  * Fetch the table via its table region id.
405  *
406  * @param {String} tableRegionId
407  * @returns {HTMLElement}
408  */
409 export const getTableFromId = tableRegionId => {
410     const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));
413     if (!tableRoot) {
414         // The table is not a dynamic table.
415         throw new Error("The table specified is not a dynamic table and cannot be updated");
416     }
418     return tableRoot;
419 };
421 export {
422     Events
423 };