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){
54 // Reset configuration fields when another preconfigured tool is selected.
55 self.resetToolFields();
59 self.toggleEditButtons();
61 if (self.getSelectedToolTypeOption().getAttribute('toolproxy')){
62 var allowname = Y.one('#id_instructorchoicesendname');
63 allowname.set('checked', !self.getSelectedToolTypeOption().getAttribute('noname'));
65 var allowemail = Y.one('#id_instructorchoicesendemailaddr');
66 allowemail.set('checked', !self.getSelectedToolTypeOption().getAttribute('noemail'));
68 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
69 allowgrades.set('checked', !self.getSelectedToolTypeOption().getAttribute('nogrades'));
70 self.toggleGradeSection();
74 var contentItemButton = Y.one('[name="selectcontent"]');
75 var contentItemUrl = contentItemButton.getAttribute('data-contentitemurl');
76 // Handle configure from link button click.
77 contentItemButton.on('click', function() {
78 var contentItemId = self.getContentItemId();
80 // Get activity name and description values.
81 var title = Y.one('#id_name').get('value').trim();
82 var text = Y.one('#id_introeditor').get('value').trim();
84 // Set data to be POSTed.
87 course: self.settings.courseId,
92 require(['mod_lti/contentitem'], function(contentitem) {
93 contentitem.init(contentItemUrl, postData, function() {
94 M.mod_lti.editor.toggleGradeSection();
100 this.createTypeEditorButtons();
102 this.toggleEditButtons();
104 var textAreas = new Y.NodeList([
105 Y.one('#id_toolurl'),
106 Y.one('#id_securetoolurl'),
107 Y.one('#id_resourcekey'),
108 Y.one('#id_password')
112 textAreas.on('keyup', function(e){
113 clearTimeout(debounce);
115 // If no more changes within 2 seconds, look up the matching tool URL
116 debounce = setTimeout(function(){
121 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
122 allowgrades.on('change', this.toggleGradeSection, this);
127 toggleGradeSection: function(e) {
131 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
132 var gradefieldset = Y.one('#id_modstandardgrade');
133 if (!allowgrades.get('checked')) {
134 gradefieldset.hide();
136 gradefieldset.show();
140 clearToolCache: function(){
142 this.toolTypeCache = {};
145 updateAutomaticToolMatch: function(field){
149 var typeSelector = Y.one('#id_typeid');
151 var id = field.get('id') + '_lti_automatch_tool';
152 var automatchToolDisplay = Y.one('#' + id);
154 if(!automatchToolDisplay){
155 automatchToolDisplay = Y.Node.create('<span />')
157 .setStyle('padding-left', '1em');
159 toolurl.insert(automatchToolDisplay, 'after');
162 var url = toolurl.get('value');
164 // Hide the display if the url box is empty
166 automatchToolDisplay.setStyle('display', 'none');
168 automatchToolDisplay.set('innerHTML', '');
169 automatchToolDisplay.setStyle('display', '');
172 var selectedToolType = parseInt(typeSelector.get('value'));
173 var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
175 // A specific tool type is selected (not "auto")
176 // We still need to check with the server to get privacy settings
177 if(selectedToolType > 0){
178 // If the entered domain matches the domain of the tool configuration...
179 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
180 var match = domainRegex.exec(url);
181 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
182 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('using_tool_configuration', 'lti') + selectedOption.get('text'));
184 // The entered URL does not match the domain of the tool configuration
185 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('domain_mismatch', 'lti'));
189 var key = Y.one('#id_resourcekey');
190 var secret = Y.one('#id_password');
192 // Indicate the tool is manually configured
193 // We still check the Launch URL with the server as course/site tools may override privacy settings
194 if(key.get('value') !== '' && secret.get('value') !== ''){
195 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('custom_config', 'lti'));
198 var continuation = function(toolInfo, inputfield){
199 if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) {
200 self.updatePrivacySettings(toolInfo);
202 if(toolInfo.toolname){
203 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('using_tool_configuration', 'lti') + toolInfo.toolname);
204 } else if(!selectedToolType) {
205 // Inform them custom configuration is in use
206 if(key.get('value') === '' || secret.get('value') === ''){
207 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('tool_config_not_found', 'lti'));
210 if (toolInfo.cartridge) {
211 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url +
212 '" />' + M.util.get_string('using_tool_cartridge', 'lti'));
216 // Cache urls which have already been checked to increase performance
217 // Don't use URL cache if tool type manually selected
218 if(selectedToolType && self.toolTypeCache[selectedToolType]){
219 return continuation(self.toolTypeCache[selectedToolType]);
220 } else if(self.urlCache[url] && !selectedToolType){
221 return continuation(self.urlCache[url]);
222 } else if(!selectedToolType && !url) {
223 // No tool type or url set
224 return continuation({}, field);
226 self.findToolByUrl(url, selectedToolType, function(toolInfo){
228 // Cache the result based on whether the URL or tool type was used to look up the tool
229 if(!selectedToolType){
230 self.urlCache[url] = toolInfo;
232 self.toolTypeCache[selectedToolType] = toolInfo;
235 Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
237 continuation(toolInfo);
244 * Updates display of privacy settings to show course / site tool configuration settings.
246 updatePrivacySettings: function(toolInfo){
247 if(!toolInfo || !toolInfo.toolid){
249 sendname: M.mod_lti.LTI_SETTING_DELEGATE,
250 sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
251 acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
255 var setting, control;
257 var privacyControls = {
258 sendname: Y.one('#id_instructorchoicesendname'),
259 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
260 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
263 // Store a copy of user entered privacy settings as we may overwrite them
264 if(!this.userPrivacySettings){
265 this.userPrivacySettings = {};
268 for(setting in privacyControls){
269 if(privacyControls.hasOwnProperty(setting)){
270 control = privacyControls[setting];
272 // Only store the value if it hasn't been forced by the editor
273 if(!control.get('disabled')){
274 this.userPrivacySettings[setting] = control.get('checked');
279 // Update UI based on course / site tool configuration
280 for(setting in privacyControls){
281 if(privacyControls.hasOwnProperty(setting)){
282 var settingValue = toolInfo[setting];
283 control = privacyControls[setting];
285 if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
286 control.set('disabled', true);
287 control.set('checked', false);
288 control.set('title', M.util.get_string('forced_help', 'lti'));
289 } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
290 control.set('disabled', true);
291 control.set('checked', true);
292 control.set('title', M.util.get_string('forced_help', 'lti'));
293 } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
294 control.set('disabled', false);
296 // Get the value out of the stored copy
297 control.set('checked', this.userPrivacySettings[setting]);
298 control.set('title', '');
303 this.toggleGradeSection();
306 getSelectedToolTypeOption: function(){
307 var typeSelector = Y.one('#id_typeid');
309 return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
313 * Separate tool listing into option groups. Server-side select control
314 * doesn't seem to support this.
316 addOptGroups: function(){
317 var typeSelector = Y.one('#id_typeid');
319 if(typeSelector.one('option[courseTool=1]')){
320 // One ore more course tools exist
322 var globalGroup = Y.Node.create('<optgroup />')
323 .set('id', 'global_tool_group')
324 .set('label', M.util.get_string('global_tool_types', 'lti'));
326 var courseGroup = Y.Node.create('<optgroup />')
327 .set('id', 'course_tool_group')
328 .set('label', M.util.get_string('course_tool_types', 'lti'));
330 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
331 globalGroup.append(node);
334 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
335 courseGroup.append(node);
338 if(globalOptions.size() > 0){
339 typeSelector.append(globalGroup);
342 if(courseOptions.size() > 0){
343 typeSelector.append(courseGroup);
349 * Adds buttons for creating, editing, and deleting tool types.
350 * Javascript is a requirement to edit course level tools at this point.
352 createTypeEditorButtons: function(){
355 var typeSelector = Y.one('#id_typeid');
357 var createIcon = function(id, tooltip, iconUrl){
358 return Y.Node.create('<a />')
360 .set('title', tooltip)
361 .setStyle('margin-left', '.5em')
362 .set('href', 'javascript:void(0);')
363 .append(Y.Node.create('<img src="' + iconUrl + '" />'));
366 var addIcon = createIcon('lti_add_tool_type', M.util.get_string('addtype', 'lti'), this.settings.add_icon_url);
367 var editIcon = createIcon('lti_edit_tool_type', M.util.get_string('edittype', 'lti'), this.settings.edit_icon_url);
368 var deleteIcon = createIcon('lti_delete_tool_type', M.util.get_string('deletetype', 'lti'), this.settings.delete_icon_url);
370 editIcon.on('click', function(e){
371 var toolTypeId = typeSelector.get('value');
373 if(self.getSelectedToolTypeOption().getAttribute('editable')){
374 window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
376 alert(M.util.get_string('cannot_edit', 'lti'));
380 addIcon.on('click', function(e){
381 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
384 deleteIcon.on('click', function(e){
385 var toolTypeId = typeSelector.get('value');
387 if(self.getSelectedToolTypeOption().getAttribute('editable')){
388 if(confirm(M.util.get_string('delete_confirmation', 'lti'))){
389 self.deleteTool(toolTypeId);
392 alert(M.util.get_string('cannot_delete', 'lti'));
396 typeSelector.insert(addIcon, 'after');
397 addIcon.insert(editIcon, 'after');
398 editIcon.insert(deleteIcon, 'after');
401 toggleEditButtons: function(){
402 var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
403 var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
405 // Make the edit / delete icons look enabled / disabled.
406 // Does not work in older browsers, but alerts will catch those cases.
407 if(this.getSelectedToolTypeOption().getAttribute('editable')){
408 lti_edit_tool_type.setStyle('opacity', '1');
409 lti_delete_tool_type.setStyle('opacity', '1');
411 lti_edit_tool_type.setStyle('opacity', '.2');
412 lti_delete_tool_type.setStyle('opacity', '.2');
416 addToolType: function(toolType){
417 var typeSelector = Y.one('#id_typeid');
418 var course_tool_group = Y.one('#course_tool_group');
420 var option = Y.Node.create('<option />')
421 .set('text', toolType.name)
422 .set('value', toolType.id)
423 .set('selected', 'selected')
424 .setAttribute('editable', '1')
425 .setAttribute('courseTool', '1')
426 .setAttribute('domain', toolType.tooldomain);
428 if(course_tool_group){
429 course_tool_group.append(option);
431 typeSelector.append(option);
434 // Adding the new tool may affect which tool gets matched automatically
435 this.clearToolCache();
436 this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
437 this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
438 this.toggleEditButtons();
440 require(["core/notification"], function (notification) {
441 notification.addNotification({
442 message: M.util.get_string('tooltypeadded', 'lti'),
448 updateToolType: function(toolType){
449 var typeSelector = Y.one('#id_typeid');
451 var option = typeSelector.one('option[value="' + toolType.id + '"]');
452 option.set('text', toolType.name)
453 .set('domain', toolType.tooldomain);
455 // Editing the tool may affect which tool gets matched automatically
456 this.clearToolCache();
457 this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
458 this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
460 require(["core/notification"], function (notification) {
461 notification.addNotification({
462 message: M.util.get_string('tooltypeupdated', 'lti'),
468 deleteTool: function(toolTypeId){
471 Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
474 self.getSelectedToolTypeOption().remove();
476 // Editing the tool may affect which tool gets matched automatically
477 self.clearToolCache();
478 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
479 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
481 require(["core/notification"], function (notification) {
482 notification.addNotification({
483 message: M.util.get_string('tooltypedeleted', 'lti'),
489 require(["core/notification"], function (notification) {
490 notification.addNotification({
491 message: M.util.get_string('tooltypenotdeleted', 'lti'),
500 findToolByUrl: function(url, toolId, callback){
503 Y.io(self.settings.ajax_url, {
504 data: {action: 'find_tool_config',
505 course: self.settings.courseId,
511 success: function(transactionid, xhr){
512 var response = xhr.response;
514 var toolInfo = Y.JSON.parse(response);
526 * Gets the tool type ID of the selected tool that supports Content-Item selection.
528 * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise.
530 getContentItemId: function() {
531 var selected = this.getSelectedToolTypeOption();
532 if (selected.getAttribute('data-contentitem')) {
533 return selected.getAttribute('data-id');
539 * Resets the values of fields related to the LTI tool settings.
541 resetToolFields: function() {
542 // Reset values for all text fields.
543 var fields = Y.all('#id_toolurl, #id_securetoolurl, #id_instructorcustomparameters, #id_icon, #id_secureicon');
544 fields.set('value', null);
546 // Reset value for launch container select box.
547 Y.one('#id_launchcontainer').set('value', 1);