2
0
mirror of https://github.com/tenrok/vue2-datepicker.git synced 2026-06-09 14:32:27 +03:00
Files
vue2-datepicker/src/date-picker.vue
T
2020-04-22 13:50:29 +08:00

474 lines
13 KiB
Vue

<template>
<div
:class="{
[`${prefixClass}-datepicker`]: true,
[`${prefixClass}-datepicker-range`]: range,
[`${prefixClass}-datepicker-inline`]: inline,
disabled: disabled,
}"
>
<div
v-if="!inline"
:class="`${prefixClass}-input-wrapper`"
@mousedown="openPopup"
@touchstart="openPopup"
>
<slot name="input">
<input
ref="input"
v-bind="{ name: 'date', type: 'text', autocomplete: 'off', value: text, ...inputAttr }"
:class="inputClass"
:disabled="disabled"
:readonly="!editable"
:placeholder="placeholder"
@keydown="handleInputKeydown"
@focus="handleInputFocus"
@blur="handleInputBlur"
@input="handleInputInput"
@change="handleInputChange"
/>
</slot>
<i v-if="showClearIcon" :class="`${prefixClass}-icon-clear`" @mousedown.stop="handleClear">
<slot name="icon-clear">
<icon-close></icon-close>
</slot>
</i>
<i :class="`${prefixClass}-icon-calendar`">
<slot name="icon-calendar">
<icon-calendar></icon-calendar>
</slot>
</i>
</div>
<Popup
ref="popup"
:class="popupClass"
:style="popupStyle"
:inline="inline"
:visible="popupVisible"
:append-to-body="appendToBody"
@clickoutside="handleClickOutSide"
>
<div
v-if="hasSlot('sidebar') || shortcuts.length"
:class="`${prefixClass}-datepicker-sidebar`"
>
<slot name="sidebar" :value="currentValue" :emit="emitValue"></slot>
<button
v-for="(v, i) in shortcuts"
:key="i"
type="button"
:class="`${prefixClass}-btn ${prefixClass}-btn-text ${prefixClass}-btn-shortcut`"
@click="handleSelectShortcut(v)"
>
{{ v.text }}
</button>
</div>
<div :class="`${prefixClass}-datepicker-content`">
<div v-if="hasSlot('header')" :class="`${prefixClass}-datepicker-header`">
<slot name="header" :value="currentValue" :emit="emitValue"></slot>
</div>
<div :class="`${prefixClass}-datepicker-body`">
<slot name="content" :value="currentValue" :emit="emitValue">
<component
:is="currentComponent"
ref="picker"
v-bind="currentComponentProps"
@select="handleSelectDate"
></component>
</slot>
</div>
<div v-if="hasSlot('footer') || confirm" :class="`${prefixClass}-datepicker-footer`">
<slot name="footer" :value="currentValue" :emit="emitValue"></slot>
<button
v-if="confirm"
type="button"
:class="`${prefixClass}-btn ${prefixClass}-datepicker-btn-confirm`"
@click="handleConfirmDate"
>
{{ confirmText }}
</button>
</div>
</div>
</Popup>
</div>
</template>
<script>
import { parse, format, getWeek } from 'date-format-parse';
import { isValidDate, isValidRangeDate } from './util/date';
import { pick, isObject, mergeDeep } from './util/base';
import { getLocale, getLocaleFieldValue } from './locale';
import Popup from './popup';
import IconCalendar from './icon/icon-calendar';
import IconClose from './icon/icon-close';
import CalendarPanel from './calendar/calendar-panel';
import CalendarRange from './calendar/calendar-range';
import TimePanel from './time/time-panel';
import TimeRange from './time/time-range';
import DatetimePanel from './datetime/datetime-panel';
import DatetimeRange from './datetime/datetime-range';
const componentMap = {
default: CalendarPanel,
time: TimePanel,
datetime: DatetimePanel,
};
const componentRangeMap = {
default: CalendarRange,
time: TimeRange,
datetime: DatetimeRange,
};
export default {
name: 'DatePicker',
components: {
IconCalendar,
IconClose,
Popup,
},
provide() {
return {
t: this.getLocaleFieldValue,
getWeek: this.getWeek,
prefixClass: this.prefixClass,
};
},
props: {
...DatetimePanel.props,
value: {},
valueType: {
type: String,
default: 'date', // date, format, timestamp, or token like 'YYYY-MM-DD'
},
type: {
type: String, // ['date', 'datetime', 'time', 'year', 'month', 'week']
default: 'date',
},
format: {
type: [String, Object],
default() {
const map = {
date: 'YYYY-MM-DD',
datetime: 'YYYY-MM-DD HH:mm:ss',
year: 'YYYY',
month: 'YYYY-MM',
time: 'HH:mm:ss',
week: 'w',
};
return map[this.type] || map.date;
},
},
range: {
type: Boolean,
default: false,
},
rangeSeparator: {
type: String,
default: ' ~ ',
},
lang: {
type: [String, Object],
},
placeholder: {
type: String,
default: '',
},
editable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: true,
},
prefixClass: {
type: String,
default: 'mx',
},
inputClass: {
default() {
return `${this.prefixClass}-input`;
},
},
inputAttr: {
type: Object,
default() {
return {};
},
},
appendToBody: {
type: Boolean,
default: true,
},
open: {
type: Boolean,
default: undefined,
},
popupClass: {},
popupStyle: {
type: Object,
default: () => {
return {};
},
},
inline: {
type: Boolean,
default: false,
},
confirm: {
type: Boolean,
default: false,
},
confirmText: {
type: String,
default: 'OK',
},
shortcuts: {
type: Array,
validator(value) {
return (
Array.isArray(value) &&
value.every(
v => isObject(v) && typeof v.text === 'string' && typeof v.onClick === 'function'
)
);
},
default() {
return [];
},
},
},
data() {
return {
// cache the innervalue, wait to confirm
currentValue: null,
userInput: null,
defaultOpen: false,
};
},
computed: {
currentComponent() {
const map = this.range ? componentRangeMap : componentMap;
return map[this.type] || map.default;
},
currentComponentProps() {
const props = {
...pick(this, Object.keys(this.currentComponent.props)),
value: this.currentValue,
};
return props;
},
popupVisible() {
return !this.disabled && (typeof this.open === 'boolean' ? this.open : this.defaultOpen);
},
innerValue() {
let { value } = this;
if (this.range) {
value = Array.isArray(value) ? value.slice(0, 2) : [null, null];
return value.map(this.value2date);
}
return this.value2date(this.value);
},
text() {
if (this.userInput !== null) {
return this.userInput;
}
if (!this.isValidValue(this.innerValue)) {
return '';
}
const fmt = this.format;
if (Array.isArray(this.innerValue)) {
return this.innerValue.map(v => this.formatDate(v, fmt)).join(this.rangeSeparator);
}
return this.formatDate(this.innerValue, fmt);
},
showClearIcon() {
return !this.disabled && this.clearable && this.text;
},
locale() {
if (isObject(this.lang)) {
return mergeDeep(getLocale(), this.lang);
}
return getLocale(this.lang);
},
},
watch: {
innerValue: {
immediate: true,
handler(val) {
this.currentValue = val;
},
},
},
methods: {
handleClickOutSide(evt) {
const { target } = evt;
if (!this.$el.contains(target)) {
this.closePopup();
}
},
getWeek(date, options) {
if (isObject(this.format) && typeof this.format.getWeek === 'function') {
return this.format.getWeek(date, options);
}
return getWeek(date, options);
},
parseDate(value, fmt) {
if (isObject(this.format) && typeof this.format.parse === 'function') {
return this.format.parse(value, fmt);
}
const backupDate = new Date();
return parse(value, fmt, { locale: this.locale.formatLocale, backupDate });
},
formatDate(date, fmt) {
if (isObject(this.format) && typeof this.format.stringify === 'function') {
return this.format.stringify(date, fmt);
}
return format(date, fmt, { locale: this.locale.formatLocale });
},
// transform the outer value to inner date
value2date(value) {
switch (this.valueType) {
case 'date':
return value instanceof Date ? new Date(value.getTime()) : new Date(NaN);
case 'timestamp':
return typeof value === 'number' ? new Date(value) : new Date(NaN);
case 'format':
return typeof value === 'string' ? this.parseDate(value, this.format) : new Date(NaN);
default:
return typeof value === 'string' ? this.parseDate(value, this.valueType) : new Date(NaN);
}
},
// transform the inner date to outer value
date2value(date) {
if (!isValidDate(date)) return null;
switch (this.valueType) {
case 'date':
return date;
case 'timestamp':
return date.getTime();
case 'format':
return this.formatDate(date, this.format);
default:
return this.formatDate(date, this.valueType);
}
},
emitValue(date, type) {
// fix IE11/10 trigger input event when input is focused. (placeholder !== '')
this.userInput = null;
const value = Array.isArray(date) ? date.map(this.date2value) : this.date2value(date);
this.$emit('input', value);
this.$emit('change', value, type);
this.afterEmitValue(type);
return value;
},
afterEmitValue(type) {
// this.type === 'datetime', click the time should close popup
if (!type || type === this.type || type === 'time') {
this.closePopup();
}
},
isValidValue(value) {
const validate = this.range ? isValidRangeDate : isValidDate;
return validate(value);
},
handleSelectDate(val, type) {
if (this.confirm) {
this.currentValue = val;
} else {
this.emitValue(val, type);
}
},
handleClear() {
this.emitValue(this.range ? [null, null] : null);
this.$emit('clear');
},
handleConfirmDate() {
const value = this.emitValue(this.currentValue);
this.$emit('confirm', value);
},
handleSelectShortcut(item) {
if (isObject(item) && typeof item.onClick === 'function') {
const date = item.onClick(this);
if (date) {
this.emitValue(date);
}
}
},
openPopup(evt) {
if (this.popupVisible) return;
this.defaultOpen = true;
this.$emit('open', evt);
this.$emit('update:open', true);
},
closePopup() {
if (!this.popupVisible) return;
this.defaultOpen = false;
this.$emit('close');
this.$emit('update:open', false);
},
blur() {
this.$refs.input.blur();
},
focus() {
this.$refs.input.focus();
},
handleInputChange() {
if (!this.editable || this.userInput === null) return;
const text = this.userInput.trim();
this.userInput = null;
if (text === '') {
this.handleClear();
return;
}
let date;
if (this.range) {
let arr = text.split(this.rangeSeparator);
if (arr.length !== 2) {
arr = text.split(this.rangeSeparator.trim());
}
date = arr.map(v => this.parseDate(v.trim(), this.format));
} else {
date = this.parseDate(text, this.format);
}
if (this.isValidValue(date)) {
this.emitValue(date);
this.blur();
} else {
this.$emit('input-error', text);
}
},
handleInputInput(evt) {
this.userInput = evt.target.value;
},
handleInputKeydown(evt) {
const { keyCode } = evt;
// Tab 9 or Enter 13
if (keyCode === 9) {
this.closePopup();
} else if (keyCode === 13) {
this.handleInputChange();
}
},
handleInputBlur(evt) {
// tab close
this.$emit('blur', evt);
},
handleInputFocus(evt) {
this.openPopup(evt);
this.$emit('focus', evt);
},
hasSlot(name) {
return !!(this.$slots[name] || this.$scopedSlots[name]);
},
getLocaleFieldValue(path) {
return getLocaleFieldValue(path, this.locale);
},
},
};
</script>