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