admin/tool/lp/amd/src/competencypicker.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. * Competency picker.
  17. *
  18. * To handle 'save' events use: picker.on('save')
  19. * This will receive a object with either a single 'competencyId', or an array in 'competencyIds'
  20. * depending on the value of multiSelect.
  21. *
  22. * @module tool_lp/competencypicker
  23. * @copyright 2015 Frédéric Massart - FMCorz.net
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. */
  26. define(['jquery',
  27. 'core/notification',
  28. 'core/ajax',
  29. 'core/templates',
  30. 'tool_lp/dialogue',
  31. 'core/str',
  32. 'tool_lp/tree',
  33. 'core/pending'
  34. ],
  35. function($, Notification, Ajax, Templates, Dialogue, Str, Tree, Pending) {
  36. /**
  37. * Competency picker class.
  38. * @param {Number} pageContextId The page context ID.
  39. * @param {Number|false} singleFramework The ID of the framework when limited to one.
  40. * @param {String} pageContextIncludes One of 'children', 'parents', 'self'.
  41. * @param {Boolean} multiSelect Support multi-select in the tree.
  42. */
  43. var Picker = function(pageContextId, singleFramework, pageContextIncludes, multiSelect) {
  44. var self = this;
  45. self._eventNode = $('<div></div>');
  46. self._frameworks = [];
  47. self._reset();
  48. self._pageContextId = pageContextId;
  49. self._pageContextIncludes = pageContextIncludes || 'children';
  50. self._multiSelect = (typeof multiSelect === 'undefined' || multiSelect === true);
  51. if (singleFramework) {
  52. self._frameworkId = singleFramework;
  53. self._singleFramework = true;
  54. }
  55. };
  56. /** @property {Array} The competencies fetched. */
  57. Picker.prototype._competencies = null;
  58. /** @property {Array} The competencies that cannot be picked. */
  59. Picker.prototype._disallowedCompetencyIDs = null;
  60. /** @property {Node} The node we attach the events to. */
  61. Picker.prototype._eventNode = null;
  62. /** @property {Array} The list of frameworks fetched. */
  63. Picker.prototype._frameworks = null;
  64. /** @property {Number} The current framework ID. */
  65. Picker.prototype._frameworkId = null;
  66. /** @property {Number} The page context ID. */
  67. Picker.prototype._pageContextId = null;
  68. /** @property {Number} Relevant contexts inclusion. */
  69. Picker.prototype._pageContextIncludes = null;
  70. /** @property {Dialogue} The reference to the dialogue. */
  71. Picker.prototype._popup = null;
  72. /** @property {String} The string we filter the competencies with. */
  73. Picker.prototype._searchText = '';
  74. /** @property {Object} The competency that was selected. */
  75. Picker.prototype._selectedCompetencies = null;
  76. /** @property {Boolean} Whether we can browse frameworks or not. */
  77. Picker.prototype._singleFramework = false;
  78. /** @property {Boolean} Do we allow multi select? */
  79. Picker.prototype._multiSelect = true;
  80. /** @property {Boolean} Do we allow to display hidden framework? */
  81. Picker.prototype._onlyVisible = true;
  82. /**
  83. * Hook to executed after the view is rendered.
  84. *
  85. * @method _afterRender
  86. */
  87. Picker.prototype._afterRender = function() {
  88. var self = this;
  89. // Initialise the tree.
  90. var tree = new Tree(self._find('[data-enhance=linktree]'), self._multiSelect);
  91. // To prevent jiggling we only show the tree after it is enhanced.
  92. self._find('[data-enhance=linktree]').show();
  93. tree.on('selectionchanged', function(evt, params) {
  94. var selected = params.selected;
  95. evt.preventDefault();
  96. var validIds = [];
  97. $.each(selected, function(index, item) {
  98. var compId = $(item).data('id'),
  99. valid = true;
  100. if (typeof compId === 'undefined') {
  101. // Do not allow picking nodes with no id.
  102. valid = false;
  103. } else {
  104. $.each(self._disallowedCompetencyIDs, function(i, id) {
  105. if (id == compId) {
  106. valid = false;
  107. }
  108. });
  109. }
  110. if (valid) {
  111. validIds.push(compId);
  112. }
  113. });
  114. self._selectedCompetencies = validIds;
  115. // TODO Implement disabling of nodes in the tree module somehow.
  116. if (!self._selectedCompetencies.length) {
  117. self._find('[data-region="competencylinktree"] [data-action="add"]').attr('disabled', 'disabled');
  118. } else {
  119. self._find('[data-region="competencylinktree"] [data-action="add"]').removeAttr('disabled');
  120. }
  121. });
  122. // Add listener for framework change.
  123. if (!self._singleFramework) {
  124. self._find('[data-action="chooseframework"]').change(function(e) {
  125. self._frameworkId = $(e.target).val();
  126. self._loadCompetencies().then(self._refresh.bind(self)).catch(Notification.exception);
  127. });
  128. }
  129. // Add listener for search.
  130. self._find('[data-region="filtercompetencies"] button').click(function(e) {
  131. e.preventDefault();
  132. $(e.target).attr('disabled', 'disabled');
  133. self._searchText = self._find('[data-region="filtercompetencies"] input').val() || '';
  134. return self._refresh().always(function() {
  135. $(e.target).removeAttr('disabled');
  136. });
  137. });
  138. // Add listener for cancel.
  139. self._find('[data-region="competencylinktree"] [data-action="cancel"]').click(function(e) {
  140. e.preventDefault();
  141. self.close();
  142. });
  143. // Add listener for add.
  144. self._find('[data-region="competencylinktree"] [data-action="add"]').click(function(e) {
  145. e.preventDefault();
  146. var pendingPromise = new Pending();
  147. if (!self._selectedCompetencies.length) {
  148. return;
  149. }
  150. if (self._multiSelect) {
  151. self._trigger('save', {competencyIds: self._selectedCompetencies});
  152. } else {
  153. // We checked above that the array has at least one value.
  154. self._trigger('save', {competencyId: self._selectedCompetencies[0]});
  155. }
  156. // The dialogue here is a YUI dialogue and doesn't support Promises at all.
  157. // However, it is typically synchronous so this shoudl suffice.
  158. self.close();
  159. pendingPromise.resolve();
  160. });
  161. // The list of selected competencies will be modified while looping (because of the listeners above).
  162. var currentItems = self._selectedCompetencies.slice(0);
  163. $.each(currentItems, function(index, id) {
  164. var node = self._find('[data-id=' + id + ']');
  165. if (node.length) {
  166. tree.toggleItem(node);
  167. tree.updateFocus(node);
  168. }
  169. });
  170. };
  171. /**
  172. * Close the dialogue.
  173. *
  174. * @method close
  175. */
  176. Picker.prototype.close = function() {
  177. var self = this;
  178. self._popup.close();
  179. self._reset();
  180. };
  181. /**
  182. * Opens the picker.
  183. *
  184. * @method display
  185. * @return {Promise}
  186. */
  187. Picker.prototype.display = function() {
  188. var self = this;
  189. return $.when(Str.get_string('competencypicker', 'tool_lp'), self._render())
  190. .then(function(title, render) {
  191. self._popup = new Dialogue(
  192. title,
  193. render[0],
  194. self._afterRender.bind(self)
  195. );
  196. return;
  197. }).catch(Notification.exception);
  198. };
  199. /**
  200. * Fetch the competencies.
  201. *
  202. * @param {Number} frameworkId The frameworkId.
  203. * @param {String} searchText Limit the competencies to those matching the text.
  204. * @method _fetchCompetencies
  205. * @return {Promise}
  206. */
  207. Picker.prototype._fetchCompetencies = function(frameworkId, searchText) {
  208. var self = this;
  209. return Ajax.call([
  210. {methodname: 'core_competency_search_competencies', args: {
  211. searchtext: searchText,
  212. competencyframeworkid: frameworkId
  213. }}
  214. ])[0].done(function(competencies) {
  215. /**
  216. * @param {Object} parent
  217. * @param {Array} competencies
  218. */
  219. function addCompetencyChildren(parent, competencies) {
  220. for (var i = 0; i < competencies.length; i++) {
  221. if (competencies[i].parentid == parent.id) {
  222. parent.haschildren = true;
  223. competencies[i].children = [];
  224. competencies[i].haschildren = false;
  225. parent.children[parent.children.length] = competencies[i];
  226. addCompetencyChildren(competencies[i], competencies);
  227. }
  228. }
  229. }
  230. // Expand the list of competencies into a tree.
  231. var i, comp;
  232. var tree = [];
  233. for (i = 0; i < competencies.length; i++) {
  234. comp = competencies[i];
  235. if (comp.parentid == "0") { // Loose check for now, because WS returns a string.
  236. comp.children = [];
  237. comp.haschildren = 0;
  238. tree[tree.length] = comp;
  239. addCompetencyChildren(comp, competencies);
  240. }
  241. }
  242. self._competencies = tree;
  243. }).fail(Notification.exception);
  244. };
  245. /**
  246. * Find a node in the dialogue.
  247. *
  248. * @param {String} selector
  249. * @return {JQuery}
  250. * @method _find
  251. */
  252. Picker.prototype._find = function(selector) {
  253. return $(this._popup.getContent()).find(selector);
  254. };
  255. /**
  256. * Convenience method to get a framework object.
  257. *
  258. * @param {Number} fid The framework ID.
  259. * @return {Object}
  260. * @method _getFramework
  261. */
  262. Picker.prototype._getFramework = function(fid) {
  263. var frm;
  264. $.each(this._frameworks, function(i, f) {
  265. if (f.id == fid) {
  266. frm = f;
  267. return;
  268. }
  269. });
  270. return frm;
  271. };
  272. /**
  273. * Load the competencies.
  274. *
  275. * @method _loadCompetencies
  276. * @return {Promise}
  277. */
  278. Picker.prototype._loadCompetencies = function() {
  279. return this._fetchCompetencies(this._frameworkId, this._searchText);
  280. };
  281. /**
  282. * Load the frameworks.
  283. *
  284. * @method _loadFrameworks
  285. * @return {Promise}
  286. */
  287. Picker.prototype._loadFrameworks = function() {
  288. var promise,
  289. self = this;
  290. // Quit early because we already have the data.
  291. if (self._frameworks.length > 0) {
  292. return $.when();
  293. }
  294. if (self._singleFramework) {
  295. promise = Ajax.call([
  296. {methodname: 'core_competency_read_competency_framework', args: {
  297. id: this._frameworkId
  298. }}
  299. ])[0].then(function(framework) {
  300. return [framework];
  301. });
  302. } else {
  303. promise = Ajax.call([
  304. {methodname: 'core_competency_list_competency_frameworks', args: {
  305. sort: 'shortname',
  306. context: {contextid: self._pageContextId},
  307. includes: self._pageContextIncludes,
  308. onlyvisible: self._onlyVisible
  309. }}
  310. ])[0];
  311. }
  312. return promise.done(function(frameworks) {
  313. self._frameworks = frameworks;
  314. }).fail(Notification.exception);
  315. };
  316. /**
  317. * Register an event listener.
  318. *
  319. * @param {String} type The event type.
  320. * @param {Function} handler The event listener.
  321. * @method on
  322. */
  323. Picker.prototype.on = function(type, handler) {
  324. this._eventNode.on(type, handler);
  325. };
  326. /**
  327. * Hook to executed before render.
  328. *
  329. * @method _preRender
  330. * @return {Promise}
  331. */
  332. Picker.prototype._preRender = function() {
  333. var self = this;
  334. return self._loadFrameworks().then(function() {
  335. if (!self._frameworkId && self._frameworks.length > 0) {
  336. self._frameworkId = self._frameworks[0].id;
  337. }
  338. // We could not set a framework ID, that probably means there are no frameworks accessible.
  339. if (!self._frameworkId) {
  340. self._frameworks = [];
  341. return $.when();
  342. }
  343. return self._loadCompetencies();
  344. });
  345. };
  346. /**
  347. * Refresh the view.
  348. *
  349. * @method _refresh
  350. * @return {Promise}
  351. */
  352. Picker.prototype._refresh = function() {
  353. var self = this;
  354. return self._render().then(function(html) {
  355. self._find('[data-region="competencylinktree"]').replaceWith(html);
  356. self._afterRender();
  357. return;
  358. });
  359. };
  360. /**
  361. * Render the dialogue.
  362. *
  363. * @method _render
  364. * @return {Promise}
  365. */
  366. Picker.prototype._render = function() {
  367. var self = this;
  368. return self._preRender().then(function() {
  369. if (!self._singleFramework) {
  370. $.each(self._frameworks, function(i, framework) {
  371. if (framework.id == self._frameworkId) {
  372. framework.selected = true;
  373. } else {
  374. framework.selected = false;
  375. }
  376. });
  377. }
  378. var context = {
  379. competencies: self._competencies,
  380. framework: self._getFramework(self._frameworkId),
  381. frameworks: self._frameworks,
  382. search: self._searchText,
  383. singleFramework: self._singleFramework,
  384. };
  385. return Templates.render('tool_lp/competency_picker', context);
  386. });
  387. };
  388. /**
  389. * Reset the dialogue properties.
  390. *
  391. * This does not reset everything, just enough to reset the UI.
  392. *
  393. * @method _reset
  394. */
  395. Picker.prototype._reset = function() {
  396. this._competencies = [];
  397. this._disallowedCompetencyIDs = [];
  398. this._popup = null;
  399. this._searchText = '';
  400. this._selectedCompetencies = [];
  401. };
  402. /**
  403. * Set what competencies cannot be picked.
  404. *
  405. * This needs to be set after reset/close.
  406. *
  407. * @param {Number[]} ids The IDs.
  408. * @method _setDisallowedCompetencyIDs
  409. */
  410. Picker.prototype.setDisallowedCompetencyIDs = function(ids) {
  411. this._disallowedCompetencyIDs = ids;
  412. };
  413. /**
  414. * Trigger an event.
  415. *
  416. * @param {String} type The type of event.
  417. * @param {Object} data The data to pass to the listeners.
  418. * @method _reset
  419. */
  420. Picker.prototype._trigger = function(type, data) {
  421. this._eventNode.trigger(type, [data]);
  422. };
  423. return Picker;
  424. });