mirror of
https://github.com/directus/directus.git
synced 2026-02-03 13:54:59 -05:00
Add date(time) interface (#499)
* Add localized-format util * Add active prop to v-input * Add strings for datetime interface * Add overflow-scroll prop to v-menu * Add close-on-content-click prop to v-select * Add datetime interface * Show display value synced with prop * Sync value with prop * Set lang after user hydration * Add NL date-fns lang to test datetime * Fix locale fetching in date-fns * Dont stage value if year isnt fully filled out * Localize date fns based on shared util * Handle type, render type based display * Don't use exact on v-list-item * Pass type to interface on v-form
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
: values[field.field]
|
||||
"
|
||||
:width="field.width"
|
||||
:type="field.type"
|
||||
@input="setValue(field, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ You can add any custom (text) prefix/suffix to the value in the input using the
|
||||
| `suffix` | Show a value at the end of the input | -- |
|
||||
| `slug` | Force the value to be URL safe | `false` |
|
||||
| `slug-separator` | What character to use as separator in slugs | `-` |
|
||||
| `active` | Force the focus state | `false` |
|
||||
|
||||
Note: all other attached attributes are bound to the input HTMLELement in the component. This allows you to attach any of the standard HTML attributes like `min`, `length`, or `pattern`.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div v-if="$slots['prepend-outer']" class="prepend-outer">
|
||||
<slot name="prepend-outer" :value="value" :disabled="disabled" />
|
||||
</div>
|
||||
<div class="input" :class="{ disabled }">
|
||||
<div class="input" :class="{ disabled, active }">
|
||||
<div v-if="$slots.prepend" class="prepend">
|
||||
<slot name="prepend" :value="value" :disabled="disabled" />
|
||||
</div>
|
||||
@@ -105,6 +105,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, listeners }) {
|
||||
const input = ref<HTMLInputElement>(null);
|
||||
@@ -221,7 +225,8 @@ body {
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
&:focus-within,
|
||||
&.active {
|
||||
--arrow-color: var(--primary);
|
||||
|
||||
color: var(--foreground-normal);
|
||||
@@ -249,7 +254,7 @@ body {
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
width: 100px; // allows flex to shrink to allow for slots
|
||||
width: 20px; // allows flex to grow/shrink to allow for slots
|
||||
height: 100%;
|
||||
font-family: var(--v-input-font-family);
|
||||
background-color: transparent;
|
||||
@@ -290,12 +295,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
&.active .input {
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-page);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.append-outer {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
:is="component"
|
||||
active-class="active"
|
||||
class="v-list-item"
|
||||
exact
|
||||
:to="to"
|
||||
:class="{
|
||||
active,
|
||||
|
||||
@@ -57,8 +57,6 @@ export default {
|
||||
|
||||
## Props
|
||||
|
||||
Strap in
|
||||
|
||||
### Positioning
|
||||
|
||||
| Prop | Description | Default |
|
||||
@@ -70,22 +68,22 @@ Strap in
|
||||
| `bottom` | Aligns the menu to the bottom of the activator, going down (if possible) | `false` |
|
||||
| `left` | Aligns the menu to the left of the activator, expanding left (if possible) | `false` |
|
||||
| `right` | Aligns the menu to the right of the activator, expanding right (if possible) | `false` |
|
||||
| `offsetX` | Positions the menu along the X-Axis so as not to cover any of the activator | `false` |
|
||||
| `offsetY` | Positions the menu along the Y-Axis so as not to cover any of the activator | `false` |
|
||||
| `positionX` | "left" css value of menu. Only works with `absolute` or `fixed` | `undefined` |
|
||||
| `positionY` | "top" css value of menu. Only works with `absolute` or `fixed` | `undefined` |
|
||||
| `disabled` | Prevent the menu from being opened by clicking on the activator | `false` |
|
||||
| `offset-x` | Positions the menu along the X-Axis so as not to cover any of the activator | `false` |
|
||||
| `offset-y` | Positions the menu along the Y-Axis so as not to cover any of the activator | `false` |
|
||||
| `position-x` | "left" css value of menu. Only works with `absolute` or `fixed` | `undefined` |
|
||||
| `position-y` | "top" css value of menu. Only works with `absolute` or `fixed` | `undefined` |
|
||||
|
||||
### Behavior
|
||||
|
||||
| Prop | Description | Default |
|
||||
| --------------------- | --------------------------------------------------------- | ------- |
|
||||
| `closeOnClick` | Closes the menu when user clicks somewhere else | `true` |
|
||||
| `closeOnContentClick` | Closes the menu when user clicks on a menu item | `false` |
|
||||
| `openOnClick` | Open the menu when activator is clicked | `true` |
|
||||
| `openOnHover` | Open the menu when activator is hovered over | `false` |
|
||||
| `openDelay` | Delay in milliseconds after hover enter for menu to open | `0` |
|
||||
| `closeDelay` | Delay in milliseconds after hover leave for menu to close | `0` |
|
||||
| Prop | Description | Default |
|
||||
|-----------------------|-------------------------------------------------------------|---------|
|
||||
| `closeOnClick` | Closes the menu when user clicks somewhere else | `true` |
|
||||
| `closeOnContentClick` | Closes the menu when user clicks on a menu item | `false` |
|
||||
| `openOnClick` | Open the menu when activator is clicked | `true` |
|
||||
| `openOnHover` | Open the menu when activator is hovered over | `false` |
|
||||
| `openDelay` | Delay in milliseconds after hover enter for menu to open | `0` |
|
||||
| `closeDelay` | Delay in milliseconds after hover leave for menu to close | `0` |
|
||||
| `overflow-scroll` | Overflow the content in the menu when it reaches max height | `true` |
|
||||
|
||||
### Control
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
:style="arrowStyles"
|
||||
data-popper-arrow
|
||||
/>
|
||||
<div :class="{ active: isActive }" class="v-menu-content" @click="onContentClick">
|
||||
<div
|
||||
:class="{ active: isActive, 'overflow-scroll': overflowScroll }"
|
||||
class="v-menu-content"
|
||||
@click="onContentClick"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,6 +69,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
overflowScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const activator = ref<HTMLElement>(null);
|
||||
@@ -239,8 +247,6 @@ body {
|
||||
.v-menu-content {
|
||||
max-height: 50vh;
|
||||
padding: 0 4px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--background-subdued);
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
@@ -249,7 +255,12 @@ body {
|
||||
transition-timing-function: var(--transition-out);
|
||||
transition-duration: var(--fast);
|
||||
transition-property: opacity, transform;
|
||||
contain: content;
|
||||
|
||||
&.overflow-scroll {
|
||||
contain: content;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-list {
|
||||
--v-list-background-color: transparent;
|
||||
|
||||
@@ -22,17 +22,18 @@ Renders a dropdown input.
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Description | Default |
|
||||
|-----------------|-----------------------------------------------------|---------|
|
||||
| `items`\* | Items to render in the select | |
|
||||
| `itemText` | What item value to use for the display text | `text` |
|
||||
| `itemValue` | What item value to use for the item value | `value` |
|
||||
| `value` | Currently selected item(s) | |
|
||||
| `multiple` | Allow multiple items to be selected | `false` |
|
||||
| `placeholder` | What placeholder to show when no items are selected | |
|
||||
| `full-width` | Render the select at full width | |
|
||||
| `disabled` | Disable the select | |
|
||||
| `show-deselect` | Show the deselect option when a value has been set | |
|
||||
| Prop | Description | Default |
|
||||
|--------------------------|-----------------------------------------------------|---------|
|
||||
| `items`\* | Items to render in the select | |
|
||||
| `itemText` | What item value to use for the display text | `text` |
|
||||
| `itemValue` | What item value to use for the item value | `value` |
|
||||
| `value` | Currently selected item(s) | |
|
||||
| `multiple` | Allow multiple items to be selected | `false` |
|
||||
| `placeholder` | What placeholder to show when no items are selected | |
|
||||
| `full-width` | Render the select at full width | |
|
||||
| `disabled` | Disable the select | |
|
||||
| `show-deselect` | Show the deselect option when a value has been set | |
|
||||
| `close-on-content-click` | Close the select when selecting a value | `true` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<v-menu :disabled="disabled" class="v-select" attached>
|
||||
<template #activator="{ toggle }">
|
||||
<v-menu
|
||||
:disabled="disabled"
|
||||
class="v-select"
|
||||
attached
|
||||
:close-on-content-click="closeOnContentClick"
|
||||
>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
:full-width="fullWidth"
|
||||
readonly
|
||||
@@ -8,6 +13,7 @@
|
||||
@click="toggle"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:active="active"
|
||||
>
|
||||
<template #prepend><slot name="prepend" /></template>
|
||||
<template #append><v-icon name="expand_more" /></template>
|
||||
@@ -48,7 +54,11 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="allowOther && multiple === false" :active="usesOtherValue">
|
||||
<v-list-item
|
||||
v-if="allowOther && multiple === false"
|
||||
:active="usesOtherValue"
|
||||
@click.stop
|
||||
>
|
||||
<v-list-item-content>
|
||||
<input
|
||||
class="other-input"
|
||||
@@ -64,6 +74,7 @@
|
||||
v-for="otherValue in otherValues"
|
||||
:key="otherValue.key"
|
||||
:active="(value || []).includes(otherValue.value)"
|
||||
@click.stop
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-checkbox
|
||||
@@ -157,6 +168,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeOnContentClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { _items } = useItems();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useProjectsStore } from '@/stores/projects/';
|
||||
import { useLatencyStore } from '@/stores/latency';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useRelationsStore } from '@/stores/relations';
|
||||
import { setLanguage, Language } from '@/lang';
|
||||
|
||||
type GenericStore = {
|
||||
id: string;
|
||||
@@ -38,6 +39,7 @@ export function useStores(
|
||||
/* istanbul ignore next: useStores has a test already */
|
||||
export async function hydrate(stores = useStores()) {
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
if (appStore.state.hydrated) return;
|
||||
if (appStore.state.hydrating) return;
|
||||
@@ -51,7 +53,9 @@ export async function hydrate(stores = useStores()) {
|
||||
* following makes sure that the user store is always fetched first, before we hydrate anything
|
||||
* else.
|
||||
*/
|
||||
await useUserStore().hydrate();
|
||||
await userStore.hydrate();
|
||||
|
||||
setLanguage((userStore.state.currentUser?.locale as Language) || 'en-US');
|
||||
|
||||
await Promise.all(
|
||||
stores.filter(({ id }) => id !== 'userStore').map((store) => store.hydrate?.())
|
||||
|
||||
53
src/interfaces/datetime/datetime.story.ts
Normal file
53
src/interfaces/datetime/datetime.story.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import withPadding from '../../../.storybook/decorators/with-padding';
|
||||
import { defineComponent, ref, watch } from '@vue/composition-api';
|
||||
import { withKnobs, select } from '@storybook/addon-knobs';
|
||||
import readme from './readme.md';
|
||||
import i18n from '@/lang';
|
||||
import RawValue from '../../../.storybook/raw-value.vue';
|
||||
|
||||
export default {
|
||||
title: 'Interfaces / DateTime',
|
||||
decorators: [withPadding, withKnobs],
|
||||
parameters: {
|
||||
notes: readme,
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
i18n,
|
||||
props: {
|
||||
type: {
|
||||
default: select('Type', ['datetime', 'date', 'time'], 'datetime'),
|
||||
},
|
||||
},
|
||||
components: { RawValue },
|
||||
setup(props) {
|
||||
const value = ref('2020-04-28 14:40:00');
|
||||
|
||||
watch(
|
||||
() => props.type,
|
||||
(newType: string) => {
|
||||
if (newType === 'datetime') {
|
||||
value.value = '2020-04-28 14:40:00';
|
||||
} else if (newType === 'time') {
|
||||
value.value = '14:40:00';
|
||||
} else {
|
||||
// date
|
||||
value.value = '2020-04-28';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { value };
|
||||
},
|
||||
template: `
|
||||
<div style="max-width: 300px;">
|
||||
<interface-datetime
|
||||
v-model="value"
|
||||
:type="type"
|
||||
/>
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
308
src/interfaces/datetime/datetime.vue
Normal file
308
src/interfaces/datetime/datetime.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<v-menu attached :disabled="disabled" :overflow-scroll="false">
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
:active="active"
|
||||
@click="toggle"
|
||||
readonly
|
||||
:value="displayValue"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('enter_a_value')"
|
||||
>
|
||||
<template #append>
|
||||
<v-icon name="today" :class="{ active }" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<div class="date" v-if="type === 'datetime' || type === 'date'">
|
||||
<div class="month">
|
||||
<v-select :placeholder="$t('month')" :items="months" v-model="localValue.month" />
|
||||
</div>
|
||||
<div class="day">
|
||||
<v-select :placeholder="$t('date')" :items="days" v-model="localValue.day" />
|
||||
</div>
|
||||
<div class="year">
|
||||
<v-select
|
||||
:placeholder="$t('year')"
|
||||
:items="years"
|
||||
v-model="localValue.year"
|
||||
allow-other
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider v-if="type === 'datetime'" />
|
||||
|
||||
<div class="time" v-if="type === 'datetime' || type === 'time'">
|
||||
<div class="hour">
|
||||
<v-select :items="hours" v-model="localValue.hours" />
|
||||
</div>
|
||||
<div class="minutes">
|
||||
<v-select :items="minutesSeconds" v-model="localValue.minutes" />
|
||||
</div>
|
||||
<div class="seconds">
|
||||
<v-select :items="minutesSeconds" v-model="localValue.seconds" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<button class="to-now" @click="setToNow">{{ $t('set_to_now') }}</button>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed, reactive, PropType } from '@vue/composition-api';
|
||||
import formatLocalized from '@/utils/localized-format';
|
||||
import { i18n } from '@/lang';
|
||||
import parse from 'date-fns/parse';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
type LocalValue = {
|
||||
month: null | number;
|
||||
day: null | number;
|
||||
year: null | number;
|
||||
hours: null | number;
|
||||
minutes: null | number;
|
||||
seconds: null | number;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'datetime' | 'time' | 'date'>,
|
||||
required: true,
|
||||
validator: (val: string) => ['datetime', 'date', 'time'].includes(val),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const formatString = computed(() => {
|
||||
const date = 'yyyy-MM-dd';
|
||||
const time = 'HH:mm:ss';
|
||||
|
||||
if (props.type === 'datetime') {
|
||||
return date + ' ' + time;
|
||||
}
|
||||
|
||||
if (props.type === 'date') {
|
||||
return date;
|
||||
}
|
||||
|
||||
return time;
|
||||
});
|
||||
|
||||
const valueAsDate = computed(() =>
|
||||
props.value ? parse(props.value, formatString.value, new Date()) : null
|
||||
);
|
||||
|
||||
const displayValue = ref<string>(null);
|
||||
|
||||
syncDisplayValue();
|
||||
|
||||
const localValue = reactive({
|
||||
month: null,
|
||||
day: null,
|
||||
year: null,
|
||||
hours: 9,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
} as LocalValue);
|
||||
|
||||
syncLocalValue();
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue !== oldValue && newValue !== null && newValue.length !== 0) {
|
||||
syncLocalValue();
|
||||
syncDisplayValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => localValue,
|
||||
(newValue) => {
|
||||
if (
|
||||
newValue.year !== null &&
|
||||
String(newValue.year).length === 4 &&
|
||||
newValue.month !== null &&
|
||||
newValue.day !== null &&
|
||||
newValue.hours !== null &&
|
||||
newValue.minutes !== null &&
|
||||
newValue.seconds !== null
|
||||
) {
|
||||
const { year, month, day, hours, minutes, seconds } = newValue;
|
||||
const date = new Date(year, month, day, hours, minutes, seconds);
|
||||
emit('input', format(date, formatString.value));
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const { months, days, years, hours, minutesSeconds } = useOptions();
|
||||
|
||||
return {
|
||||
displayValue,
|
||||
months,
|
||||
days,
|
||||
years,
|
||||
hours,
|
||||
minutesSeconds,
|
||||
setToNow,
|
||||
localValue,
|
||||
};
|
||||
|
||||
function setToNow() {
|
||||
const date = new Date();
|
||||
|
||||
localValue.month = date.getMonth();
|
||||
localValue.day = date.getDate();
|
||||
localValue.year = date.getFullYear();
|
||||
localValue.hours = date.getHours();
|
||||
localValue.minutes = date.getMinutes();
|
||||
localValue.seconds = date.getSeconds();
|
||||
}
|
||||
|
||||
function syncLocalValue() {
|
||||
if (!valueAsDate.value) return;
|
||||
localValue.month = valueAsDate.value.getMonth();
|
||||
localValue.day = valueAsDate.value.getDate();
|
||||
localValue.year = valueAsDate.value?.getFullYear();
|
||||
localValue.hours = valueAsDate.value?.getHours();
|
||||
localValue.minutes = valueAsDate.value?.getMinutes();
|
||||
localValue.seconds = valueAsDate.value?.getSeconds();
|
||||
}
|
||||
|
||||
async function syncDisplayValue() {
|
||||
if (valueAsDate.value === null) return null;
|
||||
let format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
|
||||
displayValue.value = await formatLocalized(valueAsDate.value as Date, format);
|
||||
}
|
||||
|
||||
function useOptions() {
|
||||
const months = computed(() =>
|
||||
[
|
||||
i18n.t('months.january'),
|
||||
i18n.t('months.february'),
|
||||
i18n.t('months.march'),
|
||||
i18n.t('months.april'),
|
||||
i18n.t('months.may'),
|
||||
i18n.t('months.june'),
|
||||
i18n.t('months.july'),
|
||||
i18n.t('months.august'),
|
||||
i18n.t('months.september'),
|
||||
i18n.t('months.october'),
|
||||
i18n.t('months.november'),
|
||||
i18n.t('months.december'),
|
||||
].map((text, index) => ({
|
||||
text: text,
|
||||
value: index,
|
||||
}))
|
||||
);
|
||||
|
||||
const days = computed(() => {
|
||||
const days = [];
|
||||
|
||||
for (let i = 1; i <= 31; i++) {
|
||||
days.push(`${i}`);
|
||||
}
|
||||
|
||||
return days;
|
||||
});
|
||||
|
||||
const years = computed(() => {
|
||||
const current = valueAsDate.value?.getFullYear() || new Date().getFullYear();
|
||||
const years = [];
|
||||
for (let i = current - 5; i <= current + 5; i++) {
|
||||
years.push({
|
||||
text: String(i),
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
return years;
|
||||
});
|
||||
|
||||
const hours = computed(() => {
|
||||
const hours = [];
|
||||
|
||||
for (let i = 0; i <= 24; i++) {
|
||||
let hour = String(i);
|
||||
if (hour.length === 1) hour = '0' + hour;
|
||||
hours.push({
|
||||
text: hour,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return hours;
|
||||
});
|
||||
|
||||
const minutesSeconds = computed(() => {
|
||||
const values = [];
|
||||
|
||||
for (let i = 0; i <= 60; i++) {
|
||||
let val = String(i);
|
||||
if (val.length === 1) val = '0' + val;
|
||||
values.push({
|
||||
text: val,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
});
|
||||
|
||||
return { days, years, months, hours, minutesSeconds };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.date,
|
||||
.time {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
width: 100%;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.date {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.time {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.month {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.to-now {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
color: var(--primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-icon.active {
|
||||
--v-icon-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
10
src/interfaces/datetime/index.ts
Normal file
10
src/interfaces/datetime/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceDateTime from './datetime.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'datetime',
|
||||
name: i18n.t('datetime'),
|
||||
icon: 'today',
|
||||
component: InterfaceDateTime,
|
||||
options: [],
|
||||
}));
|
||||
2
src/interfaces/datetime/readme.md
Normal file
2
src/interfaces/datetime/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# DateTime interface
|
||||
|
||||
@@ -10,6 +10,7 @@ import InterfaceDropdownMultiselect from './dropdown-multiselect/';
|
||||
import InterfaceRadioButtons from './radio-buttons';
|
||||
import InterfaceCheckboxes from './checkboxes';
|
||||
import InterfaceStatus from './status';
|
||||
import InterfaceDateTime from './datetime';
|
||||
|
||||
export const interfaces = [
|
||||
InterfaceTextInput,
|
||||
@@ -24,6 +25,7 @@ export const interfaces = [
|
||||
InterfaceRadioButtons,
|
||||
InterfaceCheckboxes,
|
||||
InterfaceStatus,
|
||||
InterfaceDateTime,
|
||||
];
|
||||
|
||||
export default interfaces;
|
||||
|
||||
@@ -92,6 +92,30 @@
|
||||
|
||||
"submit": "Submit",
|
||||
|
||||
"datetime": "DateTime",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "K:mm a",
|
||||
"month": "Month",
|
||||
"date": "Date",
|
||||
"year": "Year",
|
||||
|
||||
"months": {
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December"
|
||||
},
|
||||
|
||||
"set_to_now": "Set to Now",
|
||||
|
||||
"name": "Name",
|
||||
"primary_key_field": "Primary Key Field",
|
||||
"type": "Type",
|
||||
@@ -670,20 +694,6 @@
|
||||
"max_size": "Max Size: {size}",
|
||||
"mixed": "Mixed",
|
||||
"more_options": "More options",
|
||||
"months": {
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December"
|
||||
},
|
||||
"my_activity": "My Activity",
|
||||
"my_profile": "My Profile: {name}",
|
||||
"name_bookmark": "What would you like to name this bookmark?",
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"collection_names_cannot_be_changed": "Collectie namen kunnen niet gewijzigd worden.",
|
||||
"collection_removed": "Collectie verwijderd",
|
||||
"collection_updated": "Collectie Geüpdatet",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "HH:mm",
|
||||
"collections_and_fields": "Collecties & Velden",
|
||||
"collections": {
|
||||
"directus_activity": "Activiteit",
|
||||
|
||||
25
src/utils/get-date-fns-locale/get-date-fns-locale.ts
Normal file
25
src/utils/get-date-fns-locale/get-date-fns-locale.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { i18n } from '@/lang';
|
||||
|
||||
export async function getDateFNSLocale() {
|
||||
const lang = i18n.locale;
|
||||
|
||||
const localesToTry = [lang, lang.split('-')[0], 'en-US'];
|
||||
|
||||
let locale;
|
||||
|
||||
for (const l of localesToTry) {
|
||||
try {
|
||||
const mod = await import(
|
||||
/* webpackMode: 'lazy', webpackChunkName: 'df-[index]' */
|
||||
`date-fns/locale/${l}/index.js`
|
||||
);
|
||||
|
||||
locale = mod.default;
|
||||
break;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
4
src/utils/get-date-fns-locale/index.ts
Normal file
4
src/utils/get-date-fns-locale/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { getDateFNSLocale } from './get-date-fns-locale';
|
||||
|
||||
export { getDateFNSLocale };
|
||||
export default getDateFNSLocale;
|
||||
@@ -1,5 +1,5 @@
|
||||
import formatDistanceOriginal from 'date-fns/formatDistance';
|
||||
import { i18n } from '@/lang';
|
||||
import getDateFNSLocale from '@/utils/get-date-fns-locale';
|
||||
|
||||
type LocalizedFormatDistance = (...a: Parameters<typeof formatDistanceOriginal>) => Promise<string>;
|
||||
|
||||
@@ -8,17 +8,8 @@ export const localizedFormatDistance: LocalizedFormatDistance = async (
|
||||
baseDate,
|
||||
options
|
||||
): Promise<string> => {
|
||||
const lang = i18n.locale;
|
||||
|
||||
const locale = (
|
||||
await import(
|
||||
/* webpackMode: 'lazy', webpackChunkName: 'df-[index]' */
|
||||
`date-fns/locale/${lang}/index.js`
|
||||
)
|
||||
).default;
|
||||
|
||||
return formatDistanceOriginal(date, baseDate, {
|
||||
...options,
|
||||
locale,
|
||||
locale: await getDateFNSLocale(),
|
||||
});
|
||||
};
|
||||
|
||||
4
src/utils/localized-format/index.ts
Normal file
4
src/utils/localized-format/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { localizedFormat } from './localized-format';
|
||||
|
||||
export { localizedFormat };
|
||||
export default localizedFormat;
|
||||
11
src/utils/localized-format/localized-format.ts
Normal file
11
src/utils/localized-format/localized-format.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import formatOriginal from 'date-fns/format';
|
||||
import getDateFNSLocale from '@/utils/get-date-fns-locale';
|
||||
|
||||
type localizedFormat = (...a: Parameters<typeof formatOriginal>) => Promise<string>;
|
||||
|
||||
export const localizedFormat: localizedFormat = async (date, format, options): Promise<string> => {
|
||||
return formatOriginal(date, format, {
|
||||
...options,
|
||||
locale: await getDateFNSLocale(),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user