first commit
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user