date range picker

This commit is contained in:
2022-01-14 16:11:34 +01:00
parent 0f9f7e8b96
commit af84c19a0b
12 changed files with 293 additions and 712 deletions
@@ -1,471 +0,0 @@
<template>
<div class="ui-media" :class="'display-' + configuration.display">
<div v-if="items.length > 0" class="ui-media-previews">
<div v-for="item in items" class="ui-media-preview">
<div class="ui-media-preview-image">
<img v-if="item.source" :src="getPreview(item)" :alt="item.name" />
<button v-if="!disabled" type="button" class="ui-media-preview-image-delete" @click="remove(item)" v-localize:title="'@ui.remove'"><i class="fth-x"></i></button>
<button v-if="!disabled" type="button" class="ui-media-preview-image-edit" @click="edit(item)" v-localize:title="'@ui.edit'"><i class="fth-edit-2"></i></button>
</div>
<div class="ui-media-preview-text">
<b :title="item.name">{{getFilename(item.name)}}</b>
<span class="is-filesize">{{getFileinfo(item)}}</span>
</div>
</div>
</div>
<div class="ui-media-add" v-if="canAdd">
<ui-select-button v-if="!configuration.disallowSelect" icon="fth-folder" label="@mediapicker.select_text" description="@mediapicker.select_description" :disabled="disabled" />
<div v-if="!configuration.disallowUpload" class="ui-media-add-upload">
<input type="file" :accept="acceptedFileExtensions" :multiple="multiple" @change="onUpload" />
<ui-select-button icon="fth-upload" label="@mediapicker.upload_text" :description="uploadDescription" :disabled="true" />
</div>
</div>
</div>
</template>
<script>
import { each as _each, extend as _extend, debounce as _debounce, isArray as _isArray } from 'underscore';
import Strings from 'zero/helpers/strings.js';
import Overlay from 'zero/helpers/overlay.js';
import MediaOverlay from 'zero/pages/media/media-item.vue';
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
};
export default {
name: 'uiMediaOld',
props: {
disabled: {
type: Boolean,
default: false
},
config: {
type: Object,
default: () =>
{
return defaultConfig;
}
},
value: {
type: [Array, Object]
}
},
data: () => ({
configuration: {},
items: []
}),
watch: {
config: {
deep: true,
handler: function ()
{
this.initialize();
}
},
value: {
deep: true,
handler: function ()
{
this.initialize();
}
}
},
computed: {
multiple()
{
return this.configuration.limit > 1;
},
canAdd()
{
return !this.disabled && this.configuration.limit - this.items.length > 0;
},
uploadDescription()
{
return this.configuration.fileExtensions.length > 0 ? this.displayedFileExtensions : '@mediapicker.upload_description';
},
acceptedFileExtensions()
{
return this.configuration.fileExtensions.join(',');
},
displayedFileExtensions()
{
return this.configuration.fileExtensions.join(', ').replace(/\./g, '');
}
},
created()
{
this.initialize();
},
methods: {
initialize()
{
this.configuration = _extend(defaultConfig, this.config);
if (_isArray(this.value))
{
this.items = this.value;
}
else if (!!this.value && typeof this.value === 'object' && this.value.source)
{
this.items = [this.value];
}
},
onUpload(event)
{
let remaining = this.configuration.limit - this.items.length;
const files = event.target.files;
if (files && files.length > 0)
{
for (var i = 0; i < files.length; i++)
{
if (i >= remaining)
{
break;
}
this.addFile(files[i]);
}
}
this.update();
},
addFromLibrary(item)
{
this.items.push(item);
this.update();
},
addFile(file)
{
var source = URL.createObjectURL(file);
var media = {
id: 'upload:' + Strings.guid(),
name: file.name,
source: source,
size: file.size,
mimeType: file.type
};
this.items.push(media);
var reader = new FileReader();
reader.onload = function (e)
{
media.source = e.target.result;
};
reader.readAsDataURL(file);
},
remove(item)
{
const index = this.items.indexOf(item);
this.items.splice(item, 1);
this.update();
},
update()
{
this.$emit('input', this.multiple ? this.items : this.items[0]);
},
edit(item)
{
let model = JSON.parse(JSON.stringify(item));
let options = {
title: '@iconpicker.title',
closeLabel: '@ui.close',
component: MediaOverlay,
model: model,
theme: 'dark',
disabled: this.disabled
};
return Overlay.open(options).then(value =>
{
if (value.deletionRequested)
{
this.remove(item);
}
else
{
item.alternativeText = value.alternativeText;
item.caption = value.caption;
item.focalPoint = value.focalPoint;
}
}, () => { });
},
getPreview(item)
{
if (!item.id || item.id.indexOf('upload:') > -1 || item.source.indexOf("http") === 0)
{
return item.source;
}
if (item.id.indexOf("native:") > -1)
{
return item.source + "?width=100&height=100";
}
if (!item.hasThumbnail)
{
return item.source;
}
var extension = '.' + item.source.split('.').pop();
return item.source.replace(extension, ".thumb" + extension);
},
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);
}
}
}
</script>
<style lang="scss">
.ui-media
{
}
.ui-media-add
{
}
.ui-media-add-upload
{
display: inline-block;
position: relative;
.ui-select-button + &
{
margin-left: var(--padding);
}
input[type="file"]
{
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
border: none;
cursor: pointer;
opacity: 0;
}
}
.ui-media-previews
{
.display-grid &
{
display: flex;
gap: 10px;
.ui-media-preview + .ui-media-preview
{
margin-top: 0;
}
}
}
.ui-media-previews + .ui-media-add,
.ui-media-preview + .ui-media-preview
{
margin-top: 10px;
}
.ui-media:not(.display-grid) .ui-media-previews + .ui-media-add
{
display: flex;
flex-direction: column;
.ui-select-button + .ui-media-add-upload
{
margin-left: 0;
margin-top: 10px;
}
}
.ui-media-preview
{
display: flex;
align-items: center;
}
.ui-media-preview-image
{
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
/*background: var(--color-bg);*/
border: 1px solid var(--color-line-light);
padding: 3px;
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;
}
&:hover .ui-media-preview-image-delete,
&:hover .ui-media-preview-image-edit
{
opacity: 1;
transition-delay: 0.1s;
}
.display-big &, .display-grid &
{
width: 100px;
height: 100px;
}
}
.ui-media-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-media-preview-image-delete,
.ui-media-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-media-preview-image-edit
{
right: 30px;
background: var(--color-bg);
color: var(--color-text);
.ui-media.display-default &
{
right: 24px;
}
}
</style>
@@ -1,67 +0,0 @@
<template>
<div class="ui-tags" :class="{'is-disabled': disabled }">
<ui-input-list :value="value" @input="$emit('input', $event)" :add-label="addLabel" :disabled="disabled" :max-items="maxItems" :max-length="maxLength" :plus-button="true" />
</div>
</template>
<script>
export default {
name: 'uiTags',
props: {
addLabel: {
type: String,
default: '@ui.add'
},
value: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
maxItems: {
type: Number,
default: 100
},
maxLength: {
type: Number,
default: 200
}
}
}
</script>
<style lang="scss">
.ui-tags
{
.ui-input-list
{
display: flex;
align-items: flex-start;
flex-wrap: wrap;
}
.ui-input-list-item
{
display: inline-grid;
min-width: 0;
.ui-input
{
min-width: 120px;
width: auto;
}
& + .ui-input-list-item, & + .ui-button, & + .ui-select-button
{
margin-top: 0;
margin-left: 6px;
margin-bottom: 8px;
}
}
}
</style>
@@ -1,118 +0,0 @@
import Vue from 'vue';
import Overlay from 'zero/helpers/overlay.js';
import { find as _find, extend as _extend } from 'underscore';
import Axios from 'axios';
//import PasswordChangeOverlay from 'zero/pages/password-change.vue';
export default new Vue({
data: () => ({
isAuthenticated: false,
rejectReason: null,
user: {
name: null,
email: null
}
}),
watch: {
isAuthenticated(value)
{
this.$emit('authenticated', value);
},
user(value)
{
this.$emit('user', value);
}
},
methods: {
// loads the current user into the cache
loadUser()
{
Axios.get('authentication/getUser').then(res =>
{
this.isAuthenticated = res.data.success && res.data.model;
if (res.data.success)
{
this.user = res.data.model;
}
});
},
// the cached user has been rejected by the server so we clear credentials here
rejectUser(reason)
{
this.rejectReason = reason;
this.isAuthenticated = false;
this.user = null;
},
// sets the current user and isAuthenticated to true
setUser(user)
{
if (!user)
{
this.rejectUser();
return;
}
this.isAuthenticated = true;
this.user = user;
},
// logs the user in with the passed credentials
login(model)
{
return Axios.post('authentication/loginUser', model).then(res =>
{
return new Promise((resolve, reject) =>
{
if (res.data.success)
{
this.setUser(res.data.model);
resolve(res.data);
}
else
{
this.rejectUser();
reject(res.data.errors);
}
});
});
},
// logs the current user out
logout()
{
let promise = Axios.post('authentication/logoutUser');
this.rejectUser("@login.rejectReasons.logout");
return promise;
},
// try to switch selected application for user
switchApp(appId)
{
return Axios.post('authentication/switchApp', null, { params: { appId } }).then(res =>
{
//return this.zero.reloadConfig().then(() =>
//{
this.$emit('appswitch', res.data);
return Promise.resolve(res.data.success);
//});
});
},
// open overlay to update password
openPasswordOverlay()
{
return Overlay.open({
title: '@changepasswordoverlay.title',
closeLabel: '@ui.close',
confirmLabel: '@changepasswordoverlay.confirm',
component: PasswordChangeOverlay
});
}
}
});
@@ -35,6 +35,7 @@ import uiToggle from './ui-toggle.vue';
import uiProperty from './ui-property.vue';
import uiColorpicker from './ui-colorpicker.vue';
import uiDatepicker from './ui-datepicker.vue';
import uiDaterangepicker from './ui-daterangepicker.vue';
import uiSelect from './ui-select.vue';
import uiForm from './ui-form.vue';
import uiFormHeader from './ui-form-header.vue';
@@ -81,6 +82,7 @@ export {
uiProperty,
uiColorpicker,
uiDatepicker,
uiDaterangepicker,
uiSelect,
uiForm,
uiFormHeader,
@@ -0,0 +1,66 @@
<template>
<ui-form ref="form" class="ui-daterangepicker" v-slot="form">
<div class="ui-daterangepicker-items">
<div :class="{ 'ui-split': options.rangeEnd }">
<div class="ui-daterangepicker-group">
<ui-property :label="options.fromText" :vertical="true">
<ui-datepicker v-model="fromDate" v-bind="options" />
</ui-property>
</div>
<div class="ui-daterangepicker-group" v-if="options.rangeEnd">
<ui-property :label="options.toText" :vertical="true">
<ui-datepicker v-model="toDate" v-bind="options" />
</ui-property>
</div>
</div>
</div>
<div class="app-confirm-buttons">
<ui-button type="primary" label="@ui.confirm" @click="confirm"></ui-button>
<ui-button type="light" label="@ui.close" @click="config.close"></ui-button>
</div>
</ui-form>
</template>
<script>
export default {
props: {
model: Object,
config: Object,
},
data: () => ({
options: {},
disabled: false,
fromDate: null,
toDate: null
}),
mounted()
{
this.options = this.model.options;
this.fromDate = this.model.from;
this.toDate = this.model.to;
},
methods: {
confirm()
{
this.config.confirm({
from: this.fromDate,
to: this.toDate
}, this.config);
}
}
}
</script>
<style lang="scss">
.ui-daterangepicker
{
text-align: left;
}
h3.ui-daterangepicker-group-header
{
font-weight: 400;
font-size: var(--font-size);
}
</style>
@@ -0,0 +1,157 @@
<template>
<div class="ui-daterangepicker" :class="{'is-disabled': disabled }">
<button v-if="!inline" type="button" class="ui-link" @click="schedule" v-localize="scheduleLocalize" :disabled="disabled"></button>
<div v-if="inline && value" class="ui-daterangepicker-inline">
<div class="ui-daterangepicker-group">
<ui-property :vertical="true">
<ui-datepicker v-model="value.from" :time="time" />
</ui-property>
</div>
<div class="ui-daterangepicker-group" v-if="rangeEnd">
<ui-property :vertical="true">
<ui-datepicker v-model="value.to" :time="time" />
</ui-property>
</div>
</div>
</div>
</template>
<script lang="ts">
import { generateId } from '../utils/numbers';
import { formatDate, toIsoDate } from '../utils/dates';
import * as overlays from '../services/overlay';
const DATETIME_FORMAT = 'DD.MM.YY HH:mm';
const DATE_FORMAT = 'DD.MM.YY';
export default {
name: 'uiDaterangepicker',
props: {
value: {
type: Object,
default: {
from: null,
to: null
}
},
disabled: {
type: Boolean,
default: false
},
format: {
type: String,
default: null
},
time: {
type: Boolean,
default: false,
},
maxDate: {
type: [String, Date],
default: null
},
minDate: {
type: [String, Date],
default: null
},
fromLabel: {
type: String,
default: '@ui.date.range_from'
},
toLabel: {
type: String,
default: '@ui.date.range_to'
},
amPm: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
},
rangeEnd: {
type: Boolean,
default: true
},
options: {
type: Object,
default: () => { }
}
},
data: () => ({
id: null,
output: null,
pickerOptions: {}
}),
computed: {
scheduleLocalize()
{
return {
key: !this.value || (!this.value.from && !this.value.to) ? '@ui.date.set' :
(this.value.from && !this.value.to ? '@ui.date.x' :
(!this.value.from && this.value.to ? '@ui.date.y' : '@ui.date.xtoy')),
tokens: {
x: this.value ? formatDate(this.value.from, this.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT)) : null,
y: this.value ? formatDate(this.value.to, this.format || (this.time ? DATETIME_FORMAT : DATE_FORMAT)) : null
}
};
}
},
created()
{
this.id = 'daterangepicker-' + generateId();
},
methods: {
async schedule()
{
const result = await overlays.open({
component: DaterangepickerOverlay,
model: {
from: this.value.from,
to: this.value.to,
options: {
format: this.format,
time: this.time,
max: this.maxDate,
min: this.minDate,
fromText: this.fromLabel,
toText: this.toLabel,
amPm: this.amPm,
rangeEnd: this.rangeEnd
},
},
});
if (result.eventType == 'confirm')
{
this.$emit('change', result.value);
this.$emit('input', result.value);
}
}
}
}
</script>
<style lang="scss">
.ui-daterangepicker.is-primary .ui-link
{
color: var(--color-primary);
font-weight: 700;
text-decoration-color: var(--color-primary) !important;
}
.ui-daterangepicker-inline
{
display: flex;
gap: var(--padding-s);
}
</style>
@@ -1,39 +0,0 @@
<!--<template>
<div v-if="!loading" class="ui-native-select" :disabled="disabled">
<select :value="value" @input="$emit('input', $event.target.value)" :disabled="disabled">
<option v-for="item in items" :value="item.code">{{item.name}}</option>
</select>
</div>
</template>
<script>
import LanguagesApi from 'zero/api/languages.js';
export default {
props: {
value: {
type: String
},
config: Object,
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
loading: true,
items: []
}),
mounted()
{
LanguagesApi.getSupportedCultures().then(res =>
{
this.items = res;
this.loading = false;
});
}
}
</script>-->
@@ -1,17 +0,0 @@
<template>
<ui-spacepicker :value="value" @input="$emit('input', $event)" :limit="limit" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: [String, Array],
disabled: Boolean,
limit: {
type: Number,
default: 1
}
}
}
</script>
@@ -18,6 +18,7 @@ export default function createFields(app: ZeroPluginOptions): void
app.fieldType('inputlist', defineAsyncComponent(() => import('./inputlist.vue')));
app.fieldType('nested', defineAsyncComponent(() => import('./nested.vue')));
app.fieldType('datePicker', defineAsyncComponent(() => import('./datePicker.vue')));
app.fieldType('dateRangePicker', defineAsyncComponent(() => import('./dateRangePicker.vue')));
app.fieldType('iconPicker', defineAsyncComponent(() => import('./iconPicker.vue')));
app.fieldType('colorPicker', defineAsyncComponent(() => import('./colorPicker.vue')));
}
@@ -0,0 +1,54 @@
<template>
<div class="ui-box">
<ui-daterangepicker :value="value" @input="$emit('input', $event)" :disabled="config.disabled"
v-bind="{ format, time: pickTime, maxDate, minDate, fromLabel, toLabel, amPm, inline }" />
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: {
from: null,
to: null
}
},
config: Object,
format: {
type: String,
default: null
},
pickTime: {
type: Boolean,
default: false,
},
maxDate: {
type: [String, Date],
default: null
},
minDate: {
type: [String, Date],
default: null
},
fromLabel: {
type: String,
default: '@ui.date.range_from'
},
toLabel: {
type: String,
default: '@ui.date.range_to'
},
amPm: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
}
}
}
</script>
@@ -88,6 +88,12 @@ declare module 'zero/schemas'
*/
datePicker(options?: DatePickerFieldOptions): ZeroEditorField;
/**
* Create a date range picker
* @param {DateRangePickerFieldOptions} options - Custom options
*/
dateRangePicker(options?: DateRangePickerFieldOptions): ZeroEditorField;
/**
* Create an icon picker
* @param {DatePickerFieldOptions} options - Custom options
@@ -107,6 +113,13 @@ declare module 'zero/schemas'
colors?: boolean;
}
export interface DateRangePickerFieldOptions extends DatePickerFieldOptions
{
inline?: boolean;
fromLabel?: string;
toLabel?: string;
}
export interface DatePickerFieldOptions
{
format?: string;