<template> <transition name="el-zoom-in-top" @after-enter="handleEnter" @after-leave="handleLeave"> <div v-show="visible" class="el-picker-panel el-date-picker el-popper" :class="[{ 'has-sidebar': $slots.sidebar || shortcuts, 'has-time': showTime }, popperClass]"> <div class="el-picker-panel__body-wrapper"> <slot name="sidebar" class="el-picker-panel__sidebar"></slot> <div class="el-picker-panel__sidebar" v-if="shortcuts"> <button type="button" class="el-picker-panel__shortcut" v-for="(shortcut, key) in shortcuts" :key="key" @click="handleShortcutClick(shortcut)">{{ shortcut.text }}</button> </div> <div class="el-picker-panel__body"> <div class="el-date-picker__time-header" v-if="showTime"> <span class="el-date-picker__editor-wrap"> <el-input :placeholder="t('el.datepicker.selectDate')" :value="visibleDate" size="small" @input="val => userInputDate = val" @change="handleVisibleDateChange" /> </span> <span class="el-date-picker__editor-wrap" v-clickoutside="handleTimePickClose"> <el-input ref="input" @focus="timePickerVisible = true" :placeholder="t('el.datepicker.selectTime')" :value="visibleTime" size="small" @input="val => userInputTime = val" @change="handleVisibleTimeChange" /> <time-picker ref="timepicker" :time-arrow-control="arrowControl" @pick="handleTimePick" :visible="timePickerVisible" @mounted="proxyTimePickerDataProperties"> </time-picker> </span> </div> <div class="el-date-picker__header" :class="{ 'el-date-picker__header--bordered': currentView === 'year' || currentView === 'month' }" v-show="currentView !== 'time'"> <button type="button" @click="prevYear" :aria-label="t(`el.datepicker.prevYear`)" class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left"> </button> <button type="button" @click="prevMonth" v-show="currentView === 'date'" :aria-label="t(`el.datepicker.prevMonth`)" class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-arrow-left"> </button> <span @click="showYearPicker" role="button" class="el-date-picker__header-label">{{ yearLabel }}</span> <span @click="showMonthPicker" v-show="currentView === 'date'" role="button" class="el-date-picker__header-label" :class="{ active: currentView === 'month' }">{{t(`el.datepicker.month${ month + 1 }`)}}</span> <button type="button" @click="nextYear" :aria-label="t(`el.datepicker.nextYear`)" class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right"> </button> <button type="button" @click="nextMonth" v-show="currentView === 'date'" :aria-label="t(`el.datepicker.nextMonth`)" class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-arrow-right"> </button> </div> <div class="el-picker-panel__content"> <date-table v-show="currentView === 'date'" @pick="handleDatePick" :selection-mode="selectionMode" :first-day-of-week="firstDayOfWeek" :value="value" :default-value="defaultValue ? new Date(defaultValue) : null" :date="date" :cell-class-name="cellClassName" :disabled-date="disabledDate"> </date-table> <year-table v-show="currentView === 'year'" @pick="handleYearPick" :value="value" :default-value="defaultValue ? new Date(defaultValue) : null" :date="date" :disabled-date="disabledDate"> </year-table> <month-table v-show="currentView === 'month'" @pick="handleMonthPick" :value="value" :default-value="defaultValue ? new Date(defaultValue) : null" :date="date" :disabled-date="disabledDate"> </month-table> </div> </div> </div> <div class="el-picker-panel__footer" v-show="footerVisible && currentView === 'date'"> <el-button size="mini" type="text" class="el-picker-panel__link-btn" @click="changeToNow" v-show="selectionMode !== 'dates'"> {{ t('el.datepicker.now') }} </el-button> <el-button plain size="mini" class="el-picker-panel__link-btn" @click="confirm"> {{ t('el.datepicker.confirm') }} </el-button> </div> </div> </transition> </template> <script type="text/babel"> import { formatDate, parseDate, getWeekNumber, isDate, modifyDate, modifyTime, modifyWithTimeString, clearMilliseconds, clearTime, prevYear, nextYear, prevMonth, nextMonth, changeYearMonthAndClampDate, extractDateFormat, extractTimeFormat, timeWithinRange } from 'element-ui/src/utils/date-util'; import Clickoutside from 'element-ui/src/utils/clickoutside'; import Locale from 'element-ui/src/mixins/locale'; import ElInput from 'element-ui/packages/input'; import ElButton from 'element-ui/packages/button'; import TimePicker from './time'; import YearTable from '../basic/year-table'; import MonthTable from '../basic/month-table'; import DateTable from '../basic/date-table'; export default { mixins: [Locale], directives: { Clickoutside }, watch: { showTime(val) { /* istanbul ignore if */ if (!val) return; this.$nextTick(_ => { const inputElm = this.$refs.input.$el; if (inputElm) { this.pickerWidth = inputElm.getBoundingClientRect().width + 10; } }); }, value(val) { if (this.selectionMode === 'dates' && this.value) return; if (isDate(val)) { this.date = new Date(val); } else { this.date = this.getDefaultValue(); } }, defaultValue(val) { if (!isDate(this.value)) { this.date = val ? new Date(val) : new Date(); } }, timePickerVisible(val) { if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners()); }, selectionMode(newVal) { if (newVal === 'month') { /* istanbul ignore next */ if (this.currentView !== 'year' || this.currentView !== 'month') { this.currentView = 'month'; } } else if (newVal === 'dates') { this.currentView = 'date'; } } }, methods: { proxyTimePickerDataProperties() { const format = timeFormat => {this.$refs.timepicker.format = timeFormat;}; const value = value => {this.$refs.timepicker.value = value;}; const date = date => {this.$refs.timepicker.date = date;}; const selectableRange = selectableRange => {this.$refs.timepicker.selectableRange = selectableRange;}; this.$watch('value', value); this.$watch('date', date); this.$watch('selectableRange', selectableRange); format(this.timeFormat); value(this.value); date(this.date); selectableRange(this.selectableRange); }, handleClear() { this.date = this.getDefaultValue(); this.$emit('pick', null); }, emit(value, ...args) { if (!value) { this.$emit('pick', value, ...args); } else if (Array.isArray(value)) { const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date)); this.$emit('pick', dates, ...args); } else { this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args); } this.userInputDate = null; this.userInputTime = null; }, // resetDate() { // this.date = new Date(this.date); // }, showMonthPicker() { this.currentView = 'month'; }, showYearPicker() { this.currentView = 'year'; }, // XXX: 没用到 // handleLabelClick() { // if (this.currentView === 'date') { // this.showMonthPicker(); // } else if (this.currentView === 'month') { // this.showYearPicker(); // } // }, prevMonth() { this.date = prevMonth(this.date); }, nextMonth() { this.date = nextMonth(this.date); }, prevYear() { if (this.currentView === 'year') { this.date = prevYear(this.date, 10); } else { this.date = prevYear(this.date); } }, nextYear() { if (this.currentView === 'year') { this.date = nextYear(this.date, 10); } else { this.date = nextYear(this.date); } }, handleShortcutClick(shortcut) { if (shortcut.onClick) { shortcut.onClick(this); } }, handleTimePick(value, visible, first) { if (isDate(value)) { const newDate = this.value ? modifyTime(this.value, value.getHours(), value.getMinutes(), value.getSeconds()) : modifyWithTimeString(this.getDefaultValue(), this.defaultTime); this.date = newDate; this.emit(this.date, true); } else { this.emit(value, true); } if (!first) { this.timePickerVisible = visible; } }, handleTimePickClose() { this.timePickerVisible = false; }, handleMonthPick(month) { if (this.selectionMode === 'month') { this.date = modifyDate(this.date, this.year, month, 1); this.emit(this.date); } else { this.date = changeYearMonthAndClampDate(this.date, this.year, month); // TODO: should emit intermediate value ?? // this.emit(this.date); this.currentView = 'date'; } }, handleDatePick(value) { if (this.selectionMode === 'day') { let newDate = this.value ? modifyDate(this.value, value.getFullYear(), value.getMonth(), value.getDate()) : modifyWithTimeString(value, this.defaultTime); // change default time while out of selectableRange if (!this.checkDateWithinRange(newDate)) { newDate = modifyDate(this.selectableRange[0][0], value.getFullYear(), value.getMonth(), value.getDate()); } this.date = newDate; this.emit(this.date, this.showTime); } else if (this.selectionMode === 'week') { this.emit(value.date); } else if (this.selectionMode === 'dates') { this.emit(value, true); // set false to keep panel open } }, handleYearPick(year) { if (this.selectionMode === 'year') { this.date = modifyDate(this.date, year, 0, 1); this.emit(this.date); } else { this.date = changeYearMonthAndClampDate(this.date, year, this.month); // TODO: should emit intermediate value ?? // this.emit(this.date, true); this.currentView = 'month'; } }, changeToNow() { // NOTE: not a permanent solution // consider disable "now" button in the future if ((!this.disabledDate || !this.disabledDate(new Date())) && this.checkDateWithinRange(new Date())) { this.date = new Date(); this.emit(this.date); } }, confirm() { if (this.selectionMode === 'dates') { this.emit(this.value); } else { // value were emitted in handle{Date,Time}Pick, nothing to update here // deal with the scenario where: user opens the picker, then confirm without doing anything const value = this.value ? this.value : modifyWithTimeString(this.getDefaultValue(), this.defaultTime); this.date = new Date(value); // refresh date this.emit(value); } }, resetView() { if (this.selectionMode === 'month') { this.currentView = 'month'; } else if (this.selectionMode === 'year') { this.currentView = 'year'; } else { this.currentView = 'date'; } }, handleEnter() { document.body.addEventListener('keydown', this.handleKeydown); }, handleLeave() { this.$emit('dodestroy'); document.body.removeEventListener('keydown', this.handleKeydown); }, handleKeydown(event) { const keyCode = event.keyCode; const list = [38, 40, 37, 39]; if (this.visible && !this.timePickerVisible) { if (list.indexOf(keyCode) !== -1) { this.handleKeyControl(keyCode); event.stopPropagation(); event.preventDefault(); } if (keyCode === 13 && this.userInputDate === null && this.userInputTime === null) { // Enter this.emit(this.date, false); } } }, handleKeyControl(keyCode) { const mapping = { 'year': { 38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setFullYear(date.getFullYear() + step) }, 'month': { 38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setMonth(date.getMonth() + step) }, 'week': { 38: -1, 40: 1, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step * 7) }, 'day': { 38: -7, 40: 7, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step) } }; const mode = this.selectionMode; const year = 3.1536e10; const now = this.date.getTime(); const newDate = new Date(this.date.getTime()); while (Math.abs(now - newDate.getTime()) <= year) { const map = mapping[mode]; map.offset(newDate, map[keyCode]); if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) { continue; } this.date = newDate; this.$emit('pick', newDate, true); break; } }, handleVisibleTimeChange(value) { const time = parseDate(value, this.timeFormat); if (time && this.checkDateWithinRange(time)) { this.date = modifyDate(time, this.year, this.month, this.monthDate); this.userInputTime = null; this.$refs.timepicker.value = this.date; this.timePickerVisible = false; this.emit(this.date, true); } }, handleVisibleDateChange(value) { const date = parseDate(value, this.dateFormat); if (date) { if (typeof this.disabledDate === 'function' && this.disabledDate(date)) { return; } this.date = modifyTime(date, this.date.getHours(), this.date.getMinutes(), this.date.getSeconds()); this.userInputDate = null; this.resetView(); this.emit(this.date, true); } }, isValidValue(value) { return value && !isNaN(value) && ( typeof this.disabledDate === 'function' ? !this.disabledDate(value) : true ) && this.checkDateWithinRange(value); }, getDefaultValue() { // if default-value is set, return it // otherwise, return now (the moment this method gets called) return this.defaultValue ? new Date(this.defaultValue) : new Date(); }, checkDateWithinRange(date) { return this.selectableRange.length > 0 ? timeWithinRange(date, this.selectableRange, this.format || 'HH:mm:ss') : true; } }, components: { TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton }, data() { return { popperClass: '', date: new Date(), value: '', defaultValue: null, // use getDefaultValue() for time computation defaultTime: null, showTime: false, selectionMode: 'day', shortcuts: '', visible: false, currentView: 'date', disabledDate: '', cellClassName: '', selectableRange: [], firstDayOfWeek: 7, showWeekNumber: false, timePickerVisible: false, format: '', arrowControl: false, userInputDate: null, userInputTime: null }; }, computed: { year() { return this.date.getFullYear(); }, month() { return this.date.getMonth(); }, week() { return getWeekNumber(this.date); }, monthDate() { return this.date.getDate(); }, footerVisible() { return this.showTime || this.selectionMode === 'dates'; }, visibleTime() { if (this.userInputTime !== null) { return this.userInputTime; } else { return formatDate(this.value || this.defaultValue, this.timeFormat); } }, visibleDate() { if (this.userInputDate !== null) { return this.userInputDate; } else { return formatDate(this.value || this.defaultValue, this.dateFormat); } }, yearLabel() { const yearTranslation = this.t('el.datepicker.year'); if (this.currentView === 'year') { const startYear = Math.floor(this.year / 10) * 10; if (yearTranslation) { return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation; } return startYear + ' - ' + (startYear + 9); } return this.year + ' ' + yearTranslation; }, timeFormat() { if (this.format) { return extractTimeFormat(this.format); } else { return 'HH:mm:ss'; } }, dateFormat() { if (this.format) { return extractDateFormat(this.format); } else { return 'yyyy-MM-dd'; } } } }; </script>