mod/lti/amd/src/tool_card_controller.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. * Controls all of the behaviour and interaction with a tool type card. These are
  17. * listed on the LTI tool type management page.
  18. *
  19. * See template: mod_lti/tool_card
  20. *
  21. * @module mod_lti/tool_card_controller
  22. * @copyright 2015 Ryan Wyllie <ryan@moodle.com>
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. * @since 3.1
  25. */
  26. define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'core/modal',
  27. 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys',
  28. 'core/str'],
  29. function($, ajax, notification, templates, Modal, toolType, ltiEvents, KEYS, str) {
  30. var SELECTORS = {
  31. DELETE_BUTTON: '.delete',
  32. NAME_ELEMENT: '.name',
  33. DESCRIPTION_ELEMENT: '.description',
  34. CAPABILITIES_CONTAINER: '.capabilities-container',
  35. ACTIVATE_BUTTON: '.tool-card-footer a.activate',
  36. };
  37. // Timeout in seconds.
  38. var ANNOUNCEMENT_TIMEOUT = 2000;
  39. /**
  40. * Return the delete button element.
  41. *
  42. * @method getDeleteButton
  43. * @private
  44. * @param {JQuery} element jQuery object representing the tool card.
  45. * @return {JQuery} jQuery object
  46. */
  47. var getDeleteButton = function(element) {
  48. return element.find(SELECTORS.DELETE_BUTTON);
  49. };
  50. /**
  51. * Return the element representing the tool type name.
  52. *
  53. * @method getNameElement
  54. * @private
  55. * @param {JQuery} element jQuery object representing the tool card.
  56. * @return {JQuery} jQuery object
  57. */
  58. var getNameElement = function(element) {
  59. return element.find(SELECTORS.NAME_ELEMENT);
  60. };
  61. /**
  62. * Return the element representing the tool type description.
  63. *
  64. * @method getDescriptionElement
  65. * @private
  66. * @param {JQuery} element jQuery object representing the tool card.
  67. * @return {JQuery} jQuery object
  68. */
  69. var getDescriptionElement = function(element) {
  70. return element.find(SELECTORS.DESCRIPTION_ELEMENT);
  71. };
  72. /**
  73. * Return the activate button for the type.
  74. *
  75. * @method getActivateButton
  76. * @private
  77. * @param {Object} element jQuery object representing the tool card.
  78. * @return {Object} jQuery object
  79. */
  80. var getActivateButton = function(element) {
  81. return element.find(SELECTORS.ACTIVATE_BUTTON);
  82. };
  83. /**
  84. * Checks if the type card has an activate button.
  85. *
  86. * @method hasActivateButton
  87. * @private
  88. * @param {JQuery} element jQuery object representing the tool card.
  89. * @return {Boolean} true if has active buton
  90. */
  91. var hasActivateButton = function(element) {
  92. return getActivateButton(element).length ? true : false;
  93. };
  94. /**
  95. * Return the element that contains the capabilities approval for
  96. * the user.
  97. *
  98. * @method getCapabilitiesContainer
  99. * @private
  100. * @param {Object} element jQuery object representing the tool card.
  101. * @return {Object} The element
  102. */
  103. var getCapabilitiesContainer = function(element) {
  104. return element.find(SELECTORS.CAPABILITIES_CONTAINER);
  105. };
  106. /**
  107. * Checks if the tool type has capabilities that need approval. If it
  108. * does then the container will be present.
  109. *
  110. * @method hasCapabilitiesContainer
  111. * @private
  112. * @param {JQuery} element jQuery object representing the tool card.
  113. * @return {Boolean} true if has capbilities.
  114. */
  115. var hasCapabilitiesContainer = function(element) {
  116. return getCapabilitiesContainer(element).length ? true : false;
  117. };
  118. /**
  119. * Get the type id.
  120. *
  121. * @method getTypeId
  122. * @private
  123. * @param {Object} element jQuery object representing the tool card.
  124. * @return {String} Type ID
  125. */
  126. var getTypeId = function(element) {
  127. return element.attr('data-type-id');
  128. };
  129. /**
  130. * Stop any announcement currently visible on the card.
  131. *
  132. * @method clearAllAnnouncements
  133. * @private
  134. * @param {JQuery} element jQuery object representing the tool card.
  135. */
  136. var clearAllAnnouncements = function(element) {
  137. element.removeClass('announcement loading success fail capabilities');
  138. };
  139. /**
  140. * Show the loading announcement.
  141. *
  142. * @method startLoading
  143. * @private
  144. * @param {JQuery} element jQuery object representing the tool card.
  145. */
  146. var startLoading = function(element) {
  147. clearAllAnnouncements(element);
  148. element.addClass('announcement loading');
  149. };
  150. /**
  151. * Hide the loading announcement.
  152. *
  153. * @method stopLoading
  154. * @private
  155. * @param {JQuery} element jQuery object representing the tool card.
  156. */
  157. var stopLoading = function(element) {
  158. element.removeClass('announcement loading');
  159. };
  160. /**
  161. * Show the success announcement. The announcement is only
  162. * visible for 2 seconds.
  163. *
  164. * @method announceSuccess
  165. * @private
  166. * @param {JQuery} element jQuery object representing the tool card.
  167. * @return {Promise} jQuery Deferred object
  168. */
  169. var announceSuccess = function(element) {
  170. var promise = $.Deferred();
  171. clearAllAnnouncements(element);
  172. element.addClass('announcement success');
  173. setTimeout(function() {
  174. element.removeClass('announcement success');
  175. promise.resolve();
  176. }, ANNOUNCEMENT_TIMEOUT);
  177. return promise;
  178. };
  179. /**
  180. * Show the failure announcement. The announcement is only
  181. * visible for 2 seconds.
  182. *
  183. * @method announceFailure
  184. * @private
  185. * @param {JQuery} element jQuery object representing the tool card.
  186. * @return {Promise} jQuery Deferred object
  187. */
  188. var announceFailure = function(element) {
  189. var promise = $.Deferred();
  190. clearAllAnnouncements(element);
  191. element.addClass('announcement fail');
  192. setTimeout(function() {
  193. element.removeClass('announcement fail');
  194. promise.resolve();
  195. }, ANNOUNCEMENT_TIMEOUT);
  196. return promise;
  197. };
  198. /**
  199. * Delete the tool type from the Moodle server. Triggers a success
  200. * or failure announcement depending on the result.
  201. *
  202. * @method deleteType
  203. * @private
  204. * @param {JQuery} element jQuery object representing the tool card.
  205. * @return {Promise} jQuery Deferred object
  206. */
  207. var deleteType = function(element) {
  208. var promise = $.Deferred();
  209. var typeId = getTypeId(element);
  210. startLoading(element);
  211. if (typeId === "") {
  212. return $.Deferred().resolve();
  213. }
  214. str.get_strings([
  215. {
  216. key: 'delete',
  217. component: 'mod_lti'
  218. },
  219. {
  220. key: 'delete_confirmation',
  221. component: 'mod_lti'
  222. },
  223. {
  224. key: 'delete',
  225. component: 'mod_lti'
  226. },
  227. {
  228. key: 'cancel',
  229. component: 'core'
  230. },
  231. ])
  232. .done(function(strs) {
  233. notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
  234. toolType.delete(typeId)
  235. .done(function() {
  236. stopLoading(element);
  237. announceSuccess(element)
  238. .done(function() {
  239. element.remove();
  240. })
  241. .fail(notification.exception)
  242. .always(function() {
  243. // Always resolve because even if the announcement fails the type was deleted.
  244. promise.resolve();
  245. });
  246. })
  247. .fail(function(error) {
  248. announceFailure(element);
  249. promise.reject(error);
  250. });
  251. }, function() {
  252. stopLoading(element);
  253. promise.resolve();
  254. });
  255. })
  256. .fail(function(error) {
  257. stopLoading(element);
  258. notification.exception(error);
  259. promise.reject(error);
  260. });
  261. return promise;
  262. };
  263. /**
  264. * Save a given value in a data attribute on the element.
  265. *
  266. * @method setValueSnapshot
  267. * @private
  268. * @param {JQuery} element jQuery object representing the element.
  269. * @param {String} value to be saved.
  270. */
  271. var setValueSnapshot = function(element, value) {
  272. element.attr('data-val-snapshot', value);
  273. };
  274. /**
  275. * Return the saved value from the element.
  276. *
  277. * @method getValueSnapshot
  278. * @private
  279. * @param {JQuery} element jQuery object representing the element.
  280. * @return {String} the saved value.
  281. */
  282. var getValueSnapshot = function(element) {
  283. return element.attr('data-val-snapshot');
  284. };
  285. /**
  286. * Save the current value of the tool description.
  287. *
  288. * @method snapshotDescription
  289. * @private
  290. * @param {JQuery} element jQuery object representing the tool card.
  291. */
  292. var snapshotDescription = function(element) {
  293. var descriptionElement = getDescriptionElement(element);
  294. if (descriptionElement.hasClass('loading')) {
  295. return;
  296. }
  297. var description = descriptionElement.text().trim();
  298. setValueSnapshot(descriptionElement, description);
  299. };
  300. /**
  301. * Send a request to update the description value for this tool
  302. * in the Moodle server.
  303. *
  304. * @method updateDescription
  305. * @private
  306. * @param {JQuery} element jQuery object representing the tool card.
  307. * @return {Promise} jQuery Deferred object
  308. */
  309. var updateDescription = function(element) {
  310. var typeId = getTypeId(element);
  311. // Return early if we don't have an id because it's
  312. // required to save the changes.
  313. if (typeId === "") {
  314. return $.Deferred().resolve();
  315. }
  316. var descriptionElement = getDescriptionElement(element);
  317. // Return early if we're already saving a value.
  318. if (descriptionElement.hasClass('loading')) {
  319. return $.Deferred().resolve();
  320. }
  321. var description = descriptionElement.text().trim();
  322. var snapshotVal = getValueSnapshot(descriptionElement);
  323. // If the value hasn't change then don't bother sending the
  324. // update request.
  325. if (snapshotVal == description) {
  326. return $.Deferred().resolve();
  327. }
  328. descriptionElement.addClass('loading');
  329. var promise = toolType.update({id: typeId, description: description});
  330. promise.done(function(type) {
  331. descriptionElement.removeClass('loading');
  332. // Make sure the text is updated with the description from the
  333. // server, just in case the update didn't work.
  334. descriptionElement.text(type.description);
  335. }).fail(notification.exception);
  336. // Probably need to handle failures better so that we can revert
  337. // the value in the input for the user.
  338. promise.fail(function() {
  339. descriptionElement.removeClass('loading');
  340. });
  341. return promise;
  342. };
  343. /**
  344. * Save the current value of the tool name.
  345. *
  346. * @method snapshotName
  347. * @private
  348. * @param {JQuery} element jQuery object representing the tool card.
  349. */
  350. var snapshotName = function(element) {
  351. var nameElement = getNameElement(element);
  352. if (nameElement.hasClass('loading')) {
  353. return;
  354. }
  355. var name = nameElement.text().trim();
  356. setValueSnapshot(nameElement, name);
  357. };
  358. /**
  359. * Send a request to update the name value for this tool
  360. * in the Moodle server.
  361. *
  362. * @method updateName
  363. * @private
  364. * @param {JQuery} element jQuery object representing the tool card.
  365. * @return {Promise} jQuery Deferred object
  366. */
  367. var updateName = function(element) {
  368. var typeId = getTypeId(element);
  369. // Return if we don't have an id.
  370. if (typeId === "") {
  371. return $.Deferred().resolve();
  372. }
  373. var nameElement = getNameElement(element);
  374. // Return if we're already saving.
  375. if (nameElement.hasClass('loading')) {
  376. return $.Deferred().resolve();
  377. }
  378. var name = nameElement.text().trim();
  379. var snapshotVal = getValueSnapshot(nameElement);
  380. // If the value hasn't change then don't bother sending the
  381. // update request.
  382. if (snapshotVal == name) {
  383. return $.Deferred().resolve();
  384. }
  385. nameElement.addClass('loading');
  386. var promise = toolType.update({id: typeId, name: name});
  387. promise.done(function(type) {
  388. nameElement.removeClass('loading');
  389. // Make sure the text is updated with the name from the
  390. // server, just in case the update didn't work.
  391. nameElement.text(type.name);
  392. });
  393. // Probably need to handle failures better so that we can revert
  394. // the value in the input for the user.
  395. promise.fail(function() {
  396. nameElement.removeClass('loading');
  397. });
  398. return promise;
  399. };
  400. /**
  401. * Send a request to update the state for this tool to be configured (active)
  402. * in the Moodle server. A success or failure announcement is triggered depending
  403. * on the result.
  404. *
  405. * @method setStatusActive
  406. * @private
  407. * @param {JQuery} element jQuery object representing the tool card.
  408. * @return {Promise} jQuery Deferred object
  409. */
  410. var setStatusActive = function(element) {
  411. var id = getTypeId(element);
  412. // Return if we don't have an id.
  413. if (id === "") {
  414. return $.Deferred().resolve();
  415. }
  416. startLoading(element);
  417. var promise = toolType.update({
  418. id: id,
  419. state: toolType.constants.state.configured
  420. });
  421. promise.then(function(toolTypeData) {
  422. stopLoading(element);
  423. announceSuccess(element);
  424. return toolTypeData;
  425. }).then(function(toolTypeData) {
  426. return templates.render('mod_lti/tool_card', toolTypeData);
  427. }).then(function(html, js) {
  428. templates.replaceNode(element, html, js);
  429. return;
  430. }).catch(function() {
  431. stopLoading(element);
  432. announceFailure(element);
  433. });
  434. return promise;
  435. };
  436. /**
  437. * Show the capabilities approval screen to show which groups of data this
  438. * type requires access to in Moodle (if any).
  439. *
  440. * @method displayCapabilitiesApproval
  441. * @private
  442. * @param {JQuery} element jQuery object representing the tool card.
  443. */
  444. var displayCapabilitiesApproval = function(element) {
  445. element.addClass('announcement capabilities');
  446. };
  447. /**
  448. * Hide the capabilities approval screen.
  449. *
  450. * @method hideCapabilitiesApproval
  451. * @private
  452. * @param {JQuery} element jQuery object representing the tool card.
  453. */
  454. var hideCapabilitiesApproval = function(element) {
  455. element.removeClass('announcement capabilities');
  456. };
  457. /**
  458. * The user wishes to activate this tool so show them the capabilities that
  459. * they need to agree to or if there are none then set the tool type's state
  460. * to active.
  461. *
  462. * @method activateToolType
  463. * @private
  464. * @param {JQuery} element jQuery object representing the tool card.
  465. */
  466. var activateToolType = function(element) {
  467. if (hasCapabilitiesContainer(element)) {
  468. displayCapabilitiesApproval(element);
  469. } else {
  470. setStatusActive(element);
  471. }
  472. };
  473. /**
  474. * Sets up the listeners for user interaction on this tool type card.
  475. *
  476. * @method registerEventListeners
  477. * @private
  478. * @param {JQuery} element jQuery object representing the tool card.
  479. */
  480. var registerEventListeners = function(element) {
  481. var deleteButton = getDeleteButton(element);
  482. deleteButton.click(function(e) {
  483. e.preventDefault();
  484. deleteType(element);
  485. });
  486. deleteButton.keypress(function(e) {
  487. if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
  488. if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {
  489. e.preventDefault();
  490. deleteButton.click();
  491. }
  492. }
  493. });
  494. var descriptionElement = getDescriptionElement(element);
  495. descriptionElement.focus(function(e) {
  496. e.preventDefault();
  497. // Save a copy of the current value for the description so that
  498. // we can check if the user has changed it before sending a request to
  499. // the server.
  500. snapshotDescription(element);
  501. });
  502. descriptionElement.blur(function(e) {
  503. e.preventDefault();
  504. updateDescription(element);
  505. });
  506. descriptionElement.keypress(function(e) {
  507. if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
  508. if (e.keyCode == KEYS.ENTER) {
  509. e.preventDefault();
  510. descriptionElement.blur();
  511. }
  512. }
  513. });
  514. var nameElement = getNameElement(element);
  515. nameElement.focus(function(e) {
  516. e.preventDefault();
  517. // Save a copy of the current value for the name so that
  518. // we can check if the user has changed it before sending a request to
  519. // the server.
  520. snapshotName(element);
  521. });
  522. nameElement.blur(function(e) {
  523. e.preventDefault();
  524. updateName(element);
  525. });
  526. nameElement.keypress(function(e) {
  527. if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
  528. if (e.keyCode == KEYS.ENTER) {
  529. e.preventDefault();
  530. nameElement.blur();
  531. }
  532. }
  533. });
  534. // Only pending tool type cards have an activate button.
  535. if (hasActivateButton(element)) {
  536. var activateButton = getActivateButton(element);
  537. activateButton.click(function(e) {
  538. e.preventDefault();
  539. activateToolType(element);
  540. });
  541. activateButton.keypress(function(e) {
  542. if (!e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey) {
  543. if (e.keyCode == KEYS.ENTER || e.keyCode == KEYS.SPACE) {
  544. e.preventDefault();
  545. activateButton.click();
  546. }
  547. }
  548. });
  549. }
  550. if (hasCapabilitiesContainer(element)) {
  551. var capabilitiesContainer = getCapabilitiesContainer(element);
  552. capabilitiesContainer.on(ltiEvents.CAPABILITIES_AGREE, function() {
  553. setStatusActive(element);
  554. });
  555. capabilitiesContainer.on(ltiEvents.CAPABILITIES_DECLINE, function() {
  556. hideCapabilitiesApproval(element);
  557. });
  558. }
  559. };
  560. /**
  561. * Sets up the templates for the tool configuration modal on this tool type card.
  562. *
  563. * @method registerModal
  564. * @private
  565. * @param {JQuery} element jQuery object representing the tool card.
  566. */
  567. var registerModal = function(element) {
  568. const configurationLink = element.find('#' + element.data('uniqid') + '-' + element.data('deploymentid'));
  569. if (!configurationLink.length) {
  570. return;
  571. }
  572. const trigger = configurationLink.get(0);
  573. trigger.addEventListener('click', (e) => {
  574. e.preventDefault();
  575. var context = {
  576. 'uniqid': element.data('uniqid'),
  577. 'platformid': element.data('platformid'),
  578. 'clientid': element.data('clientid'),
  579. 'deploymentid': element.data('deploymentid'),
  580. 'urls': {
  581. 'publickeyset': element.data('publickeyseturl'),
  582. 'accesstoken': element.data('accesstokenurl'),
  583. 'authrequest': element.data('authrequesturl')
  584. }
  585. };
  586. var bodyPromise = templates.render('mod_lti/tool_config_modal_body', context);
  587. var mailTo = 'mailto:?subject=' + encodeURIComponent(element.data('mailtosubject')) +
  588. '&body=' + encodeURIComponent(element.data('platformidstr')) + ':%20' +
  589. encodeURIComponent(element.data('platformid')) + '%0D%0A' +
  590. encodeURIComponent(element.data('clientidstr')) + ':%20' +
  591. encodeURIComponent(element.data('clientid')) + '%0D%0A' +
  592. encodeURIComponent(element.data('deploymentidstr')) + ':%20' +
  593. encodeURIComponent(element.data('deploymentid')) + '%0D%0A' +
  594. encodeURIComponent(element.data('publickeyseturlstr')) + ':%20' +
  595. encodeURIComponent(element.data('publickeyseturl')) + '%0D%0A' +
  596. encodeURIComponent(element.data('accesstokenurlstr')) + ':%20' +
  597. encodeURIComponent(element.data('accesstokenurl')) + '%0D%0A' +
  598. encodeURIComponent(element.data('authrequesturlstr')) + ':%20' +
  599. encodeURIComponent(element.data('authrequesturl')) + '%0D%0A';
  600. context = {
  601. 'mailto': mailTo
  602. };
  603. var footerPromise = templates.render('mod_lti/tool_config_modal_footer', context);
  604. Modal.create({
  605. large: true,
  606. title: element.data('modaltitle'),
  607. body: bodyPromise,
  608. footer: footerPromise,
  609. show: true
  610. });
  611. });
  612. };
  613. return /** @alias module:mod_lti/tool_card_controller */ {
  614. /**
  615. * Initialise this module.
  616. *
  617. * @param {JQuery} element jQuery object representing the tool card.
  618. */
  619. init: function(element) {
  620. registerEventListeners(element);
  621. registerModal(element);
  622. }
  623. };
  624. });