266 lines
7.6 KiB
Vue

<template>
<Popover
@open="selectCurrentMonthYear"
class="flex w-full [&>div:first-child]:w-full"
>
<template #target="{ togglePopover }">
<input
readonly
type="text"
:placeholder="placeholder"
:value="value && formatter ? formatter(value) : value"
@focus="!readonly ? togglePopover() : null"
:class="[
'form-input block h-7 w-full cursor-pointer select-none rounded border-gray-400 text-sm placeholder-gray-500',
inputClass,
]"
/>
</template>
<template #body="{ togglePopover }">
<div
class="my-2 w-fit select-none space-y-3 rounded border border-gray-50 bg-white p-3 text-base shadow"
>
<div class="flex items-center text-gray-700">
<div
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded hover:bg-gray-100"
>
<FeatherIcon
@click="prevMonth"
name="chevron-left"
class="h-5 w-5"
/>
</div>
<div class="flex-1 text-center text-lg font-medium text-blue-500">
{{ formatMonth }}
</div>
<div
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded hover:bg-gray-100"
>
<FeatherIcon
@click="nextMonth"
name="chevron-right"
class="h-5 w-5"
/>
</div>
</div>
<div class="flex space-x-2">
<Input
type="text"
:value="value"
@change="selectDate(getDate($event)) || togglePopover()"
></Input>
<Button class="h-7" @click="selectDate(getDate()) || togglePopover()">
Today
</Button>
</div>
<div class="mt-2 flex flex-col items-center justify-center text-base">
<div class="flex w-full items-center space-x-1 text-gray-600">
<div
class="flex h-[30px] w-[30px] items-center justify-center text-center"
v-for="(d, i) in ['S', 'M', 'T', 'W', 'T', 'F', 'S']"
:key="i"
>
{{ d }}
</div>
</div>
<div v-for="(week, i) in datesAsWeeks" :key="i" class="mt-1">
<div class="flex w-full items-center">
<div
v-for="date in week"
:key="toValue(date)"
class="mr-1 flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded last:mr-0 hover:bg-blue-50 hover:text-blue-500"
:class="{
'text-gray-600': date.getMonth() !== currentMonth - 1,
'text-blue-500': toValue(date) === toValue(today),
'bg-blue-50 font-semibold text-blue-500':
toValue(date) === value,
}"
@click="
() => {
selectDate(date)
togglePopover()
}
"
>
{{ date.getDate() }}
</div>
</div>
</div>
</div>
<div class="mt-1 flex w-full justify-end">
<div
class="cursor-pointer rounded px-2 py-1 hover:bg-gray-100"
@click="
() => {
selectDate('')
togglePopover()
}
"
>
Clear
</div>
</div>
</div>
</template>
</Popover>
</template>
<script>
import { Popover } from 'frappe-ui'
export default {
name: 'DatePicker',
props: ['value', 'placeholder', 'formatter', 'readonly', 'inputClass'],
emits: ['change'],
components: {
Popover,
},
data() {
return {
currentYear: null,
currentMonth: null,
}
},
created() {
this.selectCurrentMonthYear()
},
computed: {
today() {
return this.getDate()
},
datesAsWeeks() {
let datesAsWeeks = []
let dates = this.dates.slice()
while (dates.length) {
let week = dates.splice(0, 7)
datesAsWeeks.push(week)
}
return datesAsWeeks
},
dates() {
if (!(this.currentYear && this.currentMonth)) {
return []
}
let monthIndex = this.currentMonth - 1
let year = this.currentYear
let firstDayOfMonth = this.getDate(year, monthIndex, 1)
let lastDayOfMonth = this.getDate(year, monthIndex + 1, 0)
let leftPaddingCount = firstDayOfMonth.getDay()
let rightPaddingCount = 6 - lastDayOfMonth.getDay()
let leftPadding = this.getDatesAfter(firstDayOfMonth, -leftPaddingCount)
let rightPadding = this.getDatesAfter(lastDayOfMonth, rightPaddingCount)
let daysInMonth = this.getDaysInMonth(monthIndex, year)
let datesInMonth = this.getDatesAfter(firstDayOfMonth, daysInMonth - 1)
let dates = [
...leftPadding,
firstDayOfMonth,
...datesInMonth,
...rightPadding,
]
if (dates.length < 42) {
const finalPadding = this.getDatesAfter(dates.at(-1), 42 - dates.length)
dates = dates.concat(...finalPadding)
}
return dates
},
formatMonth() {
let date = this.getDate(this.currentYear, this.currentMonth - 1, 1)
return date.toLocaleString('en-US', {
month: 'short',
year: 'numeric',
})
},
},
methods: {
selectDate(date) {
this.$emit('change', this.toValue(date))
},
selectCurrentMonthYear() {
let date = this.value ? this.getDate(this.value) : this.getDate()
if (date === 'Invalid Date') {
date = this.getDate()
}
this.currentYear = date.getFullYear()
this.currentMonth = date.getMonth() + 1
},
prevMonth() {
this.changeMonth(-1)
},
nextMonth() {
this.changeMonth(1)
},
changeMonth(adder) {
this.currentMonth = this.currentMonth + adder
if (this.currentMonth < 1) {
this.currentMonth = 12
this.currentYear = this.currentYear - 1
}
if (this.currentMonth > 12) {
this.currentMonth = 1
this.currentYear = this.currentYear + 1
}
},
getDatesAfter(date, count) {
let incrementer = 1
if (count < 0) {
incrementer = -1
count = Math.abs(count)
}
let dates = []
while (count) {
date = this.getDate(
date.getFullYear(),
date.getMonth(),
date.getDate() + incrementer
)
dates.push(date)
count--
}
if (incrementer === -1) {
return dates.reverse()
}
return dates
},
getDaysInMonth(monthIndex, year) {
let daysInMonthMap = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
let daysInMonth = daysInMonthMap[monthIndex]
if (monthIndex === 1 && this.isLeapYear(year)) {
return 29
}
return daysInMonth
},
isLeapYear(year) {
if (year % 400 === 0) return true
if (year % 100 === 0) return false
if (year % 4 === 0) return true
return false
},
toValue(date) {
if (!date) {
return ''
}
// toISOString is buggy and reduces the day by one
// this is because it considers the UTC timestamp
// in order to circumvent that we need to use luxon/moment
// but that refactor could take some time, so fixing the time difference
// as suggested in this answer.
// https://stackoverflow.com/a/16084846/3541205
date.setHours(0, -date.getTimezoneOffset(), 0, 0)
return date.toISOString().slice(0, 10)
},
getDate(...args) {
let d = new Date(...args)
return d
},
},
}
</script>