Merge branch 'MDL-76310-311' of https://github.com/paulholden/moodle into MOODLE_311_...
[moodle.git] / mod / lti / mod_form.js
blob639823688b9a4042a0640e3c1bdef0d6cf18fe80
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.
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/>.
16 /**
17  * Javascript extensions for the External Tool activity editor.
18  *
19  * @package    mod
20  * @subpackage lti
21  * @copyright  Copyright (c) 2011 Moodlerooms Inc. (http://www.moodlerooms.com)
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 (function(){
25     var Y;
27     M.mod_lti = M.mod_lti || {};
29     M.mod_lti.LTI_SETTING_NEVER = 0;
30     M.mod_lti.LTI_SETTING_ALWAYS = 1;
31     M.mod_lti.LTI_SETTING_DELEGATE = 2;
33     M.mod_lti.editor = {
34         init: function(yui3, settings){
35             if(yui3){
36                 Y = yui3;
37             }
39             var self = this;
40             this.settings = Y.JSON.parse(settings);
42             this.urlCache = {};
43             this.toolTypeCache = {};
45             var updateToolMatches = function(){
46                 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
47                 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
48             };
50             var typeSelector = Y.one('#id_typeid');
51             if (typeSelector) {
52                 this.addOptGroups();
54                 typeSelector.on('change', function(e){
55                     // Reset configuration fields when another preconfigured tool is selected.
56                     self.resetToolFields();
58                     updateToolMatches();
60                     self.toggleEditButtons();
62                     if (self.getSelectedToolTypeOption().getAttribute('toolproxy')){
63                         var allowname = Y.one('#id_instructorchoicesendname');
64                         allowname.set('checked', !self.getSelectedToolTypeOption().getAttribute('noname'));
66                         var allowemail = Y.one('#id_instructorchoicesendemailaddr');
67                         allowemail.set('checked', !self.getSelectedToolTypeOption().getAttribute('noemail'));
69                         var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
70                         allowgrades.set('checked', !self.getSelectedToolTypeOption().getAttribute('nogrades'));
71                         self.toggleGradeSection();
72                     }
73                 });
75                 this.createTypeEditorButtons();
77                 this.toggleEditButtons();
78             }
80             var contentItemButton = Y.one('[name="selectcontent"]');
81             if (contentItemButton) {
82                 var contentItemUrl = contentItemButton.getAttribute('data-contentitemurl');
83                 // Handle configure from link button click.
84                 contentItemButton.on('click', function() {
85                     var contentItemId = self.getContentItemId();
86                     if (contentItemId) {
87                         // Get activity name and description values.
88                         var title = Y.one('#id_name').get('value').trim();
89                         var text = Y.one('#id_introeditor').get('value').trim();
91                         // Set data to be POSTed.
92                         var postData = {
93                             id: contentItemId,
94                             course: self.settings.courseId,
95                             title: title,
96                             text: text
97                         };
99                         require(['mod_lti/contentitem'], function(contentitem) {
100                             contentitem.init(contentItemUrl, postData, function(returnData) {
101                                 if (!returnData.multiple) {
102                                     M.mod_lti.editor.toggleGradeSection();
103                                 }
104                             });
105                         });
106                     }
107                 });
108             }
110             var textAreas = new Y.NodeList([
111                 Y.one('#id_toolurl'),
112                 Y.one('#id_securetoolurl'),
113                 Y.one('#id_resourcekey'),
114                 Y.one('#id_password')
115             ]);
117             var debounce;
118             textAreas.on('keyup', function(e){
119                 clearTimeout(debounce);
121                 // If no more changes within 2 seconds, look up the matching tool URL
122                 debounce = setTimeout(function(){
123                     updateToolMatches();
124                 }, 2000);
125             });
127             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
128             allowgrades.on('change', this.toggleGradeSection, this);
130             if (typeSelector) {
131                 updateToolMatches();
132             }
133         },
135         toggleGradeSection: function(e) {
136             if (e) {
137                 e.preventDefault();
138             }
139             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
140             var gradefieldset = Y.one('#id_modstandardgrade');
141             if (!allowgrades.get('checked')) {
142                 gradefieldset.hide();
143             } else {
144                 gradefieldset.show();
145             }
146         },
148         clearToolCache: function(){
149             this.urlCache = {};
150             this.toolTypeCache = {};
151         },
153         updateAutomaticToolMatch: function(field){
154             if (!field) {
155                 return;
156             }
158             var self = this;
160             var toolurl = field;
161             var typeSelector = Y.one('#id_typeid');
163             var id = field.get('id') + '_lti_automatch_tool';
164             var automatchToolDisplay = Y.one('#' + id);
166             if(!automatchToolDisplay){
167                 automatchToolDisplay = Y.Node.create('<span />')
168                                         .set('id', id)
169                                         .setStyle('padding-left', '1em');
171                 toolurl.insert(automatchToolDisplay, 'after');
172             }
174             var url = toolurl.get('value');
176             // Hide the display if the url box is empty
177             if(!url){
178                 automatchToolDisplay.setStyle('display', 'none');
179             } else {
180                 automatchToolDisplay.set('innerHTML', '');
181                 automatchToolDisplay.setStyle('display', '');
182             }
184             var selectedToolType = parseInt(typeSelector.get('value'));
185             var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
187             // A specific tool type is selected (not "auto")
188             // We still need to check with the server to get privacy settings
189             if(selectedToolType > 0){
190                 // If the entered domain matches the domain of the tool configuration...
191                 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
192                 var match = domainRegex.exec(url);
193                 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
194                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('using_tool_configuration', 'lti') + selectedOption.get('text'));
195                 } else {
196                     // The entered URL does not match the domain of the tool configuration
197                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('domain_mismatch', 'lti'));
198                 }
199             }
201             var key = Y.one('#id_resourcekey');
202             var secret = Y.one('#id_password');
204             // Indicate the tool is manually configured
205             // We still check the Launch URL with the server as course/site tools may override privacy settings
206             if(key.get('value') !== '' && secret.get('value') !== ''){
207                 automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('custom_config', 'lti'));
208             }
210             var continuation = function(toolInfo, inputfield){
211                 if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) {
212                     self.updatePrivacySettings(toolInfo);
213                 }
214                 if(toolInfo.toolname){
215                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('using_tool_configuration', 'lti') + toolInfo.toolname);
216                 } else if(!selectedToolType) {
217                     // Inform them custom configuration is in use
218                     if(key.get('value') === '' || secret.get('value') === ''){
219                         automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('tool_config_not_found', 'lti'));
220                     }
221                 }
222                 if (toolInfo.cartridge) {
223                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url +
224                                              '" />' + M.util.get_string('using_tool_cartridge', 'lti'));
225                 }
226             };
228             // Cache urls which have already been checked to increase performance
229             // Don't use URL cache if tool type manually selected
230             if(selectedToolType && self.toolTypeCache[selectedToolType]){
231                 return continuation(self.toolTypeCache[selectedToolType]);
232             } else if(self.urlCache[url] && !selectedToolType){
233                 return continuation(self.urlCache[url]);
234             } else if(!selectedToolType && !url) {
235                 // No tool type or url set
236                 return continuation({}, field);
237             } else {
238                 self.findToolByUrl(url, selectedToolType, function(toolInfo){
239                     if(toolInfo){
240                         // Cache the result based on whether the URL or tool type was used to look up the tool
241                         if(!selectedToolType){
242                             self.urlCache[url] = toolInfo;
243                         } else {
244                             self.toolTypeCache[selectedToolType] = toolInfo;
245                         }
247                         Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
249                         continuation(toolInfo);
250                     }
251                 });
252             }
253         },
255         /**
256          * Updates display of privacy settings to show course / site tool configuration settings.
257          */
258         updatePrivacySettings: function(toolInfo){
259             if(!toolInfo || !toolInfo.toolid){
260                 toolInfo = {
261                     sendname: M.mod_lti.LTI_SETTING_DELEGATE,
262                     sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
263                     acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
264                 }
265             }
267             var setting, control;
269             var privacyControls = {
270                 sendname: Y.one('#id_instructorchoicesendname'),
271                 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
272                 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
273             };
275             // Store a copy of user entered privacy settings as we may overwrite them
276             if(!this.userPrivacySettings){
277                 this.userPrivacySettings = {};
278             }
280             for(setting in privacyControls){
281                 if(privacyControls.hasOwnProperty(setting)){
282                     control = privacyControls[setting];
284                     // Only store the value if it hasn't been forced by the editor
285                     if(!control.get('disabled')){
286                         this.userPrivacySettings[setting] = control.get('checked');
287                     }
288                 }
289             }
291             // Update UI based on course / site tool configuration
292             for(setting in privacyControls){
293                 if(privacyControls.hasOwnProperty(setting)){
294                     var settingValue = toolInfo[setting];
295                     control = privacyControls[setting];
297                     if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
298                         control.set('disabled', true);
299                         control.set('checked', false);
300                         control.set('title', M.util.get_string('forced_help', 'lti'));
301                     } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
302                         control.set('disabled', true);
303                         control.set('checked', true);
304                         control.set('title', M.util.get_string('forced_help', 'lti'));
305                     } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
306                         control.set('disabled', false);
308                         // Get the value out of the stored copy
309                         control.set('checked', this.userPrivacySettings[setting]);
310                         control.set('title', '');
311                     }
312                 }
313             }
315             this.toggleGradeSection();
316         },
318         getSelectedToolTypeOption: function(){
319             var typeSelector = Y.one('#id_typeid');
321             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
322         },
324         /**
325          * Separate tool listing into option groups. Server-side select control
326          * doesn't seem to support this.
327          */
328         addOptGroups: function(){
329             var typeSelector = Y.one('#id_typeid');
331             if(typeSelector.one('option[courseTool=1]')){
332                 // One ore more course tools exist
334                 var globalGroup = Y.Node.create('<optgroup />')
335                                     .set('id', 'global_tool_group')
336                                     .set('label', M.util.get_string('global_tool_types', 'lti'));
338                 var courseGroup = Y.Node.create('<optgroup />')
339                                     .set('id', 'course_tool_group')
340                                     .set('label', M.util.get_string('course_tool_types', 'lti'));
342                 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
343                     globalGroup.append(node);
344                 });
346                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
347                     courseGroup.append(node);
348                 });
350                 if(globalOptions.size() > 0){
351                     typeSelector.append(globalGroup);
352                 }
354                 if(courseOptions.size() > 0){
355                     typeSelector.append(courseGroup);
356                 }
357             }
358         },
360         /**
361          * Adds buttons for creating, editing, and deleting tool types.
362          * Javascript is a requirement to edit course level tools at this point.
363          */
364         createTypeEditorButtons: function(){
365             var self = this;
367             var typeSelector = Y.one('#id_typeid');
369             var createIcon = function(id, tooltip, iconUrl){
370                 return Y.Node.create('<a />')
371                         .set('id', id)
372                         .set('title', tooltip)
373                         .setStyle('margin-left', '.5em')
374                         .set('href', 'javascript:void(0);')
375                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
376             }
378             var addIcon = createIcon('lti_add_tool_type', M.util.get_string('addtype', 'lti'), this.settings.add_icon_url);
379             var editIcon = createIcon('lti_edit_tool_type', M.util.get_string('edittype', 'lti'), this.settings.edit_icon_url);
380             var deleteIcon  = createIcon('lti_delete_tool_type', M.util.get_string('deletetype', 'lti'), this.settings.delete_icon_url);
382             editIcon.on('click', function(e){
383                 var toolTypeId = typeSelector.get('value');
385                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
386                     window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
387                 } else {
388                     alert(M.util.get_string('cannot_edit', 'lti'));
389                 }
390             });
392             addIcon.on('click', function(e){
393                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
394             });
396             deleteIcon.on('click', function(e){
397                 var toolTypeId = typeSelector.get('value');
399                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
400                     if(confirm(M.util.get_string('delete_confirmation', 'lti'))){
401                         self.deleteTool(toolTypeId);
402                     }
403                 } else {
404                     alert(M.util.get_string('cannot_delete', 'lti'));
405                 }
406             });
408             typeSelector.insert(addIcon, 'after');
409             addIcon.insert(editIcon, 'after');
410             editIcon.insert(deleteIcon, 'after');
411         },
413         toggleEditButtons: function(){
414             var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
415             var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
417             // Make the edit / delete icons look enabled / disabled.
418             // Does not work in older browsers, but alerts will catch those cases.
419             if(this.getSelectedToolTypeOption().getAttribute('editable')){
420                 lti_edit_tool_type.setStyle('opacity', '1');
421                 lti_delete_tool_type.setStyle('opacity', '1');
422             } else {
423                 lti_edit_tool_type.setStyle('opacity', '.2');
424                 lti_delete_tool_type.setStyle('opacity', '.2');
425             }
426         },
428         addToolType: function(toolType){
429             var typeSelector = Y.one('#id_typeid');
430             var course_tool_group = Y.one('#course_tool_group');
432             var option = Y.Node.create('<option />')
433                             .set('text', toolType.name)
434                             .set('value', toolType.id)
435                             .set('selected', 'selected')
436                             .setAttribute('editable', '1')
437                             .setAttribute('courseTool', '1')
438                             .setAttribute('domain', toolType.tooldomain);
440             if(course_tool_group){
441                 course_tool_group.append(option);
442             } else {
443                 typeSelector.append(option);
444             }
446             // Adding the new tool may affect which tool gets matched automatically
447             this.clearToolCache();
448             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
449             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
450             this.toggleEditButtons();
452             require(["core/notification"], function (notification) {
453                 notification.addNotification({
454                     message: M.util.get_string('tooltypeadded', 'lti'),
455                     type: "success"
456                 });
457             });
458         },
460         updateToolType: function(toolType){
461             var typeSelector = Y.one('#id_typeid');
463             var option = typeSelector.one('option[value="' + toolType.id + '"]');
464             option.set('text', toolType.name)
465                   .set('domain', toolType.tooldomain);
467             // Editing the tool may affect which tool gets matched automatically
468             this.clearToolCache();
469             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
470             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
472             require(["core/notification"], function (notification) {
473                 notification.addNotification({
474                     message: M.util.get_string('tooltypeupdated', 'lti'),
475                     type: "success"
476                 });
477             });
478         },
480         deleteTool: function(toolTypeId){
481             var self = this;
483             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
484                 on: {
485                     success: function(){
486                         self.getSelectedToolTypeOption().remove();
488                         // Editing the tool may affect which tool gets matched automatically
489                         self.clearToolCache();
490                         self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
491                         self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
493                         require(["core/notification"], function (notification) {
494                             notification.addNotification({
495                                 message: M.util.get_string('tooltypedeleted', 'lti'),
496                                 type: "success"
497                             });
498                         });
499                     },
500                     failure: function(){
501                         require(["core/notification"], function (notification) {
502                             notification.addNotification({
503                                 message: M.util.get_string('tooltypenotdeleted', 'lti'),
504                                 type: "problem"
505                             });
506                         });
507                     }
508                 }
509             });
510         },
512         findToolByUrl: function(url, toolId, callback){
513             var self = this;
515             Y.io(self.settings.ajax_url, {
516                 data: {action: 'find_tool_config',
517                         course: self.settings.courseId,
518                         toolurl: url,
519                         toolid: toolId || 0
520                 },
522                 on: {
523                     success: function(transactionid, xhr){
524                         var response = xhr.response;
526                         var toolInfo = Y.JSON.parse(response);
528                         callback(toolInfo);
529                     },
530                     failure: function(){
532                     }
533                 }
534             });
535         },
537         /**
538          * Gets the tool type ID of the selected tool that supports Content-Item selection.
539          *
540          * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise.
541          */
542         getContentItemId: function() {
543             try {
544                 var selected = this.getSelectedToolTypeOption();
545                 if (selected.getAttribute('data-contentitem')) {
546                     return selected.getAttribute('data-id');
547                 }
548                 return false;
549             } catch (err) {
550                 // Tool selector not available - check for hidden fields instead.
551                 var content = Y.one('input[name="contentitem"]');
552                 if (!content || !content.get('value')) {
553                     return false;
554                 }
555                 return Y.one('input[name="typeid"]').get('value');
556             }
557         },
559         /**
560          * Resets the values of fields related to the LTI tool settings.
561          */
562         resetToolFields: function() {
563             // Reset values for all text fields.
564             var fields = Y.all('#id_toolurl, #id_securetoolurl, #id_instructorcustomparameters, #id_icon, #id_secureicon');
565             fields.set('value', null);
567             // Reset value for launch container select box.
568             Y.one('#id_launchcontainer').set('value', 1);
569         }
570     };
571 })();