first commit

This commit is contained in:
sajjadtalkhabi
2026-05-13 01:43:05 +03:30
commit d2a577f254
297 changed files with 36274 additions and 0 deletions
+257
View File
@@ -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>