MDL-40308: Fix javascript for lti mod_form
[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             this.addOptGroups();
47             var updateToolMatches = function(){
48                 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
49                 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
50             };
52             var typeSelector = Y.one('#id_typeid');
53             typeSelector.on('change', function(e){
54                 updateToolMatches();
56                 self.toggleEditButtons();
57             });
59             this.createTypeEditorButtons();
61             this.toggleEditButtons();
63             var textAreas = new Y.NodeList([
64                 Y.one('#id_toolurl'),
65                 Y.one('#id_securetoolurl'),
66                 Y.one('#id_resourcekey'),
67                 Y.one('#id_password')
68             ]);
70             var debounce;
71             textAreas.on('keyup', function(e){
72                 clearTimeout(debounce);
74                 // If no more changes within 2 seconds, look up the matching tool URL
75                 debounce = setTimeout(function(){
76                     updateToolMatches();
77                 }, 2000);
78             });
80             updateToolMatches();
81         },
83         clearToolCache: function(){
84             this.urlCache = {};
85             this.toolTypeCache = {};
86         },
88         updateAutomaticToolMatch: function(field){
89             var self = this;
91             var toolurl = field;
92             var typeSelector = Y.one('#id_typeid');
94             var id = field.get('id') + '_lti_automatch_tool';
95             var automatchToolDisplay = Y.one('#' + id);
97             if(!automatchToolDisplay){
98                 automatchToolDisplay = Y.Node.create('<span />')
99                                         .set('id', id)
100                                         .setStyle('padding-left', '1em');
102                 toolurl.insert(automatchToolDisplay, 'after');
103             }
105             var url = toolurl.get('value');
107             // Hide the display if the url box is empty
108             if(!url){
109                 automatchToolDisplay.setStyle('display', 'none');
110             } else {
111                 automatchToolDisplay.set('innerHTML', '');
112                 automatchToolDisplay.setStyle('display', '');
113             }
115             var selectedToolType = parseInt(typeSelector.get('value'));
116             var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
118             // A specific tool type is selected (not "auto")
119             // We still need to check with the server to get privacy settings
120             if(selectedToolType > 0){
121                 // If the entered domain matches the domain of the tool configuration...
122                 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
123                 var match = domainRegex.exec(url);
124                 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
125                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.using_tool_configuration + selectedOption.get('text'));
126                 } else {
127                     // The entered URL does not match the domain of the tool configuration
128                     automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.str.lti.domain_mismatch);
129                 }
130             }
132             var key = Y.one('#id_resourcekey');
133             var secret = Y.one('#id_password');
135             // Indicate the tool is manually configured
136             // We still check the Launch URL with the server as course/site tools may override privacy settings
137             if(key.get('value') !== '' && secret.get('value') !== ''){
138                 automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.custom_config);
139             }
141             var continuation = function(toolInfo){
142                 self.updatePrivacySettings(toolInfo);
144                 if(toolInfo.toolname){
145                     automatchToolDisplay.set('innerHTML',  '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.str.lti.using_tool_configuration + toolInfo.toolname);
146                 } else if(!selectedToolType) {
147                     // Inform them custom configuration is in use
148                     if(key.get('value') === '' || secret.get('value') === ''){
149                         automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.str.lti.tool_config_not_found);
150                     }
151                 }
152             };
154             // Cache urls which have already been checked to increase performance
155             // Don't use URL cache if tool type manually selected
156             if(selectedToolType && self.toolTypeCache[selectedToolType]){
157                 return continuation(self.toolTypeCache[selectedToolType]);
158             } else if(self.urlCache[url] && !selectedToolType){
159                 return continuation(self.urlCache[url]);
160             } else if(!selectedToolType && !url) {
161                 // No tool type or url set
162                 return continuation({});
163             } else {
164                 self.findToolByUrl(url, selectedToolType, function(toolInfo){
165                     if(toolInfo){
166                         // Cache the result based on whether the URL or tool type was used to look up the tool
167                         if(!selectedToolType){
168                             self.urlCache[url] = toolInfo;
169                         } else {
170                             self.toolTypeCache[selectedToolType] = toolInfo;
171                         }
173                         continuation(toolInfo);
174                     }
175                 });
176             }
177         },
179         /**
180          * Updates display of privacy settings to show course / site tool configuration settings.
181          */
182         updatePrivacySettings: function(toolInfo){
183             if(!toolInfo || !toolInfo.toolid){
184                 toolInfo = {
185                     sendname: M.mod_lti.LTI_SETTING_DELEGATE,
186                     sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
187                     acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
188                 }
189             }
191             var setting, control;
193             var privacyControls = {
194                 sendname: Y.one('#id_instructorchoicesendname'),
195                 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
196                 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
197             };
199             // Store a copy of user entered privacy settings as we may overwrite them
200             if(!this.userPrivacySettings){
201                 this.userPrivacySettings = {};
202             }
204             for(setting in privacyControls){
205                 if(privacyControls.hasOwnProperty(setting)){
206                     control = privacyControls[setting];
208                     // Only store the value if it hasn't been forced by the editor
209                     if(!control.get('disabled')){
210                         this.userPrivacySettings[setting] = control.get('checked');
211                     }
212                 }
213             }
215             // Update UI based on course / site tool configuration
216             for(setting in privacyControls){
217                 if(privacyControls.hasOwnProperty(setting)){
218                     var settingValue = toolInfo[setting];
219                     control = privacyControls[setting];
221                     if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
222                         control.set('disabled', true);
223                         control.set('checked', false);
224                         control.set('title', M.str.lti.forced_help);
225                     } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
226                         control.set('disabled', true);
227                         control.set('checked', true);
228                         control.set('title', M.str.lti.forced_help);
229                     } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
230                         control.set('disabled', false);
232                         // Get the value out of the stored copy
233                         control.set('checked', this.userPrivacySettings[setting]);
234                         control.set('title', '');
235                     }
236                 }
237             }
238         },
240         getSelectedToolTypeOption: function(){
241             var typeSelector = Y.one('#id_typeid');
243             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
244         },
246         /**
247          * Separate tool listing into option groups. Server-side select control
248          * doesn't seem to support this.
249          */
250         addOptGroups: function(){
251             var typeSelector = Y.one('#id_typeid');
253             if(typeSelector.one('option[courseTool=1]')){
254                 // One ore more course tools exist
256                 var globalGroup = Y.Node.create('<optgroup />')
257                                     .set('id', 'global_tool_group')
258                                     .set('label', M.str.lti.global_tool_types);
260                 var courseGroup = Y.Node.create('<optgroup />')
261                                     .set('id', 'course_tool_group')
262                                     .set('label', M.str.lti.course_tool_types);
264                 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
265                     globalGroup.append(node);
266                 });
268                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
269                     courseGroup.append(node);
270                 });
272                 if(globalOptions.size() > 0){
273                     typeSelector.append(globalGroup);
274                 }
276                 if(courseOptions.size() > 0){
277                     typeSelector.append(courseGroup);
278                 }
279             }
280         },
282         /**
283          * Adds buttons for creating, editing, and deleting tool types.
284          * Javascript is a requirement to edit course level tools at this point.
285          */
286         createTypeEditorButtons: function(){
287             var self = this;
289             var typeSelector = Y.one('#id_typeid');
291             var createIcon = function(id, tooltip, iconUrl){
292                 return Y.Node.create('<a />')
293                         .set('id', id)
294                         .set('title', tooltip)
295                         .setStyle('margin-left', '.5em')
296                         .set('href', 'javascript:void(0);')
297                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
298             }
300             var addIcon = createIcon('lti_add_tool_type', M.str.lti.addtype, this.settings.add_icon_url);
301             var editIcon = createIcon('lti_edit_tool_type', M.str.lti.edittype, this.settings.edit_icon_url);
302             var deleteIcon  = createIcon('lti_delete_tool_type', M.str.lti.deletetype, this.settings.delete_icon_url);
304             editIcon.on('click', function(e){
305                 var toolTypeId = typeSelector.get('value');
307                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
308                     window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
309                 } else {
310                     alert(M.str.lti.cannot_edit);
311                 }
312             });
314             addIcon.on('click', function(e){
315                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
316             });
318             deleteIcon.on('click', function(e){
319                 var toolTypeId = typeSelector.get('value');
321                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
322                     if(confirm(M.str.lti.delete_confirmation)){
323                         self.deleteTool(toolTypeId);
324                     }
325                 } else {
326                     alert(M.str.lti.cannot_delete);
327                 }
328             });
330             typeSelector.insert(addIcon, 'after');
331             addIcon.insert(editIcon, 'after');
332             editIcon.insert(deleteIcon, 'after');
333         },
335         toggleEditButtons: function(){
336             var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
337             var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
339             // Make the edit / delete icons look enabled / disabled.
340             // Does not work in older browsers, but alerts will catch those cases.
341             if(this.getSelectedToolTypeOption().getAttribute('editable')){
342                 lti_edit_tool_type.setStyle('opacity', '1');
343                 lti_delete_tool_type.setStyle('opacity', '1');
344             } else {
345                 lti_edit_tool_type.setStyle('opacity', '.2');
346                 lti_delete_tool_type.setStyle('opacity', '.2');
347             }
348         },
350         addToolType: function(toolType){
351             var typeSelector = Y.one('#id_typeid');
352             var course_tool_group = Y.one('#course_tool_group');
354             var option = Y.Node.create('<option />')
355                             .set('text', toolType.name)
356                             .set('value', toolType.id)
357                             .set('selected', 'selected')
358                             .setAttribute('editable', '1')
359                             .setAttribute('courseTool', '1')
360                             .setAttribute('domain', toolType.tooldomain);
362             if(course_tool_group){
363                 course_tool_group.append(option);
364             } else {
365                 typeSelector.append(option);
366             }
368             // Adding the new tool may affect which tool gets matched automatically
369             this.clearToolCache();
370             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
371             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
372         },
374         updateToolType: function(toolType){
375             var typeSelector = Y.one('#id_typeid');
377             var option = typeSelector.one('option[value="' + toolType.id + '"]');
378             option.set('text', toolType.name)
379                   .set('domain', toolType.tooldomain);
381             // Editing the tool may affect which tool gets matched automatically
382             this.clearToolCache();
383             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
384             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
385         },
387         deleteTool: function(toolTypeId){
388             var self = this;
390             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
391                 on: {
392                     success: function(){
393                         self.getSelectedToolTypeOption().remove();
395                         // Editing the tool may affect which tool gets matched automatically
396                         self.clearToolCache();
397                         self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
398                         self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
399                     },
400                     failure: function(){
402                     }
403                 }
404             });
405         },
407         findToolByUrl: function(url, toolId, callback){
408             var self = this;
410             Y.io(self.settings.ajax_url, {
411                 data: {action: 'find_tool_config',
412                         course: self.settings.courseId,
413                         toolurl: url,
414                         toolid: toolId || 0
415                 },
417                 on: {
418                     success: function(transactionid, xhr){
419                         var response = xhr.response;
421                         var toolInfo = Y.JSON.parse(response);
423                         callback(toolInfo);
424                     },
425                     failure: function(){
427                     }
428                 }
429             });
430         }
432     };
433 })();