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 Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
175 continuation(toolInfo);
182 * Updates display of privacy settings to show course / site tool configuration settings.
184 updatePrivacySettings: function(toolInfo){
185 if(!toolInfo || !toolInfo.toolid){
187 sendname: M.mod_lti.LTI_SETTING_DELEGATE,
188 sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
189 acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
193 var setting, control;
195 var privacyControls = {
196 sendname: Y.one('#id_instructorchoicesendname'),
197 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
198 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
201 // Store a copy of user entered privacy settings as we may overwrite them
202 if(!this.userPrivacySettings){
203 this.userPrivacySettings = {};
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');
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', '');
242 getSelectedToolTypeOption: function(){
243 var typeSelector = Y.one('#id_typeid');
245 return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
249 * Separate tool listing into option groups. Server-side select control
250 * doesn't seem to support this.
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);
270 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
271 courseGroup.append(node);
274 if(globalOptions.size() > 0){
275 typeSelector.append(globalGroup);
278 if(courseOptions.size() > 0){
279 typeSelector.append(courseGroup);
285 * Adds buttons for creating, editing, and deleting tool types.
286 * Javascript is a requirement to edit course level tools at this point.
288 createTypeEditorButtons: function(){
291 var typeSelector = Y.one('#id_typeid');
293 var createIcon = function(id, tooltip, iconUrl){
294 return Y.Node.create('<a />')
296 .set('title', tooltip)
297 .setStyle('margin-left', '.5em')
298 .set('href', 'javascript:void(0);')
299 .append(Y.Node.create('<img src="' + iconUrl + '" />'));
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');
312 alert(M.str.lti.cannot_edit);
316 addIcon.on('click', function(e){
317 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
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);
328 alert(M.str.lti.cannot_delete);
332 typeSelector.insert(addIcon, 'after');
333 addIcon.insert(editIcon, 'after');
334 editIcon.insert(deleteIcon, 'after');
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');
347 lti_edit_tool_type.setStyle('opacity', '.2');
348 lti_delete_tool_type.setStyle('opacity', '.2');
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);
367 typeSelector.append(option);
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'));
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'));
389 deleteTool: function(toolTypeId){
392 Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
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'));
409 findToolByUrl: function(url, toolId, callback){
412 Y.io(self.settings.ajax_url, {
413 data: {action: 'find_tool_config',
414 course: self.settings.courseId,
420 success: function(transactionid, xhr){
421 var response = xhr.response;
423 var toolInfo = Y.JSON.parse(response);