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/>.
  15. /**
  16. * Module to handle dynamic table features.
  17. *
  18. * @module core_table/dynamic
  19. * @copyright 2020 Simey Lameze <simey@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import * as Selectors from 'core_table/local/dynamic/selectors';
  23. import Events from './local/dynamic/events';
  24. import Pending from 'core/pending';
  25. import {addIconToContainer} from 'core/loadingicon';
  26. import {fetch as fetchTableData} from 'core_table/local/dynamic/repository';
  27. import Notification from 'core/notification';
  28. import {replaceNode} from 'core/templates';
  29. let watching = false;
  30. /**
  31. * Ensure that a table is a dynamic table.
  32. *
  33. * @param {HTMLElement} tableRoot
  34. * @returns {Bool}
  35. */
  36. const checkTableIsDynamic = tableRoot => {
  37. if (!tableRoot) {
  38. // The table is not a dynamic table.
  39. throw new Error("The table specified is not a dynamic table and cannot be updated");
  40. }
  41. if (!tableRoot.matches(Selectors.main.region)) {
  42. // The table is not a dynamic table.
  43. throw new Error("The table specified is not a dynamic table and cannot be updated");
  44. }
  45. return true;
  46. };
  47. /**
  48. * Get the filterset data from a known dynamic table.
  49. *
  50. * @param {HTMLElement} tableRoot
  51. * @returns {Object}
  52. */
  53. const getFiltersetFromTable = tableRoot => {
  54. return JSON.parse(tableRoot.dataset.tableFilters);
  55. };
  56. /**
  57. * Update the specified table based on its current values.
  58. *
  59. * @param {HTMLElement} tableRoot
  60. * @param {Bool} resetContent
  61. * @returns {Promise}
  62. */
  63. export const refreshTableContent = (tableRoot, resetContent = false) => {
  64. const filterset = getFiltersetFromTable(tableRoot);
  65. addIconToContainer(tableRoot);
  66. const pendingPromise = new Pending('core_table/dynamic:refreshTableContent');
  67. return fetchTableData(
  68. tableRoot.dataset.tableComponent,
  69. tableRoot.dataset.tableHandler,
  70. tableRoot.dataset.tableUniqueid,
  71. {
  72. sortData: JSON.parse(tableRoot.dataset.tableSortData),
  73. joinType: filterset.jointype,
  74. filters: filterset.filters,
  75. firstinitial: tableRoot.dataset.tableFirstInitial,
  76. lastinitial: tableRoot.dataset.tableLastInitial,
  77. pageNumber: tableRoot.dataset.tablePageNumber,
  78. pageSize: tableRoot.dataset.tablePageSize,
  79. hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),
  80. },
  81. resetContent,
  82. )
  83. .then(data => {
  84. const tableRootReplacement = replaceNode(tableRoot, data.html, '');
  85. // Update the tableRoot.
  86. return tableRootReplacement[0];
  87. }).then(tableRoot => {
  88. tableRoot.dispatchEvent(new CustomEvent(Events.tableContentRefreshed, {
  89. bubbles: true,
  90. }));
  91. return tableRoot;
  92. })
  93. .then(tableRoot => {
  94. pendingPromise.resolve();
  95. return tableRoot;
  96. });
  97. };
  98. export const updateTable = (tableRoot, {
  99. sortBy = null,
  100. sortOrder = null,
  101. filters = null,
  102. firstInitial = null,
  103. lastInitial = null,
  104. pageNumber = null,
  105. pageSize = null,
  106. hiddenColumns = null,
  107. } = {}, refreshContent = true) => {
  108. checkTableIsDynamic(tableRoot);
  109. const pendingPromise = new Pending('core_table/dynamic:updateTable');
  110. let tableConfigChanged = false;
  111. // Update sort fields.
  112. if (sortBy && sortOrder) {
  113. // Always update the table if requested and there were sort fields.
  114. // These fields are only ever normalised in the backend.
  115. tableConfigChanged = true;
  116. const sortData = JSON.parse(tableRoot.dataset.tableSortData);
  117. sortData.unshift({
  118. sortby: sortBy,
  119. sortorder: parseInt(sortOrder, 10),
  120. });
  121. tableRoot.dataset.tableSortData = JSON.stringify(sortData);
  122. }
  123. // Update initials.
  124. if (firstInitial !== null) {
  125. if (tableRoot.dataset.tableFirstInitial !== firstInitial) {
  126. tableConfigChanged = true;
  127. }
  128. tableRoot.dataset.tableFirstInitial = firstInitial;
  129. }
  130. if (lastInitial !== null) {
  131. if (tableRoot.dataset.tableLastInitial !== lastInitial) {
  132. tableConfigChanged = true;
  133. }
  134. tableRoot.dataset.tableLastInitial = lastInitial;
  135. }
  136. if (pageSize !== null) {
  137. if (tableRoot.dataset.tablePageSize != pageSize) {
  138. tableConfigChanged = true;
  139. }
  140. tableRoot.dataset.tablePageSize = pageSize;
  141. }
  142. // Update filters.
  143. if (filters) {
  144. const filterJson = JSON.stringify(filters);
  145. if (tableRoot.dataset.tableFilters !== filterJson) {
  146. tableConfigChanged = true;
  147. }
  148. tableRoot.dataset.tableFilters = filterJson;
  149. }
  150. // Reset to page 1 when table content is being altered by filtering or sorting.
  151. // This ensures the table page being loaded always exists, and gives a consistent experience.
  152. if (tableConfigChanged) {
  153. pageNumber = 1;
  154. }
  155. // Update hidden columns.
  156. if (hiddenColumns) {
  157. const columnJson = JSON.stringify(hiddenColumns);
  158. if (tableRoot.dataset.tableHiddenColumns !== columnJson) {
  159. tableConfigChanged = true;
  160. }
  161. tableRoot.dataset.tableHiddenColumns = columnJson;
  162. }
  163. if (pageNumber !== null) {
  164. if (tableRoot.dataset.tablePageNumber != pageNumber) {
  165. tableConfigChanged = true;
  166. }
  167. tableRoot.dataset.tablePageNumber = pageNumber;
  168. }
  169. // Refresh.
  170. if (refreshContent && tableConfigChanged) {
  171. return refreshTableContent(tableRoot)
  172. .then(tableRoot => {
  173. pendingPromise.resolve();
  174. return tableRoot;
  175. });
  176. } else {
  177. pendingPromise.resolve();
  178. return Promise.resolve(tableRoot);
  179. }
  180. };
  181. /**
  182. * Get the table dataset for the specified tableRoot, ensuring that the provided table is a dynamic table.
  183. *
  184. * @param {HTMLElement} tableRoot
  185. * @returns {DOMStringMap}
  186. */
  187. const getTableData = tableRoot => {
  188. checkTableIsDynamic(tableRoot);
  189. return tableRoot.dataset;
  190. };
  191. /**
  192. * Update the specified table using the new filters.
  193. *
  194. * @param {HTMLElement} tableRoot
  195. * @param {Object} filters
  196. * @param {Bool} refreshContent
  197. * @returns {Promise}
  198. */
  199. export const setFilters = (tableRoot, filters, refreshContent = true) =>
  200. updateTable(tableRoot, {filters}, refreshContent);
  201. /**
  202. * Get the filter data for the specified table.
  203. *
  204. * @param {HTMLElement} tableRoot
  205. * @returns {Object}
  206. */
  207. export const getFilters = tableRoot => {
  208. checkTableIsDynamic(tableRoot);
  209. return getFiltersetFromTable(tableRoot);
  210. };
  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);
  222. /**
  223. * Set the page number.
  224. *
  225. * @param {HTMLElement} tableRoot
  226. * @param {String} pageNumber
  227. * @param {Bool} refreshContent
  228. * @returns {Promise}
  229. */
  230. export const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>
  231. updateTable(tableRoot, {pageNumber}, refreshContent);
  232. /**
  233. * Get the current page number.
  234. *
  235. * @param {HTMLElement} tableRoot
  236. * @returns {Number}
  237. */
  238. export const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumber;
  239. /**
  240. * Set the page size.
  241. *
  242. * @param {HTMLElement} tableRoot
  243. * @param {Number} pageSize
  244. * @param {Bool} refreshContent
  245. * @returns {Promise}
  246. */
  247. export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
  248. updateTable(tableRoot, {pageSize, pageNumber: 1}, refreshContent);
  249. /**
  250. * Get the current page size.
  251. *
  252. * @param {HTMLElement} tableRoot
  253. * @returns {Number}
  254. */
  255. export const getPageSize = tableRoot => getTableData(tableRoot).tablePageSize;
  256. /**
  257. * Update the first initial to show.
  258. *
  259. * @param {HTMLElement} tableRoot
  260. * @param {String} firstInitial
  261. * @param {Bool} refreshContent
  262. * @returns {Promise}
  263. */
  264. export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>
  265. updateTable(tableRoot, {firstInitial}, refreshContent);
  266. /**
  267. * Get the current first initial filter.
  268. *
  269. * @param {HTMLElement} tableRoot
  270. * @returns {String}
  271. */
  272. export const getFirstInitial = tableRoot => getTableData(tableRoot).tableFirstInitial;
  273. /**
  274. * Update the last initial to show.
  275. *
  276. * @param {HTMLElement} tableRoot
  277. * @param {String} lastInitial
  278. * @param {Bool} refreshContent
  279. * @returns {Promise}
  280. */
  281. export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
  282. updateTable(tableRoot, {lastInitial}, refreshContent);
  283. /**
  284. * Get the current last initial filter.
  285. *
  286. * @param {HTMLElement} tableRoot
  287. * @returns {String}
  288. */
  289. export const getLastInitial = tableRoot => getTableData(tableRoot).tableLastInitial;
  290. /**
  291. * Hide a column in the participants table.
  292. *
  293. * @param {HTMLElement} tableRoot
  294. * @param {String} columnToHide
  295. * @param {Bool} refreshContent
  296. * @returns {Promise}
  297. */
  298. export const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {
  299. const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
  300. hiddenColumns.push(columnToHide);
  301. return updateTable(tableRoot, {hiddenColumns}, refreshContent);
  302. };
  303. /**
  304. * Make a hidden column visible in the participants table.
  305. *
  306. * @param {HTMLElement} tableRoot
  307. * @param {String} columnToShow
  308. * @param {Bool} refreshContent
  309. * @returns {Promise}
  310. */
  311. export const showColumn = (tableRoot, columnToShow, refreshContent = true) => {
  312. let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
  313. hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);
  314. return updateTable(tableRoot, {hiddenColumns}, refreshContent);
  315. };
  316. /**
  317. * Reset table preferences.
  318. *
  319. * @param {HTMLElement} tableRoot
  320. * @returns {Promise}
  321. */
  322. const resetTablePreferences = tableRoot => refreshTableContent(tableRoot, true);
  323. /**
  324. * Set up listeners to handle table updates.
  325. */
  326. export const init = () => {
  327. if (watching) {
  328. // Already watching.
  329. return;
  330. }
  331. watching = true;
  332. document.addEventListener('click', e => {
  333. const tableRoot = e.target.closest(Selectors.main.region);
  334. if (!tableRoot) {
  335. return;
  336. }
  337. const sortableLink = e.target.closest(Selectors.table.links.sortableColumn);
  338. if (sortableLink) {
  339. e.preventDefault();
  340. setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder)
  341. .catch(Notification.exception);
  342. }
  343. const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);
  344. if (firstInitialLink !== null) {
  345. e.preventDefault();
  346. setFirstInitial(tableRoot, firstInitialLink.dataset.initial).catch(Notification.exception);
  347. }
  348. const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);
  349. if (lastInitialLink !== null) {
  350. e.preventDefault();
  351. setLastInitial(tableRoot, lastInitialLink.dataset.initial).catch(Notification.exception);
  352. }
  353. const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);
  354. if (pageItem) {
  355. e.preventDefault();
  356. setPageNumber(tableRoot, pageItem.dataset.pageNumber).catch(Notification.exception);
  357. }
  358. const hide = e.target.closest(Selectors.table.links.hide);
  359. if (hide) {
  360. e.preventDefault();
  361. hideColumn(tableRoot, hide.dataset.column).catch(Notification.exception);
  362. }
  363. const show = e.target.closest(Selectors.table.links.show);
  364. if (show) {
  365. e.preventDefault();
  366. showColumn(tableRoot, show.dataset.column).catch(Notification.exception);
  367. }
  368. const resetTablePreferencesLink = e.target.closest('.resettable a');
  369. if (resetTablePreferencesLink) {
  370. e.preventDefault();
  371. resetTablePreferences(tableRoot).catch(Notification.exception);
  372. }
  373. const showCountLink = e.target.closest(Selectors.showCount.links.toggle);
  374. if (showCountLink) {
  375. e.preventDefault();
  376. setPageSize(tableRoot, showCountLink.dataset.targetPageSize).catch(Notification.exception);
  377. }
  378. });
  379. };
  380. /**
  381. * Fetch the table via its table region id.
  382. *
  383. * @param {String} tableRegionId
  384. * @returns {HTMLElement}
  385. */
  386. export const getTableFromId = tableRegionId => {
  387. const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));
  388. if (!tableRoot) {
  389. // The table is not a dynamic table.
  390. throw new Error("The table specified is not a dynamic table and cannot be updated");
  391. }
  392. return tableRoot;
  393. };
  394. export {
  395. Events
  396. };