Add file drag and drop and asynchronous upload
This commit is contained in:
parent
6161eaef9e
commit
738b6ae583
|
|
@ -3,6 +3,7 @@ Version 1.0.26 (unreleased)
|
||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|
||||||
|
* Add file drag and drop and asynchronous upload
|
||||||
* Enable/Disable users
|
* Enable/Disable users
|
||||||
* Add setting option to disable private projects
|
* Add setting option to disable private projects
|
||||||
* Add new config option to disable logout
|
* Add new config option to disable logout
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -1,10 +1,10 @@
|
||||||
BUILD_DIR = /tmp
|
BUILD_DIR = /tmp
|
||||||
|
|
||||||
CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown screenshot filters gantt project))
|
CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table form button alert tooltip header board task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive dropdown upload filters gantt project))
|
||||||
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
|
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
|
||||||
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
|
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
|
||||||
|
|
||||||
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot Calendar Board Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router))
|
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Search App Screenshot FileUpload Calendar Board Swimlane Gantt Task Project Subtask TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart CompareHoursColumnChart Router))
|
||||||
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete))
|
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min jquery.textcomplete))
|
||||||
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class File extends Base
|
||||||
|
|
||||||
$this->response->html($this->helper->layout->task('file/new', array(
|
$this->response->html($this->helper->layout->task('file/new', array(
|
||||||
'task' => $task,
|
'task' => $task,
|
||||||
'max_size' => ini_get('upload_max_filesize'),
|
'max_size' => $this->helper->text->phpToBytes(ini_get('upload_max_filesize')),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,29 @@ class Text extends Base
|
||||||
return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
|
return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of bytes from PHP size
|
||||||
|
*
|
||||||
|
* @param integer $val PHP size (example: 2M)
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function phpToBytes($val)
|
||||||
|
{
|
||||||
|
$val = trim($val);
|
||||||
|
$last = strtolower($val[strlen($val)-1]);
|
||||||
|
|
||||||
|
switch ($last) {
|
||||||
|
case 'g':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$val *= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if needle is contained in the haystack
|
* Return true if needle is contained in the haystack
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,33 @@
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2><?= t('Attach a document') ?></h2>
|
<h2><?= t('Attach a document') ?></h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="file-done" style="display:none">
|
||||||
|
<p class="alert alert-success">
|
||||||
|
<?= t('All files have been uploaded successfully.') ?>
|
||||||
|
<?= $this->url->link(t('View uploaded files'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form action="<?= $this->url->href('file', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" enctype="multipart/form-data">
|
<div id="file-error-max-size" style="display:none">
|
||||||
<?= $this->form->csrf() ?>
|
<p class="alert alert-error">
|
||||||
<input type="file" name="files[]" multiple />
|
<?= t('The maximum allowed file size is %sB.', $this->text->bytes($max_size)) ?>
|
||||||
<div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></div>
|
<a href="#" id="file-browser"><?= t('Choose files again') ?></a>
|
||||||
<div class="form-actions">
|
</p>
|
||||||
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
|
</div>
|
||||||
<?= t('or') ?>
|
|
||||||
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
|
<div
|
||||||
|
id="file-dropzone"
|
||||||
|
data-max-size="<?= $max_size ?>"
|
||||||
|
data-url="<?= $this->url->href('file', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
|
||||||
|
<div id="file-dropzone-inner">
|
||||||
|
<?= t('Drag and drop your files here') ?> <?= t('or') ?> <a href="#" id="file-browser"><?= t('choose files') ?></a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<input type="file" name="files[]" multiple style="display:none" id="file-form-element">
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue" id="file-upload-button" disabled>
|
||||||
|
<?= t('or') ?>
|
||||||
|
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,19 +0,0 @@
|
||||||
#screenshot-zone {
|
|
||||||
position: relative;
|
|
||||||
border: 2px dashed #ccc;
|
|
||||||
width: 90%;
|
|
||||||
height: 250px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#screenshot-inner {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 48%;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#screenshot-zone.screenshot-pasted {
|
|
||||||
border: 2px solid #333;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
#file-dropzone,
|
||||||
|
#screenshot-zone {
|
||||||
|
position: relative;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
width: 99%;
|
||||||
|
height: 250px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-dropzone-inner,
|
||||||
|
#screenshot-inner {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 48%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#screenshot-zone.screenshot-pasted {
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-list {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-list li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dotted #ddd;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-list li.file-error {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #b94a48;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ function App() {
|
||||||
this.task = new Task();
|
this.task = new Task();
|
||||||
this.project = new Project();
|
this.project = new Project();
|
||||||
this.subtask = new Subtask();
|
this.subtask = new Subtask();
|
||||||
|
this.file = new FileUpload(this);
|
||||||
this.keyboardShortcuts();
|
this.keyboardShortcuts();
|
||||||
this.chosen();
|
this.chosen();
|
||||||
this.poll();
|
this.poll();
|
||||||
|
|
@ -39,6 +40,7 @@ App.prototype.listen = function() {
|
||||||
this.task.listen();
|
this.task.listen();
|
||||||
this.swimlane.listen();
|
this.swimlane.listen();
|
||||||
this.subtask.listen();
|
this.subtask.listen();
|
||||||
|
this.file.listen();
|
||||||
this.search.focus();
|
this.search.focus();
|
||||||
this.autoComplete();
|
this.autoComplete();
|
||||||
this.datePicker();
|
this.datePicker();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
function FileUpload(app) {
|
||||||
|
this.app = app;
|
||||||
|
this.files = [];
|
||||||
|
this.currentFile = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUpload.prototype.listen = function() {
|
||||||
|
var dropzone = document.getElementById("file-dropzone");
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (dropzone) {
|
||||||
|
dropzone.ondragover = dropzone.ondragenter = function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
dropzone.ondrop = function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
self.files = e.dataTransfer.files;
|
||||||
|
self.show();
|
||||||
|
$("#file-error-max-size").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on("click", "#file-browser", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$("#file-form-element").get(0).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", "#file-upload-button", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self.currentFile = 0;
|
||||||
|
self.checkFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#file-form-element").change(function() {
|
||||||
|
self.files = document.getElementById("file-form-element").files;
|
||||||
|
self.show();
|
||||||
|
$("#file-error-max-size").hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.show = function() {
|
||||||
|
$("#file-list").remove();
|
||||||
|
|
||||||
|
if (this.files.length > 0) {
|
||||||
|
$("#file-upload-button").prop("disabled", false);
|
||||||
|
$("#file-dropzone-inner").hide();
|
||||||
|
|
||||||
|
var ul = jQuery("<ul>", {"id": "file-list"});
|
||||||
|
|
||||||
|
for (var i = 0; i < this.files.length; i++) {
|
||||||
|
var percentage = jQuery("<span>", {"id": "file-percentage-" + i}).append("(0%)");
|
||||||
|
var progress = jQuery("<progress>", {"id": "file-progress-" + i, "value": 0});
|
||||||
|
var li = jQuery("<li>", {"id": "file-label-" + i})
|
||||||
|
.append(progress)
|
||||||
|
.append(" ")
|
||||||
|
.append(this.files[i].name)
|
||||||
|
.append(" ")
|
||||||
|
.append(percentage);
|
||||||
|
|
||||||
|
ul.append(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#file-dropzone").append(ul);
|
||||||
|
} else {
|
||||||
|
$("#file-dropzone-inner").show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.checkFiles = function() {
|
||||||
|
var max = parseInt($("#file-dropzone").data("max-size"));
|
||||||
|
|
||||||
|
for (var i = 0; i < this.files.length; i++) {
|
||||||
|
if (this.files[i].size > max) {
|
||||||
|
$("#file-error-max-size").show();
|
||||||
|
$("#file-label-" + i).addClass("file-error");
|
||||||
|
$("#file-upload-button").prop("disabled", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploadFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.uploadFiles = function() {
|
||||||
|
if (this.files.length > 0) {
|
||||||
|
this.uploadFile(this.files[this.currentFile]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.uploadFile = function(file) {
|
||||||
|
var dropzone = document.getElementById("file-dropzone");
|
||||||
|
var url = dropzone.dataset.url;
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
var fd = new FormData();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener("progress", this.updateProgress.bind(this));
|
||||||
|
xhr.upload.addEventListener("load", this.transferComplete.bind(this));
|
||||||
|
|
||||||
|
xhr.open("POST", url, true);
|
||||||
|
fd.append('files[]', file);
|
||||||
|
xhr.send(fd);
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.updateProgress = function(e) {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
$("#file-progress-" + this.currentFile).val(e.loaded / e.total);
|
||||||
|
$("#file-percentage-" + this.currentFile).text('(' + Math.floor((e.loaded / e.total) * 100) + '%)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FileUpload.prototype.transferComplete = function() {
|
||||||
|
this.currentFile++;
|
||||||
|
|
||||||
|
if (this.currentFile < this.files.length) {
|
||||||
|
this.uploadFile(this.files[this.currentFile]);
|
||||||
|
} else {
|
||||||
|
$("#file-upload-button").prop("disabled", true);
|
||||||
|
$("#file-upload-button").parent().hide();
|
||||||
|
$("#file-done").show();
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue