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 = {};
45 var updateToolMatches = function(){
46 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
47 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
50 var typeSelector = Y.one('#id_typeid');
54 typeSelector.on('change', function(e){
55 // Reset configuration fields when another preconfigured tool is selected.
56 self.resetToolFields();
60 self.toggleEditButtons();
62 if (self.getSelectedToolTypeOption().getAttribute('toolproxy')){
63 var allowname = Y.one('#id_instructorchoicesendname');
64 allowname.set('checked', !self.getSelectedToolTypeOption().getAttribute('noname'));
66 var allowemail = Y.one('#id_instructorchoicesendemailaddr');
67 allowemail.set('checked', !self.getSelectedToolTypeOption().getAttribute('noemail'));
69 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
70 allowgrades.set('checked', !self.getSelectedToolTypeOption().getAttribute('nogrades'));
71 self.toggleGradeSection();
75 this.createTypeEditorButtons();
77 this.toggleEditButtons();
80 var contentItemButton = Y.one('[name="selectcontent"]');
81 if (contentItemButton) {
82 var contentItemUrl = contentItemButton.getAttribute('data-contentitemurl');
83 // Handle configure from link button click.
84 contentItemButton.on('click', function() {
85 var contentItemId = self.getContentItemId();
87 // Get activity name and description values.
88 var title = Y.one('#id_name').get('value').trim();
89 var text = Y.one('#id_introeditor').get('value').trim();
91 // Set data to be POSTed.
94 course: self.settings.courseId,
99 require(['mod_lti/contentitem'], function(contentitem) {
100 contentitem.init(contentItemUrl, postData, function(returnData) {
101 if (!returnData.multiple) {
102 M.mod_lti.editor.toggleGradeSection();
110 var textAreas = new Y.NodeList([
111 Y.one('#id_toolurl'),
112 Y.one('#id_securetoolurl'),
113 Y.one('#id_resourcekey'),
114 Y.one('#id_password')
118 textAreas.on('keyup', function(e){
119 clearTimeout(debounce);
121 // If no more changes within 2 seconds, look up the matching tool URL
122 debounce = setTimeout(function(){
127 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
128 allowgrades.on('change', this.toggleGradeSection, this);
135 toggleGradeSection: function(e) {
139 var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
140 var gradefieldset = Y.one('#id_modstandardgrade');
141 if (!allowgrades.get('checked')) {
142 gradefieldset.hide();
144 gradefieldset.show();
148 clearToolCache: function(){
150 this.toolTypeCache = {};
153 updateAutomaticToolMatch: function(field){
161 var typeSelector = Y.one('#id_typeid');
163 var id = field.get('id') + '_lti_automatch_tool';
164 var automatchToolDisplay = Y.one('#' + id);
166 if(!automatchToolDisplay){
167 automatchToolDisplay = Y.Node.create('<span />')
169 .setStyle('padding-left', '1em');
171 toolurl.insert(automatchToolDisplay, 'after');
174 var url = toolurl.get('value');
176 // Hide the display if the url box is empty
178 automatchToolDisplay.setStyle('display', 'none');
180 automatchToolDisplay.set('innerHTML', '');
181 automatchToolDisplay.setStyle('display', '');
184 var selectedToolType = parseInt(typeSelector.get('value'));
185 var selectedOption = typeSelector.one('option[value="' + selectedToolType + '"]');
187 // A specific tool type is selected (not "auto")
188 // We still need to check with the server to get privacy settings
189 if(selectedToolType > 0){
190 // If the entered domain matches the domain of the tool configuration...
191 var domainRegex = /(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i;
192 var match = domainRegex.exec(url);
193 if(match && match[1] && match[1].toLowerCase() === selectedOption.getAttribute('domain').toLowerCase()){
194 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'));
196 // The entered URL does not match the domain of the tool configuration
197 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('domain_mismatch', 'lti'));
201 var key = Y.one('#id_resourcekey');
202 var secret = Y.one('#id_password');
204 // Indicate the tool is manually configured
205 // We still check the Launch URL with the server as course/site tools may override privacy settings
206 if(key.get('value') !== '' && secret.get('value') !== ''){
207 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url + '" />' + M.util.get_string('custom_config', 'lti'));
210 var continuation = function(toolInfo, inputfield){
211 if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) {
212 self.updatePrivacySettings(toolInfo);
214 if(toolInfo.toolname){
215 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);
216 } else if(!selectedToolType) {
217 // Inform them custom configuration is in use
218 if(key.get('value') === '' || secret.get('value') === ''){
219 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.warning_icon_url + '" />' + M.util.get_string('tool_config_not_found', 'lti'));
222 if (toolInfo.cartridge) {
223 automatchToolDisplay.set('innerHTML', '<img style="vertical-align:text-bottom" src="' + self.settings.green_check_icon_url +
224 '" />' + M.util.get_string('using_tool_cartridge', 'lti'));
228 // Cache urls which have already been checked to increase performance
229 // Don't use URL cache if tool type manually selected
230 if(selectedToolType && self.toolTypeCache[selectedToolType]){
231 return continuation(self.toolTypeCache[selectedToolType]);
232 } else if(self.urlCache[url] && !selectedToolType){
233 return continuation(self.urlCache[url]);
234 } else if(!selectedToolType && !url) {
235 // No tool type or url set
236 return continuation({}, field);
238 self.findToolByUrl(url, selectedToolType, function(toolInfo){
240 // Cache the result based on whether the URL or tool type was used to look up the tool
241 if(!selectedToolType){
242 self.urlCache[url] = toolInfo;
244 self.toolTypeCache[selectedToolType] = toolInfo;
247 Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
249 continuation(toolInfo);
256 * Updates display of privacy settings to show course / site tool configuration settings.
258 updatePrivacySettings: function(toolInfo){
259 if(!toolInfo || !toolInfo.toolid){
261 sendname: M.mod_lti.LTI_SETTING_DELEGATE,
262 sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
263 acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
267 var setting, control;
269 var privacyControls = {
270 sendname: Y.one('#id_instructorchoicesendname'),
271 sendemailaddr: Y.one('#id_instructorchoicesendemailaddr'),
272 acceptgrades: Y.one('#id_instructorchoiceacceptgrades')
275 // Store a copy of user entered privacy settings as we may overwrite them
276 if(!this.userPrivacySettings){
277 this.userPrivacySettings = {};
280 for(setting in privacyControls){
281 if(privacyControls.hasOwnProperty(setting)){
282 control = privacyControls[setting];
284 // Only store the value if it hasn't been forced by the editor
285 if(!control.get('disabled')){
286 this.userPrivacySettings[setting] = control.get('checked');
291 // Update UI based on course / site tool configuration
292 for(setting in privacyControls){
293 if(privacyControls.hasOwnProperty(setting)){
294 var settingValue = toolInfo[setting];
295 control = privacyControls[setting];
297 if(settingValue == M.mod_lti.LTI_SETTING_NEVER){
298 control.set('disabled', true);
299 control.set('checked', false);
300 control.set('title', M.util.get_string('forced_help', 'lti'));
301 } else if(settingValue == M.mod_lti.LTI_SETTING_ALWAYS){
302 control.set('disabled', true);
303 control.set('checked', true);
304 control.set('title', M.util.get_string('forced_help', 'lti'));
305 } else if(settingValue == M.mod_lti.LTI_SETTING_DELEGATE){
306 control.set('disabled', false);
308 // Get the value out of the stored copy
309 control.set('checked', this.userPrivacySettings[setting]);
310 control.set('title', '');
315 this.toggleGradeSection();
318 getSelectedToolTypeOption: function(){
319 var typeSelector = Y.one('#id_typeid');
321 return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
325 * Separate tool listing into option groups. Server-side select control
326 * doesn't seem to support this.
328 addOptGroups: function(){
329 var typeSelector = Y.one('#id_typeid');
331 if(typeSelector.one('option[courseTool=1]')){
332 // One ore more course tools exist
334 var globalGroup = Y.Node.create('<optgroup />')
335 .set('id', 'global_tool_group')
336 .set('label', M.util.get_string('global_tool_types', 'lti'));
338 var courseGroup = Y.Node.create('<optgroup />')
339 .set('id', 'course_tool_group')
340 .set('label', M.util.get_string('course_tool_types', 'lti'));
342 var globalOptions = typeSelector.all('option[globalTool=1]').remove().each(function(node){
343 globalGroup.append(node);
346 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
347 courseGroup.append(node);
350 if(globalOptions.size() > 0){
351 typeSelector.append(globalGroup);
354 if(courseOptions.size() > 0){
355 typeSelector.append(courseGroup);
361 * Adds buttons for creating, editing, and deleting tool types.
362 * Javascript is a requirement to edit course level tools at this point.
364 createTypeEditorButtons: function(){
367 var typeSelector = Y.one('#id_typeid');
369 var createIcon = function(id, tooltip, iconUrl){
370 return Y.Node.create('<a />')
372 .set('title', tooltip)
373 .setStyle('margin-left', '.5em')
374 .set('href', 'javascript:void(0);')
375 .append(Y.Node.create('<img src="' + iconUrl + '" />'));
378 var addIcon = createIcon('lti_add_tool_type', M.util.get_string('addtype', 'lti'), this.settings.add_icon_url);
379 var editIcon = createIcon('lti_edit_tool_type', M.util.get_string('edittype', 'lti'), this.settings.edit_icon_url);
380 var deleteIcon = createIcon('lti_delete_tool_type', M.util.get_string('deletetype', 'lti'), this.settings.delete_icon_url);
382 editIcon.on('click', function(e){
383 var toolTypeId = typeSelector.get('value');
385 if(self.getSelectedToolTypeOption().getAttribute('editable')){
386 window.open(self.settings.instructor_tool_type_edit_url + '&action=edit&typeid=' + toolTypeId, 'edit_tool');
388 alert(M.util.get_string('cannot_edit', 'lti'));
392 addIcon.on('click', function(e){
393 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
396 deleteIcon.on('click', function(e){
397 var toolTypeId = typeSelector.get('value');
399 if(self.getSelectedToolTypeOption().getAttribute('editable')){
400 if(confirm(M.util.get_string('delete_confirmation', 'lti'))){
401 self.deleteTool(toolTypeId);
404 alert(M.util.get_string('cannot_delete', 'lti'));
408 typeSelector.insert(addIcon, 'after');
409 addIcon.insert(editIcon, 'after');
410 editIcon.insert(deleteIcon, 'after');
413 toggleEditButtons: function(){
414 var lti_edit_tool_type = Y.one('#lti_edit_tool_type');
415 var lti_delete_tool_type = Y.one('#lti_delete_tool_type');
417 // Make the edit / delete icons look enabled / disabled.
418 // Does not work in older browsers, but alerts will catch those cases.
419 if(this.getSelectedToolTypeOption().getAttribute('editable')){
420 lti_edit_tool_type.setStyle('opacity', '1');
421 lti_delete_tool_type.setStyle('opacity', '1');
423 lti_edit_tool_type.setStyle('opacity', '.2');
424 lti_delete_tool_type.setStyle('opacity', '.2');
428 addToolType: function(toolType){
429 var typeSelector = Y.one('#id_typeid');
430 var course_tool_group = Y.one('#course_tool_group');
432 var option = Y.Node.create('<option />')
433 .set('text', toolType.name)
434 .set('value', toolType.id)
435 .set('selected', 'selected')
436 .setAttribute('editable', '1')
437 .setAttribute('courseTool', '1')
438 .setAttribute('domain', toolType.tooldomain);
440 if(course_tool_group){
441 course_tool_group.append(option);
443 typeSelector.append(option);
446 // Adding the new tool may affect which tool gets matched automatically
447 this.clearToolCache();
448 this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
449 this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
450 this.toggleEditButtons();
452 require(["core/notification"], function (notification) {
453 notification.addNotification({
454 message: M.util.get_string('tooltypeadded', 'lti'),
460 updateToolType: function(toolType){
461 var typeSelector = Y.one('#id_typeid');
463 var option = typeSelector.one('option[value="' + toolType.id + '"]');
464 option.set('text', toolType.name)
465 .set('domain', toolType.tooldomain);
467 // Editing the tool may affect which tool gets matched automatically
468 this.clearToolCache();
469 this.updateAutomaticToolMatch(Y.one('#id_toolurl'));
470 this.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
472 require(["core/notification"], function (notification) {
473 notification.addNotification({
474 message: M.util.get_string('tooltypeupdated', 'lti'),
480 deleteTool: function(toolTypeId){
483 Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
486 self.getSelectedToolTypeOption().remove();
488 // Editing the tool may affect which tool gets matched automatically
489 self.clearToolCache();
490 self.updateAutomaticToolMatch(Y.one('#id_toolurl'));
491 self.updateAutomaticToolMatch(Y.one('#id_securetoolurl'));
493 require(["core/notification"], function (notification) {
494 notification.addNotification({
495 message: M.util.get_string('tooltypedeleted', 'lti'),
501 require(["core/notification"], function (notification) {
502 notification.addNotification({
503 message: M.util.get_string('tooltypenotdeleted', 'lti'),
512 findToolByUrl: function(url, toolId, callback){
515 Y.io(self.settings.ajax_url, {
516 data: {action: 'find_tool_config',
517 course: self.settings.courseId,
523 success: function(transactionid, xhr){
524 var response = xhr.response;
526 var toolInfo = Y.JSON.parse(response);
538 * Gets the tool type ID of the selected tool that supports Content-Item selection.
540 * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise.
542 getContentItemId: function() {
544 var selected = this.getSelectedToolTypeOption();
545 if (selected.getAttribute('data-contentitem')) {
546 return selected.getAttribute('data-id');
550 // Tool selector not available - check for hidden fields instead.
551 var content = Y.one('input[name="contentitem"]');
552 if (!content || !content.get('value')) {
555 return Y.one('input[name="typeid"]').get('value');
560 * Resets the values of fields related to the LTI tool settings.
562 resetToolFields: function() {
563 // Reset values for all text fields.
564 var fields = Y.all('#id_toolurl, #id_securetoolurl, #id_instructorcustomparameters, #id_icon, #id_secureicon');
565 fields.set('value', null);
567 // Reset value for launch container select box.
568 Y.one('#id_launchcontainer').set('value', 1);