428 lines
9.3 KiB
Vue
428 lines
9.3 KiB
Vue
<template>
|
||
<div class="ui-mediapicker">
|
||
<div v-if="previews.length > 0" class="ui-mediapicker-previews">
|
||
<div v-for="item in previews" class="ui-mediapicker-preview">
|
||
<div v-if="!item.error" class="ui-mediapicker-preview-image" :class="{'media-pattern': item.thumbnailSource }">
|
||
<img v-if="item.thumbnailSource" :src="item.thumbnailSource" :alt="item.name" />
|
||
<button v-if="!disabled" type="button" class="ui-mediapicker-preview-image-delete" @click="remove(item)" v-localize:title="'@ui.remove'"><ui-icon symbol="fth-x" :size="12" /></button>
|
||
<button v-if="!disabled" type="button" class="ui-mediapicker-preview-image-edit" @click="edit(item)" v-localize:title="'@ui.edit'"><ui-icon symbol="fth-edit-2" :size="12" /></button>
|
||
</div>
|
||
<div v-if="!item.error" class="ui-mediapicker-preview-text">
|
||
<b :title="item.name">{{getFilename(item.name)}}</b>
|
||
<span class="is-filesize">{{getFileinfo(item)}}</span>
|
||
</div>
|
||
<div v-if="item.error">
|
||
<ui-select-button icon="fth-alert-circle color-red" label="@errors.preview.notfound" description="@errors.preview.notfound_text" @click="remove({ id: item.id })" :tokens="{ id: item.id }" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="canAdd" class="ui-mediapicker-select" :class="{'is-disabled': disabled }">
|
||
<input ref="input" type="hidden" :value="value" />
|
||
<ui-select-button icon="fth-plus" label="@mediapicker.select_text" description="@mediapicker.select_description" @click="pick" :disabled="disabled" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
<script>
|
||
import MediaApi from 'zero/api/media.js'
|
||
import PickMediaOverlay from './overlay.vue';
|
||
import Overlay from 'zero/helpers/overlay.js';
|
||
import { each as _each, extend as _extend, debounce as _debounce, isArray as _isArray } from 'underscore';
|
||
import Strings from 'zero/helpers/strings.js';
|
||
|
||
const TYPES = {
|
||
ALL: 'all',
|
||
IMAGE: 'image',
|
||
VIDEO: 'video',
|
||
DOCUMENT: 'document',
|
||
OTHER: 'other'
|
||
};
|
||
|
||
const DISPLAY = {
|
||
DEFAULT: 'default',
|
||
BIG: 'big',
|
||
GRID: 'grid'
|
||
};
|
||
|
||
const MAX_FILENAME_LENGTH = 32;
|
||
|
||
const defaultConfig = {
|
||
// maximum media items
|
||
limit: 1,
|
||
// media type
|
||
type: TYPES.ALL,
|
||
// how the previews are displayed
|
||
display: DISPLAY.DEFAULT,
|
||
// whether the select button is disallowed
|
||
disallowSelect: false,
|
||
// whether file upload is disallowed
|
||
disallowUpload: false,
|
||
// allowed file extensions
|
||
fileExtensions: [],
|
||
// maximum file size in MiB
|
||
maxFileSize: 10,
|
||
// whether to operate on the core database
|
||
shared: false
|
||
};
|
||
|
||
export default {
|
||
name: 'uiMediapicker',
|
||
|
||
props: {
|
||
value: {
|
||
type: [Array, String],
|
||
default: null
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
config: {
|
||
type: Object,
|
||
default: () =>
|
||
{
|
||
return defaultConfig;
|
||
}
|
||
},
|
||
},
|
||
|
||
|
||
data: () => ({
|
||
configuration: null,
|
||
previews: []
|
||
}),
|
||
|
||
|
||
watch: {
|
||
value()
|
||
{
|
||
this.updatePreviews();
|
||
}
|
||
},
|
||
|
||
|
||
created()
|
||
{
|
||
this.configuration = JSON.parse(JSON.stringify(_extend(defaultConfig, this.config)));
|
||
},
|
||
|
||
|
||
mounted()
|
||
{
|
||
this.updatePreviews();
|
||
},
|
||
|
||
|
||
computed: {
|
||
multiple()
|
||
{
|
||
return this.configuration.limit > 1;
|
||
},
|
||
canAdd()
|
||
{
|
||
return !this.disabled && this.configuration.limit - this.previews.length > 0;
|
||
},
|
||
},
|
||
|
||
|
||
methods: {
|
||
|
||
updatePreviews()
|
||
{
|
||
//console.info('preview', JSON.parse(JSON.stringify(this.value)));
|
||
let ids = [];
|
||
|
||
if (typeof this.value === 'string')
|
||
{
|
||
ids = [this.value];
|
||
}
|
||
else if (_isArray(this.value))
|
||
{
|
||
ids = this.value;
|
||
}
|
||
|
||
this.previews = [];
|
||
|
||
if (!ids || ids.length < 1)
|
||
{
|
||
this.$emit('previews', this.multiple ? [] : null);
|
||
return;
|
||
}
|
||
|
||
MediaApi.getByIds(ids).then(res =>
|
||
{
|
||
_each(ids, id =>
|
||
{
|
||
let value = res[id];
|
||
|
||
if (!value)
|
||
{
|
||
this.previews.push({
|
||
id: id,
|
||
error: true
|
||
});
|
||
}
|
||
else
|
||
{
|
||
this.previews.push(value);
|
||
}
|
||
});
|
||
|
||
this.$emit('previews', this.multiple ? this.previews : this.previews[0]);
|
||
});
|
||
},
|
||
|
||
|
||
remove(item)
|
||
{
|
||
let newValue = this.value;
|
||
|
||
if (typeof this.value === 'string')
|
||
{
|
||
newValue = null;
|
||
}
|
||
else if (_isArray(this.value))
|
||
{
|
||
const index = this.value.indexOf(item.id);
|
||
newValue.splice(index, 1);
|
||
}
|
||
|
||
this.onChange(newValue);
|
||
},
|
||
|
||
|
||
getFilename(name)
|
||
{
|
||
if (name.length < MAX_FILENAME_LENGTH)
|
||
{
|
||
return name;
|
||
}
|
||
|
||
const parts = name.split('.');
|
||
const extension = parts.pop();
|
||
|
||
return parts.join('.').substring(0, MAX_FILENAME_LENGTH - 6) + '...' + extension;
|
||
},
|
||
|
||
|
||
getFileinfo(item)
|
||
{
|
||
if (item.dimension)
|
||
{
|
||
return `${Strings.filesize(item.size)} – ${item.dimension.width} × ${item.dimension.height}`;
|
||
}
|
||
|
||
return Strings.filesize(item.size);
|
||
},
|
||
|
||
|
||
onChange(value)
|
||
{
|
||
this.$emit('change', value);
|
||
this.$emit('input', value);
|
||
//this.updatePreviews();
|
||
// TODO this does not trigger the forms dirty flag
|
||
},
|
||
|
||
|
||
pick()
|
||
{
|
||
if (this.disabled)
|
||
{
|
||
return;
|
||
}
|
||
|
||
let options = _extend({
|
||
title: '@iconpicker.title',
|
||
closeLabel: '@ui.close',
|
||
component: PickMediaOverlay,
|
||
model: this.configuration.limit > 1 ? this.value[0] : this.value,
|
||
folderId: null, //'mediaFolders.97-A',
|
||
width: 520
|
||
}, typeof this.config === 'object' ? this.config : {});
|
||
|
||
options.display = 'dialog';
|
||
|
||
return Overlay.open(options).then(value =>
|
||
{
|
||
let newValue = this.value;
|
||
|
||
if (this.multiple)
|
||
{
|
||
if (!newValue)
|
||
{
|
||
newValue = [];
|
||
}
|
||
newValue.push(value.id);
|
||
}
|
||
else
|
||
{
|
||
newValue = value.id;
|
||
}
|
||
|
||
this.onChange(newValue);
|
||
|
||
return new Promise(resolve => resolve(newValue));
|
||
});
|
||
}
|
||
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.ui-mediapicker-previews
|
||
{
|
||
.display-grid &
|
||
{
|
||
display: flex;
|
||
gap: 10px;
|
||
|
||
.ui-mediapicker-preview + .ui-mediapicker-preview
|
||
{
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ui-mediapicker-previews + .ui-mediapicker-select,
|
||
.ui-mediapicker-preview + .ui-mediapicker-preview
|
||
{
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.ui-mediapicker:not(.display-grid) .ui-mediapicker-previews + .ui-mediapicker-add
|
||
{
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.ui-select-button + .ui-mediapicker-add-upload
|
||
{
|
||
margin-left: 0;
|
||
margin-top: 10px;
|
||
}
|
||
}
|
||
|
||
.ui-mediapicker-preview
|
||
{
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.ui-mediapicker-preview-image
|
||
{
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 80px;
|
||
height: 80px;
|
||
/*background: var(--color-bg);*/
|
||
border: 1px solid var(--color-line);
|
||
padding: 0;
|
||
border-radius: var(--radius);
|
||
color: var(--color-text);
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
img
|
||
{
|
||
border-radius: 3px;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
margin: auto;
|
||
display: block;
|
||
color: transprent;
|
||
overflow: hidden;
|
||
font-size: 0;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
&.is-icon
|
||
{
|
||
width: 48px;
|
||
height: 48px;
|
||
display: inline-flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: var(--radius);
|
||
background: var(--color-box);
|
||
color: var(--color-text);
|
||
text-align: center;
|
||
font-size: 16px;
|
||
}
|
||
|
||
&:hover .ui-mediapicker-preview-image-delete,
|
||
&:hover .ui-mediapicker-preview-image-edit
|
||
{
|
||
opacity: 1;
|
||
transition-delay: 0.1s;
|
||
}
|
||
|
||
.display-big &, .display-grid &
|
||
{
|
||
width: 100px;
|
||
height: 100px;
|
||
}
|
||
}
|
||
|
||
.ui-mediapicker-preview-text
|
||
{
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 16px;
|
||
font-size: var(--font-size);
|
||
|
||
.is-filesize
|
||
{
|
||
color: var(--color-text-dim);
|
||
margin-top: 3px;
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
|
||
.display-grid &
|
||
{
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.ui-mediapicker-preview-image-delete,
|
||
.ui-mediapicker-preview-image-edit
|
||
{
|
||
opacity: 0;
|
||
transition: opacity 0.15s ease;
|
||
position: absolute;
|
||
display: inline-block;
|
||
right: 3px;
|
||
bottom: 3px;
|
||
width: 24px;
|
||
height: 24px;
|
||
line-height: 26px;
|
||
border-radius: 20px;
|
||
background: var(--color-negative);
|
||
color: var(--color-primary-text);
|
||
z-index: 2;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
|
||
.ui-media.display-default &
|
||
{
|
||
width: 20px;
|
||
height: 20px;
|
||
line-height: 22px;
|
||
}
|
||
}
|
||
|
||
.ui-mediapicker-preview-image-edit
|
||
{
|
||
right: 30px;
|
||
background: var(--color-box);
|
||
color: var(--color-text);
|
||
|
||
.ui-media.display-default &
|
||
{
|
||
right: 24px;
|
||
}
|
||
}
|
||
</style> |