1 // This file is part of Moodle - http://moodle.org/
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.
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/>.
17 * Javascript library for enableing a drag and drop upload interface
21 * @copyright 2011 Davo Smith
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 M.form_dndupload.init = function(Y, options) {
28 var dnduploadhelper = {
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
35 // itemid used for repository upload
37 // accepted filetypes accepted by this form passed to repository
39 // maximum size of files allowed in this form
41 // Maximum combined size of files allowed in this form. {@link FILE_AREA_MAX_BYTES_UNLIMITED}
43 // unqiue id of this form field used for html elements
45 // upload repository id, used for upload
47 // container which holds the node which recieves drag events
49 // filemanager element we are working with
51 // callback to filepicker element to refesh when uploaded
53 // Nasty hack to distinguish between dragenter(first entry),
54 // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
57 // Holds the progress bar elements for each file.
59 // Number of request in queue and number of request uploading.
61 // Number of request upload.
62 numberOfRequestUpload: 0,
65 * Initalise the drag and drop upload interface
66 * Note: one and only one of options.filemanager and options.formcallback must be defined
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
83 init: function(Y, options) {
86 if (!this.browser_supported()) {
87 Y.one('body').addClass('dndnotsupported');
88 return; // Browser does not support the required functionality
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
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
118 this.callback = options.formcallback;
120 if (M.cfg.developerdebug) {
121 alert('dndupload: Need to define either options.filemanager or options.formcallback');
127 this.init_page_events();
131 * Check the browser has the required functionality
132 * @return true if browser supports drag/drop upload
134 browser_supported: function() {
136 if (typeof FileReader == 'undefined') {
139 if (typeof FormData == 'undefined') {
146 * Get upload repoistory from array of enabled repositories
148 * @param array repositories repository objects passed from filepicker
149 * @param returns int id of upload repository or false if not found
151 get_upload_repositoryid: function(repositories) {
152 for (var i in repositories) {
153 if (repositories[i].type == "upload") {
154 return repositories[i].id;
162 * Initialise drag events on node container, all events need
163 * to be processed for drag and drop to work
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);
173 * Initialise whole-page events (to show / hide the 'drop files here'
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();
186 * Check if the filemanager / filepicker is disabled
187 * @return bool - true if disabled
189 is_disabled: function() {
190 return (this.container.ancestor('.fitem.disabled') != null);
194 * Show the 'drop files here' message when file(s) are dragged
197 drag_enter_page: function(e) {
198 if (this.is_disabled()) {
201 if (!this.has_files(e)) {
205 this.pageentercount++;
206 if (this.pageentercount >= 2) {
207 this.pageentercount = 2;
211 this.show_drop_target();
217 * Hide the 'drop files here' message when file(s) are dragged off
220 drag_leave_page: function(e) {
221 this.pageentercount--;
222 if (this.pageentercount == 1) {
225 this.pageentercount = 0;
227 this.hide_drop_target();
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
237 * @param e event object
238 * @return boolean true if a valid file drag event
240 check_drag: function(e) {
241 if (this.is_disabled()) {
244 if (!this.has_files(e)) {
255 * Handle a dragenter event, highlight the destination node
256 * when a suitable drag event occurs
258 drag_enter: function(e) {
259 if (!this.check_drag(e)) {
264 if (this.entercount >= 2) {
265 this.entercount = 2; // Just moved over a child element - nothing to do
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();
280 * Handle a dragleave event, Remove the highlight if dragged from
283 drag_leave: function(e) {
284 if (!this.check_drag(e)) {
289 if (this.entercount == 1) {
290 return false; // Just moved over a child element - nothing to do
294 this.hide_upload_ready();
299 * Handle a dragover event. Required to intercept to prevent the browser from
300 * handling the drag and drop event as normal
302 drag_over: function(e) {
303 if (!this.check_drag(e)) {
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)
315 if (!this.check_drag(e, true)) {
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) {
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)
343 callbackClearProgress: Y.bind('clear_progress', this),
344 callbackStartProgress: Y.bind('startProgress', this),
346 this.show_progress();
347 var uploader = new dnduploader(options);
348 uploader.start_upload();
350 if (files.length >= 1) {
353 options: this.options,
354 repositoryid: this.repositoryid,
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)
368 callbackClearProgress: Y.bind('clear_progress', this),
369 callbackStartProgress: Y.bind('startProgress', this),
371 this.show_progress();
372 uploader = new dnduploader(options);
373 uploader.start_upload();
381 * Increase number of request upload.
383 increaseNumberOfRequestUpload: function() {
384 this.numberOfRequestUpload++;
388 * Increase total request.
390 * @param {number} newFileCount Number of new files.
392 increaseTotalRequest: function(newFileCount) {
393 this.totalOfRequest += newFileCount;
397 * Decrease number of request upload.
399 decreaseNumberOfRequestUpload: function() {
400 this.numberOfRequestUpload--;
404 * Return number of request upload.
408 getNumberOfRequestUpload: function() {
409 return this.numberOfRequestUpload;
413 * Return number of request upload.
417 getTotalRequestUpload: function() {
418 return this.totalOfRequest;
422 * Return number of request upload.
426 resetNumberOfRequestUpload: function() {
427 this.numberOfRequestUpload = 0;
428 this.totalOfRequest = 0;
432 * Check to see if the drag event has any files in it
434 * @param e event object
435 * @return boolean true if event has files
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
441 var types = e._event.dataTransfer.types || [];
442 for (var i=0; i<types.length; i++) {
443 if (types[i] == 'Files') {
451 * Highlight the area where files could be dropped
453 show_drop_target: function() {
454 this.container.addClass('dndupload-ready');
457 hide_drop_target: function() {
458 this.container.removeClass('dndupload-ready');
462 * Highlight the destination node (ready to drop)
464 show_upload_ready: function() {
465 this.container.addClass('dndupload-over');
469 * Remove highlight on destination node
471 hide_upload_ready: function() {
472 this.container.removeClass('dndupload-over');
476 * Show the element showing the upload in progress
478 show_progress: function() {
479 this.container.addClass('dndupload-inprogress');
483 * Hide the element showing upload in progress
485 hide_progress: function() {
486 if (!Object.keys(this.progressbars).length) {
487 this.container.removeClass('dndupload-inprogress');
492 * Tell the attached filemanager element (if any) to refresh on file
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);
507 * Clear the all progress bars.
509 clear_progress: function() {
511 for (filename in this.progressbars) {
512 if (this.progressbars.hasOwnProperty(filename)) {
513 this.progressbars[filename].progressouter.remove(true);
514 delete this.progressbars[filename];
520 * Show the current progress of the uploaded file.
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'));
530 * Start to show the progress of the uploaded file.
532 * @param {String} filename Name of file upload.
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) + '…';
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>' +
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
560 var dnduploader = function(options) {
561 dnduploader.superclass.constructor.apply(this, arguments);
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.
569 // The function to call when all uploads complete.
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.
577 // The ID of the 'upload' repository.
579 // Array of files already in the current folder (to check for name clashes).
581 // Total number of files already uploaded (to check for exceeding limits).
583 // Number of new files will be upload in this dndupload (to check for exceeding limits).
585 // Total size of the files present in the area.
587 // The list of files to upload.
589 // This list of files with name clashes.
591 // Size of the current queue.
593 // Set to true if the user has clicked on 'overwrite all'.
595 // Set to true if the user has clicked on 'rename all'.
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,
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
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;
636 if (!this.initialise_queue(params.files)) {
637 if (this.callbackcancel) {
638 this.callbackcancel();
644 * Entry point for starting the upload process (starts by processing any
647 start_upload: function() {
648 this.process_renames(); // Automatically calls 'do_upload' once renames complete.
652 * Display a message in a popup
653 * @param string msg - the message to display
654 * @param string type - 'error' or 'info'
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');
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,
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) {
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);
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
691 initialise_queue: function(files) {
692 this.uploadqueue = [];
693 this.renamequeue = [];
696 // Loop through the files and find any name clashes with existing files.
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', {
704 size: maxbytesdisplay
706 this.uploadqueue = []; // No uploads if one file is too big.
710 if (this.has_name_clash(files[i].name)) {
711 this.renamequeue.push(files[i]);
713 if (!this.add_to_upload_queue(files[i], files[i].name, false)) {
717 this.queuesize += files[i].size;
723 * Generate the display for file size
724 * @param int size The size to convert to human readable form
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;
741 size = parseInt(size, 10) + ' ' + b;
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
754 add_to_upload_queue: function(file, filename, overwrite) {
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');
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');
778 this.uploadqueue.push({file:file, filename:filename, overwrite:overwrite});
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.
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();
794 return this.currentfilecount + this.newFileCount + totalOfFiles;
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.
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);
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();
826 if (this.renameall) {
827 if (this.add_to_upload_queue(file, newname, false)) {
828 this.process_renames();
833 // Ask the user what to do with this file.
836 var process_dlg_node;
838 process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfilemultiple);
840 process_dlg_node = Y.Node.create(M.core_filepicker.templates.processexistingfile);
842 var node = process_dlg_node;
844 var process_dlg = new M.core.dialogue({
846 headerContent: M.util.get_string('fileexistsdialogheader', 'repository'),
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) {
857 if (self.add_to_upload_queue(file, file.name, true)) {
858 self.process_renames();
862 // Rename uploaded file.
863 node.one('.fp-dlg-butrename').on('click', function(e) {
866 if (self.add_to_upload_queue(file, newname, false)) {
867 self.process_renames();
871 // Cancel all uploads.
872 node.one('.fp-dlg-butcancel').on('click', function(e) {
875 if (self.callbackcancel) {
876 this.notifyUploadCompleted();
877 self.callbackClearProgress();
878 self.callbackcancel();
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');
886 node.one('.fp-dlg-butrenameall').setStyle('display', 'none');
890 // If there are more files still to go, offer the 'overwrite/rename all' options.
892 // Overwrite all original files.
893 node.one('.fp-dlg-butoverwriteall').on('click', function(e) {
896 this.overwriteall = true;
897 if (self.add_to_upload_queue(file, file.name, true)) {
898 self.process_renames();
902 // Rename all new files.
903 node.one('.fp-dlg-butrenameall').on('click', function(e) {
906 this.renameall = true;
907 if (self.add_to_upload_queue(file, newname, false)) {
908 self.process_renames();
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();
921 process_dlg.destroy(true);
929 * Trigger upload completed event.
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);
939 * Trigger form upload start events.
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);
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
954 has_name_clash: function(filename) {
955 // Check against the already uploaded files
957 for (i=0; i<this.currentfiles.length; i++) {
958 if (filename == this.currentfiles[i].filename) {
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) {
972 * Gets a unique file name
974 * @param string filename
975 * @return string the unique filename generated
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);
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
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);
995 if (this.callbackNumberOfRequestUpload && !this.callbackNumberOfRequestUpload.get()) {
996 this.uploadfinished(lastresult);
1002 * Run the callback to the filemanager/filepicker
1004 uploadfinished: function(lastresult) {
1005 this.callbackNumberOfRequestUpload.reset();
1006 this.callback(lastresult);
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
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();
1026 if (self.callbackNumberOfRequestUpload) {
1027 self.callbackNumberOfRequestUpload.increase();
1030 // Start progress bar.
1031 xhr.onloadstart = function() {
1032 self.callbackStartProgress(filename);
1033 self.notifyUploadStarted();
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);
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);
1051 self.print_msg(result.error, 'error'); // TODO add filename?
1052 self.uploadfinished();
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;
1061 result.client_id = self.options.clientid;
1062 if (self.callbackprogress) {
1063 self.callbackprogress(filename, 100);
1067 if (self.callbackNumberOfRequestUpload) {
1068 self.callbackNumberOfRequestUpload.decrease();
1070 self.do_upload(result); // continue uploading
1072 self.print_msg(M.util.get_string('serverconnection', 'error'), 'error');
1073 self.uploadfinished();
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);
1087 if (this.options.filemanager) { // Filepickers do not have folders
1088 formdata.append('savepath', this.options.filemanager.currentpath);
1090 formdata.append('title', filename);
1092 formdata.append('overwrite', 1);
1094 if (this.options.contextid) {
1095 formdata.append('ctx_id', this.options.contextid);
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]);
1105 formdata.append('accepted_types[]', this.options.acceptedtypes);
1108 // Send the file & required details.
1109 var uploadUrl = this.api;
1110 if (uploadUrl.indexOf('?') !== -1) {
1111 uploadUrl += '&action=upload';
1113 uploadUrl += '?action=upload';
1115 xhr.open("POST", uploadUrl, true);
1121 dnduploadhelper.init(Y, options);