Merge branch 'MDL-73801' of https://github.com/paulholden/moodle
[moodle.git] / lib / form / dndupload.js
blobebe96e6f903e67c1bdabd1da56cae8022380e9c1
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.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Javascript library for enableing a drag and drop upload interface
18  *
19  * @package    moodlecore
20  * @subpackage form
21  * @copyright  2011 Davo Smith
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 M.form_dndupload = {}
27 M.form_dndupload.init = function(Y, options) {
28     var dnduploadhelper = {
29         // YUI object.
30         Y: null,
31         // URL for upload requests
32         url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
33         // options may include: itemid, acceptedtypes, maxfiles, maxbytes, clientid, repositoryid, author, contextid
34         options: {},
35         // itemid used for repository upload
36         itemid: null,
37         // accepted filetypes accepted by this form passed to repository
38         acceptedtypes: [],
39         // maximum size of files allowed in this form
40         maxbytes: 0,
41         // Maximum combined size of files allowed in this form. {@link FILE_AREA_MAX_BYTES_UNLIMITED}
42         areamaxbytes: -1,
43         // unqiue id of this form field used for html elements
44         clientid: '',
45         // upload repository id, used for upload
46         repositoryid: 0,
47         // container which holds the node which recieves drag events
48         container: null,
49         // filemanager element we are working with
50         filemanager: null,
51         // callback  to filepicker element to refesh when uploaded
52         callback: null,
53         // Nasty hack to distinguish between dragenter(first entry),
54         // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
55         entercount: 0,
56         pageentercount: 0,
57         // Holds the progress bar elements for each file.
58         progressbars: {},
59         // Number of request in queue and number of request uploading.
60         totalOfRequest: 0,
61         // Number of request upload.
62         numberOfRequestUpload: 0,
64         /**
65          * Initalise the drag and drop upload interface
66          * Note: one and only one of options.filemanager and options.formcallback must be defined
67          *
68          * @param Y the YUI object
69          * @param object options {
70          *            itemid: itemid used for repository upload in this form
71          *            acceptdtypes: accepted filetypes by this form
72          *            maxfiles: maximum number of files this form allows
73          *            maxbytes: maximum size of files allowed in this form
74          *            areamaxbytes: maximum combined size of files allowed in this form
75          *            clientid: unqiue id of this form field used for html elements
76          *            contextid: id of the current cotnext
77          *            containerid: htmlid of container
78          *            repositories: array of repository objects passed from filepicker
79          *            filemanager: filemanager element we are working with
80          *            formcallback: callback  to filepicker element to refesh when uploaded
81          *          }
82          */
83         init: function(Y, options) {
84             this.Y = Y;
86             if (!this.browser_supported()) {
87                 Y.one('body').addClass('dndnotsupported');
88                 return; // Browser does not support the required functionality
89             }
91             // try and retrieve enabled upload repository
92             this.repositoryid = this.get_upload_repositoryid(options.repositories);
94             if (!this.repositoryid) {
95                 Y.one('body').addClass('dndnotsupported');
96                 return; // no upload repository is enabled to upload to
97             }
99             Y.one('body').addClass('dndsupported');
101             this.options = options;
102             this.acceptedtypes = options.acceptedtypes;
103             this.clientid = options.clientid;
104             this.maxbytes = options.maxbytes;
105             this.areamaxbytes = options.areamaxbytes;
106             this.itemid = options.itemid;
107             this.author = options.author;
108             this.container = this.Y.one('#'+options.containerid);
110             if (options.filemanager) {
111                 // Needed to tell the filemanager to redraw when files uploaded
112                 // and to check how many files are already uploaded
113                 this.filemanager = options.filemanager;
114             } else if (options.formcallback) {
116                 // Needed to tell the filepicker to update when a new
117                 // file is uploaded
118                 this.callback = options.formcallback;
119             } else {
120                 if (M.cfg.developerdebug) {
121                     alert('dndupload: Need to define either options.filemanager or options.formcallback');
122                 }
123                 return;
124             }
126             this.init_events();
127             this.init_page_events();
128         },
130         /**
131          * Check the browser has the required functionality
132          * @return true if browser supports drag/drop upload
133          */
134         browser_supported: function() {
136             if (typeof FileReader == 'undefined') {
137                 return false;
138             }
139             if (typeof FormData == 'undefined') {
140                 return false;
141             }
142             return true;
143         },
145         /**
146          * Get upload repoistory from array of enabled repositories
147          *
148          * @param array repositories repository objects passed from filepicker
149          * @param returns int id of upload repository or false if not found
150          */
151         get_upload_repositoryid: function(repositories) {
152             for (var i in repositories) {
153                 if (repositories[i].type == "upload") {
154                     return repositories[i].id;
155                 }
156             }
158             return false;
159         },
161         /**
162          * Initialise drag events on node container, all events need
163          * to be processed for drag and drop to work
164          */
165         init_events: function() {
166             this.Y.on('dragenter', this.drag_enter, this.container, this);
167             this.Y.on('dragleave', this.drag_leave, this.container, this);
168             this.Y.on('dragover',  this.drag_over,  this.container, this);
169             this.Y.on('drop',      this.drop,      this.container, this);
170         },
172         /**
173          * Initialise whole-page events (to show / hide the 'drop files here'
174          * message)
175          */
176         init_page_events: function() {
177             this.Y.on('dragenter', this.drag_enter_page, 'body', this);
178             this.Y.on('dragleave', this.drag_leave_page, 'body', this);
179             this.Y.on('drop', function() {
180                 this.pageentercount = 0;
181                 this.hide_drop_target();
182             }.bind(this));
183         },
185         /**
186          * Check if the filemanager / filepicker is disabled
187          * @return bool - true if disabled
188          */
189         is_disabled: function() {
190             return (this.container.ancestor('.fitem.disabled') != null);
191         },
193         /**
194          * Show the 'drop files here' message when file(s) are dragged
195          * onto the page
196          */
197         drag_enter_page: function(e) {
198             if (this.is_disabled()) {
199                 return false;
200             }
201             if (!this.has_files(e)) {
202                 return false;
203             }
205             this.pageentercount++;
206             if (this.pageentercount >= 2) {
207                 this.pageentercount = 2;
208                 return false;
209             }
211             this.show_drop_target();
213             return false;
214         },
216         /**
217          * Hide the 'drop files here' message when file(s) are dragged off
218          * the page again
219          */
220         drag_leave_page: function(e) {
221             this.pageentercount--;
222             if (this.pageentercount == 1) {
223                 return false;
224             }
225             this.pageentercount = 0;
227             this.hide_drop_target();
229             return false;
230         },
232         /**
233          * Check if the drag contents are valid and then call
234          * preventdefault / stoppropagation to let the browser know
235          * we will handle this drag/drop
236          *
237          * @param e event object
238          * @return boolean true if a valid file drag event
239          */
240         check_drag: function(e) {
241             if (this.is_disabled()) {
242                 return false;
243             }
244             if (!this.has_files(e)) {
245                 return false;
246             }
248             e.preventDefault();
249             e.stopPropagation();
251             return true;
252         },
254         /**
255          * Handle a dragenter event, highlight the destination node
256          * when a suitable drag event occurs
257          */
258         drag_enter: function(e) {
259             if (!this.check_drag(e)) {
260                 return true;
261             }
263             this.entercount++;
264             if (this.entercount >= 2) {
265                 this.entercount = 2; // Just moved over a child element - nothing to do
266                 return false;
267             }
269             // These lines are needed if the user has dragged something directly
270             // from application onto the 'fileupload' box, without crossing another
271             // part of the page first
272             this.pageentercount = 2;
273             this.show_drop_target();
275             this.show_upload_ready();
276             return false;
277         },
279         /**
280          * Handle a dragleave event, Remove the highlight if dragged from
281          * node
282          */
283         drag_leave: function(e) {
284             if (!this.check_drag(e)) {
285                 return true;
286             }
288             this.entercount--;
289             if (this.entercount == 1) {
290                 return false; // Just moved over a child element - nothing to do
291             }
293             this.entercount = 0;
294             this.hide_upload_ready();
295             return false;
296         },
298         /**
299          * Handle a dragover event. Required to intercept to prevent the browser from
300          * handling the drag and drop event as normal
301          */
302         drag_over: function(e) {
303             if (!this.check_drag(e)) {
304                 return true;
305             }
307             return false;
308         },
310         /**
311          * Handle a drop event.  Remove the highlight and then upload each
312          * of the files (until we reach the file limit, or run out of files)
313          */
314         drop: function(e) {
315             if (!this.check_drag(e, true)) {
316                 return true;
317             }
319             this.entercount = 0;
320             this.pageentercount = 0;
321             this.hide_upload_ready();
322             this.hide_drop_target();
324             var files = e._event.dataTransfer.files;
325             if (this.filemanager) {
326                 var options = {
327                     files: files,
328                     options: this.options,
329                     repositoryid: this.repositoryid,
330                     currentfilecount: this.filemanager.filecount, // All files uploaded.
331                     currentfiles: this.filemanager.options.list, // Only the current folder.
332                     callback: Y.bind('update_filemanager', this),
333                     callbackprogress: Y.bind('update_progress', this),
334                     callbackcancel: Y.bind('hide_progress', this),
335                     callbackNumberOfRequestUpload: {
336                         get: Y.bind('getNumberOfRequestUpload', this),
337                         increase: Y.bind('increaseNumberOfRequestUpload', this),
338                         decrease: Y.bind('decreaseNumberOfRequestUpload', this),
339                         getTotal: Y.bind('getTotalRequestUpload', this),
340                         increaseTotal: Y.bind('increaseTotalRequest', this),
341                         reset: Y.bind('resetNumberOfRequestUpload', this)
342                     },
343                     callbackClearProgress: Y.bind('clear_progress', this),
344                     callbackStartProgress: Y.bind('startProgress', this),
345                 };
346                 this.show_progress();
347                 var uploader = new dnduploader(options);
348                 uploader.start_upload();
349             } else {
350                 if (files.length >= 1) {
351                     options = {
352                         files:[files[0]],
353                         options: this.options,
354                         repositoryid: this.repositoryid,
355                         currentfilecount: 0,
356                         currentfiles: [],
357                         callback: Y.bind('update_filemanager', this),
358                         callbackprogress: Y.bind('update_progress', this),
359                         callbackcancel: Y.bind('hide_progress', this),
360                         callbackNumberOfRequestUpload: {
361                             get: Y.bind('getNumberOfRequestUpload', this),
362                             increase: Y.bind('increaseNumberOfRequestUpload', this),
363                             decrease: Y.bind('decreaseNumberOfRequestUpload', this),
364                             getTotal: Y.bind('getTotalRequestUpload', this),
365                             increaseTotal: Y.bind('increaseTotalRequest', this),
366                             reset: Y.bind('resetNumberOfRequestUpload', this)
367                         },
368                         callbackClearProgress: Y.bind('clear_progress', this),
369                         callbackStartProgress: Y.bind('startProgress', this),
370                     };
371                     this.show_progress();
372                     uploader = new dnduploader(options);
373                     uploader.start_upload();
374                 }
375             }
377             return false;
378         },
380         /**
381          * Increase number of request upload.
382          */
383         increaseNumberOfRequestUpload: function() {
384             this.numberOfRequestUpload++;
385         },
387         /**
388          * Increase total request.
389          *
390          * @param {number} newFileCount Number of new files.
391          */
392         increaseTotalRequest: function(newFileCount) {
393             this.totalOfRequest += newFileCount;
394         },
396         /**
397          * Decrease number of request upload.
398          */
399         decreaseNumberOfRequestUpload: function() {
400             this.numberOfRequestUpload--;
401         },
403         /**
404          * Return number of request upload.
405          *
406          * @returns {number}
407          */
408         getNumberOfRequestUpload: function() {
409             return this.numberOfRequestUpload;
410         },
412         /**
413          * Return number of request upload.
414          *
415          * @returns {number}
416          */
417         getTotalRequestUpload: function() {
418             return this.totalOfRequest;
419         },
421         /**
422          * Return number of request upload.
423          *
424          * @returns {number}
425          */
426         resetNumberOfRequestUpload: function() {
427             this.numberOfRequestUpload = 0;
428             this.totalOfRequest = 0;
429         },
431         /**
432          * Check to see if the drag event has any files in it
433          *
434          * @param e event object
435          * @return boolean true if event has files
436          */
437         has_files: function(e) {
438             // In some browsers, dataTransfer.types may be null for a
439             // 'dragover' event, so ensure a valid Array is always
440             // inspected.
441             var types = e._event.dataTransfer.types || [];
442             for (var i=0; i<types.length; i++) {
443                 if (types[i] == 'Files') {
444                     return true;
445                 }
446             }
447             return false;
448         },
450         /**
451          * Highlight the area where files could be dropped
452          */
453         show_drop_target: function() {
454             this.container.addClass('dndupload-ready');
455         },
457         hide_drop_target: function() {
458             this.container.removeClass('dndupload-ready');
459         },
461         /**
462          * Highlight the destination node (ready to drop)
463          */
464         show_upload_ready: function() {
465             this.container.addClass('dndupload-over');
466         },
468         /**
469          * Remove highlight on destination node
470          */
471         hide_upload_ready: function() {
472             this.container.removeClass('dndupload-over');
473         },
475         /**
476          * Show the element showing the upload in progress
477          */
478         show_progress: function() {
479             this.container.addClass('dndupload-inprogress');
480         },
482         /**
483          * Hide the element showing upload in progress
484          */
485         hide_progress: function() {
486             if (!Object.keys(this.progressbars).length) {
487                 this.container.removeClass('dndupload-inprogress');
488             }
489         },
491         /**
492          * Tell the attached filemanager element (if any) to refresh on file
493          * upload
494          */
495         update_filemanager: function(params) {
496             this.clear_progress();
497             this.hide_progress();
498             if (this.filemanager) {
499                 // update the filemanager that we've uploaded the files
500                 this.filemanager.filepicker_callback();
501             } else if (this.callback) {
502                 this.callback(params);
503             }
504         },
506         /**
507          * Clear the all progress bars.
508          */
509         clear_progress: function() {
510             var filename;
511             for (filename in this.progressbars) {
512                 if (this.progressbars.hasOwnProperty(filename)) {
513                     this.progressbars[filename].progressouter.remove(true);
514                     delete this.progressbars[filename];
515                 }
516             }
517         },
519         /**
520          * Show the current progress of the uploaded file.
521          */
522         update_progress: function(filename, percent) {
523             this.startProgress(filename);
524             this.progressbars[filename].progressinner.setStyle('width', percent + '%');
525             this.progressbars[filename].progressinner.setAttribute('aria-valuenow', percent);
526             this.progressbars[filename].progressinnertext.setContent(percent + '% ' + M.util.get_string('complete', 'moodle'));
527         },
529         /**
530          * Start to show the progress of the uploaded file.
531          *
532          * @param {String} filename Name of file upload.
533          */
534         startProgress: function(filename) {
535             if (this.progressbars[filename] === undefined) {
536                 var dispfilename = filename;
537                 if (dispfilename.length > 50) {
538                     dispfilename = dispfilename.substr(0, 49) + '&hellip;';
539                 }
540                 var progressouter = this.container.create('<div>' + dispfilename +
541                     '<div class="progress">' +
542                     '   <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">' +
543                     '       <span class="sr-only"></span>' +
544                     '   </div>' +
545                     '</div></div>');
546                 var progressinner = progressouter.one('.progress-bar');
547                 var progressinnertext = progressinner.one('.sr-only');
548                 var progresscontainer = this.container.one('.dndupload-progressbars');
549                 progresscontainer.appendChild(progressouter);
551                 this.progressbars[filename] = {
552                     progressouter: progressouter,
553                     progressinner: progressinner,
554                     progressinnertext: progressinnertext
555                 };
556             }
557         }
558     };
560     var dnduploader = function(options) {
561         dnduploader.superclass.constructor.apply(this, arguments);
562     };
564     Y.extend(dnduploader, Y.Base, {
565         // The URL to send the upload data to.
566         api: M.cfg.wwwroot+'/repository/repository_ajax.php',
567         // Options passed into the filemanager/filepicker element.
568         options: {},
569         // The function to call when all uploads complete.
570         callback: null,
571         // The function to call as the upload progresses
572         callbackprogress: null,
573         // The function to call if the upload is cancelled
574         callbackcancel: null,
575         // The list of files dropped onto the element.
576         files: null,
577         // The ID of the 'upload' repository.
578         repositoryid: 0,
579         // Array of files already in the current folder (to check for name clashes).
580         currentfiles: null,
581         // Total number of files already uploaded (to check for exceeding limits).
582         currentfilecount: 0,
583         // Number of new files will be upload in this dndupload (to check for exceeding limits).
584         newFileCount: 0,
585         // Total size of the files present in the area.
586         currentareasize: 0,
587         // The list of files to upload.
588         uploadqueue: [],
589         // This list of files with name clashes.
590         renamequeue: [],
591         // Size of the current queue.
592         queuesize: 0,
593         // Set to true if the user has clicked on 'overwrite all'.
594         overwriteall: false,
595         // Set to true if the user has clicked on 'rename all'.
596         renameall: false,
597         // The file manager helper.
598         filemanagerhelper: null,
599         // The function to call as the number of request upload.
600         callbackNumberOfRequestUpload: null,
601         // The function to call as the clear progresses.
602         callbackClearProgress: null,
603         // The function to call as the start progress.
604         callbackStartProgress: null,
606         /**
607          * Initialise the settings for the dnduploader
608          * @param object params - includes:
609          *                     options (copied from the filepicker / filemanager)
610          *                     repositoryid - ID of the upload repository
611          *                     callback - the function to call when uploads are complete
612          *                     currentfiles - the list of files already in the current folder in the filemanager
613          *                     currentfilecount - the total files already in the filemanager
614          *                     files - the list of files to upload
615          * @return void
616          */
617         initializer: function(params) {
618             this.options = params.options;
619             this.repositoryid = params.repositoryid;
620             this.callback = params.callback;
621             this.callbackprogress = params.callbackprogress;
622             this.callbackcancel = params.callbackcancel;
623             this.currentfiles = params.currentfiles;
624             this.currentfilecount = params.currentfilecount;
625             this.currentareasize = 0;
626             this.filemanagerhelper = this.options.filemanager;
627             this.callbackNumberOfRequestUpload = params.callbackNumberOfRequestUpload;
628             this.callbackClearProgress = params.callbackClearProgress;
629             this.callbackStartProgress = params.callbackStartProgress;
631             // Retrieve the current size of the area.
632             for (var i = 0; i < this.currentfiles.length; i++) {
633                 this.currentareasize += this.currentfiles[i].size;
634             };
636             if (!this.initialise_queue(params.files)) {
637                 if (this.callbackcancel) {
638                     this.callbackcancel();
639                 }
640             }
641         },
643         /**
644          * Entry point for starting the upload process (starts by processing any
645          * renames needed)
646          */
647         start_upload: function() {
648             this.process_renames(); // Automatically calls 'do_upload' once renames complete.
649         },
651         /**
652          * Display a message in a popup
653          * @param string msg - the message to display
654          * @param string type - 'error' or 'info'
655          */
656         print_msg: function(msg, type) {
657             var header = M.util.get_string('error', 'moodle');
658             if (type != 'error') {
659                 type = 'info'; // one of only two types excepted
660                 header = M.util.get_string('info', 'moodle');
661             }
662             if (!this.msg_dlg) {
663                 this.msg_dlg_node = Y.Node.create(M.core_filepicker.templates.message);
664                 this.msg_dlg_node.generateID();
666                 this.msg_dlg = new M.core.dialogue({
667                     bodyContent: this.msg_dlg_node,
668                     centered: true,
669                     modal: true,
670                     visible: false
671                 });
672                 this.msg_dlg.plug(Y.Plugin.Drag,{handles:['#'+this.msg_dlg_node.get('id')+' .yui3-widget-hd']});
673                 this.msg_dlg_node.one('.fp-msg-butok').on('click', function(e) {
674                     e.preventDefault();
675                     this.msg_dlg.hide();
676                 }, this);
677             }
679             this.msg_dlg.set('headerContent', header);
680             this.msg_dlg_node.removeClass('fp-msg-info').removeClass('fp-msg-error').addClass('fp-msg-'+type)
681             this.msg_dlg_node.one('.fp-msg-text').setContent(msg);
682             this.msg_dlg.show();
683         },
685         /**
686          * Check the size of each file and add to either the uploadqueue or, if there
687          * is a name clash, the renamequeue
688          * @param FileList files - the files to upload
689          * @return void
690          */
691         initialise_queue: function(files) {
692             this.uploadqueue = [];
693             this.renamequeue = [];
694             this.queuesize = 0;
696             // Loop through the files and find any name clashes with existing files.
697             var i;
698             for (i=0; i<files.length; i++) {
699                 if (this.options.maxbytes > 0 && files[i].size > this.options.maxbytes) {
700                     // Check filesize before attempting to upload.
701                     var maxbytesdisplay = this.display_size(this.options.maxbytes);
702                     this.print_msg(M.util.get_string('maxbytesfile', 'error', {
703                             file: files[i].name,
704                             size: maxbytesdisplay
705                         }), 'error');
706                     this.uploadqueue = []; // No uploads if one file is too big.
707                     return;
708                 }
710                 if (this.has_name_clash(files[i].name)) {
711                     this.renamequeue.push(files[i]);
712                 } else {
713                     if (!this.add_to_upload_queue(files[i], files[i].name, false)) {
714                         return false;
715                     }
716                 }
717                 this.queuesize += files[i].size;
718             }
719             return true;
720         },
722         /**
723          * Generate the display for file size
724          * @param int size The size to convert to human readable form
725          * @return string
726          */
727         display_size: function(size) {
728             // This is snippet of code (with some changes) is from the display_size function in moodlelib.
729             var gb = M.util.get_string('sizegb', 'moodle'),
730                 mb = M.util.get_string('sizemb', 'moodle'),
731                 kb = M.util.get_string('sizekb', 'moodle'),
732                 b  = M.util.get_string('sizeb', 'moodle');
734             if (size >= 1073741824) {
735                 size = Math.round(size / 1073741824 * 10) / 10 + gb;
736             } else if (size >= 1048576) {
737                 size = Math.round(size / 1048576 * 10) / 10 + mb;
738             } else if (size >= 1024) {
739                 size = Math.round(size / 1024 * 10) / 10 + kb;
740             } else {
741                 size = parseInt(size, 10) + ' ' + b;
742             }
744             return size;
745         },
747         /**
748          * Add a single file to the uploadqueue, whilst checking the maxfiles limit
749          * @param File file - the file to add
750          * @param string filename - the name to give the file on upload
751          * @param bool overwrite - true to overwrite the existing file
752          * @return bool true if added successfully
753          */
754         add_to_upload_queue: function(file, filename, overwrite) {
755             if (!overwrite) {
756                 this.newFileCount++;
757             }
759             // The value for "unlimited files" is -1, so 0 should mean 0.
760             if (this.options.maxfiles >= 0 && this.getTotalNumberOfFiles() > this.options.maxfiles) {
761                 // Too many files - abort entire upload.
762                 this.uploadqueue = [];
763                 this.renamequeue = [];
764                 this.print_msg(M.util.get_string('maxfilesreached', 'moodle', this.options.maxfiles), 'error');
765                 return false;
766             }
767             // The new file will cause the area to reach its limit, we cancel the upload of all files.
768             // -1 is the value defined by FILE_AREA_MAX_BYTES_UNLIMITED.
769             if (this.options.areamaxbytes > -1) {
770                 var sizereached = this.currentareasize + this.queuesize + file.size;
771                 if (sizereached > this.options.areamaxbytes) {
772                     this.uploadqueue = [];
773                     this.renamequeue = [];
774                     this.print_msg(M.util.get_string('maxareabytesreached', 'moodle'), 'error');
775                     return false;
776                 }
777             }
778             this.uploadqueue.push({file:file, filename:filename, overwrite:overwrite});
779             return true;
780         },
782         /**
783          * Get total number of files: Number of uploaded files, number of files unloading in other dndupload,
784          * number of files need to be upload in this dndupload.
785          * @return number Total number of files.
786          */
787         getTotalNumberOfFiles: function() {
788             // Get number of files we added into other dndupload.
789             let totalOfFiles = 0;
790             if(this.callbackNumberOfRequestUpload) {
791                 totalOfFiles = this.callbackNumberOfRequestUpload.getTotal();
792             }
794             return this.currentfilecount + this.newFileCount + totalOfFiles;
795         },
797         /**
798          * Take the next file from the renamequeue and ask the user what to do with
799          * it. Called recursively until the queue is empty, then calls do_upload.
800          * @return void
801          */
802         process_renames: function() {
803             if (this.renamequeue.length == 0) {
804                 // All rename processing complete - start the actual upload.
805                 if(this.callbackNumberOfRequestUpload && this.uploadqueue.length > 0) {
806                     this.callbackNumberOfRequestUpload.increaseTotal(this.newFileCount);
807                 }
808                 this.do_upload();
809                 return;
810             }
811             var multiplefiles = (this.renamequeue.length > 1);
813             // Get the next file from the rename queue.
814             var file = this.renamequeue.shift();
815             // Generate a non-conflicting name for it.
816             var newname = this.generate_unique_name(file.name);
818             // If the user has clicked on overwrite/rename ALL then process
819             // this file, as appropriate, then process the rest of the queue.
820             if (this.overwriteall) {
821                 if (this.add_to_upload_queue(file, file.name, true)) {
822                     this.process_renames();
823                 }
824                 return;
825             }
826             if (this.renameall) {
827                 if (this.add_to_upload_queue(file, newname, false)) {
828                     this.process_renames();
829                 }
830                 return;
831             }
833             // Ask the user what to do with this file.
834             var self = this;
836             var process_dlg_node;
837             if (multiplefiles) {
838                 process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfilemultiple);
839             } else {
840                 process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfile);
841             }
842             var node = process_dlg_node;
843             node.generateID();
844             var process_dlg = new M.core.dialogue({
845                 bodyContent: node,
846                 headerContent: M.util.get_string('fileexistsdialogheader', 'repository'),
847                 centered: true,
848                 modal: true,
849                 visible: false
850             });
851             process_dlg.plug(Y.Plugin.Drag,{handles:['#'+node.get('id')+' .yui3-widget-hd']});
853             // Overwrite original.
854             node.one('.fp-dlg-butoverwrite').on('click', function(e) {
855                 e.preventDefault();
856                 process_dlg.hide();
857                 if (self.add_to_upload_queue(file, file.name, true)) {
858                     self.process_renames();
859                 }
860             }, this);
862             // Rename uploaded file.
863             node.one('.fp-dlg-butrename').on('click', function(e) {
864                 e.preventDefault();
865                 process_dlg.hide();
866                 if (self.add_to_upload_queue(file, newname, false)) {
867                     self.process_renames();
868                 }
869             }, this);
871             // Cancel all uploads.
872             node.one('.fp-dlg-butcancel').on('click', function(e) {
873                 e.preventDefault();
874                 process_dlg.hide();
875                 if (self.callbackcancel) {
876                     this.notifyUploadCompleted();
877                     self.callbackClearProgress();
878                     self.callbackcancel();
879                 }
880             }, this);
882             // When we are at the file limit, only allow 'overwrite', not rename.
883             if (this.getTotalNumberOfFiles() == this.options.maxfiles) {
884                 node.one('.fp-dlg-butrename').setStyle('display', 'none');
885                 if (multiplefiles) {
886                     node.one('.fp-dlg-butrenameall').setStyle('display', 'none');
887                 }
888             }
890             // If there are more files still to go, offer the 'overwrite/rename all' options.
891             if (multiplefiles) {
892                 // Overwrite all original files.
893                 node.one('.fp-dlg-butoverwriteall').on('click', function(e) {
894                     e.preventDefault();
895                     process_dlg.hide();
896                     this.overwriteall = true;
897                     if (self.add_to_upload_queue(file, file.name, true)) {
898                         self.process_renames();
899                     }
900                 }, this);
902                 // Rename all new files.
903                 node.one('.fp-dlg-butrenameall').on('click', function(e) {
904                     e.preventDefault();
905                     process_dlg.hide();
906                     this.renameall = true;
907                     if (self.add_to_upload_queue(file, newname, false)) {
908                         self.process_renames();
909                     }
910                 }, this);
911             }
912             node.one('.fp-dlg-text').setContent(M.util.get_string('fileexists', 'moodle', file.name));
913             process_dlg_node.one('.fp-dlg-butrename').setContent(M.util.get_string('renameto', 'repository', newname));
915             // Destroy the dialog once it has been hidden.
916             process_dlg.after('visibleChange', function(e) {
917                 if (!process_dlg.get('visible')) {
918                     if (self.callbackcancel) {
919                         self.callbackcancel();
920                     }
921                     process_dlg.destroy(true);
922                 }
923             }, this);
925             process_dlg.show();
926         },
928         /**
929          * Trigger upload completed event.
930          */
931         notifyUploadCompleted: function() {
932             require(['core_form/events'], function(FormEvent) {
933                 const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid;
934                 FormEvent.triggerUploadCompleted(elementId);
935             }.bind(this));
936          },
938         /**
939          * Trigger form upload start events.
940          */
941         notifyUploadStarted: function() {
942             require(['core_form/events'], function(FormEvent) {
943                 const elementId = this.filemanagerhelper ? this.filemanagerhelper.filemanager.get('id') : this.options.containerid;
944                 FormEvent.triggerUploadStarted(elementId);
945             }.bind(this));
946         },
948         /**
949          * Checks if there is already a file with the given name in the current folder
950          * or in the list of already uploading files
951          * @param string filename - the name to test
952          * @return bool true if the name already exists
953          */
954         has_name_clash: function(filename) {
955             // Check against the already uploaded files
956             var i;
957             for (i=0; i<this.currentfiles.length; i++) {
958                 if (filename == this.currentfiles[i].filename) {
959                     return true;
960                 }
961             }
962             // Check against the uploading files that have already been processed
963             for (i=0; i<this.uploadqueue.length; i++) {
964                 if (filename == this.uploadqueue[i].filename) {
965                     return true;
966                 }
967             }
968             return false;
969         },
971         /**
972          * Gets a unique file name
973          *
974          * @param string filename
975          * @return string the unique filename generated
976          */
977         generate_unique_name: function(filename) {
978             // Loop through increating numbers until a unique name is found.
979             while (this.has_name_clash(filename)) {
980                 filename = increment_filename(filename);
981             }
982             return filename;
983         },
985         /**
986          * Upload the next file from the uploadqueue - called recursively after each
987          * upload is complete, then handles the callback to the filemanager/filepicker
988          * @param lastresult - the last result from the server
989          */
990         do_upload: function(lastresult) {
991             if (this.uploadqueue.length > 0) {
992                 var filedetails = this.uploadqueue.shift();
993                 this.upload_file(filedetails.file, filedetails.filename, filedetails.overwrite);
994             } else {
995                 if (this.callbackNumberOfRequestUpload && !this.callbackNumberOfRequestUpload.get()) {
996                     this.uploadfinished(lastresult);
997                 }
998             }
999         },
1001         /**
1002          * Run the callback to the filemanager/filepicker
1003          */
1004         uploadfinished: function(lastresult) {
1005             this.callbackNumberOfRequestUpload.reset();
1006             this.callback(lastresult);
1007         },
1009         /**
1010          * Upload a single file via an AJAX call to the 'upload' repository. Automatically
1011          * calls do_upload as each upload completes.
1012          * @param File file - the file to upload
1013          * @param string filename - the name to give the file
1014          * @param bool overwrite - true if the existing file should be overwritten
1015          */
1016         upload_file: function(file, filename, overwrite) {
1018             // This would be an ideal place to use the Y.io function
1019             // however, this does not support data encoded using the
1020             // FormData object, which is needed to transfer data from
1021             // the DataTransfer object into an XMLHTTPRequest
1022             // This can be converted when the YUI issue has been integrated:
1023             // http://yuilibrary.com/projects/yui3/ticket/2531274
1024             var xhr = new XMLHttpRequest();
1025             var self = this;
1026             if (self.callbackNumberOfRequestUpload) {
1027                 self.callbackNumberOfRequestUpload.increase();
1028             }
1030             // Start progress bar.
1031             xhr.onloadstart = function() {
1032                 self.callbackStartProgress(filename);
1033                 self.notifyUploadStarted();
1034             };
1036             // Update the progress bar
1037             xhr.upload.addEventListener('progress', function(e) {
1038                 if (e.lengthComputable && self.callbackprogress) {
1039                     var percentage = Math.round((e.loaded * 100) / e.total);
1040                     self.callbackprogress(filename, percentage);
1041                 }
1042             }, false);
1044             xhr.onreadystatechange = function() { // Process the server response
1045                 if (xhr.readyState == 4) {
1046                     self.notifyUploadCompleted();
1047                     if (xhr.status == 200) {
1048                         var result = JSON.parse(xhr.responseText);
1049                         if (result) {
1050                             if (result.error) {
1051                                 self.print_msg(result.error, 'error'); // TODO add filename?
1052                                 self.uploadfinished();
1053                             } else {
1054                                 // Only update the filepicker if there were no errors
1055                                 if (result.event == 'fileexists') {
1056                                     // Do not worry about this, as we only care about the last
1057                                     // file uploaded, with the filepicker
1058                                     result.file = result.newfile.filename;
1059                                     result.url = result.newfile.url;
1060                                 }
1061                                 result.client_id = self.options.clientid;
1062                                 if (self.callbackprogress) {
1063                                     self.callbackprogress(filename, 100);
1064                                 }
1065                             }
1066                         }
1067                         if (self.callbackNumberOfRequestUpload) {
1068                             self.callbackNumberOfRequestUpload.decrease();
1069                         }
1070                         self.do_upload(result); // continue uploading
1071                     } else {
1072                         self.print_msg(M.util.get_string('serverconnection', 'error'), 'error');
1073                         self.uploadfinished();
1074                     }
1075                 }
1076             };
1078             // Prepare the data to send
1079             var formdata = new FormData();
1080             formdata.append('repo_upload_file', file); // The FormData class allows us to attach a file
1081             formdata.append('sesskey', M.cfg.sesskey);
1082             formdata.append('repo_id', this.repositoryid);
1083             formdata.append('itemid', this.options.itemid);
1084             if (this.options.author) {
1085                 formdata.append('author', this.options.author);
1086             }
1087             if (this.options.filemanager) { // Filepickers do not have folders
1088                 formdata.append('savepath', this.options.filemanager.currentpath);
1089             }
1090             formdata.append('title', filename);
1091             if (overwrite) {
1092                 formdata.append('overwrite', 1);
1093             }
1094             if (this.options.contextid) {
1095                 formdata.append('ctx_id', this.options.contextid);
1096             }
1098             // Accepted types can be either a string or an array, but an array is
1099             // expected in the processing script, so make sure we are sending an array
1100             if (this.options.acceptedtypes.constructor == Array) {
1101                 for (var i=0; i<this.options.acceptedtypes.length; i++) {
1102                     formdata.append('accepted_types[]', this.options.acceptedtypes[i]);
1103                 }
1104             } else {
1105                 formdata.append('accepted_types[]', this.options.acceptedtypes);
1106             }
1108             // Send the file & required details.
1109             var uploadUrl = this.api;
1110             if (uploadUrl.indexOf('?') !== -1) {
1111                 uploadUrl += '&action=upload';
1112             } else {
1113                 uploadUrl += '?action=upload';
1114             }
1115             xhr.open("POST", uploadUrl, true);
1116             xhr.send(formdata);
1117             return true;
1118         }
1119     });
1121     dnduploadhelper.init(Y, options);