fix: design

This commit is contained in:
sajjadtalkhabi
2026-05-19 22:53:05 +03:30
parent c82701888a
commit 8990ceaddd
44 changed files with 2678 additions and 435 deletions
+468 -119
View File
@@ -4,7 +4,7 @@
<div
:id="inputId"
ref="referenceEl"
ref="referenceRef"
tabindex="0"
class="datepicker-field__trigger"
:class="{
@@ -22,10 +22,10 @@
</div>
<Teleport to="body">
<Transition name="fade">
<transition name="fade">
<div
v-show="isOpen"
ref="floatingEl"
ref="floatingRef"
class="date-picker-dropdown datepicker-field__dropdown"
:style="floatingStyles"
>
@@ -34,145 +34,354 @@
locale="fa"
inline
editable
:min="min"
:max="max"
compact-time
:min="jalaliMin"
:max="jalaliMax"
:type="type"
:simple="simple"
:auto-submit="type === 'date'"
:range="range"
:auto-submit="false"
:time-picker="type === 'datetime'"
:compact-time="type === 'datetime'"
:format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
:display-format="type === 'datetime' ? 'jYYYY/jMM/jDD HH:mm' : 'jYYYY/jMM/jDD'"
:confirm="type === 'datetime'"
@update:model-value="onSelect"
/>
</div>
</Transition>
</transition>
</Teleport>
</div>
</template>
<script setup>
<script>
import SvgIcon from '@/components/icons/SvgIcon.vue'
import DatePicker from 'vue3-persian-datetime-picker'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
import { computePosition, autoUpdate, offset, shift, flip, size } from '@floating-ui/dom'
import {
formatJalaaliDate,
formatJalaaliDateTime,
jalaaliStringToIsoDate,
jalaaliStringToIsoDateTime,
} from '@/utils/date-utils'
JalaliToGregorianString,
gregorianToJalaliString,
gregorianToJalaliStringLong,
JalaliToGregorianStringWithTime,
} from '@/utils/date-convertor'
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 },
})
export default {
name: 'DatePickerField',
components: { DatePicker, SvgIcon },
const emit = defineEmits(['update:modelValue', 'change'])
props: {
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 },
range: { type: [Boolean, Array], default: false },
autoSubmit: { type: Boolean, default: true },
},
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
emits: ['update:modelValue', 'change'],
const displayValue = computed(() => internalValue.value || props.placeholder)
data() {
return {
inputId: this.name || nextUid(),
isOpen: false,
internalValue: null,
cleanup: null,
floatingStyles: {
position: 'absolute',
top: '0px',
left: '0px',
},
confirmHandler: null,
mutationObserver: null,
}
},
const syncFromProp = () => {
const v = props.modelValue
if (v && typeof v === 'string') {
internalValue.value =
props.type === 'datetime' ? formatJalaaliDateTime(v) : formatJalaaliDate(v)
} else {
internalValue.value = null
}
computed: {
displayValue() {
return this.internalValue || this.placeholder
},
jalaliMin() {
if (!this.min) return
if (this.type === 'datetime') {
return gregorianToJalaliStringLong(this.min, '/', false).trim().replace('، ', ' ')
}
return gregorianToJalaliString(this.min, '/', false)
},
jalaliMax() {
if (!this.max) return
if (this.type === 'datetime') {
return gregorianToJalaliStringLong(this.max, '/', false).trim().replace('، ', ' ')
}
return gregorianToJalaliString(this.max, '/', false)
},
},
watch: {
modelValue: {
immediate: true,
handler(val) {
this.syncFromModel(val)
},
},
},
beforeUnmount() {
this.cleanup?.()
this.removeConfirmButtonListener()
document.removeEventListener('mousedown', this.handleOutside)
},
methods: {
syncFromModel(val) {
if (val && typeof val === 'string') {
this.internalValue =
this.type === 'datetime'
? gregorianToJalaliStringLong(val, '/', false)
: gregorianToJalaliString(val, '/', false)
} else {
this.internalValue = null
}
},
toggle() {
if (this.disabled) return
this.isOpen ? this.close() : this.open()
},
open() {
this.isOpen = true
this.$nextTick(() => {
this.initFloating()
if (this.type === 'datetime') {
this.$nextTick(() => {
this.attachConfirmButtonListener()
})
}
})
},
close() {
this.isOpen = false
this.cleanup?.()
this.cleanup = null
this.removeConfirmButtonListener()
},
removeConfirmButtonListener() {
if (this.confirmHandler) {
this.confirmHandler.element.removeEventListener('click', this.confirmHandler.handler)
delete this.confirmHandler.element.dataset.confirmListener
this.confirmHandler = null
}
if (this.mutationObserver) {
this.mutationObserver.disconnect()
this.mutationObserver = null
}
},
initFloating() {
const reference = this.$refs.referenceRef
const floating = this.$refs.floatingRef
if (!reference || !floating) return
this.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 }) => {
Object.assign(this.floatingStyles, {
left: `${x}px`,
top: `${y}px`,
})
})
})
document.addEventListener('mousedown', this.handleOutside)
},
handleOutside(e) {
const r = this.$refs.referenceRef
const f = this.$refs.floatingRef
if (r?.contains(e.target) || f?.contains(e.target)) return
this.close()
document.removeEventListener('mousedown', this.handleOutside)
},
onSelect(jalaliValue) {
if (!jalaliValue) return
if (this.type === 'datetime') {
const gregorian = JalaliToGregorianStringWithTime(jalaliValue)
this.$emit('update:modelValue', gregorian)
this.$emit('change', gregorian)
this.internalValue = jalaliValue
} else {
const gregorian = JalaliToGregorianString(jalaliValue)
this.$emit('update:modelValue', gregorian)
this.$emit('change', gregorian)
this.internalValue = jalaliValue
this.close()
}
},
attachConfirmButtonListener() {
const findAndAttach = () => {
const dropdown = this.$refs.floatingRef
if (!dropdown) return false
let confirmBtn = dropdown.querySelector('.vpd-actions')
if (confirmBtn) {
const allButtons = confirmBtn.querySelectorAll('button')
for (const btn of allButtons) {
if (btn.innerHTML.includes('svg') || btn.innerText.includes('تایید')) {
confirmBtn = btn
break
}
}
}
if (confirmBtn && !Object.hasOwn(confirmBtn.dataset, 'confirmListener')) {
const handler = (e) => {
e.preventDefault()
e.stopPropagation()
this.handleConfirm()
}
confirmBtn.addEventListener('click', handler, true)
confirmBtn.dataset.confirmListener = 'true'
this.confirmHandler = { element: confirmBtn, handler }
return true
}
return false
}
if (!findAndAttach()) {
const timeouts = [100, 300, 500, 1000]
timeouts.forEach((delay) => {
setTimeout(() => {
if (this.isOpen) {
findAndAttach()
}
}, delay)
})
}
this.setupMutationObserver()
},
setupMutationObserver() {
const dropdown = this.$refs.floatingRef
if (!dropdown) return
this.mutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
setTimeout(() => {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (this.isConfirmButton(node)) {
this.attachListenerToButton(node)
}
const btns = node.querySelectorAll ? node.querySelectorAll('button') : []
for (const btn of btns) {
if (this.isConfirmButton(btn)) {
this.attachListenerToButton(btn)
}
}
}
}
}, 50)
}
}
})
this.mutationObserver.observe(dropdown, {
childList: true,
subtree: true,
})
},
isConfirmButton(element) {
if (!element || !element.tagName) return false
if (
element.classList?.contains('vpd-confirm-btn') ||
element.classList?.contains('vpd-action-btn')
) {
return true
}
if (element.innerText?.includes('تایید')) {
return true
}
if (element.innerHTML?.includes('svg')) {
const svgPaths = element.innerHTML.match(/d="[^"]*"/g) || []
const hasCheckIcon = svgPaths.some(
(path) =>
path.includes('M20 6L9 17l-5-5') ||
path.includes('M9 16.17L4.83 12') ||
path.includes('M5 13l4 4L19 7')
)
return hasCheckIcon
}
return false
},
attachListenerToButton(btn) {
if (Object.hasOwn(btn.dataset, 'confirmListener')) return
const handler = (e) => {
e.preventDefault()
e.stopPropagation()
this.handleConfirm()
}
btn.addEventListener('click', handler, true)
btn.dataset.confirmListener = 'true'
this.confirmHandler = { element: btn, handler }
},
handleConfirm() {
if (!this.internalValue) return
try {
const gregorian = JalaliToGregorianStringWithTime(this.internalValue)
this.$emit('update:modelValue', gregorian)
this.$emit('change', gregorian)
this.close()
} catch (error) {
console.error('Error in handleConfirm:', error)
}
},
},
}
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>
@@ -254,3 +463,143 @@ onBeforeUnmount(() => unmountFloating())
}
}
</style>
<style>
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-4px);
}
.date-picker-dropdown .vpd-main .vpd-input-group {
display: none;
}
.date-picker-dropdown .vpd-wrapper .vpd-container {
margin: 0 !important;
font-family: iran-yekan;
width: 100%;
}
.date-picker-dropdown .vpd-wrapper .vpd-content {
width: auto;
border-radius: 8px;
}
.date-picker-dropdown .vpd-wrapper .vpd-header .vpd-date {
display: none;
}
.date-picker-dropdown .vpd-wrapper .vpd-header {
background-color: transparent !important;
padding: 5px;
}
.date-picker-dropdown .vpd-wrapper .vpd-header .vpd-year-label {
color: #5f5f5f;
text-align: center;
}
.date-picker-dropdown .vpd-wrapper .vpd-body {
overflow-x: hidden;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-controls button {
display: flex;
align-items: center;
justify-content: center;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-picker-dropdown
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-clearfix.vpd-week
.vpd-weekday {
width: auto;
float: none;
clear: both;
padding: 0 12px;
}
@media screen and (max-width: 400px) {
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-clearfix.vpd-week,
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
padding: 0 5px;
}
}
.date-picker-dropdown
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.direction-prev
.vpd-clearfix {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days {
height: fit-content !important;
padding: 0 12px;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-clearfix.vpd-month .vpd-days .vpd-day {
width: 35px;
height: 35px;
float: right;
line-height: 35px;
}
.date-picker-dropdown
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.vpd-day:not([disabled='true']).vpd-selected
.vpd-day-effect,
.date-picker-dropdown
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.vpd-day:not([disabled='true']):hover
.vpd-day-effect {
background-color: #f36675 !important;
}
.date-picker-dropdown
.vpd-wrapper
.vpd-body
.vpd-clearfix.vpd-month
.vpd-days
.vpd-day
.vpd-day-effect {
width: 35px;
height: 35px;
top: -2px;
left: 0;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-close-addon {
background-color: #f5f5f5;
border-radius: 5px;
}
.date-picker-dropdown .vpd-wrapper .vpd-body .vpd-month-label {
width: auto;
}
</style>
+1 -1
View File
@@ -7,7 +7,7 @@
class="image-cropper__upload-btn"
@click="triggerUpload"
>
<SvgIcon name="upload" :size="64" />
<SvgIcon name="upload" color="" :size="64" />
</button>
<div v-if="isCropping && image" class="image-cropper__crop-stage">