upload selector improvements
This commit is contained in:
		@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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; }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user