work on page form; add image selector
This commit is contained in:
@ -86,3 +86,8 @@ h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
|
||||
.container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-cover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -138,6 +138,11 @@
|
||||
}
|
||||
|
||||
|
||||
// ---- panels
|
||||
.panels {
|
||||
.panel:not(.active) { display: none; }
|
||||
}
|
||||
|
||||
// ---- button
|
||||
@mixin button {
|
||||
.button, a.button, button.button {
|
||||
@ -701,3 +706,40 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/// ----------------
|
||||
.a-select-file {
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: v.$mp-3;
|
||||
}
|
||||
|
||||
.upload-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.a-select-file-list {
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: v.$mp-3;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0em 0em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
@use "./vars" as v;
|
||||
|
||||
.text-light { weight: 400; color: var(--text-color-light); }
|
||||
|
||||
// ---- layout
|
||||
.align-left { text-align: left; justify-content: left; }
|
||||
.align-right { text-align: right; justify-content: right; }
|
||||
@ -7,6 +9,7 @@
|
||||
.clear-left { clear: left !important }
|
||||
.clear-right { clear: right !important }
|
||||
.clear-both { clear: both !important }
|
||||
.clear-unset { clear: unset !important }
|
||||
|
||||
.d-inline { display: inline !important; }
|
||||
.d-block { display: block !important; }
|
||||
@ -20,6 +23,7 @@
|
||||
|
||||
.ws-nowrap { white-space: nowrap; }
|
||||
|
||||
|
||||
// ---- grid
|
||||
@mixin grid {
|
||||
display: grid;
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
|
||||
// ---- main theme & layout
|
||||
|
||||
.page {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
|
124
assets/src/components/ASelectFile.vue
Normal file
124
assets/src/components/ASelectFile.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="a-select-file">
|
||||
<div class="a-select-file-list" ref="list">
|
||||
<div class="flex-column file-preview">
|
||||
<div class="field flex-grow-1" v-if="!uploadFile">
|
||||
<label class="label">{{ uploadLabel }}</label>
|
||||
<input type="file" @change="previewFile"/>
|
||||
</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>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button float-right" @click="doUpload">
|
||||
<span class="icon">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prevUrl">
|
||||
<a href="#" @click="load(prevUrl)">
|
||||
{{ prevLabel }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="nextUrl">
|
||||
<a href="#" @click="load(nextUrl)">
|
||||
{{ nextLabel }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-select-footer">
|
||||
<slot name="footer" :item="item" :items="items"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {getCsrf} from "../model"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: { type: String },
|
||||
prevLabel: { type: String, default: "Prev" },
|
||||
nextLabel: { type: String, default: "Next" },
|
||||
listUrl: { type: String },
|
||||
uploadLabel: { type: String, default: "Upload a file" },
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
item: null,
|
||||
items: [],
|
||||
uploadFile: null,
|
||||
uploadUrl: null,
|
||||
uploadFieldName: null,
|
||||
uploadCSRF: null,
|
||||
nextUrl: "",
|
||||
prevUrl: "",
|
||||
}
|
||||
},
|
||||
|
||||
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.listUrl, {
|
||||
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.nextUrl = data.next
|
||||
this.prevUrl = data.previous
|
||||
this.items = data.results
|
||||
|
||||
this.$forceUpdate()
|
||||
this.$refs.list.scroll(0, 0)
|
||||
})
|
||||
},
|
||||
|
||||
select(item) {
|
||||
this.item = item;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.load()
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<button :title="ariaLabel"
|
||||
type="button"
|
||||
:aria-label="ariaLabel || label" :aria-description="ariaDescription"
|
||||
@click="toggle" :class="buttonClass">
|
||||
<slot name="default" :active="active">
|
||||
@ -49,17 +50,21 @@ export default {
|
||||
},
|
||||
|
||||
set(active) {
|
||||
const el = document.querySelector(this.el)
|
||||
if(active)
|
||||
el.classList.add(this.activeClass)
|
||||
else
|
||||
el.classList.remove(this.activeClass)
|
||||
if(this.el) {
|
||||
const el = document.querySelector(this.el)
|
||||
if(active)
|
||||
el.classList.add(this.activeClass)
|
||||
else
|
||||
el.classList.remove(this.activeClass)
|
||||
}
|
||||
this.active = active
|
||||
if(active)
|
||||
this.resetGroup()
|
||||
},
|
||||
|
||||
resetGroup() {
|
||||
if(!this.groupClass)
|
||||
return
|
||||
const els = document.querySelectorAll("." + this.groupClass)
|
||||
for(var el of els)
|
||||
if(el != this.$el)
|
||||
|
@ -12,13 +12,15 @@ import ASoundItem from './ASoundItem.vue'
|
||||
import ASwitch from './ASwitch.vue'
|
||||
import AStatistics from './AStatistics.vue'
|
||||
import AStreamer from './AStreamer.vue'
|
||||
import ASelectFile from "./ASelectFile.vue"
|
||||
|
||||
/**
|
||||
* Core components
|
||||
*/
|
||||
export const base = {
|
||||
AAutocomplete, ACarousel, ADropdown, AEpisode, AList, APage, APlayer, APlaylist,
|
||||
AProgress, ASoundItem, ASwitch
|
||||
AProgress, ASoundItem, ASwitch,
|
||||
ASelectFile,
|
||||
}
|
||||
|
||||
export default base
|
||||
|
@ -30,6 +30,7 @@ export default class Live {
|
||||
response.ok ? response.json()
|
||||
: Promise.reject(response)
|
||||
).then(data => {
|
||||
data = data.results
|
||||
data.forEach(item => {
|
||||
if(item.start) item.start = new Date(item.start)
|
||||
if(item.end) item.end = new Date(item.end)
|
||||
|
Reference in New Issue
Block a user