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