Merge branch 'MDL-62815_mod_lti' of git://github.com/davosmith/moodle
[moodle.git] / mod / lti / mod_form.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/>.
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() {
101                                 M.mod_lti.editor.toggleGradeSection();
102                             });
103                         });
104                     }
105                 });
106             }
108             var textAreas = new Y.NodeList([
109                 Y.one('#id_toolurl'),
110                 Y.one('#id_securetoolurl'),
111                 Y.one('#id_resourcekey'),
112                 Y.one('#id_password')
113             ]);
115             var debounce;
116             textAreas.on('keyup', function(e){
117                 clearTimeout(debounce);
119                 // If no more changes within 2 seconds, look up the matching tool URL
120                 debounce = setTimeout(function(){
121                     updateToolMatches();
122                 }, 2000);
123             });
125             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
126             allowgrades.on('change', this.toggleGradeSection, this);
128             if (typeSelector) {
129                 updateToolMatches();
130             }
131         },
133         toggleGradeSection: function(e) {
134             if (e) {
135                 e.preventDefault();
136             }
137             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
138             var gradefieldset = Y.one('#id_modstandardgrade');
139             if (!allowgrades.get('checked')) {
140                 gradefieldset.hide();
141             } else {
142                 gradefieldset.show();
143             }
144         },
146         clearToolCache: function(){
147             this.urlCache = {};
148             this.toolTypeCache = {};
149         },
151         updateAutomaticToolMatch: function(field){
152             if (!field) {
153                 return;
154             }
156             var self = this;
158             var toolurl = field;
159             var typeSelector = Y.one('#id_typeid');
161             var id = field.get('id') + '_lti_automatch_tool';
162             var automatchToolDisplay = Y.one('#' + id);
164             if(!automatchToolDisplay){
165                 automatchToolDisplay = Y.Node.create('<span />')
166                                         .set('id', id)
167                                         .setStyle('padding-left', '1em');
169                 toolurl.insert(automatchToolDisplay, 'after');
170             }
172             var url = toolurl.get('value');
174             // Hide the display if the url box is empty
175             if(!url){
176                 automatchToolDisplay.setStyle('display', 'none');
177             } else {
178                 automatchToolDisplay.set('innerHTML', '');
179                 automatchToolDisplay.setStyle('display', '');
180             }
182             var selectedToolType = parseInt(typeSelector.get('value'));
183             var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
185             // A specific tool type is selected (not "auto")
186             // We still need to check with the server to get privacy settings
187             if(selectedToolType > 0){
188                 // If the entered domain matches the domain of the tool configuration...
189                 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
190                 var match = domainRegex.exec(url);
191                 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
192                     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'));
193                 } else {
194                     // The entered URL does not match the domain of the tool configuration
195                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('domain_mismatch', 'lti'));
196                 }
197             }
199             var key = Y.one('#id_resourcekey');
200             var secret = Y.one('#id_password');
202             // Indicate the tool is manually configured
203             // We still check the Launch URL with the server as course/site tools may override privacy settings
204             if(key.get('value') !== '' && secret.get('value') !== ''){
205                 automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('custom_config', 'lti'));
206             }
208             var continuation = function(toolInfo, inputfield){
209                 if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) {
210                     self.updatePrivacySettings(toolInfo);
211                 }
212                 if(toolInfo.toolname){
213                     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);
214                 } else if(!selectedToolType) {
215                     // Inform them custom configuration is in use
216                     if(key.get('value') === '' || secret.get('value') === ''){
217                         automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('tool_config_not_found', 'lti'));
218                     }
219                 }
220                 if (toolInfo.cartridge) {
221                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url +
222                                              '" />' + M.util.get_string('using_tool_cartridge', 'lti'));
223                 }
224             };
226             // Cache urls which have already been checked to increase performance
227             // Don't use URL cache if tool type manually selected
228             if(selectedToolType && self.toolTypeCache[selectedToolType]){
229                 return continuation(self.toolTypeCache[selectedToolType]);
230             } else if(self.urlCache[url] && !selectedToolType){
231                 return continuation(self.urlCache[url]);
232             } else if(!selectedToolType && !url) {
233                 // No tool type or url set
234                 return continuation({}, field);
235             } else {
236                 self.findToolByUrl(url, selectedToolType, function(toolInfo){
237                     if(toolInfo){
238                         // Cache the result based on whether the URL or tool type was used to look up the tool
239                         if(!selectedToolType){
240                             self.urlCache[url] = toolInfo;
241                         } else {
242                             self.toolTypeCache[selectedToolType] = toolInfo;
243                         }
245                         Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
247                         continuation(toolInfo);
248                     }
249                 });
250             }
251         },
253         /**
254          * Updates display of privacy settings to show course / site tool configuration settings.
255          */
256         updatePrivacySettings: function(toolInfo){
257             if(!toolInfo || !toolInfo.toolid){
258                 toolInfo = {
259                     sendname: M.mod_lti.LTI_SETTING_DELEGATE,
260                     sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
261                     acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
262                 }
263             }
265             var setting, control;
267             var privacyControls = {
268                 sendname: Y.one('#id_instructorchoicesendname'),
269                 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
270                 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
271             };
273             // Store a copy of user entered privacy settings as we may overwrite them
274             if(!this.userPrivacySettings){
275                 this.userPrivacySettings = {};
276             }
278             for(setting in privacyControls){
279                 if(privacyControls.hasOwnProperty(setting)){
280                     control = privacyControls[setting];
282                     // Only store the value if it hasn't been forced by the editor
283                     if(!control.get('disabled')){
284                         this.userPrivacySettings[setting] = control.get('checked');
285                     }
286                 }
287             }
289             // Update UI based on course / site tool configuration
290             for(setting in privacyControls){
291                 if(privacyControls.hasOwnProperty(setting)){
292                     var settingValue = toolInfo[setting];
293                     control = privacyControls[setting];
295                     if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
296                         control.set('disabled', true);
297                         control.set('checked', false);
298                         control.set('title', M.util.get_string('forced_help', 'lti'));
299                     } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
300                         control.set('disabled', true);
301                         control.set('checked', true);
302                         control.set('title', M.util.get_string('forced_help', 'lti'));
303                     } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
304                         control.set('disabled', false);
306                         // Get the value out of the stored copy
307                         control.set('checked', this.userPrivacySettings[setting]);
308                         control.set('title', '');
309                     }
310                 }
311             }
313             this.toggleGradeSection();
314         },
316         getSelectedToolTypeOption: function(){
317             var typeSelector = Y.one('#id_typeid');
319             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
320         },
322         /**
323          * Separate tool listing into option groups. Server-side select control
324          * doesn't seem to support this.
325          */
326         addOptGroups: function(){
327             var typeSelector = Y.one('#id_typeid');
329             if(typeSelector.one('option[courseTool=1]')){
330                 // One ore more course tools exist
332                 var globalGroup = Y.Node.create('<optgroup />')
333                                     .set('id', 'global_tool_group')
334                                     .set('label', M.util.get_string('global_tool_types', 'lti'));
336                 var courseGroup = Y.Node.create('<optgroup />')
337                                     .set('id', 'course_tool_group')
338                                     .set('label', M.util.get_string('course_tool_types', 'lti'));
340                 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
341                     globalGroup.append(node);
342                 });
344                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
345                     courseGroup.append(node);
346                 });
348                 if(globalOptions.size() > 0){
349                     typeSelector.append(globalGroup);
350                 }
352                 if(courseOptions.size() > 0){
353                     typeSelector.append(courseGroup);
354                 }
355             }
356         },
358         /**
359          * Adds buttons for creating, editing, and deleting tool types.
360          * Javascript is a requirement to edit course level tools at this point.
361          */
362         createTypeEditorButtons: function(){
363             var self = this;
365             var typeSelector = Y.one('#id_typeid');
367             var createIcon = function(id, tooltip, iconUrl){
368                 return Y.Node.create('<a />')
369                         .set('id', id)
370                         .set('title', tooltip)
371                         .setStyle('margin-left', '.5em')
372                         .set('href', 'javascript:void(0);')
373                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
374             }
376             var addIcon = createIcon('lti_add_tool_type', M.util.get_string('addtype', 'lti'), this.settings.add_icon_url);
377             var editIcon = createIcon('lti_edit_tool_type', M.util.get_string('edittype', 'lti'), this.settings.edit_icon_url);
378             var deleteIcon  = createIcon('lti_delete_tool_type', M.util.get_string('deletetype', 'lti'), this.settings.delete_icon_url);
380             editIcon.on('click', function(e){
381                 var toolTypeId = typeSelector.get('value');
383                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
384                     window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
385                 } else {
386                     alert(M.util.get_string('cannot_edit', 'lti'));
387                 }
388             });
390             addIcon.on('click', function(e){
391                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
392             });
394             deleteIcon.on('click', function(e){
395                 var toolTypeId = typeSelector.get('value');
397                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
398                     if(confirm(M.util.get_string('delete_confirmation', 'lti'))){
399                         self.deleteTool(toolTypeId);
400                     }
401                 } else {
402                     alert(M.util.get_string('cannot_delete', 'lti'));
403                 }
404             });
406             typeSelector.insert(addIcon, 'after');
407             addIcon.insert(editIcon, 'after');
408             editIcon.insert(deleteIcon, 'after');
409         },
411         toggleEditButtons: function(){
412             var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
413             var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
415             // Make the edit / delete icons look enabled / disabled.
416             // Does not work in older browsers, but alerts will catch those cases.
417             if(this.getSelectedToolTypeOption().getAttribute('editable')){
418                 lti_edit_tool_type.setStyle('opacity', '1');
419                 lti_delete_tool_type.setStyle('opacity', '1');
420             } else {
421                 lti_edit_tool_type.setStyle('opacity', '.2');
422                 lti_delete_tool_type.setStyle('opacity', '.2');
423             }
424         },
426         addToolType: function(toolType){
427             var typeSelector = Y.one('#id_typeid');
428             var course_tool_group = Y.one('#course_tool_group');
430             var option = Y.Node.create('<option />')
431                             .set('text', toolType.name)
432                             .set('value', toolType.id)
433                             .set('selected', 'selected')
434                             .setAttribute('editable', '1')
435                             .setAttribute('courseTool', '1')
436                             .setAttribute('domain', toolType.tooldomain);
438             if(course_tool_group){
439                 course_tool_group.append(option);
440             } else {
441                 typeSelector.append(option);
442             }
444             // Adding the new tool may affect which tool gets matched automatically
445             this.clearToolCache();
446             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
447             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
448             this.toggleEditButtons();
450             require(["core/notification"], function (notification) {
451                 notification.addNotification({
452                     message: M.util.get_string('tooltypeadded', 'lti'),
453                     type: "success"
454                 });
455             });
456         },
458         updateToolType: function(toolType){
459             var typeSelector = Y.one('#id_typeid');
461             var option = typeSelector.one('option[value="' + toolType.id + '"]');
462             option.set('text', toolType.name)
463                   .set('domain', toolType.tooldomain);
465             // Editing the tool may affect which tool gets matched automatically
466             this.clearToolCache();
467             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
468             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
470             require(["core/notification"], function (notification) {
471                 notification.addNotification({
472                     message: M.util.get_string('tooltypeupdated', 'lti'),
473                     type: "success"
474                 });
475             });
476         },
478         deleteTool: function(toolTypeId){
479             var self = this;
481             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
482                 on: {
483                     success: function(){
484                         self.getSelectedToolTypeOption().remove();
486                         // Editing the tool may affect which tool gets matched automatically
487                         self.clearToolCache();
488                         self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
489                         self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
491                         require(["core/notification"], function (notification) {
492                             notification.addNotification({
493                                 message: M.util.get_string('tooltypedeleted', 'lti'),
494                                 type: "success"
495                             });
496                         });
497                     },
498                     failure: function(){
499                         require(["core/notification"], function (notification) {
500                             notification.addNotification({
501                                 message: M.util.get_string('tooltypenotdeleted', 'lti'),
502                                 type: "problem"
503                             });
504                         });
505                     }
506                 }
507             });
508         },
510         findToolByUrl: function(url, toolId, callback){
511             var self = this;
513             Y.io(self.settings.ajax_url, {
514                 data: {action: 'find_tool_config',
515                         course: self.settings.courseId,
516                         toolurl: url,
517                         toolid: toolId || 0
518                 },
520                 on: {
521                     success: function(transactionid, xhr){
522                         var response = xhr.response;
524                         var toolInfo = Y.JSON.parse(response);
526                         callback(toolInfo);
527                     },
528                     failure: function(){
530                     }
531                 }
532             });
533         },
535         /**
536          * Gets the tool type ID of the selected tool that supports Content-Item selection.
537          *
538          * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise.
539          */
540         getContentItemId: function() {
541             try {
542                 var selected = this.getSelectedToolTypeOption();
543                 if (selected.getAttribute('data-contentitem')) {
544                     return selected.getAttribute('data-id');
545                 }
546                 return false;
547             } catch (err) {
548                 // Tool selector not available - check for hidden fields instead.
549                 var content = Y.one('input[name="contentitem"]');
550                 if (!content || !content.get('value')) {
551                     return false;
552                 }
553                 return Y.one('input[name="typeid"]').get('value');
554             }
555         },
557         /**
558          * Resets the values of fields related to the LTI tool settings.
559          */
560         resetToolFields: function() {
561             // Reset values for all text fields.
562             var fields = Y.all('#id_toolurl, #id_securetoolurl, #id_instructorcustomparameters, #id_icon, #id_secureicon');
563             fields.set('value', null);
565             // Reset value for launch container select box.
566             Y.one('#id_launchcontainer').set('value', 1);
567         }
568     };
569 })();