117dbf4c2abed57ef762a833e45b723149437e0e
[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             // Can't look these up by ID as they seem to get random IDs.
194             // Setting an id manually from mod_form made them turn into text boxes.
195             var privacyControls = {
196                 sendname: Y.one('input[name=instructorchoicesendname]'),
197                 sendemailaddr: Y.one('input[name=instructorchoicesendemailaddr]'),
198                 acceptgrades: Y.one('input[name=instructorchoiceacceptgrades]')
199             };
201             // Store a copy of user entered privacy settings as we may overwrite them
202             if(!this.userPrivacySettings){
203                 this.userPrivacySettings = {};
204             }
206             for(setting in privacyControls){
207                 if(privacyControls.hasOwnProperty(setting)){
208                     control = privacyControls[setting];
210                     // Only store the value if it hasn't been forced by the editor
211                     if(!control.get('disabled')){
212                         this.userPrivacySettings[setting] = control.get('checked');
213                     }
214                 }
215             }
217             // Update UI based on course / site tool configuration
218             for(setting in privacyControls){
219                 if(privacyControls.hasOwnProperty(setting)){
220                     var settingValue = toolInfo[setting];
221                     control = privacyControls[setting];
223                     if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
224                         control.set('disabled', true);
225                         control.set('checked', false);
226                         control.set('title', M.str.lti.forced_help);
227                     } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
228                         control.set('disabled', true);
229                         control.set('checked', true);
230                         control.set('title', M.str.lti.forced_help);
231                     } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
232                         control.set('disabled', false);
234                         // Get the value out of the stored copy
235                         control.set('checked', this.userPrivacySettings[setting]);
236                         control.set('title', '');
237                     }
238                 }
239             }
240         },
242         getSelectedToolTypeOption: function(){
243             var typeSelector = Y.one('#id_typeid');
245             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
246         },
248         /**
249          * Separate tool listing into option groups. Server-side select control
250          * doesn't seem to support this.
251          */
252         addOptGroups: function(){
253             var typeSelector = Y.one('#id_typeid');
255             if(typeSelector.one('option[courseTool=1]')){
256                 // One ore more course tools exist
258                 var globalGroup = Y.Node.create('<optgroup />')
259                                     .set('id', 'global_tool_group')
260                                     .set('label', M.str.lti.global_tool_types);
262                 var courseGroup = Y.Node.create('<optgroup />')
263                                     .set('id', 'course_tool_group')
264                                     .set('label', M.str.lti.course_tool_types);
266                 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
267                     globalGroup.append(node);
268                 });
270                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
271                     courseGroup.append(node);
272                 });
274                 if(globalOptions.size() > 0){
275                     typeSelector.append(globalGroup);
276                 }
278                 if(courseOptions.size() > 0){
279                     typeSelector.append(courseGroup);
280                 }
281             }
282         },
284         /**
285          * Adds buttons for creating, editing, and deleting tool types.
286          * Javascript is a requirement to edit course level tools at this point.
287          */
288         createTypeEditorButtons: function(){
289             var self = this;
291             var typeSelector = Y.one('#id_typeid');
293             var createIcon = function(id, tooltip, iconUrl){
294                 return Y.Node.create('<a />')
295                         .set('id', id)
296                         .set('title', tooltip)
297                         .setStyle('margin-left', '.5em')
298                         .set('href', 'javascript:void(0);')
299                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
300             }
302             var addIcon = createIcon('lti_add_tool_type', M.str.lti.addtype, this.settings.add_icon_url);
303             var editIcon = createIcon('lti_edit_tool_type', M.str.lti.edittype, this.settings.edit_icon_url);
304             var deleteIcon  = createIcon('lti_delete_tool_type', M.str.lti.deletetype, this.settings.delete_icon_url);
306             editIcon.on('click', function(e){
307                 var toolTypeId = typeSelector.get('value');
309                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
310                     window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
311                 } else {
312                     alert(M.str.lti.cannot_edit);
313                 }
314             });
316             addIcon.on('click', function(e){
317                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
318             });
320             deleteIcon.on('click', function(e){
321                 var toolTypeId = typeSelector.get('value');
323                 if(self.getSelectedToolTypeOption().getAttribute('editable')){
324                     if(confirm(M.str.lti.delete_confirmation)){
325                         self.deleteTool(toolTypeId);
326                     }
327                 } else {
328                     alert(M.str.lti.cannot_delete);
329                 }
330             });
332             typeSelector.insert(addIcon, 'after');
333             addIcon.insert(editIcon, 'after');
334             editIcon.insert(deleteIcon, 'after');
335         },
337         toggleEditButtons: function(){
338             var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
339             var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
341             // Make the edit / delete icons look enabled / disabled.
342             // Does not work in older browsers, but alerts will catch those cases.
343             if(this.getSelectedToolTypeOption().getAttribute('editable')){
344                 lti_edit_tool_type.setStyle('opacity', '1');
345                 lti_delete_tool_type.setStyle('opacity', '1');
346             } else {
347                 lti_edit_tool_type.setStyle('opacity', '.2');
348                 lti_delete_tool_type.setStyle('opacity', '.2');
349             }
350         },
352         addToolType: function(toolType){
353             var typeSelector = Y.one('#id_typeid');
354             var course_tool_group = Y.one('#course_tool_group');
356             var option = Y.Node.create('<option />')
357                             .set('text', toolType.name)
358                             .set('value', toolType.id)
359                             .set('selected', 'selected')
360                             .setAttribute('editable', '1')
361                             .setAttribute('courseTool', '1')
362                             .setAttribute('domain', toolType.tooldomain);
364             if(course_tool_group){
365                 course_tool_group.append(option);
366             } else {
367                 typeSelector.append(option);
368             }
370             // Adding the new tool may affect which tool gets matched automatically
371             this.clearToolCache();
372             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
373             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
374         },
376         updateToolType: function(toolType){
377             var typeSelector = Y.one('#id_typeid');
379             var option = typeSelector.one('option[value="' + toolType.id + '"]');
380             option.set('text', toolType.name)
381                   .set('domain', toolType.tooldomain);
383             // Editing the tool may affect which tool gets matched automatically
384             this.clearToolCache();
385             this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
386             this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
387         },
389         deleteTool: function(toolTypeId){
390             var self = this;
392             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
393                 on: {
394                     success: function(){
395                         self.getSelectedToolTypeOption().remove();
397                         // Editing the tool may affect which tool gets matched automatically
398                         self.clearToolCache();
399                         self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
400                         self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
401                     },
402                     failure: function(){
404                     }
405                 }
406             });
407         },
409         findToolByUrl: function(url, toolId, callback){
410             var self = this;
412             Y.io(self.settings.ajax_url, {
413                 data: {action: 'find_tool_config',
414                         course: self.settings.courseId,
415                         toolurl: url,
416                         toolid: toolId || 0
417                 },
419                 on: {
420                     success: function(transactionid, xhr){
421                         var response = xhr.response;
423                         var toolInfo = Y.JSON.parse(response);
425                         callback(toolInfo);
426                     },
427                     failure: function(){
429                     }
430                 }
431             });
432         }
434     };
435 })();