MDL-29766 Add drag and drop upload to filemanager / filepicker elements

This commit is contained in:
Davo Smith 2011-11-08 20:05:19 +00:00 committed by Dan Poltawski
parent 0e84b1664d
commit f08fac7c89
9 changed files with 441 additions and 4 deletions

View File

@ -451,6 +451,9 @@ $string['displayingfirst'] = 'Only the first {$a->count} {$a->things} are displa
$string['displayingrecords'] = 'Displaying {$a} records';
$string['displayingusers'] = 'Displaying users {$a->start} to {$a->end}';
$string['displayonpage'] = 'Display on page';
$string['dndenabled'] = 'You can drag and drop files into this box to upload them';
$string['dndenabled_help'] = 'You can drag one or more files from your desktop and drop them onto the box below to upload them.<br />Note: this may not work with other web browsers';
$string['dndenabled_single'] = 'you can drag and drop a file into this box to upload it';
$string['documentation'] = 'Moodle documentation';
$string['down'] = 'Down';
$string['download'] = 'Download';

396
lib/form/dndupload.js Normal file
View File

@ -0,0 +1,396 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript library for enableing a drag and drop upload interface
*
* @package moodlecore
* @subpackage form
* @copyright 2011 Davo Smith
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
M.form_dndupload = {
// YUI object.
Y: null,
// URL for upload requests
url: M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload',
// itemid used for repository upload
itemid: null,
// accepted filetypes accepted by this form passed to repository
acceptedtypes: [],
// maximum number of files this form allows
maxfiles: 0,
// maximum size of files allowed in this form
maxbytes: 0,
// unqiue id of this form field used for html elements
clientid: '',
// upload repository id, used for upload
repositoryid: 0,
// container which holds the node which recieves drag events
container: null,
// filemanager element we are working with
filemanager: null,
// callback to filepicker element to refesh when uploaded
callback: null,
// Nasty hack to distinguish between dragenter(first entry),
// dragenter+dragleave(moving between child elements) and dragleave (leaving element)
entercount: 0,
/**
* Initalise the drag and drop upload interface
* Note: one and only one of options.filemanager and options.formcallback must be defined
*
* @param Y the YUI object
* @param object options {
* itemid: itemid used for repository upload in this form
* acceptdtypes: accepted filetypes by this form
* maxfiles: maximum number of files this form allows
* maxbytes: maximum size of files allowed in this form
* clientid: unqiue id of this form field used for html elements
* containerprefix: prefix of htmlid of container
* repositories: array of repository objects passed from filepicker
* filemanager: filemanager element we are working with
* callback: callback to filepicker element to refesh when uploaded
* }
*/
init: function(Y, options) {
this.Y = Y;
if (!this.browser_supported()) {
return; // Browser does not support the required functionality
}
// try and retrieve enabled upload repository
this.repositoryid = this.get_upload_repositoryid(options.repositories);
if (!this.repositoryid) {
return; // no upload repository is enabled to upload to
}
this.acceptedtypes = options.acceptedtypes;
this.clientid = options.clientid;
this.maxfiles = options.maxfiles;
this.maxbytes = options.maxbytes;
this.itemid = options.itemid;
this.container = this.Y.one(options.containerprefix + this.clientid);
if (options.filemanager) {
// Needed to tell the filemanager to redraw when files uploaded
// and to check how many files are already uploaded
this.filemanager = options.filemanager;
} else if (options.formcallback) {
// Needed to tell the filepicker to update when a new
// file is uploaded
this.callback = options.formcallback;
} else {
alert('dndupload: Need to define either options.filemanager or options.callback');
return;
}
this.init_events();
this.Y.one('#dndenabled-'+this.clientid).setStyle('display', 'inline');
},
/**
* Check the browser has the required functionality
* @return true if browser supports drag/drop upload
*/
browser_supported: function() {
if (typeof FileReader=='undefined') {
return false;
}
if (typeof FormData=='undefined') {
return false;
}
return true;
},
/**
* Get upload repoistory from array of enabled repositories
*
* @param array repositories repository objects passed from filepicker
* @param returns int id of upload repository or false if not found
*/
get_upload_repositoryid: function(repositories) {
for (var id in repositories) {
if(repositories[id].type == "upload") {
return id;
}
}
return false;
},
/**
* Initialise drag events on node container, all events need
* to be processed for drag and drop to work
*/
init_events: function() {
this.Y.on('dragenter', this.drag_enter, this.container, this);
this.Y.on('dragleave', this.drag_leave, this.container, this);
this.Y.on('dragover', this.drag_over, this.container, this);
this.Y.on('drop', this.drop, this.container, this);
},
/**
* Check if the drag contents are valid and then call
* preventdefault / stoppropagation to let the browser know
* we will handle this drag/drop
*
* @param e event object
* @return boolean true if a valid file drag event
*/
check_drag: function(e) {
if (!this.has_files(e)) {
return false;
}
if (this.reached_maxfiles()) {
return false;
}
e.preventDefault();
e.stopPropagation();
return true;
},
/**
* Handle a dragenter event, highlight the destination node
* when a suitable drag event occurs
*/
drag_enter: function(e) {
if (!this.check_drag(e)) {
return true;
}
this.entercount++;
if (this.entercount >= 2) {
this.entercount = 2; // Just moved over a child element - nothing to do
return false;
}
this.show_upload_ready();
return false;
},
/**
* Handle a dragleave event, Remove the highlight if dragged from
* node
*/
drag_leave: function(e) {
if (!this.check_drag(e)) {
return true;
}
this.entercount--;
if (this.entercount == 1) {
return false; // Just moved over a child element - nothing to do
}
this.entercount = 0;
this.hide_upload_ready();
return false;
},
/**
* Handle a dragover event. Required to intercept to prevent the browser from
* handling the drag and drop event as normal
*/
drag_over: function(e) {
if (!this.check_drag(e)) {
return true;
}
return false;
},
/**
* Handle a drop event. Remove the highlight and then upload each
* of the files (until we reach the file limit, or run out of files)
*/
drop: function(e) {
if (!this.check_drag(e)) {
return true;
}
this.entercount = 0;
this.hide_upload_ready();
this.show_progress_spinner();
var files = e._event.dataTransfer.files;
if (this.filemanager) {
var currentfilecount = this.filemanager.filecount;
for (var i=0, f; f=files[i]; i++) {
if (currentfilecount >= this.maxfiles && this.maxfiles != -1) {
break;
}
if (this.upload_file(f)) {
currentfilecount++;
}
}
} else {
if (files.length >= 1) {
this.upload_file(files[0]);
}
}
return false;
},
/**
* Check to see if the drag event has any files in it
*
* @param e event object
* @return boolean true if event has files
*/
has_files: function(e) {
var types = e._event.dataTransfer.types;
for (var i=0; i<types.length; i++) {
if (types[i] == 'Files') {
return true;
}
}
return false;
},
/**
* Check if reached the maximumum number of allowed files
*
* @return boolean true if reached maximum number of files
*/
reached_maxfiles: function() {
if (this.filemanager) {
if (this.filemanager.filecount >= this.maxfiles && this.maxfiles != -1) {
return true;
}
}
return false;
},
/**
* Highlight the destination node
*/
show_upload_ready: function() {
this.container.addClass('dndupload-over');
},
/**
* Remove highlight on destination node
*/
hide_upload_ready: function() {
this.container.removeClass('dndupload-over');
},
/**
* Display a progress spinner in the destination node
*/
show_progress_spinner: function() {
// add a loading spinner to show something is happening
var loadingspinner = this.Y.Node.create('<div id="dndprogresspinner-'+this.clientid+'" style="text-align: center">');
loadingspinner.append('<img src="'+M.util.image_url('i/loading_small')+'" />');
this.container.append(loadingspinner);
},
/**
* Remove progress spinner in the destination node
*/
hide_progress_spinner: function() {
this.Y.one('#dndprogresspinner-'+this.clientid).remove();
},
/**
* Tell the attached filemanager element (if any) to refresh on file
* upload
*/
update_filemanager: function() {
if (this.filemanager) {
// update the filemanager that we've uploaded the files
this.hide_progress_spinner();
this.filemanager.filepicker_callback();
}
},
/**
* Upload a single file via an AJAX call to the 'upload' repository
*/
upload_file: function(file) {
if (file.size > this.maxbytes && this.maxbytes > 0) {
// Check filesize before attempting to upload
alert(M.util.get_string('uploadformlimit', 'moodle')+"\n'"+file.name+"'");
return false;
}
// This would be an ideal place to use the Y.io function
// however, this does not support data encoded using the
// FormData object, which is needed to transfer data from
// the DataTransfer object into an XMLHTTPRequest
// This can be converted when the YUI issue has been integrated:
// http://yuilibrary.com/projects/yui3/ticket/2531274
var xhr = new XMLHttpRequest();
var self = this;
xhr.onreadystatechange = function() { // Process the server response
if (xhr.readyState == 4 && xhr.status == 200) {
var result = JSON.parse(xhr.responseText);
if (result) {
if (result.error) {
self.hide_progress_spinner();
alert(result.error);
} else if (self.callback) {
// Only update the filepicker if there were no errors
self.hide_progress_spinner();
if (result.event == 'fileexists') {
// Do not worry about this, as we only care about the last
// file uploaded, with the filepicker
result.file = result.newfile.filename;
result.url = result.newfile.url;
}
result.client_id = self.clientid;
self.callback(result);
} else {
self.update_filemanager();
}
}
}
};
// Prepare the data to send
var formdata = new FormData();
formdata.append('repo_upload_file', file); // The FormData class allows us to attach a file
formdata.append('sesskey', M.cfg.sesskey);
formdata.append('repo_id', this.repositoryid);
formdata.append('itemid', this.itemid);
if (this.filemanager) { // Filepickers do not have folders
formdata.append('savepath', this.filemanager.currentpath);
}
if (this.acceptedtypes.constructor == Array) {
for (var i=0; i<this.acceptedtypes.length; i++) {
formdata.append('accepted_types[]', this.acceptedtypes[i]);
}
} else {
formdata.append('accepted_types[]', this.acceptedtypes);
}
// Send the file & required details
xhr.open("POST", this.url, true);
xhr.send(formdata);
return true;
}
};

