b0d35a3dc83eb7cc1d0cf3cdf3a93f9c2f6a5fbc
[moodle.git] / course / dndupload.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 library for enableing a drag and drop upload to courses
18  *
19  * @package    core
20  * @subpackage course
21  * @copyright  2012 Davo Smith
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 M.course_dndupload = {
25     // YUI object.
26     Y: null,
27     // URL for upload requests
28     url: M.cfg.wwwroot + '/course/dndupload.php',
29     // maximum size of files allowed in this form
30     maxbytes: 0,
31     // ID of the course we are on
32     courseid: null,
33     // Data about the different file/data handlers that are available
34     handlers: null,
35     // Nasty hack to distinguish between dragenter(first entry),
36     // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
37     entercount: 0,
38     // Used to keep track of the section we are dragging across - to make
39     // spotting movement between sections more reliable
40     currentsection: null,
41     // Used to store the pending uploads whilst the user is being asked for further input
42     uploadqueue: null,
43     // True if the there is currently a dialog being shown (asking for a name, or giving a
44     // choice of file handlers)
45     uploaddialog: false,
46     // An array containing the last selected file handler for each file type
47     lastselected: null,
49     // The following are used to identify specific parts of the course page
51     // The type of HTML element that is a course section
52     sectiontypename: 'li',
53     // The classes that an element must have to be identified as a course section
54     sectionclasses: ['section', 'main'],
55     // The ID of the main content area of the page (for adding the 'status' div)
56     pagecontentid: 'page-content',
57     // The selector identifying the list of modules within a section (note changing this may require
58     // changes to the get_mods_element function)
59     modslistselector: 'ul.section',
61     /**
62      * Initalise the drag and drop upload interface
63      * Note: one and only one of options.filemanager and options.formcallback must be defined
64      *
65      * @param Y the YUI object
66      * @param object options {
67      *            courseid: ID of the course we are on
68      *            maxbytes: maximum size of files allowed in this form
69      *            handlers: Data about the different file/data handlers that are available
70      *          }
71      */
72     init: function(Y, options) {
73         this.Y = Y;
75         if (!this.browser_supported()) {
76             return; // Browser does not support the required functionality
77         }
79         this.maxbytes = options.maxbytes;
80         this.courseid = options.courseid;
81         this.handlers = options.handlers;
82         this.uploadqueue = new Array();
83         this.lastselected = new Array();
85         var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.');
86         var sections = this.Y.all(sectionselector);
87         if (sections.isEmpty()) {
88             return; // No sections - incompatible course format or front page.
89         }
90         sections.each( function(el) {
91             this.add_preview_element(el);
92             this.init_events(el);
93         }, this);
95         var div = this.add_status_div();
96         div.setContent(M.util.get_string('dndworking', 'moodle'));
97     },
99     /**
100      * Add a div element to tell the user that drag and drop upload
101      * is available (or to explain why it is not available)
102      * @return the DOM element to add messages to
103      */
104     add_status_div: function() {
105         var div = document.createElement('div');
106         div.id = 'dndupload-status';
107         var coursecontents = document.getElementById(this.pagecontentid);
108         if (coursecontents) {
109             coursecontents.insertBefore(div, coursecontents.firstChild);
110         }
111         return this.Y.one(div);
112     },
114     /**
115      * Check the browser has the required functionality
116      * @return true if browser supports drag/drop upload
117      */
118     browser_supported: function() {
119         if (typeof FileReader == 'undefined') {
120             return false;
121         }
122         if (typeof FormData == 'undefined') {
123             return false;
124         }
125         return true;
126     },
128     /**
129      * Initialise drag events on node container, all events need
130      * to be processed for drag and drop to work
131      * @param el the element to add events to
132      */
133     init_events: function(el) {
134         this.Y.on('dragenter', this.drag_enter, el, this);
135         this.Y.on('dragleave', this.drag_leave, el, this);
136         this.Y.on('dragover',  this.drag_over,  el, this);
137         this.Y.on('drop',      this.drop,       el, this);
138     },
140     /**
141      * Work out which course section a given element is in
142      * @param el the child DOM element within the section
143      * @return the DOM element representing the section
144      */
145     get_section: function(el) {
146         var sectionclasses = this.sectionclasses;
147         return el.ancestor( function(test) {
148             var i;
149             for (i=0; i<sectionclasses.length; i++) {
150                 if (!test.hasClass(sectionclasses[i])) {
151                     return false;
152                 }
153                 return true;
154             }
155         }, true);
156     },
158     /**
159      * Work out the number of the section we have been dropped on to, from the section element
160      * @param DOMElement section the selected section
161      * @return int the section number
162      */
163     get_section_number: function(section) {
164         var sectionid = section.get('id').split('-');
165         if (sectionid.length < 2 || sectionid[0] != 'section') {
166             return false;
167         }
168         return parseInt(sectionid[1]);
169     },
171     /**
172      * Check if the event includes data of the given type
173      * @param e the event details
174      * @param type the data type to check for
175      * @return true if the data type is found in the event data
176      */
177     types_includes: function(e, type) {
178         var i;
179         if (e._event.dataTransfer === null) {
180             // TODO MDL-33054: If we get here then something has gone wrong.
181             return false;
182         }
183         var types = e._event.dataTransfer.types;
184         if (types == null) {
185             return false;
186         }
187         for (i=0; i<types.length; i++) {
188             if (types[i] == type) {
189                 return true;
190             }
191         }
192         return false;
193     },
195     /**
196      * Look through the event data, checking it against the registered data types
197      * (in order of priority) and return details of the first matching data type
198      * @param e the event details
199      * @return mixed false if not found or an object {
200      *           realtype: the type as given by the browser
201      *           addmessage: the message to show to the user during dragging
202      *           namemessage: the message for requesting a name for the resource from the user
203      *           type: the identifier of the type (may match several 'realtype's)
204      *           }
205      */
206     drag_type: function(e) {
207         if (this.types_includes(e, 'Files')) {
208             if (this.handlers.filehandlers.length == 0) {
209                 return false; // No available file handlers - ignore this drag.
210             }
211             return {
212                 realtype: 'Files',
213                 addmessage: M.util.get_string('addfilehere', 'moodle'),
214                 namemessage: null, // Should not be asked for anyway
215                 type: 'Files'
216             };
217         }
219         // Check each of the registered types
220         var types = this.handlers.types;
221         for (var i=0; i<types.length; i++) {
222             // Check each of the different identifiers for this type
223             var dttypes = types[i].datatransfertypes;
224             for (var j=0; j<dttypes.length; j++) {
225                 if (this.types_includes(e, dttypes[j])) {
226                     return {
227                         realtype: dttypes[j],
228                         addmessage: types[i].addmessage,
229                         namemessage: types[i].namemessage,
230                         type: types[i].identifier,
231                         handlers: types[i].handlers
232                     };
233                 }
234             }
235         }
236         return false; // No types we can handle
237     },
239     /**
240      * Check the content of the drag/drop includes a type we can handle, then, if
241      * it is, notify the browser that we want to handle it
242      * @param event e
243      * @return string type of the event or false
244      */
245     check_drag: function(e) {
246         var type = this.drag_type(e);
247         if (type) {
248             // Notify browser that we will handle this drag/drop
249             e.stopPropagation();
250             e.preventDefault();
251         }
252         return type;
253     },
255     /**
256      * Handle a dragenter event: add a suitable 'add here' message
257      * when a drag event occurs, containing a registered data type
258      * @param e event data
259      * @return false to prevent the event from continuing to be processed
260      */
261     drag_enter: function(e) {
262         if (!(type = this.check_drag(e))) {
263             return false;
264         }
266         var section = this.get_section(e.currentTarget);
267         if (!section) {
268             return false;
269         }
271         if (this.currentsection && this.currentsection != section) {
272             this.currentsection = section;
273             this.entercount = 1;
274         } else {
275             this.entercount++;
276             if (this.entercount > 2) {
277                 this.entercount = 2;
278                 return false;
279             }
280         }
282         this.show_preview_element(section, type);
284         return false;
285     },
287     /**
288      * Handle a dragleave event: remove the 'add here' message (if present)
289      * @param e event data
290      * @return false to prevent the event from continuing to be processed
291      */
292     drag_leave: function(e) {
293         if (!this.check_drag(e)) {
294             return false;
295         }
297         this.entercount--;
298         if (this.entercount == 1) {
299             return false;
300         }
301         this.entercount = 0;
302         this.currentsection = null;
304         this.hide_preview_element();
305         return false;
306     },
308     /**
309      * Handle a dragover event: just prevent the browser default (necessary
310      * to allow drag and drop handling to work)
311      * @param e event data
312      * @return false to prevent the event from continuing to be processed
313      */
314     drag_over: function(e) {
315         this.check_drag(e);
316         return false;
317     },
319     /**
320      * Handle a drop event: hide the 'add here' message, check the attached
321      * data type and start the upload process
322      * @param e event data
323      * @return false to prevent the event from continuing to be processed
324      */
325     drop: function(e) {
326         if (!(type = this.check_drag(e))) {
327             return false;
328         }
330         this.hide_preview_element();
332         // Work out the number of the section we are on (from its id)
333         var section = this.get_section(e.currentTarget);
334         var sectionnumber = this.get_section_number(section);
336         // Process the file or the included data
337         if (type.type == 'Files') {
338             var files = e._event.dataTransfer.files;
339             for (var i=0, f; f=files[i]; i++) {
340                 this.handle_file(f, section, sectionnumber);
341             }
342         } else {
343             var contents = e._event.dataTransfer.getData(type.realtype);
344             if (contents) {
345                 this.handle_item(type, contents, section, sectionnumber);
346             }
347         }
349         return false;
350     },
352     /**
353      * Find or create the 'ul' element that contains all of the module
354      * instances in this section
355      * @param section the DOM element representing the section
356      * @return false to prevent the event from continuing to be processed
357      */
358     get_mods_element: function(section) {
359         // Find the 'ul' containing the list of mods
360         var modsel = section.one(this.modslistselector);
361         if (!modsel) {
362             // Create the above 'ul' if it doesn't exist
363             var modsel = document.createElement('ul');
364             modsel.className = 'section img-text';
365             var contentel = section.get('children').pop();
366             var brel = contentel.get('children').pop();
367             contentel.insertBefore(modsel, brel);
368             modsel = this.Y.one(modsel);
369         }
371         return modsel;
372     },
374     /**
375      * Add a new dummy item to the list of mods, to be replaced by a real
376      * item & link once the AJAX upload call has completed
377      * @param name the label to show in the element
378      * @param section the DOM element reperesenting the course section
379      * @return DOM element containing the new item
380      */
381     add_resource_element: function(name, section) {
382         var modsel = this.get_mods_element(section);
384         var resel = {
385             parent: modsel,
386             li: document.createElement('li'),
387             div: document.createElement('div'),
388             a: document.createElement('a'),
389             icon: document.createElement('img'),
390             namespan: document.createElement('span'),
391             progressouter: document.createElement('span'),
392             progress: document.createElement('span')
393         };
395         resel.li.className = 'activity resource modtype_resource';
397         resel.div.className = 'mod-indent';
398         resel.li.appendChild(resel.div);
400         resel.a.href = '#';
401         resel.div.appendChild(resel.a);
403         resel.icon.src = M.util.image_url('i/ajaxloader');
404         resel.a.appendChild(resel.icon);
406         resel.a.appendChild(document.createTextNode(' '));
408         resel.namespan.className = 'instancename';
409         resel.namespan.innerHTML = name;
410         resel.a.appendChild(resel.namespan);
412         resel.div.appendChild(document.createTextNode(' '));
414         resel.progressouter.className = 'dndupload-progress-outer';
415         resel.progress.className = 'dndupload-progress-inner';
416         resel.progress.innerHTML = '&nbsp;';
417         resel.progressouter.appendChild(resel.progress);
418         resel.div.appendChild(resel.progressouter);
420         modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
422         return resel;
423     },
425     /**
426      * Hide any visible dndupload-preview elements on the page
427      */
428     hide_preview_element: function() {
429         this.Y.all('li.dndupload-preview').addClass('dndupload-hidden');
430     },
432     /**
433      * Unhide the preview element for the given section and set it to display
434      * the correct message
435      * @param section the YUI node representing the selected course section
436      * @param type the details of the data type detected in the drag (including the message to display)
437      */
438     show_preview_element: function(section, type) {
439         this.hide_preview_element();
440         var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
441         preview.one('span').setContent(type.addmessage);
442     },
444     /**
445      * Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
446      * is called, otherwise Firefox will ignore events generated when the mouse is over the preview
447      * element (instead of passing them up to the parent element)
448      * @param section the YUI node representing the selected course section
449      */
450     add_preview_element: function(section) {
451         var modsel = this.get_mods_element(section);
452         var preview = {
453             li: document.createElement('li'),
454             div: document.createElement('div'),
455             icon: document.createElement('img'),
456             namespan: document.createElement('span')
457         };
459         preview.li.className = 'dndupload-preview activity resource modtype_resource dndupload-hidden';
461         preview.div.className = 'mod-indent';
462         preview.li.appendChild(preview.div);
464         preview.icon.src = M.util.image_url('t/addfile');
465         preview.div.appendChild(preview.icon);
467         preview.div.appendChild(document.createTextNode(' '));
469         preview.namespan.className = 'instancename';
470         preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
471         preview.div.appendChild(preview.namespan);
473         modsel.appendChild(preview.li);
474     },
476     /**
477      * Find the registered handler for the given file type. If there is more than one, ask the
478      * user which one to use. Then upload the file to the server
479      * @param file the details of the file, taken from the FileList in the drop event
480      * @param section the DOM element representing the selected course section
481      * @param sectionnumber the number of the selected course section
482      */
483     handle_file: function(file, section, sectionnumber) {
484         var handlers = new Array();
485         var filehandlers = this.handlers.filehandlers;
486         var extension = '';
487         var dotpos = file.name.lastIndexOf('.');
488         if (dotpos != -1) {
489             extension = file.name.substr(dotpos+1, file.name.length);
490         }
492         for (var i=0; i<filehandlers.length; i++) {
493             if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
494                 handlers.push(filehandlers[i]);
495             }
496         }
498         if (handlers.length == 0) {
499             // No handlers at all (not even 'resource'?)
500             return;
501         }
503         if (handlers.length == 1) {
504             this.upload_file(file, section, sectionnumber, handlers[0].module);
505             return;
506         }
508         this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
509     },
511     /**
512      * Show a dialog box, allowing the user to choose what to do with the file they are uploading
513      * @param handlers the available handlers to choose between
514      * @param extension the extension of the file being uploaded
515      * @param file the File object being uploaded
516      * @param section the DOM element of the section being uploaded to
517      * @param sectionnumber the number of the selected course section
518      */
519     file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
520         if (this.uploaddialog) {
521             var details = new Object();
522             details.isfile = true;
523             details.handlers = handlers;
524             details.extension = extension;
525             details.file = file;
526             details.section = section;
527             details.sectionnumber = sectionnumber;
528             this.uploadqueue.push(details);
529             return;
530         }
531         this.uploaddialog = true;
533         var timestamp = new Date().getTime();
534         var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
535         var content = '';
536         var sel;
537         if (extension in this.lastselected) {
538             sel = this.lastselected[extension];
539         } else {
540             sel = handlers[0].module;
541         }
542         content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
543         content += '<div id="dndupload_handlers'+uploadid+'">';
544         for (var i=0; i<handlers.length; i++) {
545             var id = 'dndupload_handler'+uploadid+handlers[i].module;
546             var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
547             content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
548             content += ' <label for="'+id+'">';
549             content += handlers[i].message;
550             content += '</label><br/>';
551         }
552         content += '</div>';
554         var Y = this.Y;
555         var self = this;
556         var panel = new Y.Panel({
557             bodyContent: content,
558             width: 350,
559             zIndex: 5,
560             centered: true,
561             modal: true,
562             visible: true,
563             render: true,
564             buttons: [{
565                 value: M.util.get_string('upload', 'moodle'),
566                 action: function(e) {
567                     e.preventDefault();
568                     // Find out which module was selected
569                     var module = false;
570                     var div = Y.one('#dndupload_handlers'+uploadid);
571                     div.all('input').each(function(input) {
572                         if (input.get('checked')) {
573                             module = input.get('value');
574                         }
575                     });
576                     if (!module) {
577                         return;
578                     }
579                     panel.hide();
580                     // Remember this selection for next time
581                     self.lastselected[extension] = module;
582                     // Do the upload
583                     self.upload_file(file, section, sectionnumber, module);
584                 },
585                 section: Y.WidgetStdMod.FOOTER
586             },{
587                 value: M.util.get_string('cancel', 'moodle'),
588                 action: function(e) {
589                     e.preventDefault();
590                     panel.hide();
591                 },
592                 section: Y.WidgetStdMod.FOOTER
593             }]
594         });
595         // When the panel is hidden - destroy it and then check for other pending uploads
596         panel.after("visibleChange", function(e) {
597             if (!panel.get('visible')) {
598                 panel.destroy(true);
599                 self.check_upload_queue();
600             }
601         });
602     },
604     /**
605      * Check to see if there are any other dialog boxes to show, now that the current one has
606      * been dealt with
607      */
608     check_upload_queue: function() {
609         this.uploaddialog = false;
610         if (this.uploadqueue.length == 0) {
611             return;
612         }
614         var details = this.uploadqueue.shift();
615         if (details.isfile) {
616             this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
617         } else {
618             this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
619         }
620     },
622     /**
623      * Do the file upload: show the dummy element, use an AJAX call to send the data
624      * to the server, update the progress bar for the file, then replace the dummy
625      * element with the real information once the AJAX call completes
626      * @param file the details of the file, taken from the FileList in the drop event
627      * @param section the DOM element representing the selected course section
628      * @param sectionnumber the number of the selected course section
629      */
630     upload_file: function(file, section, sectionnumber, module) {
632         // This would be an ideal place to use the Y.io function
633         // however, this does not support data encoded using the
634         // FormData object, which is needed to transfer data from
635         // the DataTransfer object into an XMLHTTPRequest
636         // This can be converted when the YUI issue has been integrated:
637         // http://yuilibrary.com/projects/yui3/ticket/2531274
638         var xhr = new XMLHttpRequest();
639         var self = this;
641         if (file.size > this.maxbytes) {
642             alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle'));
643             return;
644         }
646         // Add the file to the display
647         var resel = this.add_resource_element(file.name, section);
649         // Update the progress bar as the file is uploaded
650         xhr.upload.addEventListener('progress', function(e) {
651             if (e.lengthComputable) {
652                 var percentage = Math.round((e.loaded * 100) / e.total);
653                 resel.progress.style.width = percentage + '%';
654             }
655         }, false);
657         // Wait for the AJAX call to complete, then update the
658         // dummy element with the returned details
659         xhr.onreadystatechange = function() {
660             if (xhr.readyState == 4) {
661                 if (xhr.status == 200) {
662                     var result = JSON.parse(xhr.responseText);
663                     if (result) {
664                         if (result.error == 0) {
665                             // All OK - update the dummy element
666                             resel.icon.src = result.icon;
667                             resel.a.href = result.link;
668                             resel.namespan.innerHTML = result.name;
669                             resel.div.removeChild(resel.progressouter);
670                             resel.li.id = result.elementid;
671                             resel.div.innerHTML += result.commands;
672                             if (result.onclick) {
673                                 resel.a.onclick = result.onclick;
674                             }
675                             self.add_editing(result.elementid);
676                         } else {
677                             // Error - remove the dummy element
678                             resel.parent.removeChild(resel.li);
679                             alert(result.error);
680                         }
681                     }
682                 } else {
683                     alert(M.util.get_string('servererror', 'moodle'));
684                 }
685             }
686         };
688         // Prepare the data to send
689         var formData = new FormData();
690         formData.append('repo_upload_file', file);
691         formData.append('sesskey', M.cfg.sesskey);
692         formData.append('course', this.courseid);
693         formData.append('section', sectionnumber);
694         formData.append('module', module);
695         formData.append('type', 'Files');
697         // Send the AJAX call
698         xhr.open("POST", this.url, true);
699         xhr.send(formData);
700     },
702     /**
703      * Show a dialog box to gather the name of the resource / activity to be created
704      * from the uploaded content
705      * @param type the details of the type of content
706      * @param contents the contents to be uploaded
707      * @section the DOM element for the section being uploaded to
708      * @sectionnumber the number of the section being uploaded to
709      */
710     handle_item: function(type, contents, section, sectionnumber) {
711         if (type.handlers.length == 0) {
712             // Nothing to handle this - should not have got here
713             return;
714         }
716         if (this.uploaddialog) {
717             var details = new Object();
718             details.isfile = false;
719             details.type = type;
720             details.contents = contents;
721             details.section = section;
722             details.setcionnumber = sectionnumber;
723             this.uploadqueue.push(details);
724             return;
725         }
726         this.uploaddialog = true;
728         var timestamp = new Date().getTime();
729         var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
730         var nameid = 'dndupload_handler_name'+uploadid;
731         var content = '';
732         content += '<label for="'+nameid+'">'+type.namemessage+'</label>';
733         content += ' <input type="text" id="'+nameid+'" value="" />';
734         if (type.handlers.length > 1) {
735             content += '<div id="dndupload_handlers'+uploadid+'">';
736             var sel = type.handlers[0].module;
737             for (var i=0; i<type.handlers.length; i++) {
738                 var id = 'dndupload_handler'+uploadid;
739                 var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
740                 content += '<input type="radio" name="handler" value="'+type.handlers[i].module+'" id="'+id+'" '+checked+'/>';
741                 content += ' <label for="'+id+'">';
742                 content += type.handlers[i].message;
743                 content += '</label><br/>';
744             }
745             content += '</div>';
746         }
748         var Y = this.Y;
749         var self = this;
750         var panel = new Y.Panel({
751             bodyContent: content,
752             width: 350,
753             zIndex: 5,
754             centered: true,
755             modal: true,
756             visible: true,
757             render: true,
758             buttons: [{
759                 value: M.util.get_string('upload', 'moodle'),
760                 action: function(e) {
761                     e.preventDefault();
762                     var name = Y.one('#dndupload_handler_name'+uploadid).get('value');
763                     name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim
764                     if (name == '') {
765                         return;
766                     }
767                     var module = false;
768                     if (type.handlers.length > 1) {
769                         // Find out which module was selected
770                         var div = Y.one('#dndupload_handlers'+uploadid);
771                         div.all('input').each(function(input) {
772                             if (input.get('checked')) {
773                                 module = input.get('value');
774                             }
775                         });
776                         if (!module) {
777                             return;
778                         }
779                     } else {
780                         module = type.handlers[0].module;
781                     }
782                     panel.hide();
783                     // Do the upload
784                     self.upload_item(name, type.type, contents, section, sectionnumber, module);
785                 },
786                 section: Y.WidgetStdMod.FOOTER
787             },{
788                 value: M.util.get_string('cancel', 'moodle'),
789                 action: function(e) {
790                     e.preventDefault();
791                     panel.hide();
792                 },
793                 section: Y.WidgetStdMod.FOOTER
794             }]
795         });
796         // When the panel is hidden - destroy it and then check for other pending uploads
797         panel.after("visibleChange", function(e) {
798             if (!panel.get('visible')) {
799                 panel.destroy(true);
800                 self.check_upload_queue();
801             }
802         });
803         // Focus on the 'name' box
804         Y.one('#'+nameid).focus();
805     },
807     /**
808      * Upload any data types that are not files: display a dummy resource element, send
809      * the data to the server, update the progress bar for the file, then replace the
810      * dummy element with the real information once the AJAX call completes
811      * @param name the display name for the resource / activity to create
812      * @param type the details of the data type found in the drop event
813      * @param contents the actual data that was dropped
814      * @param section the DOM element representing the selected course section
815      * @param sectionnumber the number of the selected course section
816      */
817     upload_item: function(name, type, contents, section, sectionnumber, module) {
819         // This would be an ideal place to use the Y.io function
820         // however, this does not support data encoded using the
821         // FormData object, which is needed to transfer data from
822         // the DataTransfer object into an XMLHTTPRequest
823         // This can be converted when the YUI issue has been integrated:
824         // http://yuilibrary.com/projects/yui3/ticket/2531274
825         var xhr = new XMLHttpRequest();
826         var self = this;
828         // Add the item to the display
829         var resel = this.add_resource_element(name, section);
831         // Wait for the AJAX call to complete, then update the
832         // dummy element with the returned details
833         xhr.onreadystatechange = function() {
834             if (xhr.readyState == 4) {
835                 if (xhr.status == 200) {
836                     var result = JSON.parse(xhr.responseText);
837                     if (result) {
838                         if (result.error == 0) {
839                             // All OK - update the dummy element
840                             resel.icon.src = result.icon;
841                             resel.a.href = result.link;
842                             resel.namespan.innerHTML = result.name;
843                             resel.div.removeChild(resel.progressouter);
844                             resel.li.id = result.elementid;
845                             resel.div.innerHTML += result.commands;
846                             if (result.onclick) {
847                                 resel.a.onclick = result.onclick;
848                             }
849                             self.add_editing(result.elementid, sectionnumber);
850                         } else {
851                             // Error - remove the dummy element
852                             resel.parent.removeChild(resel.li);
853                             alert(result.error);
854                         }
855                     }
856                 } else {
857                     alert(M.util.get_string('servererror', 'moodle'));
858                 }
859             }
860         };
862         // Prepare the data to send
863         var formData = new FormData();
864         formData.append('contents', contents);
865         formData.append('displayname', name);
866         formData.append('sesskey', M.cfg.sesskey);
867         formData.append('course', this.courseid);
868         formData.append('section', sectionnumber);
869         formData.append('type', type);
870         formData.append('module', module);
872         // Send the data
873         xhr.open("POST", this.url, true);
874         xhr.send(formData);
875     },
877     /**
878      * Call the AJAX course editing initialisation to add the editing tools
879      * to the newly-created resource link
880      * @param elementid the id of the DOM element containing the new resource link
881      * @param sectionnumber the number of the selected course section
882      */
883     add_editing: function(elementid) {
884         YUI().use('moodle-course-coursebase', function(Y) {
885             M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
886         });
887     }
888 };