MDL-67264 core_course: Activity chooser new feature
[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)
6c0ae99b 56 pagecontentid: 'page',
32528f94
DS
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',
0b245bf3
HN
60 // Original onbeforeunload event.
61 originalUnloadEvent: null,
32528f94
DS
62
63 /**
64 * Initalise the drag and drop upload interface
65 * Note: one and only one of options.filemanager and options.formcallback must be defined
66 *
67 * @param Y the YUI object
68 * @param object options {
69 * courseid: ID of the course we are on
70 * maxbytes: maximum size of files allowed in this form
71 * handlers: Data about the different file/data handlers that are available
72 * }
73 */
74 init: function(Y, options) {
75 this.Y = Y;
76
77 if (!this.browser_supported()) {
78 return; // Browser does not support the required functionality
79 }
80
81 this.maxbytes = options.maxbytes;
82 this.courseid = options.courseid;
83 this.handlers = options.handlers;
84 this.uploadqueue = new Array();
85 this.lastselected = new Array();
86
87 var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.');
88 var sections = this.Y.all(sectionselector);
89 if (sections.isEmpty()) {
90 return; // No sections - incompatible course format or front page.
91 }
92 sections.each( function(el) {
93 this.add_preview_element(el);
94 this.init_events(el);
95 }, this);
96
6c0ae99b
DS
97 if (options.showstatus) {
98 this.add_status_div();
99 }
32528f94
DS
100 },
101
102 /**
103 * Add a div element to tell the user that drag and drop upload
104 * is available (or to explain why it is not available)
32528f94
DS
105 */
106 add_status_div: function() {
5a3e5fa0
SH
107 var Y = this.Y,
108 coursecontents = Y.one('#' + this.pagecontentid),
109 div,
110 handlefile = (this.handlers.filehandlers.length > 0),
111 handletext = false,
112 handlelink = false,
113 i = 0,
114 styletop,
115 styletopunit;
116
6c0ae99b
DS
117 if (!coursecontents) {
118 return;
32528f94 119 }
b64300fc 120
5a3e5fa0
SH
121 div = Y.Node.create('<div id="dndupload-status"></div>').setStyle('opacity', '0.0');
122 coursecontents.insert(div, 0);
6c0ae99b 123
5a3e5fa0 124 for (i = 0; i < this.handlers.types.length; i++) {
b64300fc 125 switch (this.handlers.types[i].identifier) {
5a3e5fa0
SH
126 case 'text':
127 case 'text/html':
128 handletext = true;
129 break;
130 case 'url':
131 handlelink = true;
132 break;
b64300fc
DS
133 }
134 }
135 $msgident = 'dndworking';
136 if (handlefile) {
137 $msgident += 'file';
138 }
139 if (handletext) {
140 $msgident += 'text';
141 }
142 if (handlelink) {
143 $msgident += 'link';
144 }
145 div.setContent(M.util.get_string($msgident, 'moodle'));
6c0ae99b 146
5a3e5fa0
SH
147 styletop = div.getStyle('top') || '0px';
148 styletopunit = styletop.replace(/^\d+/, '');
149 styletop = parseInt(styletop.replace(/\D*$/, ''), 10);
150
a3657f8b 151 var fadein = new Y.Anim({
6c0ae99b
DS
152 node: '#dndupload-status',
153 from: {
154 opacity: 0.0,
5a3e5fa0 155 top: (styletop - 30).toString() + styletopunit
6c0ae99b
DS
156 },
157
158 to: {
159 opacity: 1.0,
5a3e5fa0 160 top: styletop.toString() + styletopunit
6c0ae99b
DS
161 },
162 duration: 0.5
163 });
a3657f8b
BO
164
165 var fadeout = new Y.Anim({
166 node: '#dndupload-status',
167 from: {
168 opacity: 1.0,
169 top: styletop.toString() + styletopunit
170 },
171
172 to: {
173 opacity: 0.0,
174 top: (styletop - 30).toString() + styletopunit
175 },
176 duration: 0.5
177 });
178
179 fadein.run();
180 fadein.on('end', function(e) {
181 Y.later(3000, this, function() {
182 fadeout.run();
183 });
184 });
185
186 fadeout.on('end', function(e) {
187 Y.one('#dndupload-status').remove(true);
6c0ae99b 188 });
32528f94
DS
189 },
190
191 /**
192 * Check the browser has the required functionality
193 * @return true if browser supports drag/drop upload
194 */
195 browser_supported: function() {
32528f94
DS
196 if (typeof FileReader == 'undefined') {
197 return false;
198 }
199 if (typeof FormData == 'undefined') {
200 return false;
201 }
202 return true;
203 },
204
205 /**
206 * Initialise drag events on node container, all events need
207 * to be processed for drag and drop to work
208 * @param el the element to add events to
209 */
210 init_events: function(el) {
211 this.Y.on('dragenter', this.drag_enter, el, this);
212 this.Y.on('dragleave', this.drag_leave, el, this);
213 this.Y.on('dragover', this.drag_over, el, this);
214 this.Y.on('drop', this.drop, el, this);
215 },
216
217 /**
218 * Work out which course section a given element is in
219 * @param el the child DOM element within the section
220 * @return the DOM element representing the section
221 */
222 get_section: function(el) {
223 var sectionclasses = this.sectionclasses;
224 return el.ancestor( function(test) {
225 var i;
226 for (i=0; i<sectionclasses.length; i++) {
227 if (!test.hasClass(sectionclasses[i])) {
228 return false;
229 }
230 return true;
231 }
232 }, true);
233 },
234
235 /**
236 * Work out the number of the section we have been dropped on to, from the section element
237 * @param DOMElement section the selected section
238 * @return int the section number
239 */
240 get_section_number: function(section) {
241 var sectionid = section.get('id').split('-');
242 if (sectionid.length < 2 || sectionid[0] != 'section') {
243 return false;
244 }
245 return parseInt(sectionid[1]);
246 },
247
248 /**
249 * Check if the event includes data of the given type
250 * @param e the event details
251 * @param type the data type to check for
252 * @return true if the data type is found in the event data
253 */
254 types_includes: function(e, type) {
255 var i;
256 var types = e._event.dataTransfer.types;
d2782804 257 type = type.toLowerCase();
32528f94 258 for (i=0; i<types.length; i++) {
d2782804
AN
259 if (!types.hasOwnProperty(i)) {
260 continue;
261 }
262 if (types[i].toLowerCase() === type) {
32528f94
DS
263 return true;
264 }
265 }
266 return false;
267 },
268
269 /**
270 * Look through the event data, checking it against the registered data types
271 * (in order of priority) and return details of the first matching data type
272 * @param e the event details
66079e28 273 * @return object|false - false if not found or an object {
32528f94
DS
274 * realtype: the type as given by the browser
275 * addmessage: the message to show to the user during dragging
276 * namemessage: the message for requesting a name for the resource from the user
277 * type: the identifier of the type (may match several 'realtype's)
278 * }
279 */
280 drag_type: function(e) {
b64300fc
DS
281 // Check there is some data attached.
282 if (e._event.dataTransfer === null) {
283 return false;
284 }
285 if (e._event.dataTransfer.types === null) {
286 return false;
287 }
288 if (e._event.dataTransfer.types.length == 0) {
289 return false;
290 }
291
292 // Check for files first.
32528f94 293 if (this.types_includes(e, 'Files')) {
5a4decbc
DS
294 if (e.type != 'drop' || e._event.dataTransfer.files.length != 0) {
295 if (this.handlers.filehandlers.length == 0) {
296 return false; // No available file handlers - ignore this drag.
297 }
298 return {
299 realtype: 'Files',
300 addmessage: M.util.get_string('addfilehere', 'moodle'),
301 namemessage: null, // Should not be asked for anyway
302 type: 'Files'
303 };
5103b5e6 304 }
32528f94
DS
305 }
306
b64300fc 307 // Check each of the registered types.
32528f94
DS
308 var types = this.handlers.types;
309 for (var i=0; i<types.length; i++) {
310 // Check each of the different identifiers for this type
311 var dttypes = types[i].datatransfertypes;
312 for (var j=0; j<dttypes.length; j++) {
313 if (this.types_includes(e, dttypes[j])) {
314 return {
315 realtype: dttypes[j],
316 addmessage: types[i].addmessage,
317 namemessage: types[i].namemessage,
66079e28 318 handlermessage: types[i].handlermessage,
32528f94
DS
319 type: types[i].identifier,
320 handlers: types[i].handlers
321 };
322 }
323 }
324 }
325 return false; // No types we can handle
326 },
327
328 /**
329 * Check the content of the drag/drop includes a type we can handle, then, if
330 * it is, notify the browser that we want to handle it
331 * @param event e
332 * @return string type of the event or false
333 */
334 check_drag: function(e) {
335 var type = this.drag_type(e);
336 if (type) {
337 // Notify browser that we will handle this drag/drop
338 e.stopPropagation();
339 e.preventDefault();
340 }
341 return type;
342 },
343
344 /**
345 * Handle a dragenter event: add a suitable 'add here' message
346 * when a drag event occurs, containing a registered data type
347 * @param e event data
348 * @return false to prevent the event from continuing to be processed
349 */
350 drag_enter: function(e) {
351 if (!(type = this.check_drag(e))) {
352 return false;
353 }
354
355 var section = this.get_section(e.currentTarget);
356 if (!section) {
357 return false;
358 }
359
360 if (this.currentsection && this.currentsection != section) {
361 this.currentsection = section;
362 this.entercount = 1;
363 } else {
364 this.entercount++;
365 if (this.entercount > 2) {
366 this.entercount = 2;
367 return false;
368 }
369 }
370
371 this.show_preview_element(section, type);
372
373 return false;
374 },
375
376 /**
377 * Handle a dragleave event: remove the 'add here' message (if present)
378 * @param e event data
379 * @return false to prevent the event from continuing to be processed
380 */
381 drag_leave: function(e) {
382 if (!this.check_drag(e)) {
383 return false;
384 }
385
386 this.entercount--;
387 if (this.entercount == 1) {
388 return false;
389 }
390 this.entercount = 0;
391 this.currentsection = null;
392
393 this.hide_preview_element();
394 return false;
395 },
396
397 /**
398 * Handle a dragover event: just prevent the browser default (necessary
399 * to allow drag and drop handling to work)
400 * @param e event data
401 * @return false to prevent the event from continuing to be processed
402 */
403 drag_over: function(e) {
404 this.check_drag(e);
405 return false;
406 },
407
408 /**
409 * Handle a drop event: hide the 'add here' message, check the attached
410 * data type and start the upload process
411 * @param e event data
412 * @return false to prevent the event from continuing to be processed
413 */
414 drop: function(e) {
4e737cf3
DS
415 this.hide_preview_element();
416
32528f94
DS
417 if (!(type = this.check_drag(e))) {
418 return false;
419 }
420
32528f94
DS
421 // Work out the number of the section we are on (from its id)
422 var section = this.get_section(e.currentTarget);
423 var sectionnumber = this.get_section_number(section);
424
425 // Process the file or the included data
426 if (type.type == 'Files') {
427 var files = e._event.dataTransfer.files;
428 for (var i=0, f; f=files[i]; i++) {
429 this.handle_file(f, section, sectionnumber);
430 }
431 } else {
432 var contents = e._event.dataTransfer.getData(type.realtype);
433 if (contents) {
434 this.handle_item(type, contents, section, sectionnumber);
435 }
436 }
437
438 return false;
439 },
440
441 /**
442 * Find or create the 'ul' element that contains all of the module
443 * instances in this section
444 * @param section the DOM element representing the section
445 * @return false to prevent the event from continuing to be processed
446 */
447 get_mods_element: function(section) {
448 // Find the 'ul' containing the list of mods
449 var modsel = section.one(this.modslistselector);
450 if (!modsel) {
451 // Create the above 'ul' if it doesn't exist
66079e28 452 modsel = document.createElement('ul');
32528f94
DS
453 modsel.className = 'section img-text';
454 var contentel = section.get('children').pop();
455 var brel = contentel.get('children').pop();
456 contentel.insertBefore(modsel, brel);
457 modsel = this.Y.one(modsel);
458 }
459
460 return modsel;
461 },
462
463 /**
464 * Add a new dummy item to the list of mods, to be replaced by a real
465 * item & link once the AJAX upload call has completed
466 * @param name the label to show in the element
467 * @param section the DOM element reperesenting the course section
468 * @return DOM element containing the new item
469 */
785e09a7 470 add_resource_element: function(name, section, module) {
32528f94
DS
471 var modsel = this.get_mods_element(section);
472
473 var resel = {
474 parent: modsel,
475 li: document.createElement('li'),
476 div: document.createElement('div'),
eacc63ab 477 indentdiv: document.createElement('div'),
32528f94
DS
478 a: document.createElement('a'),
479 icon: document.createElement('img'),
480 namespan: document.createElement('span'),
bc3f5bca 481 groupingspan: document.createElement('span'),
32528f94
DS
482 progressouter: document.createElement('span'),
483 progress: document.createElement('span')
484 };
485
785e09a7 486 resel.li.className = 'activity ' + module + ' modtype_' + module;
32528f94 487
eacc63ab
FM
488 resel.indentdiv.className = 'mod-indent';
489 resel.li.appendChild(resel.indentdiv);
490
491 resel.div.className = 'activityinstance';
492 resel.indentdiv.appendChild(resel.div);
32528f94
DS
493
494 resel.a.href = '#';
495 resel.div.appendChild(resel.a);
496
497 resel.icon.src = M.util.image_url('i/ajaxloader');
eacc63ab 498 resel.icon.className = 'activityicon iconlarge';
32528f94
DS
499 resel.a.appendChild(resel.icon);
500
32528f94
DS
501 resel.namespan.className = 'instancename';
502 resel.namespan.innerHTML = name;
503 resel.a.appendChild(resel.namespan);
504
bc3f5bca
RL
505 resel.groupingspan.className = 'groupinglabel';
506 resel.div.appendChild(resel.groupingspan);
507
32528f94
DS
508 resel.progressouter.className = 'dndupload-progress-outer';
509 resel.progress.className = 'dndupload-progress-inner';
510 resel.progress.innerHTML = '&nbsp;';
511 resel.progressouter.appendChild(resel.progress);
512 resel.div.appendChild(resel.progressouter);
513
514 modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
515
516 return resel;
517 },
518
519 /**
520 * Hide any visible dndupload-preview elements on the page
521 */
522 hide_preview_element: function() {
523 this.Y.all('li.dndupload-preview').addClass('dndupload-hidden');
66079e28 524 this.Y.all('.dndupload-over').removeClass('dndupload-over');
32528f94
DS
525 },
526
527 /**
528 * Unhide the preview element for the given section and set it to display
529 * the correct message
530 * @param section the YUI node representing the selected course section
531 * @param type the details of the data type detected in the drag (including the message to display)
532 */
533 show_preview_element: function(section, type) {
534 this.hide_preview_element();
535 var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
66079e28 536 section.addClass('dndupload-over');
413bca9f
DS
537
538 // Horrible work-around to allow the 'Add X here' text to be a drop target in Firefox.
09fd07fe 539 var node = preview.one('span').getDOMNode();
413bca9f 540 node.firstChild.nodeValue = type.addmessage;
32528f94
DS
541 },
542
543 /**
544 * Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
545 * is called, otherwise Firefox will ignore events generated when the mouse is over the preview
546 * element (instead of passing them up to the parent element)
547 * @param section the YUI node representing the selected course section
548 */
549 add_preview_element: function(section) {
550 var modsel = this.get_mods_element(section);
551 var preview = {
552 li: document.createElement('li'),
553 div: document.createElement('div'),
554 icon: document.createElement('img'),
555 namespan: document.createElement('span')
556 };
557
4d40dc92 558 preview.li.className = 'dndupload-preview dndupload-hidden';
32528f94
DS
559
560 preview.div.className = 'mod-indent';
561 preview.li.appendChild(preview.div);
562
563 preview.icon.src = M.util.image_url('t/addfile');
8a3b8918 564 preview.icon.className = 'icon';
32528f94
DS
565 preview.div.appendChild(preview.icon);
566
567 preview.div.appendChild(document.createTextNode(' '));
568
569 preview.namespan.className = 'instancename';
33b24bdd 570 preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
32528f94
DS
571 preview.div.appendChild(preview.namespan);
572
573 modsel.appendChild(preview.li);
574 },
575
576 /**
577 * Find the registered handler for the given file type. If there is more than one, ask the
578 * user which one to use. Then upload the file to the server
579 * @param file the details of the file, taken from the FileList in the drop event
580 * @param section the DOM element representing the selected course section
581 * @param sectionnumber the number of the selected course section
582 */
583 handle_file: function(file, section, sectionnumber) {
584 var handlers = new Array();
585 var filehandlers = this.handlers.filehandlers;
586 var extension = '';
587 var dotpos = file.name.lastIndexOf('.');
588 if (dotpos != -1) {
04a38bd3 589 extension = file.name.substr(dotpos+1, file.name.length).toLowerCase();
32528f94
DS
590 }
591
592 for (var i=0; i<filehandlers.length; i++) {
593 if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
594 handlers.push(filehandlers[i]);
595 }
596 }
597
598 if (handlers.length == 0) {
599 // No handlers at all (not even 'resource'?)
600 return;
601 }
602
603 if (handlers.length == 1) {
604 this.upload_file(file, section, sectionnumber, handlers[0].module);
605 return;
606 }
607
608 this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
609 },
610
611 /**
612 * Show a dialog box, allowing the user to choose what to do with the file they are uploading
613 * @param handlers the available handlers to choose between
614 * @param extension the extension of the file being uploaded
615 * @param file the File object being uploaded
616 * @param section the DOM element of the section being uploaded to
617 * @param sectionnumber the number of the selected course section
618 */
619 file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
620 if (this.uploaddialog) {
621 var details = new Object();
622 details.isfile = true;
623 details.handlers = handlers;
624 details.extension = extension;
625 details.file = file;
626 details.section = section;
627 details.sectionnumber = sectionnumber;
628 this.uploadqueue.push(details);
629 return;
630 }
631 this.uploaddialog = true;
632
633 var timestamp = new Date().getTime();
634 var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
635 var content = '';
636 var sel;
637 if (extension in this.lastselected) {
638 sel = this.lastselected[extension];
639 } else {
640 sel = handlers[0].module;
641 }
33b24bdd 642 content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
32528f94
DS
643 content += '<div id="dndupload_handlers'+uploadid+'">';
644 for (var i=0; i<handlers.length; i++) {
645 var id = 'dndupload_handler'+uploadid+handlers[i].module;
646 var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
647 content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
648 content += ' <label for="'+id+'">';
649 content += handlers[i].message;
650 content += '</label><br/>';
651 }
652 content += '</div>';
653
654 var Y = this.Y;
655 var self = this;
66079e28 656 var panel = new M.core.dialogue({
32528f94 657 bodyContent: content,
66079e28 658 width: '350px',
32528f94 659 modal: true,
6f0776b6 660 visible: false,
32528f94 661 render: true,
66079e28
DS
662 align: {
663 node: null,
664 points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC]
665 }
32528f94 666 });
6f0776b6 667 panel.show();
32528f94
DS
668 // When the panel is hidden - destroy it and then check for other pending uploads
669 panel.after("visibleChange", function(e) {
670 if (!panel.get('visible')) {
671 panel.destroy(true);
672 self.check_upload_queue();
673 }
674 });
66079e28
DS
675
676 // Add the submit/cancel buttons to the bottom of the dialog.
677 panel.addButton({
678 label: M.util.get_string('upload', 'moodle'),
679 action: function(e) {
680 e.preventDefault();
681 // Find out which module was selected
682 var module = false;
683 var div = Y.one('#dndupload_handlers'+uploadid);
684 div.all('input').each(function(input) {
685 if (input.get('checked')) {
686 module = input.get('value');
687 }
688 });
689 if (!module) {
690 return;
691 }
692 panel.hide();
693 // Remember this selection for next time
694 self.lastselected[extension] = module;
695 // Do the upload
696 self.upload_file(file, section, sectionnumber, module);
697 },
698 section: Y.WidgetStdMod.FOOTER
699 });
700 panel.addButton({
701 label: M.util.get_string('cancel', 'moodle'),
702 action: function(e) {
703 e.preventDefault();
704 panel.hide();
705 },
706 section: Y.WidgetStdMod.FOOTER
707 });
32528f94
DS
708 },
709
710 /**
711 * Check to see if there are any other dialog boxes to show, now that the current one has
712 * been dealt with
713 */
714 check_upload_queue: function() {
715 this.uploaddialog = false;
716 if (this.uploadqueue.length == 0) {
717 return;
718 }
719
720 var details = this.uploadqueue.shift();
721 if (details.isfile) {
722 this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
723 } else {
724 this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
725 }
726 },
727
728 /**
729 * Do the file upload: show the dummy element, use an AJAX call to send the data
730 * to the server, update the progress bar for the file, then replace the dummy
731 * element with the real information once the AJAX call completes
732 * @param file the details of the file, taken from the FileList in the drop event
733 * @param section the DOM element representing the selected course section
734 * @param sectionnumber the number of the selected course section
735 */
736 upload_file: function(file, section, sectionnumber, module) {
737
738 // This would be an ideal place to use the Y.io function
739 // however, this does not support data encoded using the
740 // FormData object, which is needed to transfer data from
741 // the DataTransfer object into an XMLHTTPRequest
742 // This can be converted when the YUI issue has been integrated:
743 // http://yuilibrary.com/projects/yui3/ticket/2531274
744 var xhr = new XMLHttpRequest();
745 var self = this;
746
6afbdf27 747 if (this.maxbytes > 0 && file.size > this.maxbytes) {
296af14b 748 new M.core.alert({message: M.util.get_string('namedfiletoolarge', 'moodle', {filename: file.name})});
32528f94
DS
749 return;
750 }
751
752 // Add the file to the display
785e09a7 753 var resel = this.add_resource_element(file.name, section, module);
32528f94
DS
754
755 // Update the progress bar as the file is uploaded
756 xhr.upload.addEventListener('progress', function(e) {
757 if (e.lengthComputable) {
758 var percentage = Math.round((e.loaded * 100) / e.total);
759 resel.progress.style.width = percentage + '%';
760 }
761 }, false);
762
763 // Wait for the AJAX call to complete, then update the
764 // dummy element with the returned details
765 xhr.onreadystatechange = function() {
0b245bf3
HN
766 if (xhr.readyState == 1) {
767 this.originalUnloadEvent = window.onbeforeunload;
768 self.reportUploadDirtyState(true);
769 }
32528f94
DS
770 if (xhr.readyState == 4) {
771 if (xhr.status == 200) {
772 var result = JSON.parse(xhr.responseText);
773 if (result) {
774 if (result.error == 0) {
9b2ad813
AN
775 // All OK - replace the dummy element.
776 resel.li.outerHTML = result.fullcontent;
777 if (self.Y.UA.gecko > 0) {
778 // Fix a Firefox bug which makes sites with a '~' in their wwwroot
779 // log the user out when clicking on the link (before refreshing the page).
780 resel.li.outerHTML = unescape(resel.li.outerHTML);
bc3f5bca 781 }
32528f94 782 self.add_editing(result.elementid);
865c4f5d
JD
783 // Fire the content updated event.
784 require(['core/event', 'jquery'], function(event, $) {
785 event.notifyFilterContentUpdated($(result.fullcontent));
786 });
32528f94
DS
787 } else {
788 // Error - remove the dummy element
789 resel.parent.removeChild(resel.li);
296af14b 790 new M.core.alert({message: result.error});
32528f94
DS
791 }
792 }
793 } else {
296af14b 794 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
32528f94 795 }
0b245bf3 796 self.reportUploadDirtyState(false);
32528f94
DS
797 }
798 };
799
800 // Prepare the data to send
801 var formData = new FormData();
4e737cf3
DS
802 try {
803 formData.append('repo_upload_file', file);
804 } catch (e) {
805 // Edge throws an error at this point if we try to upload a folder.
806 resel.parent.removeChild(resel.li);
807 new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
808 return;
809 }
32528f94
DS
810 formData.append('sesskey', M.cfg.sesskey);
811 formData.append('course', this.courseid);
812 formData.append('section', sectionnumber);
813 formData.append('module', module);
814 formData.append('type', 'Files');
815
4e737cf3
DS
816 // Try reading the file to check it is not a folder, before sending it to the server.
817 var reader = new FileReader();
818 reader.onload = function() {
819 // File was read OK - send it to the server.
820 xhr.open("POST", self.url, true);
821 xhr.send(formData);
822 };
823 reader.onerror = function() {
824 // Unable to read the file (it is probably a folder) - display an error message.
825 resel.parent.removeChild(resel.li);
826 new M.core.alert({message: M.util.get_string('filereaderror', 'moodle', file.name)});
827 };
96eaee3d
DS
828 if (file.size > 0) {
829 // If this is a non-empty file, try reading the first few bytes.
830 // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
831 reader.readAsText(file.slice(0, 5));
832 } else {
833 // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
834 // instead of reader.onerror().
835 // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
836 reader.readAsText(file);
837 }
32528f94
DS
838 },
839
840 /**
841 * Show a dialog box to gather the name of the resource / activity to be created
842 * from the uploaded content
843 * @param type the details of the type of content
844 * @param contents the contents to be uploaded
845 * @section the DOM element for the section being uploaded to
846 * @sectionnumber the number of the section being uploaded to
847 */
848 handle_item: function(type, contents, section, sectionnumber) {
849 if (type.handlers.length == 0) {
850 // Nothing to handle this - should not have got here
851 return;
852 }
853
2748d8ef
DS
854 if (type.handlers.length == 1 && type.handlers[0].noname) {
855 // Only one handler and it doesn't need a name (i.e. a label).
856 this.upload_item('', type.type, contents, section, sectionnumber, type.handlers[0].module);
857 this.check_upload_queue();
858 return;
859 }
860
32528f94
DS
861 if (this.uploaddialog) {
862 var details = new Object();
863 details.isfile = false;
864 details.type = type;
865 details.contents = contents;
866 details.section = section;
867 details.setcionnumber = sectionnumber;
868 this.uploadqueue.push(details);
869 return;
870 }
871 this.uploaddialog = true;
872
873 var timestamp = new Date().getTime();
874 var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
875 var nameid = 'dndupload_handler_name'+uploadid;
876 var content = '';
32528f94 877 if (type.handlers.length > 1) {
66079e28 878 content += '<p>'+type.handlermessage+'</p>';
32528f94
DS
879 content += '<div id="dndupload_handlers'+uploadid+'">';
880 var sel = type.handlers[0].module;
881 for (var i=0; i<type.handlers.length; i++) {
2748d8ef 882 var id = 'dndupload_handler'+uploadid+type.handlers[i].module;
32528f94 883 var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
2748d8ef 884 content += '<input type="radio" name="handler" value="'+i+'" id="'+id+'" '+checked+'/>';
32528f94
DS
885 content += ' <label for="'+id+'">';
886 content += type.handlers[i].message;
887 content += '</label><br/>';
888 }
889 content += '</div>';
890 }
2748d8ef
DS
891 var disabled = (type.handlers[0].noname) ? ' disabled = "disabled" ' : '';
892 content += '<label for="'+nameid+'">'+type.namemessage+'</label>';
893 content += ' <input type="text" id="'+nameid+'" value="" '+disabled+' />';
32528f94
DS
894
895 var Y = this.Y;
896 var self = this;
66079e28 897 var panel = new M.core.dialogue({
32528f94 898 bodyContent: content,
66079e28 899 width: '350px',
32528f94
DS
900 modal: true,
901 visible: true,
902 render: true,
66079e28
DS
903 align: {
904 node: null,
905 points: [Y.WidgetPositionAlign.CC, Y.WidgetPositionAlign.CC]
906 }
32528f94 907 });
66079e28 908
32528f94
DS
909 // When the panel is hidden - destroy it and then check for other pending uploads
910 panel.after("visibleChange", function(e) {
911 if (!panel.get('visible')) {
912 panel.destroy(true);
913 self.check_upload_queue();
914 }
915 });
66079e28
DS
916
917 var namefield = Y.one('#'+nameid);
918 var submit = function(e) {
919 e.preventDefault();
920 var name = Y.Lang.trim(namefield.get('value'));
921 var module = false;
922 var noname = false;
923 if (type.handlers.length > 1) {
924 // Find out which module was selected
925 var div = Y.one('#dndupload_handlers'+uploadid);
926 div.all('input').each(function(input) {
927 if (input.get('checked')) {
928 var idx = input.get('value');
929 module = type.handlers[idx].module;
930 noname = type.handlers[idx].noname;
931 }
932 });
933 if (!module) {
934 return;
935 }
936 } else {
937 module = type.handlers[0].module;
938 noname = type.handlers[0].noname;
939 }
940 if (name == '' && !noname) {
941 return;
942 }
943 if (noname) {
944 name = '';
945 }
946 panel.hide();
947 // Do the upload
948 self.upload_item(name, type.type, contents, section, sectionnumber, module);
949 };
950
951 // Add the submit/cancel buttons to the bottom of the dialog.
952 panel.addButton({
953 label: M.util.get_string('upload', 'moodle'),
954 action: submit,
955 section: Y.WidgetStdMod.FOOTER,
956 name: 'submit'
957 });
958 panel.addButton({
959 label: M.util.get_string('cancel', 'moodle'),
960 action: function(e) {
961 e.preventDefault();
962 panel.hide();
963 },
964 section: Y.WidgetStdMod.FOOTER
965 });
966 var submitbutton = panel.getButton('submit').button;
967 namefield.on('key', submit, 'enter'); // Submit the form if 'enter' pressed
968 namefield.after('keyup', function() {
969 if (Y.Lang.trim(namefield.get('value')) == '') {
970 submitbutton.disable();
971 } else {
972 submitbutton.enable();
973 }
974 });
975
976 // Enable / disable the 'name' box, depending on the handler selected.
2748d8ef
DS
977 for (i=0; i<type.handlers.length; i++) {
978 if (type.handlers[i].noname) {
979 Y.one('#dndupload_handler'+uploadid+type.handlers[i].module).on('click', function (e) {
66079e28
DS
980 namefield.set('disabled', 'disabled');
981 submitbutton.enable();
2748d8ef
DS
982 });
983 } else {
984 Y.one('#dndupload_handler'+uploadid+type.handlers[i].module).on('click', function (e) {
66079e28
DS
985 namefield.removeAttribute('disabled');
986 namefield.focus();
987 if (Y.Lang.trim(namefield.get('value')) == '') {
988 submitbutton.disable();
989 }
2748d8ef
DS
990 });
991 }
992 }
66079e28
DS
993
994 // Focus on the 'name' box
995 Y.one('#'+nameid).focus();
32528f94
DS
996 },
997
998 /**
999 * Upload any data types that are not files: display a dummy resource element, send
1000 * the data to the server, update the progress bar for the file, then replace the
1001 * dummy element with the real information once the AJAX call completes
1002 * @param name the display name for the resource / activity to create
1003 * @param type the details of the data type found in the drop event
1004 * @param contents the actual data that was dropped
1005 * @param section the DOM element representing the selected course section
1006 * @param sectionnumber the number of the selected course section
5a4decbc 1007 * @param module the module chosen to handle this upload
32528f94
DS
1008 */
1009 upload_item: function(name, type, contents, section, sectionnumber, module) {
1010
1011 // This would be an ideal place to use the Y.io function
1012 // however, this does not support data encoded using the
1013 // FormData object, which is needed to transfer data from
1014 // the DataTransfer object into an XMLHTTPRequest
1015 // This can be converted when the YUI issue has been integrated:
1016 // http://yuilibrary.com/projects/yui3/ticket/2531274
1017 var xhr = new XMLHttpRequest();
1018 var self = this;
1019
1020 // Add the item to the display
785e09a7 1021 var resel = this.add_resource_element(name, section, module);
32528f94
DS
1022
1023 // Wait for the AJAX call to complete, then update the
1024 // dummy element with the returned details
1025 xhr.onreadystatechange = function() {
0b245bf3
HN
1026 if (xhr.readyState == 1) {
1027 this.originalUnloadEvent = window.onbeforeunload;
1028 self.reportUploadDirtyState(true);
1029 }
32528f94
DS
1030 if (xhr.readyState == 4) {
1031 if (xhr.status == 200) {
1032 var result = JSON.parse(xhr.responseText);
1033 if (result) {
1034 if (result.error == 0) {
9b2ad813
AN
1035 // All OK - replace the dummy element.
1036 resel.li.outerHTML = result.fullcontent;
1037 if (self.Y.UA.gecko > 0) {
1038 // Fix a Firefox bug which makes sites with a '~' in their wwwroot
1039 // log the user out when clicking on the link (before refreshing the page).
1040 resel.li.outerHTML = unescape(resel.li.outerHTML);
d022f632 1041 }
9b2ad813 1042 self.add_editing(result.elementid);
32528f94
DS
1043 } else {
1044 // Error - remove the dummy element
1045 resel.parent.removeChild(resel.li);
296af14b 1046 new M.core.alert({message: result.error});
32528f94
DS
1047 }
1048 }
1049 } else {
296af14b 1050 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
32528f94 1051 }
0b245bf3 1052 self.reportUploadDirtyState(false);
32528f94
DS
1053 }
1054 };
1055
1056 // Prepare the data to send
1057 var formData = new FormData();
1058 formData.append('contents', contents);
1059 formData.append('displayname', name);
1060 formData.append('sesskey', M.cfg.sesskey);
1061 formData.append('course', this.courseid);
1062 formData.append('section', sectionnumber);
1063 formData.append('type', type);
1064 formData.append('module', module);
1065
1066 // Send the data
1067 xhr.open("POST", this.url, true);
1068 xhr.send(formData);
1069 },
1070
1071 /**
1072 * Call the AJAX course editing initialisation to add the editing tools
1073 * to the newly-created resource link
1074 * @param elementid the id of the DOM element containing the new resource link
1075 * @param sectionnumber the number of the selected course section
1076 */
1077 add_editing: function(elementid) {
f803ce26 1078 var node = Y.one('#' + elementid);
32528f94 1079 YUI().use('moodle-course-coursebase', function(Y) {
d5367fb5
AN
1080 Y.log("Invoking setup_for_resource", 'debug', 'coursedndupload');
1081 M.course.coursebase.invoke_function('setup_for_resource', node);
32528f94 1082 });
f803ce26
SH
1083 if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
1084 M.core.actionmenu.newDOMNode(node);
1085 }
0b245bf3
HN
1086 },
1087
1088 /**
1089 * Set the event to prevent user navigate away when upload progress still running.
1090 *
1091 * @param {bool} enable true if upload progress is running, false otherwise
1092 */
1093 reportUploadDirtyState: function(enable) {
1094 if (!enable) {
1095 window.onbeforeunload = this.originalUnloadEvent;
1096 } else {
1097 window.onbeforeunload = function(e) {
1098 var warningMessage = M.util.get_string('changesmadereallygoaway', 'moodle');
1099 if (e) {
1100 e.returnValue = warningMessage;
1101 }
1102 return warningMessage;
1103 };
1104 }
32528f94
DS
1105 }
1106};