View File

@ -763,5 +763,16 @@ M.form_filemanager.init = function(Y, options) {
item.style.display = '';
}
new FileManagerHelper(options);
var manager = new FileManagerHelper(options);
var dndoptions = {
filemanager: manager,
acceptedtypes: options.accepted_types,
clientid: options.client_id,
maxfiles: options.maxfiles,
maxbytes: options.maxbytes,
itemid: options.itemid,
repositories: manager.filepicker_options.repositories,
containerprefix: '#filemanager-',
};
M.form_dndupload.init(Y, dndoptions);
};

View File

@ -274,6 +274,7 @@ function form_filemanager_render($options) {
}
$maxsize = get_string('maxfilesize', 'moodle', display_size(get_max_upload_file_size($CFG->maxbytes, $course_maxbytes, $options->maxbytes)));
$strdndenabled = get_string('dndenabled', 'moodle').$OUTPUT->help_icon('dndenabled');
$html .= <<<FMHTML
<div class="filemanager-loading mdl-align" id='filemanager-loading-{$client_id}'>
$icon_progress
@ -285,6 +286,7 @@ $icon_progress
<input type="button" class="fm-btn-mkdir" id="btncrt-{$client_id}" onclick="return false" value="{$strmakedir}" />
<input type="button" class="fm-btn-download" id="btndwn-{$client_id}" onclick="return false" {$extra} value="{$strdownload}" />
<span> $maxsize </span>
<span id="dndenabled-{$client_id}" style="display: none"> - $strdndenabled </span>
</div>
<div class="filemanager-container" id="filemanager-{$client_id}">
<ul id="draftfiles-{$client_id}" class="fm-filelist">
@ -304,7 +306,7 @@ FMHTML;
$module = array(
'name'=>'form_filemanager',
'fullpath'=>'/lib/form/filemanager.js',
'requires' => array('core_filepicker', 'base', 'io-base', 'node', 'json', 'yui2-button', 'yui2-container', 'yui2-layout', 'yui2-menu', 'yui2-treeview'),
'requires' => array('core_filepicker', 'base', 'io-base', 'node', 'json', 'yui2-button', 'yui2-container', 'yui2-layout', 'yui2-menu', 'yui2-treeview', 'core_dndupload'),
'strings' => array(array('loading', 'repository'), array('nomorefiles', 'repository'), array('confirmdeletefile', 'repository'),
array('add', 'repository'), array('accessiblefilepicker', 'repository'), array('move', 'moodle'),
array('cancel', 'moodle'), array('download', 'moodle'), array('ok', 'moodle'),

View File

@ -43,4 +43,16 @@ M.form_filepicker.init = function(Y, options) {
if (item) {
item.style.display = '';
}
var dndoptions = {
clientid: options.client_id,
acceptedtypes: options.accepted_types,
maxfiles: -1,
maxbytes: options.maxbytes,
itemid: options.itemid,
repositories: options.repositories,
formcallback: options.formcallback,
containerprefix: '#file_info_',
};
M.form_dndupload.init(Y, dndoptions);
};

View File

@ -88,7 +88,7 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input {
$html .= $OUTPUT->render($fp);
$html .= '<input type="hidden" name="'.$elname.'" id="'.$id.'" value="'.$draftitemid.'" class="filepickerhidden"/>';
$module = array('name'=>'form_filepicker', 'fullpath'=>'/lib/form/filepicker.js', 'requires'=>array('core_filepicker', 'node', 'node-event-simulate'));
$module = array('name'=>'form_filepicker', 'fullpath'=>'/lib/form/filepicker.js', 'requires'=>array('core_filepicker', 'node', 'node-event-simulate', 'core_dndupload'));
$PAGE->requires->js_init_call('M.form_filepicker.init', array($fp->options), true, $module);
$nonjsfilepicker = new moodle_url('/repository/draftfiles_manager.php', array(

View File

@ -1902,6 +1902,7 @@ class core_renderer extends renderer_base {
$strsaved = get_string('filesaved', 'repository');
$straddfile = get_string('openpicker', 'repository');
$strloading = get_string('loading', 'repository');
$strdndenabled = get_string('dndenabled_single', 'moodle');
$icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
$currentfile = $options->currentfile;
@ -1935,7 +1936,9 @@ $icon_progress
EOD;
if ($options->env != 'url') {
$html .= <<<EOD
<div id="file_info_{$client_id}" class="mdl-left filepicker-filelist">$currentfile</div>
<div id="file_info_{$client_id}" class="mdl-left filepicker-filelist">
$currentfile<span id="dndenabled-{$client_id}" style="display: none"> - $strdndenabled </span>
</div>
EOD;
}
$html .= '</div>';

View File

@ -464,6 +464,12 @@ class page_requirements_manager {
'fullpath' => '/files/module.js',
'requires' => array('node', 'event', 'overlay', 'io-base', 'json', 'yui2-treeview'));
break;
case 'core_dndupload':
$module = array('name' => 'core_dndupload',
'fullpath' => '/lib/form/dndupload.js',
'requires' => array('node', 'event', 'json'),
'strings' => array(array('uploadformlimit', 'moodle')));
break;
}
} else {

View File

@ -518,6 +518,7 @@ body.tag .managelink {padding: 5px;}
.filemanager-toolbar {margin: 5px 0;}
.filemanager-toolbar a {border: 1px solid #AACCEE;background: #F4FAFF;color: black;padding: 3px;}
.filemanager-toolbar a:hover {background: #FFFFFF;}
.filemanager-toolbar .helplink a {border: 0px; background: transparent;}
.fm-breadcrumb {margin:0;}
.filemanager-container {padding: 5px;margin: 6px 0;background: #E9F4FF;border: #AACCEE 1px solid}
.filemanager-container ul{margin:0;padding:0;}
@ -531,6 +532,9 @@ body.tag .managelink {padding: 5px;}
.fm-file-entry{border: 1px solid red;}
.fm-operation {font-weight: bold;}
.filemanager-container.dndupload-over,
.filepicker-filelist.dndupload-over {background: #8EF947;}
/*
* Backup and Restore CSS
*/