1 // This file is part of Moodle - http://moodle.org/
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.
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/>.
17 * Javascript extensions for the External Tool activity editor.
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
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;
34 init: function(yui3, settings){
40 this.settings = Y.JSON.parse(settings);
43 this.toolTypeCache = {};
47 var updateToolMatches = function(){
48 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
49 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
52 var typeSelector = Y.one('#id_typeid');
53 typeSelector.on('change', function(e){
56 self.toggleEditButtons();
59 this.createTypeEditorButtons();
61 this.toggleEditButtons();
63 var textAreas = new Y.NodeList([
65 Y.one('#id_securetoolurl'),
66 Y.one('#id_resourcekey'),
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(){
83 clearToolCache: function(){
85 this.toolTypeCache = {};
88 updateAutomaticToolMatch: function(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 />')
100 .setStyle('padding-left', '1em');
102 toolurl.insert(automatchToolDisplay, 'after');
105 var url = toolurl.get('value');
107 //Hide the display if the url box is empty
109 automatchToolDisplay.setStyle('display', 'none');
111 automatchToolDisplay.set('innerHTML', '');
112 automatchToolDisplay.setStyle('display', '');
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'));
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);
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);
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);
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({});
164 self.findToolByUrl(url, selectedToolType, function(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;
170 self.toolTypeCache[selectedToolType] = toolInfo;
173 continuation(toolInfo);
180 * Updates display of privacy settings to show course / site tool configuration settings.
182 updatePrivacySettings: function(toolInfo){
183 if(!toolInfo || !toolInfo.toolid){
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
192 var setting, control;
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]')
203 //Store a copy of user entered privacy settings as we may overwrite them
204 if(!this.userPrivacySettings){
205 this.userPrivacySettings = {};
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');
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];
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);
236 //Get the value out of the stored copy
237 control.set('checked', this.userPrivacySettings[setting]);
238 control.set('title', '');
244 getSelectedToolTypeOption: function(){
245 var typeSelector = Y.one('#id_typeid');
247 return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
251 * Separate tool listing into option groups. Server-side select control
252 * doesn't seem to support this.
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);
272 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
273 courseGroup.append(node);
276 if(globalOptions.size() > 0){
277 typeSelector.append(globalGroup);
280 if(courseOptions.size() > 0){
281 typeSelector.append(courseGroup);
287 * Adds buttons for creating, editing, and deleting tool types.
288 * Javascript is a requirement to edit course level tools at this point.
290 createTypeEditorButtons: function(){
293 var typeSelector = Y.one('#id_typeid');
295 var createIcon = function(id, tooltip, iconUrl){
296 return Y.Node.create('<a />')
298 .set('title', tooltip)
299 .setStyle('margin-left', '.5em')
300 .set('href', 'javascript:void(0);')
301 .append(Y.Node.create('<img src="' + iconUrl + '" />'));
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');
314 alert(M.str.lti.cannot_edit);
318 addIcon.on('click', function(e){
319 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
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);
330 alert(M.str.lti.cannot_delete);
334 typeSelector.insert(addIcon, 'after');
335 addIcon.insert(editIcon, 'after');
336 editIcon.insert(deleteIcon, 'after');
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');
349 lti_edit_tool_type.setStyle('opacity', '.2');
350 lti_delete_tool_type.setStyle('opacity', '.2');
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);
369 typeSelector.append(option);
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'));
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'));
391 deleteTool: function(toolTypeId){
394 Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
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'));
411 findToolByUrl: function(url, toolId, callback){
414 Y.io(self.settings.ajax_url, {
415 data: {action: 'find_tool_config',
416 course: self.settings.courseId,
422 success: function(transactionid, xhr){
423 var response = xhr.response;
425 var toolInfo = Y.JSON.parse(response);