Commit | Line | Data |
---|---|---|
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 | */ | |
24 | M.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 = ' '; | |
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 | }; |