Add AJAX file delete support and upload notifications, make UI nicer
This commit is contained in:
parent
983a32aba8
commit
f4c21006de
@ -1,40 +1,193 @@
|
||||
$(function () {
|
||||
// un-set "name" attributes to avoid submitting to server
|
||||
$(".fileupload").removeAttr('name');
|
||||
// set up all file inputs for jQuery-fileUpload
|
||||
$(".fileupload").fileupload({
|
||||
dataType: 'json',
|
||||
paramName: 'file',
|
||||
done: function (e, data) {
|
||||
// process server response
|
||||
if (data.result.is_valid) {
|
||||
var $field = $('#id_' + $(this).attr('data-field')),
|
||||
$ul = $(this).siblings('ul'),
|
||||
$li = $("<li>"),
|
||||
$remove = $('<a title="remove"></a>'),
|
||||
ids = $field.val().split(",").filter(function (v) {
|
||||
return v != '';
|
||||
});
|
||||
class MultipleFilesWidget {
|
||||
constructor(div) {
|
||||
this.root = div
|
||||
this.fieldName = this.root.getAttribute('data-field')
|
||||
this.fileList = []
|
||||
this.element = {
|
||||
list: this.root.querySelector('.filewidget--list'),
|
||||
uploadButton: this.root.querySelector('.filewidget--input'),
|
||||
field: document.querySelector(`[name="${this.fieldName}"]`)
|
||||
}
|
||||
|
||||
ids.push(data.result.id);
|
||||
|
||||
if (!$ul.length) {
|
||||
$ul = $("<ul>").insertAfter(this);
|
||||
const self = this
|
||||
|
||||
// Set up jquery-fileupload
|
||||
$(this.element.uploadButton).fileupload({
|
||||
dataType: 'json',
|
||||
paramName: 'file',
|
||||
done: function (e, data) {
|
||||
// process server response
|
||||
if (!data.result.is_valid) {
|
||||
throw new Error('Server sent us invalid data!')
|
||||
}
|
||||
|
||||
$li.text(data.result.name);
|
||||
self.fileFinished.bind(self)(data.result)
|
||||
},
|
||||
add: function (e, data) {
|
||||
$.each(data.files, (index, file) => {
|
||||
self.addFile.bind(self)(null, file.name, self.setFileInProgress.bind(self))
|
||||
});
|
||||
|
||||
// FIXME AJAXify
|
||||
//$remove.attr('href', '/files/' + data.result.id + '/delete/');
|
||||
//$remove.append($('<i class="glyphicon glyphicon-remove"/>'));
|
||||
//$li.append($remove);
|
||||
data.process().done(function () {
|
||||
data.submit()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$ul.append($li);
|
||||
// Set up listening for restore
|
||||
this.element.field.addEventListener('change', evt => {
|
||||
let idList = evt.srcElement.value.split(",")
|
||||
for (let id of idList) {
|
||||
this.addFile(id, id, this.setFileDone.bind(this))
|
||||
}
|
||||
this.viewFileList()
|
||||
})
|
||||
}
|
||||
|
||||
$field.val(ids.toString());
|
||||
addFile(id, name, stateFunc) {
|
||||
console.log('addfile here!')
|
||||
|
||||
document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
|
||||
let li = document.createElement('li')
|
||||
li.className = 'filewidget--file'
|
||||
li.innerHTML =
|
||||
`<i class='filewidget--file--icon'></i> <span class='filewidget--file--name'></span>
|
||||
<span class='filewidget--file--actions'></span>`
|
||||
|
||||
let file = {
|
||||
root: li,
|
||||
element: {
|
||||
icon: li.querySelector('.filewidget--file--icon'),
|
||||
name: li.querySelector('.filewidget--file--name'),
|
||||
actions: li.querySelector('.filewidget--file--actions'),
|
||||
},
|
||||
name: name,
|
||||
id: id || null,
|
||||
}
|
||||
|
||||
stateFunc(file)
|
||||
|
||||
this.fileList.push(file)
|
||||
this.viewFileList()
|
||||
}
|
||||
|
||||
removeFile(file) {
|
||||
// Remove file from display
|
||||
this.element.list.removeChild(file.root)
|
||||
|
||||
// Remove file from fileList
|
||||
this.fileList = this.fileList.filter(cmpFile => cmpFile === file)
|
||||
}
|
||||
|
||||
fileFinished(serverResponse) {
|
||||
// Using data, find the file
|
||||
let file = this.fileList.filter(file => file.name === serverResponse.name)[0]
|
||||
|
||||
// Set the ID
|
||||
file.id = serverResponse.id
|
||||
|
||||
// Set the file state now it's finished
|
||||
this.setFileDone(file)
|
||||
this.viewFileList()
|
||||
|
||||
// Do this last tho
|
||||
this.updateFormField()
|
||||
}
|
||||
|
||||
//
|
||||
// Update the field that keeps track of file IDs
|
||||
//
|
||||
|
||||
updateFormField() {
|
||||
console.log("updateFormField", this)
|
||||
|
||||
let oldVal = this.element.field.value
|
||||
this.element.field.value = this.fileList.filter(f => f.id != null)
|
||||
.map(f => f.id)
|
||||
.toString()
|
||||
|
||||
// Mark form as dirty
|
||||
if (this.element.field.value !== oldVal) {
|
||||
document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Manage the state of individual files in the widget
|
||||
//
|
||||
|
||||
setFileState(file, state) {
|
||||
file.element.icon.classList = `filewidget--file--icon ${state.iconClassList}`
|
||||
file.element.name.innerText = state.fileName
|
||||
file.element.actions.innerHTML = state.actions || ''
|
||||
}
|
||||
|
||||
setFileInProgress(file) {
|
||||
this.setFileState(file, {
|
||||
iconClassList: 'fa fa-spinner fa-spin',
|
||||
fileName: `${file.name} (uploading...)`
|
||||
})
|
||||
}
|
||||
|
||||
setFileDeletingInProgress(file) {
|
||||
this.setFileState(file, {
|
||||
iconClassList: 'fa fa-spinner fa-spin',
|
||||
fileName: `Deleting...`
|
||||
})
|
||||
}
|
||||
|
||||
setFileDeletingFailed(file) {
|
||||
this.setFileState(file, {
|
||||
iconClassList: 'fa fa-exclamation-triangle',
|
||||
fileName: `${file.name} (delete failed!)`
|
||||
})
|
||||
}
|
||||
|
||||
setFileDone(file) {
|
||||
console.log("setFileDone", file)
|
||||
|
||||
this.setFileState(file, {
|
||||
iconClassList: 'fa fa-file-o',
|
||||
fileName: `${file.name}`,
|
||||
actions: '<a title="Remove"><i class="fa fa-times"></i></a>'
|
||||
})
|
||||
|
||||
file.element.actions.querySelector('a').addEventListener('click', () => {
|
||||
this.setFileDeletingInProgress(file)
|
||||
|
||||
return fetch(`/files/delete/${file.id}/`, {
|
||||
method: 'POST',
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
this.removeFile(file)
|
||||
} else {
|
||||
this.setFileDeletingFailed(file)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Redraw the file list
|
||||
//
|
||||
|
||||
viewFileList() {
|
||||
console.log("viewFileList", this)
|
||||
|
||||
for (let file of this.fileList) {
|
||||
// Check if it's appended to the list
|
||||
if (!this.element.list.contains(file.root)) {
|
||||
this.element.list.appendChild(file.root)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
window.official_project_documents = new MultipleFilesWidget(
|
||||
document.querySelector('[data-field=official_project_documents_files]')
|
||||
)
|
||||
})
|
||||
|
@ -81,15 +81,17 @@ class ShortCaseStudyForm(BaseCaseStudyForm):
|
||||
]
|
||||
|
||||
|
||||
class BootstrapClearableFileInput(forms.ClearableFileInput):
|
||||
template_name = 'map/forms/widgets/file.html'
|
||||
|
||||
|
||||
class LongCaseStudyForm(BaseCaseStudyForm):
|
||||
"""Long version of the CaseStudy form."""
|
||||
|
||||
official_project_documents = forms.FileField(
|
||||
widget=forms.ClearableFileInput(attrs={
|
||||
'multiple': True,
|
||||
'data-url': reverse_lazy('files:upload'),
|
||||
'data-field': 'official_project_documents_files',
|
||||
'class': 'fileupload',
|
||||
widget=BootstrapClearableFileInput(attrs={
|
||||
'url': reverse_lazy('files:upload'),
|
||||
'field': 'official_project_documents_files',
|
||||
}), required=False
|
||||
)
|
||||
|
||||
@ -100,11 +102,9 @@ class LongCaseStudyForm(BaseCaseStudyForm):
|
||||
)
|
||||
|
||||
other_documents = forms.FileField(
|
||||
widget=forms.ClearableFileInput(attrs={
|
||||
'multiple': True,
|
||||
widget=BootstrapClearableFileInput(attrs={
|
||||
'data-url': reverse_lazy('files:upload'),
|
||||
'data-field': 'other_documents_files',
|
||||
'class': 'fileupload',
|
||||
}), required=False
|
||||
)
|
||||
|
||||
@ -115,11 +115,9 @@ class LongCaseStudyForm(BaseCaseStudyForm):
|
||||
)
|
||||
|
||||
shapefiles = forms.FileField(
|
||||
widget=forms.ClearableFileInput(attrs={
|
||||
'multiple': True,
|
||||
widget=BootstrapClearableFileInput(attrs={
|
||||
'data-url': reverse_lazy('files:upload'),
|
||||
'data-field': 'shapefiles_files',
|
||||
'class': 'fileupload',
|
||||
}), required=False
|
||||
)
|
||||
|
||||
|
@ -33,6 +33,41 @@
|
||||
border: 1px solid #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filewidget--input { position: absolute; left: -1000px; }
|
||||
.filewidget--list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.filewidget--file {
|
||||
border: 1px solid #aaa;
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
width: 34em;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.filewidget--file--icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filewidget--file--actions {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.filewidget--file--actions a {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filewidget--file--actions a:hover {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
7
apps/map/templates/map/forms/widgets/file.html
Normal file
7
apps/map/templates/map/forms/widgets/file.html
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="filewidget" data-field="{{ widget.attrs.field }}">
|
||||
<label class="filewidget--add btn btn-success">
|
||||
<input class="filewidget--input" type="file" multiple data-url="{{ widget.attrs.url }}">
|
||||
<i class="fa fa-plus"></i> Add files
|
||||
</label>
|
||||
<ul class="filewidget--list"></ul>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user