258 lines
6.4 KiB
Vue
258 lines
6.4 KiB
Vue
<template>
|
|
<div class="datepicker-field">
|
|
<label v-if="label" :for="inputId" class="datepicker-field__label">{{ label }}</label>
|
|
|
|
<div
|
|
:id="inputId"
|
|
ref="referenceEl"
|
|
tabindex="0"
|
|
class="datepicker-field__trigger"
|
|
:class="{
|
|
'datepicker-field__trigger--error': error,
|
|
'datepicker-field__trigger--disabled': disabled,
|
|
}"
|
|
@click="toggle"
|
|
>
|
|
<span class="datepicker-field__value">{{ displayValue }}</span>
|
|
<SvgIcon name="calendar" :size="20" class="datepicker-field__icon" />
|
|
</div>
|
|
|
|
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<Teleport to="body">
|
|
<Transition name="fade">
|
|
<div
|
|
v-show="isOpen"
|
|
ref="floatingEl"
|
|
class="date-picker-dropdown datepicker-field__dropdown"
|
|
:style="floatingStyles"
|
|
>
|
|
<DatePicker
|
|
v-model="internalValue"
|
|
locale="fa"
|
|
inline
|
|
editable
|
|
:min="min"
|
|
:max="max"
|
|
:type="type"
|
|
:simple="simple"
|
|
:auto-submit="type === 'date'"
|
|
:format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
|
:display-format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
|
|
@update:model-value="onSelect"
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
import DatePicker from 'vue3-persian-datetime-picker'
|
|
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
|
|
|
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
import {
|
|
formatJalaaliDate,
|
|
formatJalaaliDateTime,
|
|
jalaaliStringToIsoDate,
|
|
jalaaliStringToIsoDateTime,
|
|
} from '@/utils/date-utils'
|
|
|
|
let uid = 0
|
|
const nextUid = () => `datepicker-field-${++uid}`
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: String, default: '' },
|
|
name: { type: String, default: '' },
|
|
label: { type: String, default: '' },
|
|
placeholder: { type: String, default: 'یک تاریخ را انتخاب کنید' },
|
|
disabled: { type: Boolean, default: false },
|
|
error: { type: String, default: '' },
|
|
vibration: { type: Boolean, default: false },
|
|
min: { type: String, default: undefined },
|
|
max: { type: String, default: undefined },
|
|
type: { type: String, default: 'date' },
|
|
simple: { type: Boolean, default: false },
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
|
const inputId = computed(() => props.name || nextUid())
|
|
const referenceEl = ref(null)
|
|
const floatingEl = ref(null)
|
|
const isOpen = ref(false)
|
|
const internalValue = ref(null)
|
|
const floatingStyles = ref({ position: 'absolute', top: '0px', left: '0px' })
|
|
let cleanup = null
|
|
|
|
const displayValue = computed(() => internalValue.value || props.placeholder)
|
|
|
|
const syncFromProp = () => {
|
|
const v = props.modelValue
|
|
if (v && typeof v === 'string') {
|
|
internalValue.value =
|
|
props.type === 'datetime' ? formatJalaaliDateTime(v) : formatJalaaliDate(v)
|
|
} else {
|
|
internalValue.value = null
|
|
}
|
|
}
|
|
|
|
watch(() => props.modelValue, syncFromProp, { immediate: true })
|
|
|
|
const onSelect = (jalaliValue) => {
|
|
if (!jalaliValue) return
|
|
internalValue.value = jalaliValue
|
|
const gregorian =
|
|
props.type === 'datetime'
|
|
? jalaaliStringToIsoDateTime(jalaliValue)
|
|
: jalaaliStringToIsoDate(jalaliValue)
|
|
emit('update:modelValue', gregorian)
|
|
emit('change', gregorian)
|
|
if (props.type === 'date') close()
|
|
}
|
|
|
|
const mountFloating = () => {
|
|
const reference = referenceEl.value
|
|
const floating = floatingEl.value
|
|
if (!reference || !floating) return
|
|
cleanup = autoUpdate(reference, floating, () => {
|
|
computePosition(reference, floating, {
|
|
placement: 'bottom-start',
|
|
middleware: [
|
|
offset(6),
|
|
flip(),
|
|
shift({ padding: 8 }),
|
|
size({
|
|
apply({ rects }) {
|
|
Object.assign(floating.style, { minWidth: `${rects.reference.width}px` })
|
|
},
|
|
}),
|
|
],
|
|
}).then(({ x, y }) => {
|
|
floatingStyles.value = { position: 'absolute', left: `${x}px`, top: `${y}px` }
|
|
})
|
|
})
|
|
document.addEventListener('mousedown', handleOutsideClick)
|
|
}
|
|
|
|
const unmountFloating = () => {
|
|
cleanup?.()
|
|
cleanup = null
|
|
document.removeEventListener('mousedown', handleOutsideClick)
|
|
}
|
|
|
|
const handleOutsideClick = (event) => {
|
|
const reference = referenceEl.value
|
|
const floating = floatingEl.value
|
|
if (
|
|
reference &&
|
|
!reference.contains(event.target) &&
|
|
floating &&
|
|
!floating.contains(event.target)
|
|
) {
|
|
close()
|
|
}
|
|
}
|
|
|
|
const open = () => {
|
|
isOpen.value = true
|
|
setTimeout(mountFloating, 0)
|
|
}
|
|
|
|
const close = () => {
|
|
isOpen.value = false
|
|
unmountFloating()
|
|
}
|
|
|
|
const toggle = () => {
|
|
if (props.disabled) return
|
|
isOpen.value ? close() : open()
|
|
}
|
|
|
|
onBeforeUnmount(() => unmountFloating())
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.datepicker-field {
|
|
position: relative;
|
|
width: 100%;
|
|
margin-bottom: 0.75rem;
|
|
|
|
&__label {
|
|
display: flex;
|
|
margin-bottom: 0.25rem;
|
|
font-family: var(--font-family-fa);
|
|
font-weight: 300;
|
|
line-height: 1.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--color-prim-gray);
|
|
pointer-events: none;
|
|
}
|
|
|
|
&__trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
height: 2.35rem;
|
|
padding: 0 1.25rem;
|
|
border: 1px solid var(--color-thd-gray);
|
|
border-radius: 1.5rem;
|
|
cursor: pointer;
|
|
font-family: var(--font-family-number-fa);
|
|
font-size: 0.9rem;
|
|
transition: all 0.2s;
|
|
outline: none;
|
|
|
|
&:focus {
|
|
box-shadow: 0 0 0 1px var(--color-validate);
|
|
}
|
|
|
|
&--error {
|
|
border-color: var(--color-error);
|
|
|
|
&:focus {
|
|
box-shadow: 0 0 0 1px var(--color-error);
|
|
}
|
|
}
|
|
|
|
&--disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
|
|
&__value {
|
|
flex: 1;
|
|
text-align: start;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__icon {
|
|
color: var(--color-thd-gray);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__error {
|
|
margin-top: 0.25rem;
|
|
font-family: var(--font-family-fa);
|
|
font-size: 0.75rem;
|
|
color: var(--color-error);
|
|
}
|
|
|
|
&__dropdown {
|
|
z-index: 50;
|
|
border: 1px solid var(--color-thd-gray);
|
|
border-radius: 0.75rem;
|
|
background: #fff;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 10%), 0 4px 6px -4px rgba(0, 0, 0, 10%);
|
|
}
|
|
}
|
|
</style>
|