fix: design
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user