407 lines
16 KiB
Vue
407 lines
16 KiB
Vue
<template>
|
||
<div :class="[prefixCls]" v-clickoutside="handleClose">
|
||
<div ref="reference" :class="[prefixCls + '-rel']">
|
||
<slot>
|
||
<i-input
|
||
:key="forceInputRerender"
|
||
:element-id="elementId"
|
||
:class="[prefixCls + '-editor']"
|
||
:readonly="!editable || readonly"
|
||
:disabled="disabled"
|
||
:size="size"
|
||
:placeholder="placeholder"
|
||
:value="visualValue"
|
||
:name="name"
|
||
@on-input-change="handleInputChange"
|
||
@on-focus="handleFocus"
|
||
@on-blur="handleBlur"
|
||
@on-click="handleIconClick"
|
||
@mouseenter.native="handleInputMouseenter"
|
||
@mouseleave.native="handleInputMouseleave"
|
||
|
||
:icon="iconType"
|
||
></i-input>
|
||
</slot>
|
||
</div>
|
||
<transition :name="transition">
|
||
<Drop
|
||
@click.native="handleTransferClick"
|
||
v-show="opened"
|
||
:class="{ [prefixCls + '-transfer']: transfer }"
|
||
:placement="placement"
|
||
ref="drop"
|
||
:data-transfer="transfer"
|
||
v-transfer-dom>
|
||
<div>
|
||
<component
|
||
:is="panel"
|
||
ref="pickerPanel"
|
||
:visible="visible"
|
||
:showTime="type === 'datetime' || type === 'datetimerange'"
|
||
:confirm="isConfirm"
|
||
:selectionMode="selectionMode"
|
||
:steps="steps"
|
||
:format="format"
|
||
:value="internalValue"
|
||
:start-date="startDate"
|
||
:split-panels="splitPanels"
|
||
:show-week-numbers="showWeekNumbers"
|
||
:picker-type="type"
|
||
|
||
v-bind="ownPickerProps"
|
||
|
||
@on-pick="onPick"
|
||
@on-pick-clear="handleClear"
|
||
@on-pick-success="onPickSuccess"
|
||
@on-pick-click="disableClickOutSide = true"
|
||
@on-selection-mode-change="onSelectionModeChange"
|
||
></component>
|
||
</div>
|
||
</Drop>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
<script>
|
||
|
||
|
||
import iInput from '../../components/input/input.vue';
|
||
import Drop from '../../components/select/dropdown.vue';
|
||
import clickoutside from '../../directives/clickoutside';
|
||
import TransferDom from '../../directives/transfer-dom';
|
||
import { oneOf } from '../../utils/assist';
|
||
import { DEFAULT_FORMATS, TYPE_VALUE_RESOLVER_MAP } from './util';
|
||
import Emitter from '../../mixins/emitter';
|
||
|
||
const prefixCls = 'ivu-date-picker';
|
||
|
||
const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
|
||
|
||
export default {
|
||
name: 'CalendarPicker',
|
||
mixins: [ Emitter ],
|
||
components: { iInput, Drop },
|
||
directives: { clickoutside, TransferDom },
|
||
props: {
|
||
format: {
|
||
type: String
|
||
},
|
||
readonly: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
editable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
clearable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
confirm: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
open: {
|
||
type: Boolean,
|
||
default: null
|
||
},
|
||
multiple: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
splitPanels: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
showWeekNumbers: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
startDate: {
|
||
type: Date
|
||
},
|
||
size: {
|
||
validator (value) {
|
||
return oneOf(value, ['small', 'large', 'default']);
|
||
}
|
||
},
|
||
placeholder: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
placement: {
|
||
validator (value) {
|
||
return oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
|
||
},
|
||
default: 'bottom-start'
|
||
},
|
||
transfer: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
name: {
|
||
type: String
|
||
},
|
||
elementId: {
|
||
type: String
|
||
},
|
||
steps: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
value: {
|
||
type: [Date, String, Array]
|
||
},
|
||
options: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
},
|
||
data(){
|
||
const isRange = this.type.includes('range');
|
||
const emptyArray = isRange ? [null, null] : [null];
|
||
const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
|
||
|
||
return {
|
||
prefixCls: prefixCls,
|
||
showClose: false,
|
||
visible: false,
|
||
internalValue: initialValue,
|
||
disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
|
||
disableCloseUnderTransfer: false, // transfer 模式下,点击Drop也会触发关闭,
|
||
selectionMode: this.onSelectionModeChange(this.type),
|
||
forceInputRerender: 1
|
||
};
|
||
},
|
||
computed: {
|
||
publicValue(){
|
||
if (this.multiple){
|
||
return this.internalValue.slice();
|
||
} else {
|
||
const isRange = this.type.includes('range');
|
||
let val = this.internalValue.map(date => date instanceof Date ? new Date(date) : (date || ''));
|
||
|
||
if (this.type.match(/^time/)) val = val.map(this.formatDate);
|
||
return (isRange || this.multiple) ? val : val[0];
|
||
}
|
||
},
|
||
opened () {
|
||
return this.open === null ? this.visible : this.open;
|
||
},
|
||
iconType () {
|
||
let icon = 'ios-calendar-outline';
|
||
if (this.type === 'time' || this.type === 'timerange') icon = 'ios-clock-outline';
|
||
if (this.showClose) icon = 'ios-close';
|
||
return icon;
|
||
},
|
||
transition () {
|
||
const bottomPlaced = this.placement.match(/^bottom/);
|
||
return bottomPlaced ? 'slide-up' : 'slide-down';
|
||
},
|
||
visualValue() {
|
||
return this.formatDate(this.internalValue);
|
||
},
|
||
isConfirm(){
|
||
return this.confirm || this.type === 'datetime' || this.type === 'datetimerange' || this.multiple;
|
||
}
|
||
},
|
||
methods: {
|
||
onSelectionModeChange(type){
|
||
if (type.match(/^date/)) type = 'date';
|
||
this.selectionMode = oneOf(type, ['year', 'month', 'date', 'time']) && type;
|
||
return this.selectionMode;
|
||
},
|
||
// 开启 transfer 时,点击 Drop 即会关闭,这里不让其关闭
|
||
handleTransferClick () {
|
||
if (this.transfer) this.disableCloseUnderTransfer = true;
|
||
},
|
||
handleClose () {
|
||
if (this.disableCloseUnderTransfer) {
|
||
this.disableCloseUnderTransfer = false;
|
||
return false;
|
||
}
|
||
if (this.open !== null) return;
|
||
|
||
this.visible = false;
|
||
this.disableClickOutSide = false;
|
||
},
|
||
handleFocus () {
|
||
if (this.readonly) return;
|
||
this.visible = true;
|
||
this.$refs.pickerPanel.onToggleVisibility(true);
|
||
},
|
||
handleBlur () {
|
||
this.visible = false;
|
||
this.onSelectionModeChange(this.type);
|
||
this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
|
||
this.reset();
|
||
this.$refs.pickerPanel.onToggleVisibility(false);
|
||
|
||
},
|
||
reset(){
|
||
this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
|
||
},
|
||
handleInputChange (event) {
|
||
const isArrayValue = this.type.includes('range') || this.multiple;
|
||
const oldValue = this.visualValue;
|
||
const newValue = event.target.value;
|
||
const newDate = this.parseDate(newValue);
|
||
const disabledDateFn =
|
||
this.options &&
|
||
typeof this.options.disabledDate === 'function' &&
|
||
this.options.disabledDate;
|
||
const valueToTest = isArrayValue ? newDate : newDate[0];
|
||
const isDisabled = disabledDateFn && disabledDateFn(valueToTest);
|
||
const isValidDate = newDate.reduce((valid, date) => valid && date instanceof Date, true);
|
||
|
||
if (newValue !== oldValue && !isDisabled && isValidDate) {
|
||
this.emitChange();
|
||
this.internalValue = newDate;
|
||
} else {
|
||
this.forceInputRerender++;
|
||
}
|
||
},
|
||
handleInputMouseenter () {
|
||
if (this.readonly || this.disabled) return;
|
||
if (this.visualValue && this.clearable) {
|
||
this.showClose = true;
|
||
}
|
||
},
|
||
handleInputMouseleave () {
|
||
this.showClose = false;
|
||
},
|
||
handleIconClick () {
|
||
if (this.showClose) {
|
||
this.handleClear();
|
||
} else if (!this.disabled) {
|
||
this.handleFocus();
|
||
}
|
||
},
|
||
handleClear () {
|
||
this.visible = false;
|
||
this.internalValue = this.internalValue.map(() => null);
|
||
this.$emit('on-clear');
|
||
this.dispatch('FormItem', 'on-form-change', '');
|
||
this.emitChange();
|
||
this.reset();
|
||
|
||
setTimeout(
|
||
() => this.onSelectionModeChange(this.type),
|
||
500 // delay to improve dropdown close visual effect
|
||
);
|
||
},
|
||
emitChange () {
|
||
this.$nextTick(() => {
|
||
this.$emit('on-change', this.publicValue);
|
||
this.dispatch('FormItem', 'on-form-change', this.publicValue);
|
||
});
|
||
},
|
||
parseDate(val) {
|
||
const isRange = this.type.includes('range');
|
||
const type = this.type;
|
||
const parser = (
|
||
TYPE_VALUE_RESOLVER_MAP[type] ||
|
||
TYPE_VALUE_RESOLVER_MAP['default']
|
||
).parser;
|
||
const format = this.format || DEFAULT_FORMATS[type];
|
||
const multipleParser = TYPE_VALUE_RESOLVER_MAP['multiple'].parser;
|
||
|
||
if (val && type === 'time' && !(val instanceof Date)) {
|
||
val = parser(val, format);
|
||
} else if (this.multiple && val) {
|
||
val = multipleParser(val, format);
|
||
} else if (isRange) {
|
||
if (!val){
|
||
val = [null, null];
|
||
} else {
|
||
if (typeof val === 'string') {
|
||
val = parser(val, format);
|
||
} else if (type === 'timerange') {
|
||
val = parser(val, format);
|
||
} else {
|
||
val = val.map(date => new Date(date)); // try to parse
|
||
val = val.map(date => isNaN(date.getTime()) ? null : date); // check if parse passed
|
||
}
|
||
}
|
||
} else if (typeof val === 'string' && type.indexOf('time') !== 0){
|
||
val = parser(val, format) || null;
|
||
}
|
||
|
||
return (isRange || this.multiple) ? (val || []) : [val];
|
||
},
|
||
formatDate(value){
|
||
const format = DEFAULT_FORMATS[this.type];
|
||
|
||
if (this.multiple) {
|
||
const formatter = TYPE_VALUE_RESOLVER_MAP.multiple.formatter;
|
||
return formatter(value, this.format || format);
|
||
} else {
|
||
const {formatter} = (
|
||
TYPE_VALUE_RESOLVER_MAP[this.type] ||
|
||
TYPE_VALUE_RESOLVER_MAP['default']
|
||
);
|
||
return formatter(value, this.format || format);
|
||
}
|
||
},
|
||
onPick(dates, visible = false) {
|
||
if (this.multiple){
|
||
const pickedTimeStamp = dates.getTime();
|
||
const indexOfPickedDate = this.internalValue.findIndex(date => date && date.getTime() === pickedTimeStamp);
|
||
const allDates = [...this.internalValue, dates].filter(Boolean);
|
||
const timeStamps = allDates.map(date => date.getTime()).filter((ts, i, arr) => arr.indexOf(ts) === i && i !== indexOfPickedDate); // filter away duplicates
|
||
this.internalValue = timeStamps.map(ts => new Date(ts));
|
||
} else {
|
||
this.internalValue = Array.isArray(dates) ? dates : [dates];
|
||
}
|
||
|
||
if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
|
||
if (!this.isConfirm) this.visible = visible;
|
||
this.emitChange();
|
||
},
|
||
onPickSuccess(){
|
||
this.visible = false;
|
||
this.$emit('on-ok');
|
||
this.reset();
|
||
},
|
||
},
|
||
watch: {
|
||
visible (state) {
|
||
if (state === false){
|
||
this.$refs.drop.destroy();
|
||
const input = this.$el.querySelector('input');
|
||
if (input) input.blur();
|
||
}
|
||
this.$refs.drop.update();
|
||
this.$emit('on-open-change', state);
|
||
},
|
||
value(val) {
|
||
this.internalValue = this.parseDate(val);
|
||
|
||
},
|
||
open (val) {
|
||
this.visible = val === true;
|
||
},
|
||
type(type){
|
||
this.onSelectionModeChange(type);
|
||
},
|
||
publicValue(now, before){
|
||
const newValue = JSON.stringify(now);
|
||
const oldValue = JSON.stringify(before);
|
||
const shouldEmitInput = newValue !== oldValue || typeof now !== typeof before;
|
||
if (shouldEmitInput) this.$emit('input', now); // to update v-model
|
||
},
|
||
},
|
||
mounted () {
|
||
const initialValue = this.value;
|
||
const parsedValue = this.publicValue;
|
||
if (typeof initialValue !== typeof parsedValue || JSON.stringify(initialValue) !== JSON.stringify(parsedValue)){
|
||
this.$emit('input', this.publicValue); // to update v-model
|
||
}
|
||
if (this.open !== null) this.visible = this.open;
|
||
}
|
||
};
|
||
</script>
|