module.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /**
  2. * JavaScript for the user selectors.
  3. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4. * @package userselector
  5. */
  6. // Define the core_user namespace if it has not already been defined
  7. M.core_user = M.core_user || {};
  8. // Define a user selectors array for against the cure_user namespace
  9. M.core_user.user_selectors = [];
  10. /**
  11. * Retrieves an instantiated user selector or null if there isn't one by the requested name
  12. * @param {string} name The name of the selector to retrieve
  13. * @return bool
  14. */
  15. M.core_user.get_user_selector = function (name) {
  16. return this.user_selectors[name] || null;
  17. };
  18. /**
  19. * Initialise a new user selector.
  20. *
  21. * @param {YUI} Y The YUI3 instance
  22. * @param {string} name the control name/id.
  23. * @param {string} hash the hash that identifies this selector in the user's session.
  24. * @param {array} extrafields extra fields we are displaying for each user in addition to fullname.
  25. * @param {string} lastsearch The last search that took place
  26. */
  27. M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearch) {
  28. // Creates a new user_selector object
  29. var user_selector = {
  30. /** This id/name used for this control in the HTML. */
  31. name : name,
  32. /** Array of fields to display for each user, in addition to fullname. */
  33. extrafields: extrafields,
  34. /** Number of seconds to delay before submitting a query request */
  35. querydelay : 0.5,
  36. /** The input element that contains the search term. */
  37. searchfield : Y.one('#' + name + '_searchtext'),
  38. /** The clear button. */
  39. clearbutton : null,
  40. /** The select element that contains the list of users. */
  41. listbox : Y.one('#' + name),
  42. /** Used to hold the timeout id of the timeout that waits before doing a search. */
  43. timeoutid : null,
  44. /** Stores any in-progress remote requests. */
  45. iotransactions : {},
  46. /** The last string that we searched for, so we can avoid unnecessary repeat searches. */
  47. lastsearch : lastsearch,
  48. /** Whether any options where selected last time we checked. Used by
  49. * handle_selection_change to track when this status changes. */
  50. selectionempty : true,
  51. /**
  52. * Initialises the user selector object
  53. * @constructor
  54. */
  55. init : function() {
  56. // Hide the search button and replace it with a label.
  57. var searchbutton = Y.one('#' + this.name + '_searchbutton');
  58. this.searchfield.insert(Y.Node.create('<label for="' + this.name + '_searchtext">' + searchbutton.get('value') + '</label>'), this.searchfield);
  59. searchbutton.remove();
  60. // Hook up the event handler for when the search text changes.
  61. this.searchfield.on('keyup', this.handle_keyup, this);
  62. // Hook up the event handler for when the selection changes.
  63. this.listbox.on('keyup', this.handle_selection_change, this);
  64. this.listbox.on('click', this.handle_selection_change, this);
  65. this.listbox.on('change', this.handle_selection_change, this);
  66. // And when the search any substring preference changes. Do an immediate re-search.
  67. Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this);
  68. // Define our custom event.
  69. //this.createEvent('selectionchanged');
  70. this.selectionempty = this.is_selection_empty();
  71. // Replace the Clear submit button with a clone that is not a submit button.
  72. var clearbtn = Y.one('#' + this.name + '_clearbutton');
  73. this.clearbutton = Y.Node.create('<input type="button" value="' + clearbtn.get('value') + '" />');
  74. clearbtn.replace(Y.Node.getDOMNode(this.clearbutton));
  75. this.clearbutton.set('id', this.name + "_clearbutton");
  76. this.clearbutton.on('click', this.handle_clear, this);
  77. this.clearbutton.set('disabled', (this.get_search_text() == ''));
  78. this.send_query(false);
  79. },
  80. /**
  81. * Key up hander for the search text box.
  82. * @param {Y.Event} e the keyup event.
  83. */
  84. handle_keyup : function(e) {
  85. // Trigger an ajax search after a delay.
  86. this.cancel_timeout();
  87. this.timeoutid = Y.later(this.querydelay * 1000, e, function(obj){obj.send_query(false)}, this);
  88. // Enable or diable the clear button.
  89. this.clearbutton.set('disabled', (this.get_search_text() == ''));
  90. // If enter was pressed, prevent a form submission from happening.
  91. if (e.keyCode == 13) {
  92. e.halt();
  93. }
  94. },
  95. /**
  96. * Handles when the selection has changed. If the selection has changed from
  97. * empty to not-empty, or vice versa, then fire the event handlers.
  98. */
  99. handle_selection_change : function() {
  100. var isselectionempty = this.is_selection_empty();
  101. if (isselectionempty !== this.selectionempty) {
  102. this.fire('user_selector:selectionchanged', isselectionempty);
  103. }
  104. this.selectionempty = isselectionempty;
  105. },
  106. /**
  107. * Trigger a re-search when the 'search any substring' option is changed.
  108. */
  109. handle_searchanywhere_change : function() {
  110. if (this.lastsearch != '' && this.get_search_text() != '') {
  111. this.send_query(true);
  112. }
  113. },
  114. /**
  115. * Click handler for the clear button..
  116. */
  117. handle_clear : function() {
  118. this.searchfield.set('value', '');
  119. this.clearbutton.set('disabled',true);
  120. this.send_query(false);
  121. },
  122. /**
  123. * Fires off the ajax search request.
  124. */
  125. send_query : function(forceresearch) {
  126. // Cancel any pending timeout.
  127. this.cancel_timeout();
  128. var value = this.get_search_text();
  129. this.searchfield.set('class', '');
  130. if (this.lastsearch == value && !forceresearch) {
  131. return;
  132. }
  133. // Try to cancel existing transactions.
  134. Y.Object.each(this.iotransactions, function(trans) {
  135. trans.abort();
  136. });
  137. var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
  138. method: 'POST',
  139. data: 'selectorid=' + hash + '&sesskey=' + M.cfg.sesskey + '&search=' + value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
  140. on: {
  141. complete: this.handle_response
  142. },
  143. context:this
  144. });
  145. this.iotransactions[iotrans.id] = iotrans;
  146. this.lastsearch = value;
  147. this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center');
  148. },
  149. /**
  150. * Handle what happens when we get some data back from the search.
  151. * @param {int} requestid not used.
  152. * @param {object} response the list of users that was returned.
  153. */
  154. handle_response : function(requestid, response) {
  155. try {
  156. delete this.iotransactions[requestid];
  157. if (!Y.Object.isEmpty(this.iotransactions)) {
  158. // More searches pending. Wait until they are all done.
  159. return;
  160. }
  161. this.listbox.setStyle('background','');
  162. var data = Y.JSON.parse(response.responseText);
  163. if (data.error) {
  164. this.searchfield.addClass('error');
  165. return new M.core.ajaxException(data);
  166. }
  167. this.output_options(data);
  168. } catch (e) {
  169. this.listbox.setStyle('background','');
  170. this.searchfield.addClass('error');
  171. return new M.core.exception(e);
  172. }
  173. },
  174. /**
  175. * This method should do the same sort of thing as the PHP method
  176. * user_selector_base::output_options.
  177. * @param {object} data the list of users to populate the list box with.
  178. */
  179. output_options : function(data) {
  180. // Clear out the existing options, keeping any ones that are already selected.
  181. var selectedusers = {};
  182. this.listbox.all('optgroup').each(function(optgroup){
  183. optgroup.all('option').each(function(option){
  184. if (option.get('selected')) {
  185. selectedusers[option.get('value')] = {
  186. id : option.get('value'),
  187. name : option.get('innerText') || option.get('textContent'),
  188. disabled: option.get('disabled')
  189. }
  190. }
  191. option.remove();
  192. }, this);
  193. optgroup.remove();
  194. }, this);
  195. // Output each optgroup.
  196. var count = 0;
  197. for (var key in data.results) {
  198. var groupdata = data.results[key];
  199. this.output_group(groupdata.name, groupdata.users, selectedusers, true);
  200. count ++;
  201. }
  202. if (!count) {
  203. var searchstr = (this.lastsearch != '') ? this.insert_search_into_str(M.util.get_string('nomatchingusers', 'moodle'), this.lastsearch) : M.util.get_string('none', 'moodle');
  204. this.output_group(searchstr, {}, selectedusers, true)
  205. }
  206. // If there were previously selected users who do not match the search, show them too.
  207. if (this.get_option('preserveselected') && selectedusers) {
  208. this.output_group(this.insert_search_into_str(M.util.get_string('previouslyselectedusers', 'moodle'), this.lastsearch), selectedusers, true, false);
  209. }
  210. this.handle_selection_change();
  211. },
  212. /**
  213. * This method should do the same sort of thing as the PHP method
  214. * user_selector_base::output_optgroup.
  215. *
  216. * @param {string} groupname the label for this optgroup.v
  217. * @param {object} users the users to put in this optgroup.
  218. * @param {boolean|object} selectedusers if true, select the users in this group.
  219. * @param {boolean} processsingle
  220. */
  221. output_group : function(groupname, users, selectedusers, processsingle) {
  222. var optgroup = Y.Node.create('<optgroup></optgroup>');
  223. var count = 0;
  224. for (var key in users) {
  225. var user = users[key];
  226. var option = Y.Node.create('<option value="' + user.id + '">' + user.name + '</option>');
  227. if (user.disabled) {
  228. option.set('disabled', true);
  229. } else if (selectedusers === true || selectedusers[user.id]) {
  230. option.set('selected', true);
  231. delete selectedusers[user.id];
  232. } else {
  233. option.set('selected', false);
  234. }
  235. optgroup.append(option);
  236. if (user.infobelow) {
  237. extraoption = Y.Node.create('<option disabled="disabled" class="userselector-infobelow"/>');
  238. extraoption.appendChild(document.createTextNode(user.infobelow));
  239. optgroup.append(extraoption);
  240. }
  241. count ++;
  242. }
  243. if (count > 0) {
  244. optgroup.set('label', groupname + ' (' + count + ')');
  245. if (processsingle && count === 1 && this.get_option('autoselectunique') && option.get('disabled') == false) {
  246. option.set('selected', true);
  247. }
  248. } else {
  249. optgroup.set('label', groupname);
  250. optgroup.append(Y.Node.create('<option disabled="disabled">\u00A0</option>'));
  251. }
  252. this.listbox.append(optgroup);
  253. },
  254. /**
  255. * Replace
  256. * @param {string} str
  257. * @param {string} search The search term
  258. * @return string
  259. */
  260. insert_search_into_str : function(str, search) {
  261. return str.replace("%%SEARCHTERM%%", search);
  262. },
  263. /**
  264. * Gets the search text
  265. * @return String the value to search for, with leading and trailing whitespace trimmed.
  266. */
  267. get_search_text : function() {
  268. return this.searchfield.get('value').toString().replace(/^ +| +$/, '');
  269. },
  270. /**
  271. * Returns true if the selection is empty (nothing is selected)
  272. * @return Boolean check all the options and return whether any are selected.
  273. */
  274. is_selection_empty : function() {
  275. var selection = false;
  276. this.listbox.all('option').each(function(){
  277. if (this.get('selected')) {
  278. selection = true;
  279. }
  280. });
  281. return !(selection);
  282. },
  283. /**
  284. * Cancel the search delay timeout, if there is one.
  285. */
  286. cancel_timeout : function() {
  287. if (this.timeoutid) {
  288. clearTimeout(this.timeoutid);
  289. this.timeoutid = null;
  290. }
  291. },
  292. /**
  293. * @param {string} name The name of the option to retrieve
  294. * @return the value of one of the option checkboxes.
  295. */
  296. get_option : function(name) {
  297. var checkbox = Y.one('#userselector_' + name + 'id');
  298. if (checkbox) {
  299. return (checkbox.get('checked'));
  300. } else {
  301. return false;
  302. }
  303. }
  304. };
  305. // Augment the user selector with the EventTarget class so that we can use
  306. // custom events
  307. Y.augment(user_selector, Y.EventTarget, null, null, {});
  308. // Initialise the user selector
  309. user_selector.init();
  310. // Store the user selector so that it can be retrieved
  311. this.user_selectors[name] = user_selector;
  312. // Return the user selector
  313. return user_selector;
  314. };
  315. /**
  316. * Initialise a class that updates the user's preferences when they change one of
  317. * the options checkboxes.
  318. * @constructor
  319. * @param {YUI} Y
  320. * @return Tracker object
  321. */
  322. M.core_user.init_user_selector_options_tracker = function(Y) {
  323. // Create a user selector options tracker
  324. var user_selector_options_tracker = {
  325. /**
  326. * Initlises the option tracker and gets everything going.
  327. * @constructor
  328. */
  329. init : function() {
  330. var settings = [
  331. 'userselector_preserveselected',
  332. 'userselector_autoselectunique',
  333. 'userselector_searchanywhere'
  334. ];
  335. for (var s in settings) {
  336. var setting = settings[s];
  337. Y.one('#' + setting + 'id').on('click', this.set_user_preference, this, setting);
  338. }
  339. },
  340. /**
  341. * Sets a user preference for the options tracker
  342. * @param {Y.Event|null} e
  343. * @param {string} name The name of the preference to set
  344. */
  345. set_user_preference : function(e, name) {
  346. M.util.set_user_preference(name, Y.one('#' + name + 'id').get('checked'));
  347. }
  348. };
  349. // Initialise the options tracker
  350. user_selector_options_tracker.init();
  351. // Return it just incase it is ever wanted
  352. return user_selector_options_tracker;
  353. };