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
28 // URL for upload requests
29 url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
30 // itemid used for repository upload
32 // accepted filetypes accepted by this form passed to repository
34 // maximum number of files this form allows
36 // maximum size of files allowed in this form
38 // unqiue id of this form field used for html elements
40 // upload repository id, used for upload
42 // container which holds the node which recieves drag events
44 // filemanager element we are working with
46 // callback to filepicker element to refesh when uploaded
48 // Nasty hack to distinguish between dragenter(first entry),
49 // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
54 * Initalise the drag and drop upload interface
55 * Note: one and only one of options.filemanager and options.formcallback must be defined
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
70 init: function(Y, options) {
73 if (!this.browser_supported()) {
74 return; // Browser does not support the required functionality
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
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
99 this.callback = options.formcallback;
101 alert('dndupload: Need to define either options.filemanager or options.callback');
106 this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline');
110 * Check the browser has the required functionality
111 * @return true if browser supports drag/drop upload
113 browser_supported: function() {
115 if (typeof FileReader=='undefined') {
118 if (typeof FormData=='undefined') {
125 * Get upload repoistory from array of enabled repositories
127 * @param array repositories repository objects passed from filepicker
128 * @param returns int id of upload repository or false if not found
130 get_upload_repositoryid: function(repositories) {
131 for (var id in repositories) {
132 if(repositories[id].type == "upload") {
141 * Initialise drag events on node container, all events need
142 * to be processed for drag and drop to work
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);
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
156 * @param e event object
157 * @return boolean true if a valid file drag event
159 check_drag: function(e) {
160 if (!this.has_files(e)) {
164 if (this.reached_maxfiles()) {
175 * Handle a dragenter event, highlight the destination node
176 * when a suitable drag event occurs
178 drag_enter: function(e) {
179 if (!this.check_drag(e)) {
184 if (this.entercount >= 2) {
185 this.entercount = 2; // Just moved over a child element - nothing to do
189 this.show_upload_ready();
194 * Handle a dragleave event, Remove the highlight if dragged from
197 drag_leave: function(e) {
198 if (!this.check_drag(e)) {
203 if (this.entercount == 1) {
204 return false; // Just moved over a child element - nothing to do
208 this.hide_upload_ready();
213 * Handle a dragover event. Required to intercept to prevent the browser from
214 * handling the drag and drop event as normal
216 drag_over: function(e) {
217 if (!this.check_drag(e)) {
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)
229 if (!this.check_drag(e)) {
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) {
244 if (this.upload_file(f)) {
249 if (files.length >= 1) {
250 this.upload_file(files[0]);
258 * Check to see if the drag event has any files in it
260 * @param e event object
261 * @return boolean true if event has files
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') {
274 * Check if reached the maximumum number of allowed files
276 * @return boolean true if reached maximum number of files
278 reached_maxfiles: function() {
279 if (this.filemanager) {
280 if (this.filemanager.filecount >= this.maxfiles && this.maxfiles != -1) {
288 * Highlight the destination node
290 show_upload_ready: function() {
291 this.container.addClass('dndupload-over');
295 * Remove highlight on destination node
297 hide_upload_ready: function() {
298 this.container.removeClass('dndupload-over');
302 * Display a progress spinner in the destination node
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);
312 * Remove progress spinner in the destination node
314 hide_progress_spinner: function() {
315 this.Y.one('#dndprogresspinner-'+this.clientid).remove();
319 * Tell the attached filemanager element (if any) to refresh on file
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();
331 * Upload a single file via an AJAX call to the 'upload' repository
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+"'");
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();
348 xhr.onreadystatechange = function() { // Process the server response
349 if (xhr.readyState == 4 && xhr.status == 200) {
350 var result = JSON.parse(xhr.responseText);
353 self.hide_progress_spinner();
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;
364 result.client_id = self.clientid;
365 self.callback(result);
367 self.update_filemanager();
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);
383 if (this.acceptedtypes.constructor == Array) {
384 for (var i=0; i<this.acceptedtypes.length; i++) {
385 formdata.append('accepted_types[]', this.acceptedtypes[i]);
388 formdata.append('accepted_types[]', this.acceptedtypes);
391 // Send the file & required details
392 xhr.open("POST", this.url, true);