Input Time Range v1.0
The <e-input-time-range> component is a component that can be used for .
Usage
An e-input-time-range has attributes that must be provided
<e-input-time-range
id=""
value=""
label=""
name=""
placeholder=""
required=""
disabled=""
options=""
meta=""
/>
Attributes : e-input-time-range extends e-input
| Value | Type | Optional | Default |
|---|---|---|---|
| value | String | yes | '' |
| meta | EInputMeta_TimeRange | yes | new EInputMeta_TimeRange() |
Attributes : e-input
| Value | Type | Optional | Default |
|---|---|---|---|
| id | String | yes | '' |
| value | String | yes | '' |
| label | String | yes | '' |
| name | String | yes | '' |
| placeholder | String | yes | '' |
| required | Boolean | yes | false |
| disabled | Boolean | yes | false |
| options | EFormFieldOptionType[] | yes | [] |
| meta | EInputMeta | yes | { disabled: false } |
Attributes : e-input-meta_time-range
| Value | Type | Optional | Default |
|---|---|---|---|
| disabled | Boolean | yes | false |
| searchable | Boolean | yes | true |
| nullable | Boolean | yes | false |
| customSplitter | String | yes | ' ⇒ ' |
| minDate | RawDateString | yes | '1970-01-01' |
| maxDate | RawDateString | yes | '9999-12-31' |
Examples
Below a few interactive examples of e-input-time-range can be found.
Default
<e-input-time-range
id="timerangeInputWithOptions"
value="-"
/>
internal labelled, label="timerangeInput with Label"
<e-input-time-range
id="timerangeInputWithLabel"
label="timeRangeInput with Label"
value="-"
/>
separate labelled, label="timerangeInput with Label"
<div class="input-example-field">
<e-input-label
id="timerangeInputWithExternalLabel"
value="timeRangeInput with Label"
/>
<e-input-time-range
id="timerangeInputWithExternalLabel"
value="-"
/>
</div>
Source Code
e-input-time-range.vue
<script lang="ts" setup>
import { computed, reactive, toRef, ref, unref, watch } from 'vue';
import { capitalize, cloneDeep, forEach, isNaN, map } from 'lodash-es';
import { useDateFormat } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import {
useMetaUtils,
ButtonTheme,
ButtonType,
EFormFieldModel,
EFormFieldOptionType
} from 'types';
import { EInput_TimeRange } from 'types/e-input/interfaces/EInput_TimeRange';
import { EInputMeta_Select, EInputMeta_DateTimeLocal } from 'types/e-input/meta_interfaces';
import EButton from '../e-button/e-button.vue';
import EFormField from '../e-form-field/e-form-field.vue';
import { defaultSplitter, optionsList } from 'data/filters/options/timeRange';
import { isValidDate, getTimeRangeDates, getTimePeriodDates } from 'types/utils/dateUtils';
export type Props = EInput_TimeRange;
const props = withDefaults(defineProps<Props>(), {
value: '',
id: '',
label: '',
placeholder: '',
required: false,
disabled: false,
options: () => {
return [] as EFormFieldOptionType[];
},
meta: () => {
return {
disabled: false,
searchable: true,
nullable: false
};
}
});
const emit = defineEmits<{
(e: 'blur', event: FocusEvent): void;
(e: 'keyup.enter', event: KeyboardEvent): void;
(e: 'update:value', value: string): void;
}>();
const { t, te } = useI18n({ useScope: 'global' });
const customMenuTitle = ref(t('timeRange.giveInput'));
const localPattern = ref('YYYY-MM-DDTHH:mm');
const localValue = ref(props.value || '-');
const showDropdown = ref(false);
const isValid = ref(true);
const metaData = computed(() => useMetaUtils(toRef(props.meta)));
const isDisabled = computed(() => unref(metaData).isDisabled.value);
const minDate = computed(() => new Date(props.meta?.minDate || '1970-01-01'));
const maxDate = computed(() => new Date(props.meta?.maxDate || '9999-12-31'));
const customSplitter = computed(() => props.meta.customSplitter ?? defaultSplitter);
const dates = reactive({
default: {
start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
end: new Date()
},
custom: {
start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
end: new Date()
},
result: {
start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
end: new Date()
}
});
const formattedDates = computed(() => {
return {
start: useDateFormat(dates.custom.start, localPattern),
end: useDateFormat(dates.custom.end, localPattern)
};
});
const customResult = computed(
() => unref(formattedDates.value.start) + customSplitter.value + unref(formattedDates.value.end)
);
const containsSplitter = computed(() => customResult.value?.includes(customSplitter.value));
const canSubmitCustom = computed(
() => isValid.value && !isNaN(dates.custom.start) && !isNaN(dates.custom.end)
);
const formattedTimeRange = computed(() => {
const start = useDateFormat(dates.result.start, localPattern);
const end = useDateFormat(dates.result.end, localPattern);
return unref(start) + customSplitter.value + unref(end);
});
const baseTimeRangeOptions = computed(() => {
return map(optionsList, option => {
const translatedPeriod =
option.label.period && te(option.label.period) ? t(option.label.period) : '';
const translatedFormat =
option.label.format && te(option.label.format)
? t(option.label.format, { count: option.label?.count })
: '';
const translationParams = {
period: translatedPeriod,
count: option.label?.count,
format: translatedFormat
};
const label = te(option.label.key) ? t(option.label.key, translationParams) : option.value;
return {
label: label,
value: option.value,
disabled: option.disabled,
hidden: option.hidden
};
});
});
const timeRangeOptions = computed(() => {
const result = cloneDeep(baseTimeRangeOptions.value);
const optionIndex = result.findIndex(
o => o.value.toLowerCase() === customResult.value.toLowerCase()
);
// Add custom options
const optionList: EFormFieldOptionType[] = props.options || [];
forEach(optionList, option => {
const labelValue = option.label || option.value;
result.push({
id: option.id,
label: te(labelValue) ? t(labelValue) : labelValue,
value: option.value,
disabled: option.disabled,
hidden: option.hidden
});
});
if (containsSplitter.value || (localValue.value === 'custom' && optionIndex === -1)) {
result.push({
value: customResult.value,
label: customResult.value,
hidden: true
});
}
return result;
});
const timeRangeOptionlist = computed(() => {
return map(timeRangeOptions.value, timeRange => {
return {
id: timeRange.value,
text: timeRange.label,
value: timeRange.value,
disabled: false,
hidden: timeRange.hidden
} as EFormFieldOptionType;
});
});
const selectFormFieldModel = computed(() => {
return {
type: 'select',
id: props.id,
value: localValue.value,
label: props.label,
showLabel: props.label !== undefined,
placeholder: props.placeholder,
required: props.required,
hidden: false,
meta: {
...props.meta
} as EInputMeta_Select,
options: timeRangeOptionlist.value
} as EFormFieldModel;
});
const startDateValue = ref(unref(formattedDates.value.start));
const startDateFormFieldModel = computed(() => {
const label = capitalize(t('timeRange.input.start', [t('timeRange.format.time')]));
return {
type: 'datetime-local',
id: 'startDate',
value: startDateValue.value,
label: label,
showLabel: true,
placeholder: props.placeholder,
required: false,
hidden: false,
meta: {
timeDisabled: false,
requireTime: false,
minDate: minDate.value,
maxDate: maxDate.value
} as EInputMeta_DateTimeLocal,
options: []
} as EFormFieldModel;
});
const endDateValue = ref(unref(formattedDates.value.end));
const endDateFormFieldModel = computed(() => {
const label = capitalize(t('timeRange.input.end', [t('timeRange.format.time')]));
return {
type: 'datetime-local',
id: 'endDate',
value: endDateValue.value,
label: label,
showLabel: false,
placeholder: props.placeholder,
required: false,
hidden: false,
meta: {
timeDisabled: false,
requireTime: false,
minDate: minDate.value,
maxDate: maxDate.value
} as EInputMeta_DateTimeLocal,
options: []
} as EFormFieldModel;
});
const stateClasses = computed(() => {
return {
required: props.required,
disabled: isDisabled.value
};
});
function setValidity(data: { key: string; newValue: boolean }) {
isValid.value = data.newValue;
}
function setDateValue(data: { key: string; newValue: string }) {
const newDate = data.newValue ? new Date(data.newValue) : null;
switch (data.key) {
case 'startDate':
dates.custom.start = newDate;
break;
case 'endDate':
dates.custom.end = newDate;
break;
}
}
function optionSelected(data: { key: string; newValue: string }) {
showDropdown.value = data.newValue === 'custom';
if (!showDropdown.value) {
localValue.value = data.newValue;
emit('update:value', data.newValue);
}
}
function setResult() {
showDropdown.value = false;
updateInputRefs();
dates.result.start = dates.custom.start;
dates.result.end = dates.custom.end;
}
function updateInputRefs() {
startDateValue.value = unref(useDateFormat(dates.custom.start, localPattern));
endDateValue.value = unref(useDateFormat(dates.custom.end, localPattern));
}
// Update default date values, ensure they're valid dates.
function updateDefaultValues(newDates: { start: Date; end: Date }) {
if (isValidDate(newDates.start)) dates.default.start = cloneDeep(newDates.start);
if (isValidDate(newDates.end)) dates.default.end = cloneDeep(newDates.end);
}
function emitCustomRange() {
localValue.value = customResult.value;
if (isValidDate(dates.custom.start) && isValidDate(dates.custom.end)) {
setResult();
localValue.value = formattedTimeRange.value;
emit('update:value', localValue.value);
}
}
function resetLocalValue() {
localValue.value = props.value || '-';
showDropdown.value = localValue.value === 'custom';
}
function setDateRange(newDateRange: string) {
const _dates = newDateRange?.split(customSplitter.value);
const newDates =
_dates?.length > 1 ? getTimeRangeDates(newDateRange) : getTimePeriodDates(newDateRange);
dates.custom.start = newDates.start;
dates.custom.end = newDates.end;
updateInputRefs();
updateDefaultValues(newDates);
localValue.value = newDateRange;
}
watch(
() => props.value,
newValue => {
if (newValue) {
setDateRange(newValue);
}
},
{
immediate: true
}
);
</script>
<template>
<div
class="e-input-time-range"
:class="stateClasses"
>
<div
class="input-container"
role="none"
>
<e-form-field
:model="selectFormFieldModel"
@emit:value="optionSelected"
/>
<div
v-if="showDropdown"
class="time-range-selection"
role="combobox"
aria-labelledby="custom-time-range-menu"
>
<div
class="dropdown-container"
role="none"
>
<label
id="custom-time-range-menu"
:title="customMenuTitle"
:aria-label="customMenuTitle"
v-text="customMenuTitle"
/>
<div class="inputs">
<e-form-field
:model="startDateFormFieldModel"
@emit:validity="setValidity"
@emit:value="setDateValue"
/>
<e-form-field
:model="endDateFormFieldModel"
@emit:validity="setValidity"
@emit:value="setDateValue"
/>
</div>
<div class="functions">
<e-button
:type="ButtonType.Default"
:text="t('functions.ok')"
:theme="ButtonTheme.Primary"
:disabled="!canSubmitCustom"
@clicked="emitCustomRange"
/>
<e-button
:type="ButtonType.Reset"
:text="t('functions.cancel')"
@click="resetLocalValue"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.e-input-time-range {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
border: none;
position: relative;
gap: 10px;
.current-value-label {
min-width: 60px;
}
.input-container {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1 1 auto;
position: relative;
border-radius: 15px;
.e-form-field {
display: flex;
width: 100%;
flex-direction: row;
flex: 1 1 auto;
align-items: center;
:deep(label) {
width: 30%;
}
:deep(input) {
flex: 1 1 auto;
}
:deep(.e-input-select) {
width: 70%;
min-width: 200px;
}
}
label {
min-width: 30px;
}
.time-range-selection {
position: relative;
z-index: 9000;
left: calc(50% - 5px);
.dropdown-container {
border-radius: 15px;
border: 1px solid var(--e-foreground);
background-color: var(--e-background);
padding: 10px;
position: absolute;
top: 20px;
display: flex;
flex-direction: column;
&::before {
background-color: var(--e-foreground);
content: '';
display: inline-block;
border: 1px solid var(--e-foreground);
position: absolute;
left: 50%;
top: -25px;
height: 20px;
width: 20px;
transform: translateX(-50%) translateY(100%) rotate(45deg);
z-index: -1;
}
label {
padding: 10px 20px;
margin: 0;
}
.inputs {
display: flex;
flex-direction: column;
gap: 10px;
:user-invalid {
color: var(--e-red);
}
}
.e-form-field {
:deep(label) {
max-width: 80px;
}
}
.functions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px;
.e-button {
width: 100px;
text-align: center;
}
}
}
}
}
.field-row {
display: flex;
flex-direction: column;
}
.date-row {
display: flex;
flex-direction: column;
label {
padding: 0 20px;
}
.inputs {
display: flex;
flex-direction: row;
padding: 5px 15px;
gap: 10px;
.e-input {
background-color: var(--e-input-background);
padding: 5px;
border-radius: 10px;
border: none;
flex: 1 1 auto;
}
}
}
&.disabled {
.input-container {
label {
cursor: not-allowed;
user-input: none;
}
}
}
}
</style>