lib/form/amd/src/filetypes.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. * This module allows to enhance the form elements MoodleQuickForm_filetypes
  17. *
  18. * @module core_form/filetypes
  19. * @copyright 2017 David Mudrak <david@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. * @since 3.3
  22. */
  23. define(['jquery', 'core/log', 'core/modal_events', 'core/modal_save_cancel', 'core/ajax',
  24. 'core/templates', 'core/tree'],
  25. function($, Log, ModalEvents, ModalSaveCancel, Ajax, Templates, Tree) {
  26. "use strict";
  27. /**
  28. * Constructor of the FileTypes instances.
  29. *
  30. * @constructor
  31. * @param {String} elementId The id of the form element to enhance
  32. * @param {String} elementLabel The label of the form element used as the modal selector title
  33. * @param {String} onlyTypes Limit the list of offered types to this
  34. * @param {Bool} allowAll Allow presence of the "All file types" item
  35. */
  36. var FileTypes = function(elementId, elementLabel, onlyTypes, allowAll) {
  37. this.elementId = elementId;
  38. this.elementLabel = elementLabel;
  39. this.onlyTypes = onlyTypes;
  40. this.allowAll = allowAll;
  41. this.inputField = $('#' + elementId);
  42. this.wrapperBrowserTrigger = $('[data-filetypesbrowser="' + elementId + '"]');
  43. this.wrapperDescriptions = $('[data-filetypesdescriptions="' + elementId + '"]');
  44. if (!this.wrapperBrowserTrigger.length) {
  45. // This is a valid case. Most probably the element is frozen and
  46. // the filetypes browser should not be available.
  47. return;
  48. }
  49. if (!this.inputField.length || !this.wrapperDescriptions.length) {
  50. Log.error('core_form/filetypes: Unexpected DOM structure, unable to enhance filetypes field ' + elementId);
  51. return;
  52. }
  53. this.prepareBrowserTrigger()
  54. .then(function() {
  55. return this.prepareBrowserModal();
  56. }.bind(this))
  57. .then(function() {
  58. return this.prepareBrowserTree();
  59. }.bind(this));
  60. };
  61. /**
  62. * Create and set the browser trigger widget (this.browserTrigger).
  63. *
  64. * @method prepareBrowserTrigger
  65. * @returns {Promise}
  66. */
  67. FileTypes.prototype.prepareBrowserTrigger = function() {
  68. return Templates.render('core_form/filetypes-trigger', {})
  69. .then(function(html) {
  70. this.wrapperBrowserTrigger.html(html);
  71. this.browserTrigger = this.wrapperBrowserTrigger.find('[data-filetypeswidget="browsertrigger"]');
  72. }.bind(this));
  73. };
  74. /**
  75. * Create and set the modal for displaying the browser (this.browserModal).
  76. *
  77. * @method prepareBrowserModal
  78. * @returns {Promise}
  79. */
  80. FileTypes.prototype.prepareBrowserModal = function() {
  81. return ModalSaveCancel.create({
  82. title: this.elementLabel,
  83. })
  84. .then(function(modal) {
  85. this.browserModal = modal;
  86. return modal;
  87. }.bind(this))
  88. .then(function() {
  89. // Because we have custom conditional modal trigger, we need to
  90. // handle the focus after closing ourselves, too.
  91. this.browserModal.getRoot().on(ModalEvents.hidden, function() {
  92. this.browserTrigger.focus();
  93. }.bind(this));
  94. this.browserModal.getRoot().on(ModalEvents.save, function() {
  95. this.saveBrowserModal();
  96. }.bind(this));
  97. }.bind(this));
  98. };
  99. /**
  100. * Create and set the tree in the browser modal's body.
  101. *
  102. * @method prepareBrowserTree
  103. * @returns {Promise}
  104. */
  105. FileTypes.prototype.prepareBrowserTree = function() {
  106. this.browserTrigger.on('click', function(e) {
  107. e.preventDefault();
  108. // We want to display the browser modal only when the associated input
  109. // field is not frozen (disabled).
  110. if (this.inputField.is('[disabled]')) {
  111. return;
  112. }
  113. var bodyContent = this.loadBrowserModalBody();
  114. bodyContent.then(function() {
  115. // Turn the list of groups and extensions into the tree.
  116. this.browserTree = new Tree(this.browserModal.getBody());
  117. // Override the behaviour of the Enter and Space keys to toggle our checkbox,
  118. // rather than toggle the tree node expansion status.
  119. this.browserTree.handleKeyDown = function(item, e) {
  120. if (e.keyCode == this.browserTree.keys.enter || e.keyCode == this.browserTree.keys.space) {
  121. e.preventDefault();
  122. e.stopPropagation();
  123. this.toggleCheckbox(item.attr('data-filetypesbrowserkey'));
  124. } else {
  125. Tree.prototype.handleKeyDown.call(this.browserTree, item, e);
  126. }
  127. }.bind(this);
  128. if (this.allowAll) {
  129. // Hide all other items if "All file types" is enabled.
  130. this.hideOrShowItemsDependingOnAllowAll(this.browserModal.getRoot()
  131. .find('input[type="checkbox"][data-filetypesbrowserkey="*"]').first());
  132. // And do the same whenever we click that checkbox.
  133. this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey="*"]', function(e) {
  134. this.hideOrShowItemsDependingOnAllowAll($(e.currentTarget));
  135. }.bind(this));
  136. }
  137. // Synchronize checked status if the file extension is present in multiple groups.
  138. this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey]', function(e) {
  139. var checkbox = $(e.currentTarget);
  140. var key = checkbox.attr('data-filetypesbrowserkey');
  141. this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]')
  142. .prop('checked', checkbox.prop('checked'));
  143. }.bind(this));
  144. }.bind(this))
  145. .then(function() {
  146. this.browserModal.show();
  147. }.bind(this));
  148. this.browserModal.setBody(bodyContent);
  149. }.bind(this));
  150. // Return a resolved promise.
  151. return $.when();
  152. };
  153. /**
  154. * Load the browser modal body contents.
  155. *
  156. * @returns {Promise}
  157. */
  158. FileTypes.prototype.loadBrowserModalBody = function() {
  159. var args = {
  160. onlytypes: this.onlyTypes.join(),
  161. allowall: this.allowAll,
  162. current: this.inputField.val()
  163. };
  164. return Ajax.call([{
  165. methodname: 'core_form_get_filetypes_browser_data',
  166. args: args
  167. }])[0].then(function(browserData) {
  168. return Templates.render('core_form/filetypes-browser', {
  169. elementid: this.elementId,
  170. groups: browserData.groups
  171. });
  172. }.bind(this));
  173. };
  174. /**
  175. * Change the checked status of the given file type (group or extension).
  176. *
  177. * @method toggleCheckbox
  178. * @param {String} key
  179. */
  180. FileTypes.prototype.toggleCheckbox = function(key) {
  181. var checkbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]').first();
  182. checkbox.prop('checked', !checkbox.prop('checked'));
  183. };
  184. /**
  185. * Update the associated input field with selected file types.
  186. *
  187. * @method saveBrowserModal
  188. */
  189. FileTypes.prototype.saveBrowserModal = function() {
  190. // Check the "All file types" first.
  191. if (this.allowAll) {
  192. var allcheckbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="*"]');
  193. if (allcheckbox.length && allcheckbox.prop('checked')) {
  194. this.inputField.val('*');
  195. this.updateDescriptions(['*']);
  196. return;
  197. }
  198. }
  199. // Iterate over all checked boxes and populate the list.
  200. var newvalue = [];
  201. this.browserModal.getRoot().find('input[type="checkbox"]').each(/** @this represents the checkbox */ function() {
  202. var checkbox = $(this);
  203. var key = checkbox.attr('data-filetypesbrowserkey');
  204. if (checkbox.prop('checked')) {
  205. newvalue.push(key);
  206. }
  207. });
  208. // Remove duplicates (e.g. file types present in multiple groups).
  209. newvalue = newvalue.filter(function(x, i, a) {
  210. return a.indexOf(x) == i;
  211. });
  212. this.inputField.val(newvalue.join(' '));
  213. this.updateDescriptions(newvalue);
  214. };
  215. /**
  216. * Describe the selected filetypes in the form when saving the browser.
  217. *
  218. * @param {Array} keys List of keys to describe
  219. * @returns {Promise}
  220. */
  221. FileTypes.prototype.updateDescriptions = function(keys) {
  222. var descriptions = [];
  223. keys.forEach(function(key) {
  224. descriptions.push({
  225. description: this.browserModal.getRoot().find('[data-filetypesname="' + key + '"]').first().text().trim(),
  226. extensions: this.browserModal.getRoot().find('[data-filetypesextensions="' + key + '"]').first().text().trim()
  227. });
  228. }.bind(this));
  229. var templatedata = {
  230. hasdescriptions: (descriptions.length > 0),
  231. descriptions: descriptions
  232. };
  233. return Templates.render('core_form/filetypes-descriptions', templatedata)
  234. .then(function(html) {
  235. this.wrapperDescriptions.html(html);
  236. }.bind(this));
  237. };
  238. /**
  239. * If "All file types" is checked, all other browser items are made hidden, and vice versa.
  240. *
  241. * @param {jQuery} allcheckbox The "All file types" checkbox.
  242. */
  243. FileTypes.prototype.hideOrShowItemsDependingOnAllowAll = function(allcheckbox) {
  244. var others = this.browserModal.getRoot().find('[role="treeitem"][data-filetypesbrowserkey!="*"]');
  245. if (allcheckbox.prop('checked')) {
  246. others.hide();
  247. } else {
  248. others.show();
  249. }
  250. };
  251. return {
  252. init: function(elementId, elementLabel, onlyTypes, allowAll) {
  253. new FileTypes(elementId, elementLabel, onlyTypes, allowAll);
  254. }
  255. };
  256. });