upload selector improvements

This commit is contained in:
bkfox
2024-03-17 21:00:07 +01:00
parent de858f45e8
commit 024db5f307
9 changed files with 226 additions and 77 deletions

View File

@ -1,5 +1,5 @@
<template>
<component :is="tag" @click="call" :class="buttonClass">
<component :is="tag" @click.capture.stop="call" type="button" :class="buttonClass">
<span v-if="promise && runIcon">
<i :class="runIcon"></i>
</span>
@ -27,6 +27,8 @@ export default {
data: Object,
//! Action method, by default, `POST`
method: { type: String, default: 'POST'},
//! If provided open confirmation box before proceeding
confirm: { type: String, default: ''},
//! Action url
url: String,
//! Extra request options
@ -60,6 +62,9 @@ export default {
call() {
if(this.promise || !this.url)
return
if(this.confirm && !confirm(this.confirm))
return
const options = Model.getOptions({
...this.fetchOptions,
method: this.method,

View File

@ -1,27 +1,29 @@
<template>
<div class="a-select-file">
<div :class="['a-select-file-list', listClass]" ref="list">
<div class="flex-column">
<div ref="list" :class="['a-select-file-list', listClass]">
<!-- upload -->
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
<div class="field flex-grow-1" v-if="!uploadFile">
<label class="label">{{ uploadLabel }}</label>
<input type="file" @change="previewFile"/>
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
</div>
<slot name="upload-preview" :item="uploadFile"></slot>
<div v-if="uploadFile">
<button class="button secondary" @click="removeUpload">
<span class="icon">
<i class="fa fa-trash"></i>
<div class="flex-grow-1">
<slot name="upload-form"></slot>
</div>
</form>
<div class="flex-column" v-else>
<slot name="upload-preview" :upload="upload"></slot>
<div class="flex-row">
<progress :max="upload.total" :value="upload.loaded"/>
<button type="button" class="button small square ml-2" @click="uploadAbort">
<span class="icon small">
<i class="fa fa-close"></i>
</span>
</button>
<button class="button float-right" @click="doUpload">
<span class="icon">
<i class="fa fa-upload"></i>
</span>
Upload
</button>
</div>
</div>
<!-- tiles -->
<div v-if="prevUrl">
<a href="#" @click="load(prevUrl)">
{{ prevLabel }}
@ -30,7 +32,7 @@
<template v-for="item in items" v-bind:key="item.id">
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
<slot :item="item"></slot>
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
</div>
</template>
@ -55,55 +57,37 @@ export default {
prevLabel: { type: String, default: "Prev" },
nextLabel: { type: String, default: "Next" },
listUrl: { type: String },
uploadUrl: { type: String },
uploadFieldName: { type: String, default: "file" },
uploadLabel: { type: String, default: "Upload a file" },
},
data() {
return {
STATE: {
DEFAULT: 0,
UPLOADING: 1,
},
state: 0,
item: null,
items: [],
uploadFile: null,
uploadUrl: null,
uploadFieldName: null,
uploadCSRF: null,
nextUrl: "",
prevUrl: "",
lastUrl: "",
upload: {},
}
},
methods: {
previewFile(event) {
const [file] = event.target.files
this.uploadFile = file && {
file: file,
src: URL.createObjectURL(file)
}
},
removeUpload() {
this.uploadFile = null;
},
doUpload() {
const formData = new FormData();
formData.append('file', this.uploadFile.file)
formData.append('original_filename', this.uploadFile.file.name)
formData.append('csrfmiddlewaretoken', getCsrf())
fetch(this.uploadUrl, {
method: "POST",
body: formData
}).then(
() => {
this.uploadFile = null;
this.load()
}
)
},
load(url) {
fetch(url || this.listUrl).then(
response => response.ok ? response.json() : Promise.reject(response)
).then(data => {
this.lastUrl = url
this.nextUrl = data.next
this.prevUrl = data.previous
this.items = data.results
@ -116,6 +100,56 @@ export default {
select(item) {
this.item = item;
},
// ---- upload
uploadAbort() {
this.upload.request && this.upload.request.abort()
},
onSubmit() {
const [file] = this.$refs.uploadFile.files
if(!file)
return
this._setUploadFile(file)
const req = new XMLHttpRequest()
req.open("POST", this.uploadUrl || this.listUrl)
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
req.addEventListener("load", (e) => this.onUploadDone(e, true))
req.addEventListener("abort", (e) => this.onUploadDone(e))
req.addEventListener("error", (e) => this.onUploadDone(e))
const formData = new FormData(this.$refs.uploadForm);
formData.append('csrfmiddlewaretoken', getCsrf())
req.send(formData)
this._resetUpload(this.STATE.UPLOADING, false, req)
},
onUploadProgress(event) {
this.upload.loaded = event.loaded
this.upload.total = event.total
},
onUploadDone(reload=false) {
this._resetUpload(this.STATE.DEFAULT, true)
reload && this.load()
},
_setUploadFile(file) {
this.upload.file = file
this.upload.fileURL = file && URL.createObjectURL(file)
},
_resetUpload(state, resetFile=false, request=null) {
this.state = state
this.upload.loaded = 0
this.upload.total = 0
this.upload.request = request
if(resetFile)
this.upload.file = null
}
},
mounted() {

View File

@ -1,3 +1,4 @@
import AActionButton from './AActionButton'
import AAutocomplete from './AAutocomplete'
import ACarousel from './ACarousel'
import ADropdown from "./ADropdown"
@ -34,5 +35,5 @@ export const admin = {
export const dashboard = {
...base,
ASelectFile, AModal, APlaylistEditor,
AActionButton, ASelectFile, AModal, APlaylistEditor,
}

View File

@ -151,18 +151,19 @@
.button, a.button, button.button {
font-size: v.$text-size;
display: inline-block;
padding: v.$mp-2;
border: none; //1px var(--button-fg) solid;
padding: v.$mp-2e;
border: none;
justify-content: center;
text-align: center;
// font-size: v.$text-size-medium;
cursor: pointer;
text-decoration: none;
color: var(--button-fg);
background-color: var(--button-bg);
&.square { min-width: 2.5rem; }
&.smaller { font-size: v.$text-size-smaller; }
&.small { font-size: v.$text-size-small; }
&.square { min-width: 2.5em; }
&.secondary { background-color: var(--button-sec-bg); }
.label, label {
@ -172,8 +173,8 @@
.icon {
vertical-align: middle;
&:not(:only-child) {
&:first-child { margin: 0 v.$mp-3 0 v.$mp-1; }
&:last-child { margin: 0 v.$mp-3 0 v.$mp-1; }
&:first-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
&:last-child { margin: 0 v.$mp-3e 0 v.$mp-1e; }
}
}

View File

@ -1,7 +1,13 @@
@use "./vars" as v;
// ---- text
.text-light { weight: 400; color: var(--text-color-light); }
.bigger { font-size: v.$text-size-bigger !important; }
.big { font-size: v.$text-size-big !important; }
.smaller { font-size: v.$text-size-smaller !important; }
.small { font-size: v.$text-size-small !important; }
// ---- layout
.align-left {
text-align: left;
@ -34,6 +40,20 @@
.ws-nowrap { white-space: nowrap; }
.height-1 { height: 1em; }
.height-2 { height: 2em; }
.height-3 { height: 3em; }
.height-4 { height: 4em; }
.height-5 { height: 5em; }
.height-6 { height: 6em; }
.height-7 { height: 7em; }
.height-8 { height: 8em; }
.height-9 { height: 9em; }
.height-10 { height: 10em; }
.height-15 { height: 15em; }
.height-20 { height: 20em; }
.height-25 { height: 25em; }
// ---- grid
@mixin grid {
display: grid;