MDL-22504 Improved drag and drop status message, fixed text dragged from firefox...
[moodle.git] / course / dndupload.js
CommitLineData
32528f94
DS
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/>.
15
16/**
17 * Javascript library for enableing a drag and drop upload to courses
18 *
a0a06d05 19 * @package core
32528f94
DS
20 * @subpackage course
21 * @copyright 2012 Davo Smith
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24M.course_dndupload = {
25 // YUI object.
26 Y: null,
27 // URL for upload requests
33b24bdd 28 url: M.cfg.wwwroot + '/course/dndupload.php',
32528f94
DS
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,
48
49 // The following are used to identify specific parts of the course page
50
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',
60
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;
74
75 if (!this.browser_supported()) {
76 return; // Browser does not support the required functionality
77 }
78
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();
84
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);
94
b64300fc 95 this.add_status_div();
32528f94
DS
96 },
97
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)
32528f94
DS
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 }
b64300fc
DS
109 div = this.Y.one(div);
110
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'));
32528f94
DS
137 },
138
139 /**
140 * Check the browser has the required functionality
141 * @return true if browser supports drag/drop upload
142 */
143 browser_supported: function() {
32528f94
DS
144 if (typeof FileReader == 'undefined') {
145 return false;
146 }
147 if (typeof FormData == 'undefined') {
148 return false;
149 }
150 return true;
151 },
152
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 },
164
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 },
182
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 },
195
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 },
212
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) {
b64300fc
DS
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 }
235
236 // Check for files first.
32528f94 237 if (this.types_includes(e, 'Files')) {
5103b5e6
DS
238 if (this.handlers.filehandlers.length == 0) {
239 return false; // No available file handlers - ignore this drag.
240 }
32528f94
DS
241 return {
242 realtype: 'Files',
33b24bdd 243 addmessage: M.util.get_string('addfilehere', 'moodle'),
32528f94
DS
244 namemessage: null, // Should not be asked for anyway
245 type: 'Files'
246 };
247 }
248
b64300fc 249 // Check each of the registered types.
32528f94
DS
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 },
268
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 },
284
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 }
295
296 var section = this.get_section(e.currentTarget);
297 if (!section) {
298 return false;
299 }
300
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 }
311
312 this.show_preview_element(section, type);
313
314 return false;
315 },
316
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 }
326
327 this.entercount--;
328 if (this.entercount == 1) {
329 return false;
330 }
331 this.entercount = 0;
332 this.currentsection = null;
333
334 this.hide_preview_element();
335 return false;
336 },
337
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 },
348
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 }
359
360 this.hide_preview_element();
361
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);
365
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 }
378
379 return false;
380 },
381
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 }
400
401 return modsel;
402 },
403
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);
413
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 };
424
425 resel.li.className = 'activity resource modtype_resource';
426
427 resel.div.className = 'mod-indent';
428 resel.li.appendChild(resel.div);
429
430 resel.a.href = '#';
431 resel.div.appendChild(resel.a);
432
433 resel.icon.src = M.util.image_url('i/ajaxloader');
b64300fc 434 resel.icon.className = 'activityicon';
32528f94
DS
435 resel.a.appendChild(resel.icon);
436
437 resel.a.appendChild(document.createTextNode(' '));
438
439 resel.namespan.className = 'instancename';
440 resel.namespan.innerHTML = name;
441 resel.a.appendChild(resel.namespan);
442
443 resel.div.appendChild(document.createTextNode(' '));
444
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);
450
451 modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
452
453 return resel;
454 },
455
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 },
462
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 },
474
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 };
489
490 preview.li.className = 'dndupload-preview activity resource modtype_resource dndupload-hidden';
491
492 preview.div.className = 'mod-indent';
493 preview.li.appendChild(preview.div);
494
495 preview.icon.src = M.util.image_url('t/addfile');
496 preview.div.appendChild(preview.icon);
497
498 preview.div.appendChild(document.createTextNode(' '));
499
500 preview.namespan.className = 'instancename';
33b24bdd 501 preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
32528f94
DS
502 preview.div.appendChild(preview.namespan);
503
504 modsel.appendChild(preview.li);
505 },
506
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 }
522
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 }
528
529 if (handlers.length == 0) {
530 // No handlers at all (not even 'resource'?)
531 return;
532 }
533
534 if (handlers.length == 1) {
535 this.upload_file(file, section, sectionnumber, handlers[0].module);
536 return;
537 }
538
539 this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
540 },
541
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;
563
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 }
33b24bdd 573 content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
32528f94
DS
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>';
584
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: [{
a0a06d05 596 value: M.util.get_string('upload', 'moodle'),
32528f94
DS
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 },{
a0a06d05 618 value: M.util.get_string('cancel', 'moodle'),
32528f94
DS
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 },
634
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 }
644
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 },
652
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) {
662
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;
671
672 if (file.size > this.maxbytes) {
33b24bdd 673 alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle'));
32528f94
DS
674 return;
675 }
676
677 // Add the file to the display
678 var resel = this.add_resource_element(file.name, section);
679
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);
687
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 {
33b24bdd 714 alert(M.util.get_string('servererror', 'moodle'));
32528f94
DS
715 }
716 }
717 };
718
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');
727
728 // Send the AJAX call
729 xhr.open("POST", this.url, true);
730 xhr.send(formData);
731 },
732
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 }
746
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;
758
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 }
778
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: [{
a0a06d05 790 value: M.util.get_string('upload', 'moodle'),
32528f94
DS
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 },{
a0a06d05 819 value: M.util.get_string('cancel', 'moodle'),
32528f94
DS
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 },
837
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) {
849
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;
858
859 // Add the item to the display
860 var resel = this.add_resource_element(name, section);
861
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 {
33b24bdd 888 alert(M.util.get_string('servererror', 'moodle'));
32528f94
DS
889 }
890 }
891 };
892
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);
902
b64300fc
DS
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 }
913
32528f94
DS
914 // Send the data
915 xhr.open("POST", this.url, true);
916 xhr.send(formData);
917 },
918
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};