Merge branch 'MDL-67372-master' of git://github.com/rezaies/moodle
[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                 // Reset configuration fields when another preconfigured tool is selected.
55                 self.resetToolFields();
57                 updateToolMatches();
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();
71                 }
72             });
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();
79                 if (contentItemId) {
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.
85                     var postData = {
86                         id: contentItemId,
87                         course: self.settings.courseId,
88                         title: title,
89                         text: text
90                     };
92                     require(['mod_lti/contentitem'], function(contentitem) {
93                         contentitem.init(contentItemUrl, postData, function() {
94                             M.mod_lti.editor.toggleGradeSection();
95                         });
96                     });
97                 }
98             });
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')
109             ]);
111             var debounce;
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(){
117                     updateToolMatches();
118                 }, 2000);
119             });
121             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
122             allowgrades.on('change', this.toggleGradeSection, this);
124             updateToolMatches();
125         },
127         toggleGradeSection: function(e) {
128             if (e) {
129                 e.preventDefault();
130             }
131             var allowgrades = Y.one('#id_instructorchoiceacceptgrades');
132             var gradefieldset = Y.one('#id_modstandardgrade');
133             if (!allowgrades.get('checked')) {
134                 gradefieldset.hide();
135             } else {
136                 gradefieldset.show();
137             }
138         },
140         clearToolCache: function(){
141             this.urlCache = {};
142             this.toolTypeCache = {};
143         },
145         updateAutomaticToolMatch: function(field){
146             var self = this;
148             var toolurl = 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 />')
156                                         .set('id', id)
157                                         .setStyle('padding-left', '1em');
159                 toolurl.insert(automatchToolDisplay, 'after');
160             }
162             var url = toolurl.get('value');
164             // Hide the display if the url box is empty
165             if(!url){
166                 automatchToolDisplay.setStyle('display', 'none');
167             } else {
168                 automatchToolDisplay.set('innerHTML', '');
169                 automatchToolDisplay.setStyle('display', '');
170             }
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'));
183                 } else {
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'));
186                 }
187             }
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'));
196             }
198             var continuation = function(toolInfo, inputfield){
199                 if (inputfield === undefined || (inputfield.get('id') != 'id_securetoolurl' || inputfield.get('value'))) {
200                     self.updatePrivacySettings(toolInfo);
201                 }
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'));
208                     }
209                 }
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'));
213                 }
214             };
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);
225             } else {
226                 self.findToolByUrl(url, selectedToolType, function(toolInfo){
227                     if(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;
231                         } else {
232                             self.toolTypeCache[selectedToolType] = toolInfo;
233                         }
235                         Y.one('#id_urlmatchedtypeid').set('value', toolInfo.toolid);
237                         continuation(toolInfo);
238                     }
239                 });
240             }
241         },
243         /**
244          * Updates display of privacy settings to show course / site tool configuration settings.
245          */
246         updatePrivacySettings: function(toolInfo){
247             if(!toolInfo || !toolInfo.toolid){
248                 toolInfo = {
249                     sendname: M.mod_lti.LTI_SETTING_DELEGATE,
250                     sendemailaddr: M.mod_lti.LTI_SETTING_DELEGATE,
251                     acceptgrades: M.mod_lti.LTI_SETTING_DELEGATE
252                 }
253             }
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')
261             };
263             // Store a copy of user entered privacy settings as we may overwrite them
264             if(!this.userPrivacySettings){
265                 this.userPrivacySettings = {};
266             }
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');
275                     }
276                 }
277             }
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', '');
299                     }
300                 }
301             }
303             this.toggleGradeSection();
304         },
306         getSelectedToolTypeOption: function(){
307             var typeSelector = Y.one('#id_typeid');
309             return typeSelector.one('option[value="' + typeSelector.get('value') + '"]');
310         },
312         /**
313          * Separate tool listing into option groups. Server-side select control
314          * doesn't seem to support this.
315          */
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);
332                 });
334                 var courseOptions = typeSelector.all('option[courseTool=1]').remove().each(function(node){
335                     courseGroup.append(node);
336                 });
338                 if(globalOptions.size() > 0){
339                     typeSelector.append(globalGroup);
340                 }
342                 if(courseOptions.size() > 0){
343                     typeSelector.append(courseGroup);
344                 }
345             }
346         },
348         /**
349          * Adds buttons for creating, editing, and deleting tool types.
350          * Javascript is a requirement to edit course level tools at this point.
351          */
352         createTypeEditorButtons: function(){
353             var self = this;
355             var typeSelector = Y.one('#id_typeid');
357             var createIcon = function(id, tooltip, iconUrl){
358                 return Y.Node.create('<a />')
359                         .set('id', id)
360                         .set('title', tooltip)
361                         .setStyle('margin-left', '.5em')
362                         .set('href', 'javascript:void(0);')
363                         .append(Y.Node.create('<img src="' + iconUrl + '" />'));
364             }
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');
375                 } else {
376                     alert(M.util.get_string('cannot_edit', 'lti'));
377                 }
378             });
380             addIcon.on('click', function(e){
381                 window.open(self.settings.instructor_tool_type_edit_url + '&action=add', 'add_tool');
382             });
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);
390                     }
391                 } else {
392                     alert(M.util.get_string('cannot_delete', 'lti'));
393                 }
394             });
396             typeSelector.insert(addIcon, 'after');
397             addIcon.insert(editIcon, 'after');
398             editIcon.insert(deleteIcon, 'after');
399         },
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');
410             } else {
411                 lti_edit_tool_type.setStyle('opacity', '.2');
412                 lti_delete_tool_type.setStyle('opacity', '.2');
413             }
414         },
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);
430             } else {
431                 typeSelector.append(option);
432             }
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'),
443                     type: "success"
444                 });
445             });
446         },
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'),
463                     type: "success"
464                 });
465             });
466         },
468         deleteTool: function(toolTypeId){
469             var self = this;
471             Y.io(self.settings.instructor_tool_type_edit_url + '&action=delete&typeid=' + toolTypeId, {
472                 on: {
473                     success: function(){
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'),
484                                 type: "success"
485                             });
486                         });
487                     },
488                     failure: function(){
489                         require(["core/notification"], function (notification) {
490                             notification.addNotification({
491                                 message: M.util.get_string('tooltypenotdeleted', 'lti'),
492                                 type: "problem"
493                             });
494                         });
495                     }
496                 }
497             });
498         },
500         findToolByUrl: function(url, toolId, callback){
501             var self = this;
503             Y.io(self.settings.ajax_url, {
504                 data: {action: 'find_tool_config',
505                         course: self.settings.courseId,
506                         toolurl: url,
507                         toolid: toolId || 0
508                 },
510                 on: {
511                     success: function(transactionid, xhr){
512                         var response = xhr.response;
514                         var toolInfo = Y.JSON.parse(response);
516                         callback(toolInfo);
517                     },
518                     failure: function(){
520                     }
521                 }
522             });
523         },
525         /**
526          * Gets the tool type ID of the selected tool that supports Content-Item selection.
527          *
528          * @returns {number|boolean} The ID of the tool type if it supports Content-Item selection. False, otherwise.
529          */
530         getContentItemId: function() {
531             var selected = this.getSelectedToolTypeOption();
532             if (selected.getAttribute('data-contentitem')) {
533                 return selected.getAttribute('data-id');
534             }
535             return false;
536         },
538         /**
539          * Resets the values of fields related to the LTI tool settings.
540          */
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);
548         }
549     };
550 })();