MDL-22504 Improved drag and drop status message, fixed text dragged from firefox...
[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         this.add_status_div();
96     },
98     /**
99      * Add a div element to tell the user that drag and drop upload
100      * is available (or to explain why it is not available)
101      */
102     add_status_div: function() {
103         var div = document.createElement('div');
104         div.id = 'dndupload-status';
105         var coursecontents = document.getElementById(this.pagecontentid);
106         if (coursecontents) {
107             coursecontents.insertBefore(div, coursecontents.firstChild);
108         }
109         div = this.Y.one(div);
111         var handlefile = (this.handlers.filehandlers.length > 0);
112         var handletext = false;
113         var handlelink = false;
114         var i;
115         for (i=0; i<this.handlers.types.length; i++) {
116             switch (this.handlers.types[i].identifier) {
117             case 'text':
118             case 'text/html':
119                 handletext = true;
120                 break;
121             case 'url':
122                 handlelink = true;
123                 break;
124             }
125         }
126         $msgident = 'dndworking';
127         if (handlefile) {
128             $msgident += 'file';
129         }
130         if (handletext) {
131             $msgident += 'text';
132         }
133         if (handlelink) {
134             $msgident += 'link';
135         }
136         div.setContent(M.util.get_string($msgident, 'moodle'));
137     },
139     /**
140      * Check the browser has the required functionality
141      * @return true if browser supports drag/drop upload
142      */
143     browser_supported: function() {
144         if (typeof FileReader == 'undefined') {
145             return false;
146         }
147         if (typeof FormData == 'undefined') {
148             return false;
149         }
150         return true;
151     },
153     /**
154      * Initialise drag events on node container, all events need
155      * to be processed for drag and drop to work
156      * @param el the element to add events to
157      */
158     init_events: function(el) {
159         this.Y.on('dragenter', this.drag_enter, el, this);
160         this.Y.on('dragleave', this.drag_leave, el, this);
161         this.Y.on('dragover',  this.drag_over,  el, this);
162         this.Y.on('drop',      this.drop,       el, this);
163     },
165     /**
166      * Work out which course section a given element is in
167      * @param el the child DOM element within the section
168      * @return the DOM element representing the section
169      */
170     get_section: function(el) {
171         var sectionclasses = this.sectionclasses;
172         return el.ancestor( function(test) {
173             var i;
174             for (i=0; i<sectionclasses.length; i++) {
175                 if (!test.hasClass(sectionclasses[i])) {
176                     return false;
177                 }
178                 return true;
179             }
180         }, true);
181     },
183     /**
184      * Work out the number of the section we have been dropped on to, from the section element
185      * @param DOMElement section the selected section
186      * @return int the section number
187      */
188     get_section_number: function(section) {
189         var sectionid = section.get('id').split('-');
190         if (sectionid.length < 2 || sectionid[0] != 'section') {
191             return false;
192         }
193         return parseInt(sectionid[1]);
194     },
196     /**
197      * Check if the event includes data of the given type
198      * @param e the event details
199      * @param type the data type to check for
200      * @return true if the data type is found in the event data
201      */
202     types_includes: function(e, type) {
203         var i;
204         var types = e._event.dataTransfer.types;
205         for (i=0; i<types.length; i++) {
206             if (types[i] == type) {
207                 return true;
208             }
209         }
210         return false;
211     },
213     /**
214      * Look through the event data, checking it against the registered data types
215      * (in order of priority) and return details of the first matching data type
216      * @param e the event details
217      * @return mixed false if not found or an object {
218      *           realtype: the type as given by the browser
219      *           addmessage: the message to show to the user during dragging
220      *           namemessage: the message for requesting a name for the resource from the user
221      *           type: the identifier of the type (may match several 'realtype's)
222      *           }
223      */
224     drag_type: function(e) {
225         // Check there is some data attached.
226         if (e._event.dataTransfer === null) {
227             return false;
228         }
229         if (e._event.dataTransfer.types === null) {
230             return false;
231         }
232         if (e._event.dataTransfer.types.length == 0) {
233             return false;
234         }
236         // Check for files first.
237         if (this.types_includes(e, 'Files')) {
238             if (this.handlers.filehandlers.length == 0) {
239                 return false; // No available file handlers - ignore this drag.
240             }
241             return {
242                 realtype: 'Files',
243                 addmessage: M.util.get_string('addfilehere', 'moodle'),
244                 namemessage: null, // Should not be asked for anyway
245                 type: 'Files'
246             };
247         }
249         // Check each of the registered types.
250         var types = this.handlers.types;
251         for (var i=0; i<types.length; i++) {
252             // Check each of the different identifiers for this type
253             var dttypes = types[i].datatransfertypes;
254             for (var j=0; j<dttypes.length; j++) {
255                 if (this.types_includes(e, dttypes[j])) {
256                     return {
257                         realtype: dttypes[j],
258                         addmessage: types[i].addmessage,
259                         namemessage: types[i].namemessage,
260                         type: types[i].identifier,
261                         handlers: types[i].handlers
262                     };
263                 }
264             }
265         }
266         return false; // No types we can handle
267     },
269     /**
270      * Check the content of the drag/drop includes a type we can handle, then, if
271      * it is, notify the browser that we want to handle it
272      * @param event e
273      * @return string type of the event or false
274      */
275     check_drag: function(e) {
276         var type = this.drag_type(e);
277         if (type) {
278             // Notify browser that we will handle this drag/drop
279             e.stopPropagation();
280             e.preventDefault();
281         }
282         return type;
283     },
285     /**
286      * Handle a dragenter event: add a suitable 'add here' message
287      * when a drag event occurs, containing a registered data type
288      * @param e event data
289      * @return false to prevent the event from continuing to be processed
290      */
291     drag_enter: function(e) {
292         if (!(type = this.check_drag(e))) {
293             return false;
294         }
296         var section = this.get_section(e.currentTarget);
297         if (!section) {
298             return false;
299         }
301         if (this.currentsection && this.currentsection != section) {
302             this.currentsection = section;
303             this.entercount = 1;
304         } else {
305             this.entercount++;
306             if (this.entercount > 2) {
307                 this.entercount = 2;
308                 return false;
309             }
310         }
312         this.show_preview_element(section, type);
314         return false;
315     },
317     /**
318      * Handle a dragleave event: remove the 'add here' message (if present)
319      * @param e event data
320      * @return false to prevent the event from continuing to be processed
321      */
322     drag_leave: function(e) {
323         if (!this.check_drag(e)) {
324             return false;
325         }
327         this.entercount--;
328         if (this.entercount == 1) {
329             return false;
330         }
331         this.entercount = 0;
332         this.currentsection = null;
334         this.hide_preview_element();
335         return false;
336     },
338     /**
339      * Handle a dragover event: just prevent the browser default (necessary
340      * to allow drag and drop handling to work)
341      * @param e event data
342      * @return false to prevent the event from continuing to be processed
343      */
344     drag_over: function(e) {
345         this.check_drag(e);
346         return false;
347     },
349     /**
350      * Handle a drop event: hide the 'add here' message, check the attached
351      * data type and start the upload process
352      * @param e event data
353      * @return false to prevent the event from continuing to be processed
354      */
355     drop: function(e) {
356         if (!(type = this.check_drag(e))) {
357             return false;
358         }
360         this.hide_preview_element();
362         // Work out the number of the section we are on (from its id)
363         var section = this.get_section(e.currentTarget);
364         var sectionnumber = this.get_section_number(section);
366         // Process the file or the included data
367         if (type.type == 'Files') {
368             var files = e._event.dataTransfer.files;
369             for (var i=0, f; f=files[i]; i++) {
370                 this.handle_file(f, section, sectionnumber);
371             }
372         } else {
373             var contents = e._event.dataTransfer.getData(type.realtype);
374             if (contents) {
375                 this.handle_item(type, contents, section, sectionnumber);
376             }
377         }
379         return false;
380     },
382     /**
383      * Find or create the 'ul' element that contains all of the module
384      * instances in this section
385      * @param section the DOM element representing the section
386      * @return false to prevent the event from continuing to be processed
387      */
388     get_mods_element: function(section) {
389         // Find the 'ul' containing the list of mods
390         var modsel = section.one(this.modslistselector);
391         if (!modsel) {
392             // Create the above 'ul' if it doesn't exist
393             var modsel = document.createElement('ul');
394             modsel.className = 'section img-text';
395             var contentel = section.get('children').pop();
396             var brel = contentel.get('children').pop();
397             contentel.insertBefore(modsel, brel);
398             modsel = this.Y.one(modsel);
399         }
401         return modsel;
402     },
404     /**
405      * Add a new dummy item to the list of mods, to be replaced by a real
406      * item & link once the AJAX upload call has completed
407      * @param name the label to show in the element
408      * @param section the DOM element reperesenting the course section
409      * @return DOM element containing the new item
410      */
411     add_resource_element: function(name, section) {
412         var modsel = this.get_mods_element(section);
414         var resel = {
415             parent: modsel,
416             li: document.createElement('li'),
417             div: document.createElement('div'),
418             a: document.createElement('a'),
419             icon: document.createElement('img'),
420             namespan: document.createElement('span'),
421             progressouter: document.createElement('span'),
422             progress: document.createElement('span')
423         };
425         resel.li.className = 'activity resource modtype_resource';
427         resel.div.className = 'mod-indent';
428         resel.li.appendChild(resel.div);
430         resel.a.href = '#';
431         resel.div.appendChild(resel.a);
433         resel.icon.src = M.util.image_url('i/ajaxloader');
434         resel.icon.className = 'activityicon';
435         resel.a.appendChild(resel.icon);
437         resel.a.appendChild(document.createTextNode(' '));
439         resel.namespan.className = 'instancename';
440         resel.namespan.innerHTML = name;
441         resel.a.appendChild(resel.namespan);
443         resel.div.appendChild(document.createTextNode(' '));
445         resel.progressouter.className = 'dndupload-progress-outer';
446         resel.progress.className = 'dndupload-progress-inner';
447         resel.progress.innerHTML = '&nbsp;';
448         resel.progressouter.appendChild(resel.progress);
449         resel.div.appendChild(resel.progressouter);
451         modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
453         return resel;
454     },
456     /**
457      * Hide any visible dndupload-preview elements on the page
458      */
459     hide_preview_element: function() {
460         this.Y.all('li.dndupload-preview').addClass('dndupload-hidden');
461     },
463     /**
464      * Unhide the preview element for the given section and set it to display
465      * the correct message
466      * @param section the YUI node representing the selected course section
467      * @param type the details of the data type detected in the drag (including the message to display)
468      */
469     show_preview_element: function(section, type) {
470         this.hide_preview_element();
471         var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
472         preview.one('span').setContent(type.addmessage);
473     },
475     /**
476      * Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
477      * is called, otherwise Firefox will ignore events generated when the mouse is over the preview
478      * element (instead of passing them up to the parent element)
479      * @param section the YUI node representing the selected course section
480      */
481     add_preview_element: function(section) {
482         var modsel = this.get_mods_element(section);
483         var preview = {
484             li: document.createElement('li'),
485             div: document.createElement('div'),
486             icon: document.createElement('img'),
487             namespan: document.createElement('span')
488         };
490         preview.li.className = 'dndupload-preview activity resource modtype_resource dndupload-hidden';
492         preview.div.className = 'mod-indent';
493         preview.li.appendChild(preview.div);
495         preview.icon.src = M.util.image_url('t/addfile');
496         preview.div.appendChild(preview.icon);
498         preview.div.appendChild(document.createTextNode(' '));
500         preview.namespan.className = 'instancename';
501         preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
502         preview.div.appendChild(preview.namespan);
504         modsel.appendChild(preview.li);
505     },
507     /**
508      * Find the registered handler for the given file type. If there is more than one, ask the
509      * user which one to use. Then upload the file to the server
510      * @param file the details of the file, taken from the FileList in the drop event
511      * @param section the DOM element representing the selected course section
512      * @param sectionnumber the number of the selected course section
513      */
514     handle_file: function(file, section, sectionnumber) {
515         var handlers = new Array();
516         var filehandlers = this.handlers.filehandlers;
517         var extension = '';
518         var dotpos = file.name.lastIndexOf('.');
519         if (dotpos != -1) {
520             extension = file.name.substr(dotpos+1, file.name.length);
521         }
523         for (var i=0; i<filehandlers.length; i++) {
524             if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
525                 handlers.push(filehandlers[i]);
526             }
527         }
529         if (handlers.length == 0) {
530             // No handlers at all (not even 'resource'?)
531             return;
532         }
534         if (handlers.length == 1) {
535             this.upload_file(file, section, sectionnumber, handlers[0].module);
536             return;
537         }
539         this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
540     },
542     /**
543      * Show a dialog box, allowing the user to choose what to do with the file they are uploading
544      * @param handlers the available handlers to choose between
545      * @param extension the extension of the file being uploaded
546      * @param file the File object being uploaded
547      * @param section the DOM element of the section being uploaded to
548      * @param sectionnumber the number of the selected course section
549      */
550     file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
551         if (this.uploaddialog) {
552             var details = new Object();
553             details.isfile = true;
554             details.handlers = handlers;
555             details.extension = extension;
556             details.file = file;
557             details.section = section;
558             details.sectionnumber = sectionnumber;
559             this.uploadqueue.push(details);
560             return;
561         }
562         this.uploaddialog = true;
564         var timestamp = new Date().getTime();
565         var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
566         var content = '';
567         var sel;
568         if (extension in this.lastselected) {
569             sel = this.lastselected[extension];
570         } else {
571             sel = handlers[0].module;
572         }
573         content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
574         content += '<div id="dndupload_handlers'+uploadid+'">';
575         for (var i=0; i<handlers.length; i++) {
576             var id = 'dndupload_handler'+uploadid+handlers[i].module;
577             var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
578             content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
579             content += ' <label for="'+id+'">';
580             content += handlers[i].message;
581             content += '</label><br/>';
582         }
583         content += '</div>';
585         var Y = this.Y;
586         var self = this;
587         var panel = new Y.Panel({
588             bodyContent: content,
589             width: 350,
590             zIndex: 5,
591             centered: true,
592             modal: true,
593             visible: true,
594             render: true,
595             buttons: [{
596                 value: M.util.get_string('upload', 'moodle'),
597                 action: function(e) {
598                     e.preventDefault();
599                     // Find out which module was selected
600                     var module = false;
601                     var div = Y.one('#dndupload_handlers'+uploadid);
602                     div.all('input').each(function(input) {
603                         if (input.get('checked')) {
604                             module = input.get('value');
605                         }
606                     });
607                     if (!module) {
608                         return;
609                     }
610                     panel.hide();
611                     // Remember this selection for next time
612                     self.lastselected[extension] = module;
613                     // Do the upload
614                     self.upload_file(file, section, sectionnumber, module);
615                 },
616                 section: Y.WidgetStdMod.FOOTER
617             },{
618                 value: M.util.get_string('cancel', 'moodle'),
619                 action: function(e) {
620                     e.preventDefault();
621                     panel.hide();
622                 },
623                 section: Y.WidgetStdMod.FOOTER
624             }]
625         });
626         // When the panel is hidden - destroy it and then check for other pending uploads
627         panel.after("visibleChange", function(e) {
628             if (!panel.get('visible')) {
629                 panel.destroy(true);
630                 self.check_upload_queue();
631             }
632         });
633     },
635     /**
636      * Check to see if there are any other dialog boxes to show, now that the current one has
637      * been dealt with
638      */
639     check_upload_queue: function() {
640         this.uploaddialog = false;
641         if (this.uploadqueue.length == 0) {
642             return;
643         }
645         var details = this.uploadqueue.shift();
646         if (details.isfile) {
647             this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
648         } else {
649             this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
650         }
651     },
653     /**
654      * Do the file upload: show the dummy element, use an AJAX call to send the data
655      * to the server, update the progress bar for the file, then replace the dummy
656      * element with the real information once the AJAX call completes
657      * @param file the details of the file, taken from the FileList in the drop event
658      * @param section the DOM element representing the selected course section
659      * @param sectionnumber the number of the selected course section
660      */
661     upload_file: function(file, section, sectionnumber, module) {
663         // This would be an ideal place to use the Y.io function
664         // however, this does not support data encoded using the
665         // FormData object, which is needed to transfer data from
666         // the DataTransfer object into an XMLHTTPRequest
667         // This can be converted when the YUI issue has been integrated:
668         // http://yuilibrary.com/projects/yui3/ticket/2531274
669         var xhr = new XMLHttpRequest();
670         var self = this;
672         if (file.size > this.maxbytes) {
673             alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle'));
674             return;
675         }
677         // Add the file to the display
678         var resel = this.add_resource_element(file.name, section);
680         // Update the progress bar as the file is uploaded
681         xhr.upload.addEventListener('progress', function(e) {
682             if (e.lengthComputable) {
683                 var percentage = Math.round((e.loaded * 100) / e.total);
684                 resel.progress.style.width = percentage + '%';
685             }
686         }, false);
688         // Wait for the AJAX call to complete, then update the
689         // dummy element with the returned details
690         xhr.onreadystatechange = function() {
691             if (xhr.readyState == 4) {
692                 if (xhr.status == 200) {
693                     var result = JSON.parse(xhr.responseText);
694                     if (result) {
695                         if (result.error == 0) {
696                             // All OK - update the dummy element
697                             resel.icon.src = result.icon;
698                             resel.a.href = result.link;
699                             resel.namespan.innerHTML = result.name;
700                             resel.div.removeChild(resel.progressouter);
701                             resel.li.id = result.elementid;
702                             resel.div.innerHTML += result.commands;
703                             if (result.onclick) {
704                                 resel.a.onclick = result.onclick;
705                             }
706                             self.add_editing(result.elementid);
707                         } else {
708                             // Error - remove the dummy element
709                             resel.parent.removeChild(resel.li);
710                             alert(result.error);
711                         }
712                     }
713                 } else {
714                     alert(M.util.get_string('servererror', 'moodle'));
715                 }
716             }
717         };
719         // Prepare the data to send
720         var formData = new FormData();
721         formData.append('repo_upload_file', file);
722         formData.append('sesskey', M.cfg.sesskey);
723         formData.append('course', this.courseid);
724         formData.append('section', sectionnumber);
725         formData.append('module', module);
726         formData.append('type', 'Files');
728         // Send the AJAX call
729         xhr.open("POST", this.url, true);
730         xhr.send(formData);
731     },
733     /**
734      * Show a dialog box to gather the name of the resource / activity to be created
735      * from the uploaded content
736      * @param type the details of the type of content
737      * @param contents the contents to be uploaded
738      * @section the DOM element for the section being uploaded to
739      * @sectionnumber the number of the section being uploaded to
740      */
741     handle_item: function(type, contents, section, sectionnumber) {
742         if (type.handlers.length == 0) {
743             // Nothing to handle this - should not have got here
744             return;
745         }
747         if (this.uploaddialog) {
748             var details = new Object();
749             details.isfile = false;
750             details.type = type;
751             details.contents = contents;
752             details.section = section;
753             details.setcionnumber = sectionnumber;
754             this.uploadqueue.push(details);
755             return;
756         }
757         this.uploaddialog = true;
759         var timestamp = new Date().getTime();
760         var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
761         var nameid = 'dndupload_handler_name'+uploadid;
762         var content = '';
763         content += '<label for="'+nameid+'">'+type.namemessage+'</label>';
764         content += ' <input type="text" id="'+nameid+'" value="" />';
765         if (type.handlers.length > 1) {
766             content += '<div id="dndupload_handlers'+uploadid+'">';
767             var sel = type.handlers[0].module;
768             for (var i=0; i<type.handlers.length; i++) {
769                 var id = 'dndupload_handler'+uploadid;
770                 var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
771                 content += '<input type="radio" name="handler" value="'+type.handlers[i].module+'" id="'+id+'" '+checked+'/>';
772                 content += ' <label for="'+id+'">';
773                 content += type.handlers[i].message;
774                 content += '</label><br/>';
775             }
776             content += '</div>';
777         }
779         var Y = this.Y;
780         var self = this;
781         var panel = new Y.Panel({
782             bodyContent: content,
783             width: 350,
784             zIndex: 5,
785             centered: true,
786             modal: true,
787             visible: true,
788             render: true,
789             buttons: [{
790                 value: M.util.get_string('upload', 'moodle'),
791                 action: function(e) {
792                     e.preventDefault();
793                     var name = Y.one('#dndupload_handler_name'+uploadid).get('value');
794                     name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim
795                     if (name == '') {
796                         return;
797                     }
798                     var module = false;
799                     if (type.handlers.length > 1) {
800                         // Find out which module was selected
801                         var div = Y.one('#dndupload_handlers'+uploadid);
802                         div.all('input').each(function(input) {
803                             if (input.get('checked')) {
804                                 module = input.get('value');
805                             }
806                         });
807                         if (!module) {
808                             return;
809                         }
810                     } else {
811                         module = type.handlers[0].module;
812                     }
813                     panel.hide();
814                     // Do the upload
815                     self.upload_item(name, type.type, contents, section, sectionnumber, module);
816                 },
817                 section: Y.WidgetStdMod.FOOTER
818             },{
819                 value: M.util.get_string('cancel', 'moodle'),
820                 action: function(e) {
821                     e.preventDefault();
822                     panel.hide();
823                 },
824                 section: Y.WidgetStdMod.FOOTER
825             }]
826         });
827         // When the panel is hidden - destroy it and then check for other pending uploads
828         panel.after("visibleChange", function(e) {
829             if (!panel.get('visible')) {
830                 panel.destroy(true);
831                 self.check_upload_queue();
832             }
833         });
834         // Focus on the 'name' box
835         Y.one('#'+nameid).focus();
836     },
838     /**
839      * Upload any data types that are not files: display a dummy resource element, send
840      * the data to the server, update the progress bar for the file, then replace the
841      * dummy element with the real information once the AJAX call completes
842      * @param name the display name for the resource / activity to create
843      * @param type the details of the data type found in the drop event
844      * @param contents the actual data that was dropped
845      * @param section the DOM element representing the selected course section
846      * @param sectionnumber the number of the selected course section
847      */
848     upload_item: function(name, type, contents, section, sectionnumber, module) {
850         // This would be an ideal place to use the Y.io function
851         // however, this does not support data encoded using the
852         // FormData object, which is needed to transfer data from
853         // the DataTransfer object into an XMLHTTPRequest
854         // This can be converted when the YUI issue has been integrated:
855         // http://yuilibrary.com/projects/yui3/ticket/2531274
856         var xhr = new XMLHttpRequest();
857         var self = this;
859         // Add the item to the display
860         var resel = this.add_resource_element(name, section);
862         // Wait for the AJAX call to complete, then update the
863         // dummy element with the returned details
864         xhr.onreadystatechange = function() {
865             if (xhr.readyState == 4) {
866                 if (xhr.status == 200) {
867                     var result = JSON.parse(xhr.responseText);
868                     if (result) {
869                         if (result.error == 0) {
870                             // All OK - update the dummy element
871                             resel.icon.src = result.icon;
872                             resel.a.href = result.link;
873                             resel.namespan.innerHTML = result.name;
874                             resel.div.removeChild(resel.progressouter);
875                             resel.li.id = result.elementid;
876                             resel.div.innerHTML += result.commands;
877                             if (result.onclick) {
878                                 resel.a.onclick = result.onclick;
879                             }
880                             self.add_editing(result.elementid, sectionnumber);
881                         } else {
882                             // Error - remove the dummy element
883                             resel.parent.removeChild(resel.li);
884                             alert(result.error);
885                         }
886                     }
887                 } else {
888                     alert(M.util.get_string('servererror', 'moodle'));
889                 }
890             }
891         };
893         // Prepare the data to send
894         var formData = new FormData();
895         formData.append('contents', contents);
896         formData.append('displayname', name);
897         formData.append('sesskey', M.cfg.sesskey);
898         formData.append('course', this.courseid);
899         formData.append('section', sectionnumber);
900         formData.append('type', type);
901         formData.append('module', module);
903         // Contents from Firefox have their unicode byte-order reversed, swap back and
904         // provide as an alternative.
905         if (type == 'text/html') {
906             var fixedcontents = '';
907             for (i=0; i<contents.length; i+=2) {
908                 var val = contents.charCodeAt(i+1) * 256 + contents.charCodeAt(i);
909                 fixedcontents += String.fromCharCode(val);
910             }
911             formData.append('fixedcontents', fixedcontents);
912         }
914         // Send the data
915         xhr.open("POST", this.url, true);
916         xhr.send(formData);
917     },
919     /**
920      * Call the AJAX course editing initialisation to add the editing tools
921      * to the newly-created resource link
922      * @param elementid the id of the DOM element containing the new resource link
923      * @param sectionnumber the number of the selected course section
924      */
925     add_editing: function(elementid) {
926         YUI().use('moodle-course-coursebase', function(Y) {
927             M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
928         });
929     }
930 };