MDL-29766 Add drag and drop upload to filemanager / filepicker elements
[moodle.git] / lib / form / dndupload.js
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/>.
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 = {
26     // YUI object.
27     Y: null,
28     // URL for upload requests
29     url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
30     // itemid used for repository upload
31     itemid: null,
32     // accepted filetypes accepted by this form passed to repository
33     acceptedtypes: [],
34     // maximum number of files this form allows
35     maxfiles: 0,
36     // maximum size of files allowed in this form
37     maxbytes: 0,
38     // unqiue id of this form field used for html elements
39     clientid: '',
40     // upload repository id, used for upload
41     repositoryid: 0,
42     // container which holds the node which recieves drag events
43     container: null,
44     // filemanager element we are working with
45     filemanager: null,
46     // callback  to filepicker element to refesh when uploaded
47     callback: null,
48     // Nasty hack to distinguish between dragenter(first entry),
49     // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
50     entercount: 0,
53     /**
54      * Initalise the drag and drop upload interface
55      * Note: one and only one of options.filemanager and options.formcallback must be defined
56      *
57      * @param Y the YUI object
58      * @param object options {
59      *            itemid: itemid used for repository upload in this form
60      *            acceptdtypes: accepted filetypes by this form
61      *            maxfiles: maximum number of files this form allows
62      *            maxbytes: maximum size of files allowed in this form
63      *            clientid: unqiue id of this form field used for html elements
64      *            containerprefix: prefix of htmlid of container
65      *            repositories: array of repository objects passed from filepicker
66      *            filemanager: filemanager element we are working with
67      *            callback: callback  to filepicker element to refesh when uploaded
68      *          }
69      */
70     init: function(Y, options) {
71         this.Y = Y;
73         if (!this.browser_supported()) {
74             return; // Browser does not support the required functionality
75         }
77         // try and retrieve enabled upload repository
78         this.repositoryid = this.get_upload_repositoryid(options.repositories);
80         if (!this.repositoryid) {
81             return; // no upload repository is enabled to upload to
82         }
84         this.acceptedtypes = options.acceptedtypes;
85         this.clientid = options.clientid;
86         this.maxfiles = options.maxfiles;
87         this.maxbytes = options.maxbytes;
88         this.itemid = options.itemid;
89         this.container = this.Y.one(options.containerprefix + this.clientid);
91         if (options.filemanager) {
92             // Needed to tell the filemanager to redraw when files uploaded
93             // and to check how many files are already uploaded
94             this.filemanager = options.filemanager;
95         } else if (options.formcallback) {
97             // Needed to tell the filepicker to update when a new
98             // file is uploaded
99             this.callback = options.formcallback;
100         } else {
101             alert('dndupload: Need to define either options.filemanager or options.callback');
102             return;
103         }
105         this.init_events();
106         this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline');
107     },
109     /**
110      * Check the browser has the required functionality
111      * @return true if browser supports drag/drop upload
112      */
113     browser_supported: function() {
115         if (typeof FileReader=='undefined') {
116             return false;
117         }
118         if (typeof FormData=='undefined') {
119             return false;
120         }
121         return true;
122     },
124     /**
125      * Get upload repoistory from array of enabled repositories
126      *
127      * @param array repositories repository objects passed from filepicker
128      * @param returns int id of upload repository or false if not found
129      */
130     get_upload_repositoryid: function(repositories) {
131          for (var id in repositories) {
132              if(repositories[id].type == "upload") {
133                  return id;
134              }
135          }
137          return false;
138     },
140     /**
141      * Initialise drag events on node container, all events need
142      * to be processed for drag and drop to work
143      */
144     init_events: function() {
145         this.Y.on('dragenter', this.drag_enter, this.container, this);
146         this.Y.on('dragleave', this.drag_leave, this.container, this);
147         this.Y.on('dragover',  this.drag_over,  this.container, this);
148         this.Y.on('drop',      this.drop,      this.container, this);
149     },
151     /**
152      * Check if the drag contents are valid and then call
153      * preventdefault / stoppropagation to let the browser know
154      * we will handle this drag/drop
155      *
156      * @param e event object
157      * @return boolean true if a valid file drag event
158      */
159     check_drag: function(e) {
160         if (!this.has_files(e)) {
161             return false;
162         }
164         if (this.reached_maxfiles()) {
165             return false;
166         }
168         e.preventDefault();
169         e.stopPropagation();
171         return true;
172     },
174     /**
175      * Handle a dragenter event, highlight the destination node
176      * when a suitable drag event occurs
177      */
178     drag_enter: function(e) {
179         if (!this.check_drag(e)) {
180             return true;
181         }
183         this.entercount++;
184         if (this.entercount >= 2) {
185             this.entercount = 2; // Just moved over a child element - nothing to do
186             return false;
187         }
189         this.show_upload_ready();
190         return false;
191     },
193     /**
194      * Handle a dragleave event, Remove the highlight if dragged from
195      * node
196      */
197     drag_leave: function(e) {
198         if (!this.check_drag(e)) {
199             return true;
200         }
202         this.entercount--;
203         if (this.entercount == 1) {
204             return false; // Just moved over a child element - nothing to do
205         }
207         this.entercount = 0;
208         this.hide_upload_ready();
209         return false;
210     },
212     /**
213      * Handle a dragover event. Required to intercept to prevent the browser from
214      * handling the drag and drop event as normal
215      */
216     drag_over: function(e) {
217         if (!this.check_drag(e)) {
218             return true;
219         }
221         return false;
222     },
224     /**
225      * Handle a drop event.  Remove the highlight and then upload each
226      * of the files (until we reach the file limit, or run out of files)
227      */
228     drop: function(e) {
229         if (!this.check_drag(e)) {
230             return true;
231         }
233         this.entercount = 0;
234         this.hide_upload_ready();
235         this.show_progress_spinner();
237         var files = e._event.dataTransfer.files;
238         if (this.filemanager) {
239             var currentfilecount = this.filemanager.filecount;
240             for (var i=0, f; f=files[i]; i++) {
241                 if (currentfilecount >= this.maxfiles && this.maxfiles != -1) {
242                     break;
243                 }
244                 if (this.upload_file(f)) {
245                     currentfilecount++;
246                 }
247             }
248         } else {
249             if (files.length >= 1) {
250                 this.upload_file(files[0]);
251             }
252         }
254         return false;
255     },
257     /**
258      * Check to see if the drag event has any files in it
259      *
260      * @param e event object
261      * @return boolean true if event has files
262      */
263     has_files: function(e) {
264         var types = e._event.dataTransfer.types;
265         for (var i=0; i<types.length; i++) {
266             if (types[i] == 'Files') {
267                 return true;
268             }
269         }
270         return false;
271     },
273     /**
274      * Check if reached the maximumum number of allowed files
275      *
276      * @return boolean true if reached maximum number of files
277      */
278     reached_maxfiles: function() {
279         if (this.filemanager) {
280             if (this.filemanager.filecount >= this.maxfiles && this.maxfiles != -1) {
281                 return true;
282             }
283         }
284         return false;
285     },
287     /**
288      * Highlight the destination node
289      */
290     show_upload_ready: function() {
291         this.container.addClass('dndupload-over');
292     },
294     /**
295      * Remove highlight on destination node
296      */
297     hide_upload_ready: function() {
298         this.container.removeClass('dndupload-over');
299     },
301     /**
302      * Display a progress spinner in the destination node
303      */
304     show_progress_spinner: function() {
305         // add a loading spinner to show something is happening
306         var loadingspinner = this.Y.Node.create('<div id="dndprogresspinner-'+this.clientid+'" style="text-align: center">');
307         loadingspinner.append('<img src="'+M.util.image_url('i/loading_small')+'" />');
308         this.container.append(loadingspinner);
309     },
311     /**
312      * Remove progress spinner in the destination node
313      */
314     hide_progress_spinner: function() {
315         this.Y.one('#dndprogresspinner-'+this.clientid).remove();
316     },
318     /**
319      * Tell the attached filemanager element (if any) to refresh on file
320      * upload
321      */
322     update_filemanager: function() {
323         if (this.filemanager) {
324             // update the filemanager that we've uploaded the files
325             this.hide_progress_spinner();
326             this.filemanager.filepicker_callback();
327         }
328     },
330     /**
331      * Upload a single file via an AJAX call to the 'upload' repository
332      */
333     upload_file: function(file) {
334         if (file.size > this.maxbytes && this.maxbytes > 0) {
335             // Check filesize before attempting to upload
336             alert(M.util.get_string('uploadformlimit', 'moodle')+"\n'"+file.name+"'");
337             return false;
338         }
340         // This would be an ideal place to use the Y.io function
341         // however, this does not support data encoded using the
342         // FormData object, which is needed to transfer data from
343         // the DataTransfer object into an XMLHTTPRequest
344         // This can be converted when the YUI issue has been integrated:
345         // http://yuilibrary.com/projects/yui3/ticket/2531274
346         var xhr = new XMLHttpRequest();
347         var self = this;
348         xhr.onreadystatechange = function() { // Process the server response
349             if (xhr.readyState == 4 && xhr.status == 200) {
350                 var result = JSON.parse(xhr.responseText);
351                 if (result) {
352                     if (result.error) {
353                         self.hide_progress_spinner();
354                         alert(result.error);
355                     } else if (self.callback) {
356                         // Only update the filepicker if there were no errors
357                         self.hide_progress_spinner();
358                         if (result.event == 'fileexists') {
359                             // Do not worry about this, as we only care about the last
360                             // file uploaded, with the filepicker
361                             result.file = result.newfile.filename;
362                             result.url = result.newfile.url;
363                         }
364                         result.client_id = self.clientid;
365                         self.callback(result);
366                     } else {
367                         self.update_filemanager();
368                     }
369                 }
370             }
371         };
373         // Prepare the data to send
374         var formdata = new FormData();
375         formdata.append('repo_upload_file', file); // The FormData class allows us to attach a file
376         formdata.append('sesskey', M.cfg.sesskey);
377         formdata.append('repo_id', this.repositoryid);
378         formdata.append('itemid', this.itemid);
379         if (this.filemanager) { // Filepickers do not have folders
380             formdata.append('savepath', this.filemanager.currentpath);
381         }
383         if (this.acceptedtypes.constructor == Array) {
384             for (var i=0; i<this.acceptedtypes.length; i++) {
385                 formdata.append('accepted_types[]', this.acceptedtypes[i]);
386             }
387         } else {
388             formdata.append('accepted_types[]', this.acceptedtypes);
389         }
391         // Send the file & required details
392         xhr.open("POST", this.url, true);
393         xhr.send(formdata);
394         return true;
395     }
396 };