I think there is no need to reinvent the wheel of date handler, and moment.js is useful.
I try to modify the source code like this:
<template>
<div class="datepicker">
<template v-if="hasInput">
<input class="form-control datepicker-input" :class="{'with-reset-button': clearButton}" type="text" :placeholder="placeholder"
:style="{width:width}"
@click="inputClick"
v-model="inputValue"/>
<button v-if="clearButton && value" type="button" class="close" @click="inputValue = ''">
<span>×</span>
</button>
</template>
<div class="datepicker-popup" :style="paneStyle" @mouseover="handleMouseOver" @mouseout="handleMouseOver" v-show="displayDayView">
<div class="datepicker-ctrl">
<span class="datepicker-preBtn fa fa-chevron-left" aria-hidden="true" @click="preNextMonthClick(0)"></span>
<span class="datepicker-nextBtn fa fa-chevron-right" aria-hidden="true" @click="preNextMonthClick(1)"></span>
</div>
<template v-for="(p, pan) in pane" >
<div class="datepicker-inner">
<div class="datepicker-body">
<p class="datepicker-header" @click="switchMonthView">{{stringifyDayHeader(currDate, pan)}}</p>
<div class="datepicker-weekRange">
<span v-for="w in daysOfWeek">{{w}}</span>
</div>
<div class="datepicker-dateRange">
<span v-for="d in dateRange[pan]" class="day-cell" :class="getItemClasses(d)" :data-date="stringify(d.date)" @click="daySelect(d.date, $event)"><div>
<template v-if="d.sclass !== 'datepicker-item-gray'">
{{getSpecailDay(d.date) || d.text}}
</template>
<template v-else>
{{d.text}}
</template>
<div v-if="d.sclass !== 'datepicker-item-gray'"><slot :name="stringify(d.date)"></slot></div></div>
</span>
</div>
</div>
</div>
</template>
</div>
<div class="datepicker-popup" :style="paneStyle" v-show="displayMonthView">
<div class="datepicker-ctrl">
<span class="datepicker-preBtn fa fa-chevron-left" aria-hidden="true" @click="preNextYearClick(0)"></span>
<span class="datepicker-nextBtn fa fa-chevron-right" aria-hidden="true" @click="preNextYearClick(1)"></span>
</div>
<template v-for="(p, pan) in pane" >
<div class="datepicker-inner">
<div class="datepicker-body">
<p class="datepicker-header" @click="switchDecadeView">{{stringifyYearHeader(currDate, pan)}}</p>
<div class="datepicker-monthRange">
<template v-for="(m, $index) in months">
<span :class="{'datepicker-dateRange-item-active':
(months[parse(value).month()] === m) &&
currDate.year() + pan === parse(value).year()}"
@click="monthSelect(stringifyYearHeader(currDate, pan), $index)"
>{{m}}</span>
</template>
</div>
</div>
</div>
</template>
</div>
<div class="datepicker-popup" :style="paneStyle" v-show="displayYearView">
<div class="datepicker-ctrl">
<span class="datepicker-preBtn fa fa-chevron-left" aria-hidden="true" @click="preNextDecadeClick(0)"></span>
<span class="datepicker-nextBtn fa fa-chevron-right" aria-hidden="true" @click="preNextDecadeClick(1)"></span>
</div>
<template v-for="(p, pan) in pane" >
<div class="datepicker-inner">
<div class="datepicker-body">
<p class="datepicker-header">{{stringifyDecadeHeader(currDate, pan)}}</p>
<div class="datepicker-monthRange decadeRange">
<template v-for="decade in decadeRange[pan]">
<span :class="{'datepicker-dateRange-item-active':
parse(inputValue).year() === decade.text}"
@click.stop="yearSelect(decade.text)"
>{{decade.text}}</span>
</template>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script>
import i18n from './i18n'
import moment from 'moment'
export default {
name: 'calendar',
props: {
value: {
type: String
},
format: {
default: 'YYYY-MM-DD'
},
disabledDaysOfWeek: {
type: Array,
default () {
return []
}
},
width: {
type: String,
default: '200px'
},
clearButton: {
type: Boolean,
default: false
},
lang: {
type: String,
default: navigator.language
},
placeholder: {
type: String
},
hasInput: {
type: Boolean,
default: true
},
pane: {
type: Number,
default: 1
},
borderWidth: {
type: Number,
default: 2
},
onDayClick: {
type: Function,
default () {}
},
changePane: {
type: Function,
default () {}
},
specialDays: {
type: Object,
default () {
return {}
}
},
rangeBus: {
type: Function,
default () {
// return new Vue()
}
},
rangeStatus: {
type: Number,
default: 0
}
},
mounted () {
this._blur = (e) => {
if (!this.$el.contains(e.target) && this.hasInput) this.close()
}
this.$emit('child-created', this)
// this.inputValue = this.value
// this.dateFormat = this.format
this.currDate = this.parse(this.inputValue) || moment()
const year = this.currDate.year()
const month = this.currDate.month()
this.changePane(year, month, this.pane)
if (!this.hasInput) {
this.displayDayView = true
this.updatePaneStyle()
}
if (this.rangeStatus) {
this.eventbus = this.rangeBus()
if (typeof this.eventbus === 'object' && !this.eventbus.$on) {
console.warn('Calendar rangeBus doesn\'t exist')
this.rangeStatus = 0
}
}
if (this.rangeStatus === 2) {
this._updateRangeStart = (date) => {
this.rangeStart = date
this.currDate = date
this.inputValue = this.stringify(this.currDate)
}
this.eventbus.$on('calendar-rangestart', this._updateRangeStart)
}
document.addEventListener('click', this._blur)
},
beforeDestroy () {
document.removeEventListener('click', this._blur)
if (this.rangeStatus === 2) {
this.eventbus.$off('calendar-rangestart', this._updateRangeStart)
}
},
data () {
return {
inputValue: this.value,
dateFormat: this.format,
months: (function () {
let months = []
for (let i = 0; i < 12; i++) {
months.push(moment({month: i}).format('MMM'))
}
return months
})(),
daysOfWeek: (function () {
let daysOfWeek = []
for (let i = 0; i < 7; i++) {
daysOfWeek.push(moment().weekday(i).format('ddd'))
}
return daysOfWeek
})(),
currDate: moment(),
dateRange: [],
decadeRange: [],
paneStyle: {
width: ''
},
displayDayView: false,
displayMonthView: false,
displayYearView: false,
rangeStart: false,
rangeEnd: false
}
},
watch: {
currDate () {
this.getDateRange()
}
},
computed: {
text () {
return this.translations(this.lang)
}
},
methods: {
handleMouseOver (event) {
let target = event.target
// this.rangeEnd = false
if (!this.rangeStart) {
return true
}
if (event.type === 'mouseout') {
return true
}
while (this.$el.contains(target) && !~target.className.indexOf('day-cell')) {
target = target.parentNode
}
if (~target.className.indexOf('day-cell') && !~target.className.indexOf('datepicker-item-gray')) {
const rangeEnd = target.getAttribute('data-date')
if (this.rangeStart < this.parse(rangeEnd)) {
this.rangeEnd = this.parse(rangeEnd)
}
}
},
getItemClasses (d) {
const clazz = []
clazz.push(d.sclass)
if (this.rangeStart && this.rangeEnd && d.sclass !== 'datepicker-item-gray') {
if (d.date > this.rangeStart && d.date < this.rangeEnd) {
clazz.push('daytoday-range')
}
/* eslint-disable eqeqeq */
if (this.stringify(d.date) == this.stringify(this.rangeStart)) {
clazz.push('daytoday-start')
}
/* eslint-disable eqeqeq */
if (this.stringify(d.date) == this.stringify(this.rangeEnd)) {
clazz.push('daytoday-end')
}
}
return clazz.join(' ')
},
translations (lang) {
lang = lang || 'en'
return i18n[lang]
},
close () {
this.displayDayView = this.displayMonthView = this.displayYearView = false
},
inputClick () {
this.currDate = this.parse(this.inputValue) || moment()
if (this.displayMonthView || this.displayYearView) {
this.displayDayView = false
} else {
this.displayDayView = !this.displayDayView
}
this.updatePaneStyle()
},
updatePaneStyle () {
if (!(this.displayMonthView || this.displayYearView)) {
this.$nextTick(function () {
let offsetLeft = this.$el.offsetLeft
let offsetWidth = this.$el.querySelector('.datepicker-inner').offsetWidth
let popWidth = this.pane * offsetWidth + this.borderWidth // add border
this.paneStyle.width = popWidth + 'px'
if (this.hasInput) {
if (popWidth + offsetLeft > document.documentElement.clientWidth) {
this.paneStyle.right = '0px'
}
} else {
this.paneStyle.position = 'initial'
}
this.$forceUpdate()
})
}
},
preNextDecadeClick (flag) {
if (flag === 0) {
this.currDate = this.currDate.clone().subtract(10, 'years')
} else {
this.currDate = this.currDate.clone().add(10, 'years')
}
},
preNextMonthClick (flag) {
if (flag === 0) {
this.currDate = this.currDate.clone().subtract(1, 'months')
this.changePane(this.currDate.year(), this.currDate.month(), this.pane)
} else {
this.currDate = this.currDate.clone().add(1, 'months')
this.changePane(this.currDate.year(), this.currDate.month(), this.pane)
}
},
preNextYearClick (flag) {
if (flag === 0) {
this.currDate = this.currDate.clone().subtract(1, 'years')
} else {
this.currDate = this.currDate.clone().add(1, 'years')
}
},
yearSelect (year) {
this.displayYearView = false
this.displayMonthView = true
this.currDate = this.currDate.clone().year(year)
},
daySelect (date, event) {
let el = event.target
if (el.classList[0] === 'datepicker-item-disable') {
return false
} else {
if (this.hasInput) {
this.currDate = date
this.inputValue = this.stringify(this.currDate)
this.displayDayView = false
if (this.rangeStatus === 1) {
this.eventbus.$emit('calendar-rangestart', this.currDate)
}
} else {
this.onDayClick(date, this.stringify(date))
}
}
},
switchMonthView () {
this.displayDayView = false
this.displayMonthView = true
},
switchDecadeView () {
this.displayMonthView = false
this.displayYearView = true
},
monthSelect (year, month) {
this.displayMonthView = false
this.displayDayView = true
this.currDate = this.currDate.clone().set({
year: year,
month: month
})
this.changePane(year, month, this.pane)
},
getYearMonth (year, month) {
if (month > 11) {
year++
month = 0
} else if (month < 0) {
year--
month = 11
}
return {year: year, month: month}
},
getSpecailDay (v) {
return this.specialDays[this.stringify(v)]
},
stringifyDecadeHeader (date, pan) {
const yearStr = date.year().toString()
const firstYearOfDecade = parseInt(yearStr.substring(0, yearStr.length - 1) + 0, 10) + (pan * 10)
const lastYearOfDecade = parseInt(firstYearOfDecade, 10) + 10
return firstYearOfDecade + '-' + lastYearOfDecade
},
siblingsMonth (date, offset) {
return date.clone().add({month: offset})
},
stringifyDayHeader (date, month = 0) {
const d = this.siblingsMonth(date, month)
return d.format('YYYY MMMM')
},
parseMonth (date) {
return date.format('MMMM')
},
stringifyYearHeader (date, year = 0) {
return date.year() + year
},
stringify (date, format = this.dateFormat) {
if (!date) date = this.parse()
if (!date) return ''
return date.format(format)
},
parse (str = this.inputValue) {
return moment(str, this.dateFormat)
},
getDayCount (year, month) {
return moment({year: year, month: month}).endOf('month').date()
},
getDateRange () {
this.dateRange = []
this.decadeRange = []
for (let p = 0; p < this.pane; p++) {
let currMonth = this.siblingsMonth(this.currDate, p)
let time = {
year: currMonth.year(),
month: currMonth.month()
}
let yearStr = time.year.toString()
this.decadeRange[p] = []
let firstYearOfDecade = (yearStr.substring(0, yearStr.length - 1) + 0) - 1
for (let i = 0; i < 12; i++) {
this.decadeRange[p].push({
text: firstYearOfDecade + i + p * 10
})
}
this.dateRange[p] = []
const currMonthFirstDay = moment({year: time.year, month: time.month, day: 1})
let firstDayWeek = currMonthFirstDay.weekday()
const dayCount = this.getDayCount(time.year, time.month)
if (firstDayWeek > 0) {
const preMonth = this.getYearMonth(time.year, time.month - 1)
const prevMonthDayCount = this.getDayCount(preMonth.year, preMonth.month)
for (let i = 0; i < firstDayWeek; i++) {
const dayText = prevMonthDayCount - firstDayWeek + i + 1
this.dateRange[p].push({
text: dayText,
date: moment({year: preMonth.year, month: preMonth.month, day: dayText}),
sclass: 'datepicker-item-gray'
})
}
}
for (let i = 1; i <= dayCount; i++) {
const date = moment({year: time.year, month: time.month, day: i})
const week = date.weekday()
let sclass = ''
this.disabledDaysOfWeek.forEach((el) => {
if (week === parseInt(el, 10)) sclass = 'datepicker-item-disable'
})
if (i === this.currDate.date()) {
if (this.inputValue) {
const valueDate = this.parse(this.inputValue)
if (valueDate) {
if (valueDate.year() === time.year && valueDate.month() === time.month) {
sclass = 'datepicker-dateRange-item-active'
}
}
}
}
this.dateRange[p].push({
text: i,
date: date,
sclass: sclass
})
}
if (this.dateRange[p].length < 42) {
const nextMonthNeed = 42 - this.dateRange[p].length
const nextMonth = this.getYearMonth(time.year, time.month + 1)
for (let i = 1; i <= nextMonthNeed; i++) {
this.dateRange[p].push({
text: i,
date: moment({year: nextMonth.year, month: nextMonth.month, day: i}),
sclass: 'datepicker-item-gray'
})
}
}
}
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" lang="scss">
@import '~styles/utilities/all';
.datepicker{
position: relative;
display: inline-block;
}
input.datepicker-input.with-reset-button {
padding-right: 25px;
}
.datepicker > button.close {
position: absolute;
top: 0;
right: 0;
outline: none;
z-index: 2;
display: block;
width: 34px;
height: 34px;
line-height: 34px;
text-align: center;
}
.datepicker > button.close:focus {
opacity: .2;
}
.datepicker-popup{
position: absolute;
background-color: $white;
border-radius: $radius-large;
box-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1);
margin-top: 2px;
z-index: 1000;
@include clearfix;
}
.datepicker-inner{
width: 218px;
float: left;
}
.datepicker-body{
padding: 10px 10px;
text-align: center;
}
.datepicker-ctrl p,
.datepicker-ctrl span,
.datepicker-body span{
display: inline-block;
width: 28px;
line-height: 28px;
height: 28px;
// border-radius: 4px;
}
.datepicker-ctrl p {
width: 65%;
}
.datepicker-ctrl span {
position: absolute;
}
.datepicker-body span {
text-align: center;
}
.datepicker-monthRange span{
width: 48px;
height: 50px;
line-height: 45px;
}
.datepicker-item-disable {
background-color: $white!important;
cursor: not-allowed!important;
}
.decadeRange span:first-child,
.decadeRange span:last-child,
.datepicker-item-disable,
.datepicker-item-gray{
color: $text-light;
}
.datepicker-dateRange-item-active:hover,
.datepicker-dateRange-item-active {
background: $primary!important;
color: $text-invert!important;
}
.datepicker-monthRange {
margin-top: 10px
}
.datepicker-monthRange span,
.datepicker-ctrl span,
.datepicker-ctrl p,
.datepicker-dateRange span {
cursor: pointer;
}
.datepicker-monthRange span:hover,
.datepicker-ctrl p:hover,
.datepicker-ctrl i:hover,
.datepicker-dateRange span:hover,
.datepicker-dateRange-item-hover {
background-color : $grey-lighter;
}
.datepicker-dateRange {
.daytoday-start,
.daytoday-start:hover,
.daytoday-end,
.daytoday-end:hover{
background: $primary!important;
color: $white!important;
}
}
.datepicker-dateRange .daytoday-range,
.datepicker-dateRange .daytoday-range:hover{
background-color: $gray;
}
.datepicker-weekRange span{
font-weight: bold;
}
.datepicker-label{
background-color: #f8f8f8;
font-weight: 700;
padding: 7px 0;
text-align: center;
}
.datepicker-header {
cursor: pointer;
}
.datepicker-ctrl{
position: relative;
/*height: 30px;*/
line-height: 30px;
font-weight: bold;
text-align: center;
}
.month-btn{
font-weight: bold;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
}
.datepicker-preBtn{
left: 2px;
}
.datepicker-nextBtn{
right: 2px;
}
</style>
The language file can exclude date locale strings now, just like this:
i18n/en.js:
export default {
limit: 'Limit reached ({{limit}} items max).',
loading: 'Loading...',
minLength: 'Min. Length',
notSelected: 'Nothing Selected',
required: 'Required',
search: 'Search'
}
You can set the locale with moment.js during initialization.
By the way, if you set the default locale from 'en' to 'zh-cn':
moment.locale('zh-cn')
Monday is the first day of the week now.