Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-08-26 09:31:34 +08:00
parent 9b8c4bfab6
commit 394eb0285d
34137 changed files with 3589424 additions and 0 deletions
@@ -0,0 +1,680 @@
<template>
<transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-picker-panel el-date-range-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-range-picker__time-header" v-if="showTime">
<span class="el-date-range-picker__editors-wrap">
<span class="el-date-range-picker__time-picker-wrap">
<el-input
size="small"
:disabled="rangeState.selecting"
ref="minInput"
:placeholder="t('el.datepicker.startDate')"
class="el-date-range-picker__editor"
:value="minVisibleDate"
@input="val => handleDateInput(val, 'min')"
@change="val => handleDateChange(val, 'min')" />
</span>
<span class="el-date-range-picker__time-picker-wrap" v-clickoutside="handleMinTimeClose">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.startTime')"
:value="minVisibleTime"
@focus="minTimePickerVisible = true"
@input="val => handleTimeInput(val, 'min')"
@change="val => handleTimeChange(val, 'min')" />
<time-picker
ref="minTimePicker"
@pick="handleMinTimePick"
:time-arrow-control="arrowControl"
:visible="minTimePickerVisible"
@mounted="$refs.minTimePicker.format=timeFormat">
</time-picker>
</span>
</span>
<span class="el-icon-arrow-right"></span>
<span class="el-date-range-picker__editors-wrap is-right">
<span class="el-date-range-picker__time-picker-wrap">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.endDate')"
:value="maxVisibleDate"
:readonly="!minDate"
@input="val => handleDateInput(val, 'max')"
@change="val => handleDateChange(val, 'max')" />
</span>
<span class="el-date-range-picker__time-picker-wrap" v-clickoutside="handleMaxTimeClose">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.endTime')"
:value="maxVisibleTime"
:readonly="!minDate"
@focus="minDate && (maxTimePickerVisible = true)"
@input="val => handleTimeInput(val, 'max')"
@change="val => handleTimeChange(val, 'max')" />
<time-picker
ref="maxTimePicker"
@pick="handleMaxTimePick"
:time-arrow-control="arrowControl"
:visible="maxTimePickerVisible"
@mounted="$refs.maxTimePicker.format=timeFormat">
</time-picker>
</span>
</span>
</div>
<div class="el-picker-panel__content el-date-range-picker__content is-left">
<div class="el-date-range-picker__header">
<button
type="button"
@click="leftPrevYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
@click="leftPrevMonth"
class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
<button
type="button"
@click="leftNextYear"
v-if="unlinkPanels"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<button
type="button"
@click="leftNextMonth"
v-if="unlinkPanels"
:disabled="!enableMonthArrow"
:class="{ 'is-disabled': !enableMonthArrow }"
class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
<div>{{ leftLabel }}</div>
</div>
<date-table
selection-mode="range"
:date="leftDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
:cell-class-name="cellClassName"
@changerange="handleChangeRange"
:first-day-of-week="firstDayOfWeek"
@pick="handleRangePick">
</date-table>
</div>
<div class="el-picker-panel__content el-date-range-picker__content is-right">
<div class="el-date-range-picker__header">
<button
type="button"
@click="rightPrevYear"
v-if="unlinkPanels"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
@click="rightPrevMonth"
v-if="unlinkPanels"
:disabled="!enableMonthArrow"
:class="{ 'is-disabled': !enableMonthArrow }"
class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
<button
type="button"
@click="rightNextYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<button
type="button"
@click="rightNextMonth"
class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
<div>{{ rightLabel }}</div>
</div>
<date-table
selection-mode="range"
:date="rightDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
:cell-class-name="cellClassName"
@changerange="handleChangeRange"
:first-day-of-week="firstDayOfWeek"
@pick="handleRangePick">
</date-table>
</div>
</div>
</div>
<div class="el-picker-panel__footer" v-if="showTime">
<el-button
size="mini"
type="text"
class="el-picker-panel__link-btn"
@click="handleClear">
{{ t('el.datepicker.clear') }}
</el-button>
<el-button
plain
size="mini"
class="el-picker-panel__link-btn"
:disabled="btnDisabled"
@click="handleConfirm(false)">
{{ t('el.datepicker.confirm') }}
</el-button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
formatDate,
parseDate,
isDate,
modifyDate,
modifyTime,
modifyWithTimeString,
prevYear,
nextYear,
prevMonth,
nextMonth,
nextDate,
extractDateFormat,
extractTimeFormat
} from 'element-ui/src/utils/date-util';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Locale from 'element-ui/src/mixins/locale';
import TimePicker from './time';
import DateTable from '../basic/date-table';
import ElInput from 'element-ui/packages/input';
import ElButton from 'element-ui/packages/button';
const calcDefaultValue = (defaultValue) => {
if (Array.isArray(defaultValue)) {
return [new Date(defaultValue[0]), new Date(defaultValue[1])];
} else if (defaultValue) {
return [new Date(defaultValue), nextDate(new Date(defaultValue), 1)];
} else {
return [new Date(), nextDate(new Date(), 1)];
}
};
export default {
mixins: [Locale],
directives: { Clickoutside },
computed: {
btnDisabled() {
return !(this.minDate && this.maxDate && !this.selecting && this.isValidValue([this.minDate, this.maxDate]));
},
leftLabel() {
return this.leftDate.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.leftDate.getMonth() + 1 }`);
},
rightLabel() {
return this.rightDate.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.rightDate.getMonth() + 1 }`);
},
leftYear() {
return this.leftDate.getFullYear();
},
leftMonth() {
return this.leftDate.getMonth();
},
leftMonthDate() {
return this.leftDate.getDate();
},
rightYear() {
return this.rightDate.getFullYear();
},
rightMonth() {
return this.rightDate.getMonth();
},
rightMonthDate() {
return this.rightDate.getDate();
},
minVisibleDate() {
if (this.dateUserInput.min !== null) return this.dateUserInput.min;
if (this.minDate) return formatDate(this.minDate, this.dateFormat);
return '';
},
maxVisibleDate() {
if (this.dateUserInput.max !== null) return this.dateUserInput.max;
if (this.maxDate || this.minDate) return formatDate(this.maxDate || this.minDate, this.dateFormat);
return '';
},
minVisibleTime() {
if (this.timeUserInput.min !== null) return this.timeUserInput.min;
if (this.minDate) return formatDate(this.minDate, this.timeFormat);
return '';
},
maxVisibleTime() {
if (this.timeUserInput.max !== null) return this.timeUserInput.max;
if (this.maxDate || this.minDate) return formatDate(this.maxDate || this.minDate, this.timeFormat);
return '';
},
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';
}
},
enableMonthArrow() {
const nextMonth = (this.leftMonth + 1) % 12;
const yearOffset = this.leftMonth + 1 >= 12 ? 1 : 0;
return this.unlinkPanels && new Date(this.leftYear + yearOffset, nextMonth) < new Date(this.rightYear, this.rightMonth);
},
enableYearArrow() {
return this.unlinkPanels && this.rightYear * 12 + this.rightMonth - (this.leftYear * 12 + this.leftMonth + 1) >= 12;
}
},
data() {
return {
popperClass: '',
value: [],
defaultValue: null,
defaultTime: null,
minDate: '',
maxDate: '',
leftDate: new Date(),
rightDate: nextMonth(new Date()),
rangeState: {
endDate: null,
selecting: false,
row: null,
column: null
},
showTime: false,
shortcuts: '',
visible: '',
disabledDate: '',
cellClassName: '',
firstDayOfWeek: 7,
minTimePickerVisible: false,
maxTimePickerVisible: false,
format: '',
arrowControl: false,
unlinkPanels: false,
dateUserInput: {
min: null,
max: null
},
timeUserInput: {
min: null,
max: null
}
};
},
watch: {
minDate(val) {
this.dateUserInput.min = null;
this.timeUserInput.min = null;
this.$nextTick(() => {
if (this.$refs.maxTimePicker && this.maxDate && this.maxDate < this.minDate) {
const format = 'HH:mm:ss';
this.$refs.maxTimePicker.selectableRange = [
[
parseDate(formatDate(this.minDate, format), format),
parseDate('23:59:59', format)
]
];
}
});
if (val && this.$refs.minTimePicker) {
this.$refs.minTimePicker.date = val;
this.$refs.minTimePicker.value = val;
}
},
maxDate(val) {
this.dateUserInput.max = null;
this.timeUserInput.max = null;
if (val && this.$refs.maxTimePicker) {
this.$refs.maxTimePicker.date = val;
this.$refs.maxTimePicker.value = val;
}
},
minTimePickerVisible(val) {
if (val) {
this.$nextTick(() => {
this.$refs.minTimePicker.date = this.minDate;
this.$refs.minTimePicker.value = this.minDate;
this.$refs.minTimePicker.adjustSpinners();
});
}
},
maxTimePickerVisible(val) {
if (val) {
this.$nextTick(() => {
this.$refs.maxTimePicker.date = this.maxDate;
this.$refs.maxTimePicker.value = this.maxDate;
this.$refs.maxTimePicker.adjustSpinners();
});
}
},
value(newVal) {
if (!newVal) {
this.minDate = null;
this.maxDate = null;
} else if (Array.isArray(newVal)) {
this.minDate = isDate(newVal[0]) ? new Date(newVal[0]) : null;
this.maxDate = isDate(newVal[1]) ? new Date(newVal[1]) : null;
if (this.minDate) {
this.leftDate = this.minDate;
if (this.unlinkPanels && this.maxDate) {
const minDateYear = this.minDate.getFullYear();
const minDateMonth = this.minDate.getMonth();
const maxDateYear = this.maxDate.getFullYear();
const maxDateMonth = this.maxDate.getMonth();
this.rightDate = minDateYear === maxDateYear && minDateMonth === maxDateMonth
? nextMonth(this.maxDate)
: this.maxDate;
} else {
this.rightDate = nextMonth(this.leftDate);
}
} else {
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextMonth(this.leftDate);
}
}
},
defaultValue(val) {
if (!Array.isArray(this.value)) {
const [left, right] = calcDefaultValue(val);
this.leftDate = left;
this.rightDate = val && val[1] && this.unlinkPanels
? right
: nextMonth(this.leftDate);
}
}
},
methods: {
handleClear() {
this.minDate = null;
this.maxDate = null;
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextMonth(this.leftDate);
this.$emit('pick', null);
},
handleChangeRange(val) {
this.minDate = val.minDate;
this.maxDate = val.maxDate;
this.rangeState = val.rangeState;
},
handleDateInput(value, type) {
this.dateUserInput[type] = value;
if (value.length !== this.dateFormat.length) return;
const parsedValue = parseDate(value, this.dateFormat);
if (parsedValue) {
if (typeof this.disabledDate === 'function' &&
this.disabledDate(new Date(parsedValue))) {
return;
}
if (type === 'min') {
this.minDate = modifyDate(this.minDate || new Date(), parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
this.leftDate = new Date(parsedValue);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
} else {
this.maxDate = modifyDate(this.maxDate || new Date(), parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
this.rightDate = new Date(parsedValue);
if (!this.unlinkPanels) {
this.leftDate = prevMonth(parsedValue);
}
}
}
},
handleDateChange(value, type) {
const parsedValue = parseDate(value, this.dateFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyDate(this.minDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
if (this.minDate > this.maxDate) {
this.maxDate = this.minDate;
}
} else {
this.maxDate = modifyDate(this.maxDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
if (this.maxDate < this.minDate) {
this.minDate = this.maxDate;
}
}
}
},
handleTimeInput(value, type) {
this.timeUserInput[type] = value;
if (value.length !== this.timeFormat.length) return;
const parsedValue = parseDate(value, this.timeFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyTime(this.minDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
this.$nextTick(_ => this.$refs.minTimePicker.adjustSpinners());
} else {
this.maxDate = modifyTime(this.maxDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
this.$nextTick(_ => this.$refs.maxTimePicker.adjustSpinners());
}
}
},
handleTimeChange(value, type) {
const parsedValue = parseDate(value, this.timeFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyTime(this.minDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
if (this.minDate > this.maxDate) {
this.maxDate = this.minDate;
}
this.$refs.minTimePicker.value = this.minDate;
this.minTimePickerVisible = false;
} else {
this.maxDate = modifyTime(this.maxDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
if (this.maxDate < this.minDate) {
this.minDate = this.maxDate;
}
this.$refs.maxTimePicker.value = this.minDate;
this.maxTimePickerVisible = false;
}
}
},
handleRangePick(val, close = true) {
const defaultTime = this.defaultTime || [];
const minDate = modifyWithTimeString(val.minDate, defaultTime[0]);
const maxDate = modifyWithTimeString(val.maxDate, defaultTime[1]);
if (this.maxDate === maxDate && this.minDate === minDate) {
return;
}
this.onPick && this.onPick(val);
this.maxDate = maxDate;
this.minDate = minDate;
// workaround for https://github.com/ElemeFE/element/issues/7539, should remove this block when we don't have to care about Chromium 55 - 57
setTimeout(() => {
this.maxDate = maxDate;
this.minDate = minDate;
}, 10);
if (!close || this.showTime) return;
this.handleConfirm();
},
handleShortcutClick(shortcut) {
if (shortcut.onClick) {
shortcut.onClick(this);
}
},
handleMinTimePick(value, visible, first) {
this.minDate = this.minDate || new Date();
if (value) {
this.minDate = modifyTime(this.minDate, value.getHours(), value.getMinutes(), value.getSeconds());
}
if (!first) {
this.minTimePickerVisible = visible;
}
if (!this.maxDate || this.maxDate && this.maxDate.getTime() < this.minDate.getTime()) {
this.maxDate = new Date(this.minDate);
}
},
handleMinTimeClose() {
this.minTimePickerVisible = false;
},
handleMaxTimePick(value, visible, first) {
if (this.maxDate && value) {
this.maxDate = modifyTime(this.maxDate, value.getHours(), value.getMinutes(), value.getSeconds());
}
if (!first) {
this.maxTimePickerVisible = visible;
}
if (this.maxDate && this.minDate && this.minDate.getTime() > this.maxDate.getTime()) {
this.minDate = new Date(this.maxDate);
}
},
handleMaxTimeClose() {
this.maxTimePickerVisible = false;
},
// leftPrev*, rightNext* need to take care of `unlinkPanels`
leftPrevYear() {
this.leftDate = prevYear(this.leftDate);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
},
leftPrevMonth() {
this.leftDate = prevMonth(this.leftDate);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
},
rightNextYear() {
if (!this.unlinkPanels) {
this.leftDate = nextYear(this.leftDate);
this.rightDate = nextMonth(this.leftDate);
} else {
this.rightDate = nextYear(this.rightDate);
}
},
rightNextMonth() {
if (!this.unlinkPanels) {
this.leftDate = nextMonth(this.leftDate);
this.rightDate = nextMonth(this.leftDate);
} else {
this.rightDate = nextMonth(this.rightDate);
}
},
// leftNext*, rightPrev* are called when `unlinkPanels` is true
leftNextYear() {
this.leftDate = nextYear(this.leftDate);
},
leftNextMonth() {
this.leftDate = nextMonth(this.leftDate);
},
rightPrevYear() {
this.rightDate = prevYear(this.rightDate);
},
rightPrevMonth() {
this.rightDate = prevMonth(this.rightDate);
},
handleConfirm(visible = false) {
if (this.isValidValue([this.minDate, this.maxDate])) {
this.$emit('pick', [this.minDate, this.maxDate], visible);
}
},
isValidValue(value) {
return Array.isArray(value) &&
value && value[0] && value[1] &&
isDate(value[0]) && isDate(value[1]) &&
value[0].getTime() <= value[1].getTime() && (
typeof this.disabledDate === 'function'
? !this.disabledDate(value[0]) && !this.disabledDate(value[1])
: true
);
},
resetView() {
// NOTE: this is a hack to reset {min, max}Date on picker open.
// TODO: correct way of doing so is to refactor {min, max}Date to be dependent on value and internal selection state
// an alternative would be resetView whenever picker becomes visible, should also investigate date-panel's resetView
if (this.minDate && this.maxDate == null) this.rangeState.selecting = false;
this.minDate = this.value && isDate(this.value[0]) ? new Date(this.value[0]) : null;
this.maxDate = this.value && isDate(this.value[0]) ? new Date(this.value[1]) : null;
}
},
components: { TimePicker, DateTable, ElInput, ElButton }
};
</script>
@@ -0,0 +1,609 @@
<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"
:selection-mode="selectionMode"
:value="value"
:default-value="defaultValue ? new Date(defaultValue) : null"
:date="date"
:disabled-date="disabledDate">
</year-table>
<month-table
v-show="currentView === 'month'"
@pick="handleMonthPick"
:selection-mode="selectionMode"
: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' || currentView === 'month' || currentView === 'year')">
<el-button
size="mini"
type="text"
class="el-picker-panel__link-btn"
@click="changeToNow"
v-show="selectionMode !== 'dates' && selectionMode !== 'months' && selectionMode !== 'years'">
{{ 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 (this.selectionMode === 'months' && this.value) return;
if (this.selectionMode === 'years' && 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';
} else if (newVal === 'years') {
this.currentView = 'year';
} else if (newVal === 'months') {
this.currentView = 'month';
}
}
},
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 if (this.selectionMode === 'months') {
this.emit(month, true);
} 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 if (this.selectionMode === 'years') {
this.emit(year, true);
} 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.selectionMode === 'months' || this.selectionMode === 'years') {
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.selectionMode === 'months') {
this.currentView = 'month';
} else if (this.selectionMode === 'year' || this.selectionMode === 'years') {
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' || this.selectionMode === 'months' || this.selectionMode === 'years';
},
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>
@@ -0,0 +1,289 @@
<template>
<transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-picker-panel el-date-range-picker el-popper"
:class="[{
'has-sidebar': $slots.sidebar || shortcuts
}, 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-picker-panel__content el-date-range-picker__content is-left">
<div class="el-date-range-picker__header">
<button
type="button"
@click="leftPrevYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
v-if="unlinkPanels"
@click="leftNextYear"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<div>{{ leftLabel }}</div>
</div>
<month-table
selection-mode="range"
:date="leftDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
@changerange="handleChangeRange"
@pick="handleRangePick">
</month-table>
</div>
<div class="el-picker-panel__content el-date-range-picker__content is-right">
<div class="el-date-range-picker__header">
<button
type="button"
v-if="unlinkPanels"
@click="rightPrevYear"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
@click="rightNextYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<div>{{ rightLabel }}</div>
</div>
<month-table
selection-mode="range"
:date="rightDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
@changerange="handleChangeRange"
@pick="handleRangePick">
</month-table>
</div>
</div>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
isDate,
modifyWithTimeString,
prevYear,
nextYear,
nextMonth
} from 'element-ui/src/utils/date-util';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Locale from 'element-ui/src/mixins/locale';
import MonthTable from '../basic/month-table';
import ElInput from 'element-ui/packages/input';
import ElButton from 'element-ui/packages/button';
const calcDefaultValue = (defaultValue) => {
if (Array.isArray(defaultValue)) {
return [new Date(defaultValue[0]), new Date(defaultValue[1])];
} else if (defaultValue) {
return [new Date(defaultValue), nextMonth(new Date(defaultValue))];
} else {
return [new Date(), nextMonth(new Date())];
}
};
export default {
mixins: [Locale],
directives: { Clickoutside },
computed: {
btnDisabled() {
return !(this.minDate && this.maxDate && !this.selecting && this.isValidValue([this.minDate, this.maxDate]));
},
leftLabel() {
return this.leftDate.getFullYear() + ' ' + this.t('el.datepicker.year');
},
rightLabel() {
return this.rightDate.getFullYear() + ' ' + this.t('el.datepicker.year');
},
leftYear() {
return this.leftDate.getFullYear();
},
rightYear() {
return this.rightDate.getFullYear() === this.leftDate.getFullYear() ? this.leftDate.getFullYear() + 1 : this.rightDate.getFullYear();
},
enableYearArrow() {
return this.unlinkPanels && this.rightYear > this.leftYear + 1;
}
},
data() {
return {
popperClass: '',
value: [],
defaultValue: null,
defaultTime: null,
minDate: '',
maxDate: '',
leftDate: new Date(),
rightDate: nextYear(new Date()),
rangeState: {
endDate: null,
selecting: false,
row: null,
column: null
},
shortcuts: '',
visible: '',
disabledDate: '',
format: '',
arrowControl: false,
unlinkPanels: false
};
},
watch: {
value(newVal) {
if (!newVal) {
this.minDate = null;
this.maxDate = null;
} else if (Array.isArray(newVal)) {
this.minDate = isDate(newVal[0]) ? new Date(newVal[0]) : null;
this.maxDate = isDate(newVal[1]) ? new Date(newVal[1]) : null;
if (this.minDate) {
this.leftDate = this.minDate;
if (this.unlinkPanels && this.maxDate) {
const minDateYear = this.minDate.getFullYear();
const maxDateYear = this.maxDate.getFullYear();
this.rightDate = minDateYear === maxDateYear
? nextYear(this.maxDate)
: this.maxDate;
} else {
this.rightDate = nextYear(this.leftDate);
}
} else {
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextYear(this.leftDate);
}
}
},
defaultValue(val) {
if (!Array.isArray(this.value)) {
const [left, right] = calcDefaultValue(val);
this.leftDate = left;
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels
? right
: nextYear(this.leftDate);
}
}
},
methods: {
handleClear() {
this.minDate = null;
this.maxDate = null;
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextYear(this.leftDate);
this.$emit('pick', null);
},
handleChangeRange(val) {
this.minDate = val.minDate;
this.maxDate = val.maxDate;
this.rangeState = val.rangeState;
},
handleRangePick(val, close = true) {
const defaultTime = this.defaultTime || [];
const minDate = modifyWithTimeString(val.minDate, defaultTime[0]);
const maxDate = modifyWithTimeString(val.maxDate, defaultTime[1]);
if (this.maxDate === maxDate && this.minDate === minDate) {
return;
}
this.onPick && this.onPick(val);
this.maxDate = maxDate;
this.minDate = minDate;
// workaround for https://github.com/ElemeFE/element/issues/7539, should remove this block when we don't have to care about Chromium 55 - 57
setTimeout(() => {
this.maxDate = maxDate;
this.minDate = minDate;
}, 10);
if (!close) return;
this.handleConfirm();
},
handleShortcutClick(shortcut) {
if (shortcut.onClick) {
shortcut.onClick(this);
}
},
// leftPrev*, rightNext* need to take care of `unlinkPanels`
leftPrevYear() {
this.leftDate = prevYear(this.leftDate);
if (!this.unlinkPanels) {
this.rightDate = prevYear(this.rightDate);
}
},
rightNextYear() {
if (!this.unlinkPanels) {
this.leftDate = nextYear(this.leftDate);
}
this.rightDate = nextYear(this.rightDate);
},
// leftNext*, rightPrev* are called when `unlinkPanels` is true
leftNextYear() {
this.leftDate = nextYear(this.leftDate);
},
rightPrevYear() {
this.rightDate = prevYear(this.rightDate);
},
handleConfirm(visible = false) {
if (this.isValidValue([this.minDate, this.maxDate])) {
this.$emit('pick', [this.minDate, this.maxDate], visible);
}
},
isValidValue(value) {
return Array.isArray(value) &&
value && value[0] && value[1] &&
isDate(value[0]) && isDate(value[1]) &&
value[0].getTime() <= value[1].getTime() && (
typeof this.disabledDate === 'function'
? !this.disabledDate(value[0]) && !this.disabledDate(value[1])
: true
);
},
resetView() {
// NOTE: this is a hack to reset {min, max}Date on picker open.
// TODO: correct way of doing so is to refactor {min, max}Date to be dependent on value and internal selection state
// an alternative would be resetView whenever picker becomes visible, should also investigate date-panel's resetView
this.minDate = this.value && isDate(this.value[0]) ? new Date(this.value[0]) : null;
this.maxDate = this.value && isDate(this.value[0]) ? new Date(this.value[1]) : null;
}
},
components: { MonthTable, ElInput, ElButton }
};
</script>
@@ -0,0 +1,248 @@
<template>
<transition
name="el-zoom-in-top"
@after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-time-range-picker el-picker-panel el-popper"
:class="popperClass">
<div class="el-time-range-picker__content">
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.startTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content">
<time-spinner
ref="minSpinner"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@change="handleMinChange"
:arrow-control="arrowControl"
@select-range="setMinSelectionRange"
:date="minDate">
</time-spinner>
</div>
</div>
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.endTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content">
<time-spinner
ref="maxSpinner"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@change="handleMaxChange"
:arrow-control="arrowControl"
@select-range="setMaxSelectionRange"
:date="maxDate">
</time-spinner>
</div>
</div>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel()">{{ t('el.datepicker.cancel') }}</button>
<button
type="button"
class="el-time-panel__btn confirm"
@click="handleConfirm()"
:disabled="btnDisabled">{{ t('el.datepicker.confirm') }}</button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
parseDate,
limitTimeRange,
modifyDate,
clearMilliseconds,
timeWithinRange
} from 'element-ui/src/utils/date-util';
import Locale from 'element-ui/src/mixins/locale';
import TimeSpinner from '../basic/time-spinner';
const MIN_TIME = parseDate('00:00:00', 'HH:mm:ss');
const MAX_TIME = parseDate('23:59:59', 'HH:mm:ss');
const minTimeOfDay = function(date) {
return modifyDate(MIN_TIME, date.getFullYear(), date.getMonth(), date.getDate());
};
const maxTimeOfDay = function(date) {
return modifyDate(MAX_TIME, date.getFullYear(), date.getMonth(), date.getDate());
};
// increase time by amount of milliseconds, but within the range of day
const advanceTime = function(date, amount) {
return new Date(Math.min(date.getTime() + amount, maxTimeOfDay(date).getTime()));
};
export default {
mixins: [Locale],
components: { TimeSpinner },
computed: {
showSeconds() {
return (this.format || '').indexOf('ss') !== -1;
},
offset() {
return this.showSeconds ? 11 : 8;
},
spinner() {
return this.selectionRange[0] < this.offset ? this.$refs.minSpinner : this.$refs.maxSpinner;
},
btnDisabled() {
return this.minDate.getTime() > this.maxDate.getTime();
},
amPmMode() {
if ((this.format || '').indexOf('A') !== -1) return 'A';
if ((this.format || '').indexOf('a') !== -1) return 'a';
return '';
}
},
data() {
return {
popperClass: '',
minDate: new Date(),
maxDate: new Date(),
value: [],
oldValue: [new Date(), new Date()],
defaultValue: null,
format: 'HH:mm:ss',
visible: false,
selectionRange: [0, 2],
arrowControl: false
};
},
watch: {
value(value) {
if (Array.isArray(value)) {
this.minDate = new Date(value[0]);
this.maxDate = new Date(value[1]);
} else {
if (Array.isArray(this.defaultValue)) {
this.minDate = new Date(this.defaultValue[0]);
this.maxDate = new Date(this.defaultValue[1]);
} else if (this.defaultValue) {
this.minDate = new Date(this.defaultValue);
this.maxDate = advanceTime(new Date(this.defaultValue), 60 * 60 * 1000);
} else {
this.minDate = new Date();
this.maxDate = advanceTime(new Date(), 60 * 60 * 1000);
}
}
},
visible(val) {
if (val) {
this.oldValue = this.value;
this.$nextTick(() => this.$refs.minSpinner.emitSelectRange('hours'));
}
}
},
methods: {
handleClear() {
this.$emit('pick', null);
},
handleCancel() {
this.$emit('pick', this.oldValue);
},
handleMinChange(date) {
this.minDate = clearMilliseconds(date);
this.handleChange();
},
handleMaxChange(date) {
this.maxDate = clearMilliseconds(date);
this.handleChange();
},
handleChange() {
if (this.isValidValue([this.minDate, this.maxDate])) {
this.$refs.minSpinner.selectableRange = [[minTimeOfDay(this.minDate), this.maxDate]];
this.$refs.maxSpinner.selectableRange = [[this.minDate, maxTimeOfDay(this.maxDate)]];
this.$emit('pick', [this.minDate, this.maxDate], true);
}
},
setMinSelectionRange(start, end) {
this.$emit('select-range', start, end, 'min');
this.selectionRange = [start, end];
},
setMaxSelectionRange(start, end) {
this.$emit('select-range', start, end, 'max');
this.selectionRange = [start + this.offset, end + this.offset];
},
handleConfirm(visible = false) {
const minSelectableRange = this.$refs.minSpinner.selectableRange;
const maxSelectableRange = this.$refs.maxSpinner.selectableRange;
this.minDate = limitTimeRange(this.minDate, minSelectableRange, this.format);
this.maxDate = limitTimeRange(this.maxDate, maxSelectableRange, this.format);
this.$emit('pick', [this.minDate, this.maxDate], visible);
},
adjustSpinners() {
this.$refs.minSpinner.adjustSpinners();
this.$refs.maxSpinner.adjustSpinners();
},
changeSelectionRange(step) {
const list = this.showSeconds ? [0, 3, 6, 11, 14, 17] : [0, 3, 8, 11];
const mapping = ['hours', 'minutes'].concat(this.showSeconds ? ['seconds'] : []);
const index = list.indexOf(this.selectionRange[0]);
const next = (index + step + list.length) % list.length;
const half = list.length / 2;
if (next < half) {
this.$refs.minSpinner.emitSelectRange(mapping[next]);
} else {
this.$refs.maxSpinner.emitSelectRange(mapping[next - half]);
}
},
isValidValue(date) {
return Array.isArray(date) &&
timeWithinRange(this.minDate, this.$refs.minSpinner.selectableRange) &&
timeWithinRange(this.maxDate, this.$refs.maxSpinner.selectableRange);
},
handleKeydown(event) {
const keyCode = event.keyCode;
const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
// Left or Right
if (keyCode === 37 || keyCode === 39) {
const step = mapping[keyCode];
this.changeSelectionRange(step);
event.preventDefault();
return;
}
// Up or Down
if (keyCode === 38 || keyCode === 40) {
const step = mapping[keyCode];
this.spinner.scrollDown(step);
event.preventDefault();
return;
}
}
}
};
</script>
@@ -0,0 +1,178 @@
<template>
<transition name="el-zoom-in-top" @before-enter="handleMenuEnter" @after-leave="$emit('dodestroy')">
<div
ref="popper"
v-show="visible"
:style="{ width: width + 'px' }"
:class="popperClass"
class="el-picker-panel time-select el-popper">
<el-scrollbar noresize wrap-class="el-picker-panel__content">
<div class="time-select-item"
v-for="item in items"
:class="{ selected: value === item.value, disabled: item.disabled, default: item.value === defaultValue }"
:disabled="item.disabled"
:key="item.value"
@click="handleClick(item)">{{ item.value }}</div>
</el-scrollbar>
</div>
</transition>
</template>
<script type="text/babel">
import ElScrollbar from 'element-ui/packages/scrollbar';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
const parseTime = function(time) {
const values = (time || '').split(':');
if (values.length >= 2) {
const hours = parseInt(values[0], 10);
const minutes = parseInt(values[1], 10);
return {
hours,
minutes
};
}
/* istanbul ignore next */
return null;
};
const compareTime = function(time1, time2) {
const value1 = parseTime(time1);
const value2 = parseTime(time2);
const minutes1 = value1.minutes + value1.hours * 60;
const minutes2 = value2.minutes + value2.hours * 60;
if (minutes1 === minutes2) {
return 0;
}
return minutes1 > minutes2 ? 1 : -1;
};
const formatTime = function(time) {
return (time.hours < 10 ? '0' + time.hours : time.hours) + ':' + (time.minutes < 10 ? '0' + time.minutes : time.minutes);
};
const nextTime = function(time, step) {
const timeValue = parseTime(time);
const stepValue = parseTime(step);
const next = {
hours: timeValue.hours,
minutes: timeValue.minutes
};
next.minutes += stepValue.minutes;
next.hours += stepValue.hours;
next.hours += Math.floor(next.minutes / 60);
next.minutes = next.minutes % 60;
return formatTime(next);
};
export default {
components: { ElScrollbar },
watch: {
value(val) {
if (!val) return;
this.$nextTick(() => this.scrollToOption());
}
},
methods: {
handleClick(item) {
if (!item.disabled) {
this.$emit('pick', item.value);
}
},
handleClear() {
this.$emit('pick', null);
},
scrollToOption(selector = '.selected') {
const menu = this.$refs.popper.querySelector('.el-picker-panel__content');
scrollIntoView(menu, menu.querySelector(selector));
},
handleMenuEnter() {
const selected = this.items.map(item => item.value).indexOf(this.value) !== -1;
const hasDefault = this.items.map(item => item.value).indexOf(this.defaultValue) !== -1;
const option = (selected && '.selected') || (hasDefault && '.default') || '.time-select-item:not(.disabled)';
this.$nextTick(() => this.scrollToOption(option));
},
scrollDown(step) {
const items = this.items;
const length = items.length;
let total = items.length;
let index = items.map(item => item.value).indexOf(this.value);
while (total--) {
index = (index + step + length) % length;
if (!items[index].disabled) {
this.$emit('pick', items[index].value, true);
return;
}
}
},
isValidValue(date) {
return this.items.filter(item => !item.disabled).map(item => item.value).indexOf(date) !== -1;
},
handleKeydown(event) {
const keyCode = event.keyCode;
if (keyCode === 38 || keyCode === 40) {
const mapping = { 40: 1, 38: -1 };
const offset = mapping[keyCode.toString()];
this.scrollDown(offset);
event.stopPropagation();
return;
}
}
},
data() {
return {
popperClass: '',
start: '09:00',
end: '18:00',
step: '00:30',
value: '',
defaultValue: '',
visible: false,
minTime: '',
maxTime: '',
width: 0
};
},
computed: {
items() {
const start = this.start;
const end = this.end;
const step = this.step;
const result = [];
if (start && end && step) {
let current = start;
while (compareTime(current, end) <= 0) {
result.push({
value: current,
disabled: compareTime(current, this.minTime || '-1:-1') <= 0 ||
compareTime(current, this.maxTime || '100:100') >= 0
});
current = nextTime(current, step);
}
}
return result;
}
}
};
</script>
@@ -0,0 +1,186 @@
<template>
<transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-time-panel el-popper"
:class="popperClass">
<div class="el-time-panel__content" :class="{ 'has-seconds': showSeconds }">
<time-spinner
ref="spinner"
@change="handleChange"
:arrow-control="useArrow"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@select-range="setSelectionRange"
:date="date">
</time-spinner>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel">{{ t('el.datepicker.cancel') }}</button>
<button
type="button"
class="el-time-panel__btn"
:class="{confirm: !disabled}"
@click="handleConfirm()">{{ t('el.datepicker.confirm') }}</button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import { limitTimeRange, isDate, clearMilliseconds, timeWithinRange } from 'element-ui/src/utils/date-util';
import Locale from 'element-ui/src/mixins/locale';
import TimeSpinner from '../basic/time-spinner';
export default {
mixins: [Locale],
components: {
TimeSpinner
},
props: {
visible: Boolean,
timeArrowControl: Boolean
},
watch: {
visible(val) {
if (val) {
this.oldValue = this.value;
this.$nextTick(() => this.$refs.spinner.emitSelectRange('hours'));
} else {
this.needInitAdjust = true;
}
},
value(newVal) {
let date;
if (newVal instanceof Date) {
date = limitTimeRange(newVal, this.selectableRange, this.format);
} else if (!newVal) {
date = this.defaultValue ? new Date(this.defaultValue) : new Date();
}
this.date = date;
if (this.visible && this.needInitAdjust) {
this.$nextTick(_ => this.adjustSpinners());
this.needInitAdjust = false;
}
},
selectableRange(val) {
this.$refs.spinner.selectableRange = val;
},
defaultValue(val) {
if (!isDate(this.value)) {
this.date = val ? new Date(val) : new Date();
}
}
},
data() {
return {
popperClass: '',
format: 'HH:mm:ss',
value: '',
defaultValue: null,
date: new Date(),
oldValue: new Date(),
selectableRange: [],
selectionRange: [0, 2],
disabled: false,
arrowControl: false,
needInitAdjust: true
};
},
computed: {
showSeconds() {
return (this.format || '').indexOf('ss') !== -1;
},
useArrow() {
return this.arrowControl || this.timeArrowControl || false;
},
amPmMode() {
if ((this.format || '').indexOf('A') !== -1) return 'A';
if ((this.format || '').indexOf('a') !== -1) return 'a';
return '';
}
},
methods: {
handleCancel() {
this.$emit('pick', this.oldValue, false);
},
handleChange(date) {
// this.visible avoids edge cases, when use scrolls during panel closing animation
if (this.visible) {
this.date = clearMilliseconds(date);
// if date is out of range, do not emit
if (this.isValidValue(this.date)) {
this.$emit('pick', this.date, true);
}
}
},
setSelectionRange(start, end) {
this.$emit('select-range', start, end);
this.selectionRange = [start, end];
},
handleConfirm(visible = false, first) {
if (first) return;
const date = clearMilliseconds(limitTimeRange(this.date, this.selectableRange, this.format));
this.$emit('pick', date, visible, first);
},
handleKeydown(event) {
const keyCode = event.keyCode;
const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
// Left or Right
if (keyCode === 37 || keyCode === 39) {
const step = mapping[keyCode];
this.changeSelectionRange(step);
event.preventDefault();
return;
}
// Up or Down
if (keyCode === 38 || keyCode === 40) {
const step = mapping[keyCode];
this.$refs.spinner.scrollDown(step);
event.preventDefault();
return;
}
},
isValidValue(date) {
return timeWithinRange(date, this.selectableRange, this.format);
},
adjustSpinners() {
return this.$refs.spinner.adjustSpinners();
},
changeSelectionRange(step) {
const list = [0, 3].concat(this.showSeconds ? [6] : []);
const mapping = ['hours', 'minutes'].concat(this.showSeconds ? ['seconds'] : []);
const index = list.indexOf(this.selectionRange[0]);
const next = (index + step + list.length) % list.length;
this.$refs.spinner.emitSelectRange(mapping[next]);
}
},
mounted() {
this.$nextTick(() => this.handleConfirm(true, true));
this.$emit('mounted');
}
};
</script>