vant/src/calendar/index.js

446 lines
9.9 KiB
JavaScript

// Utils
import { isDate } from '../utils/validate/date';
import { getScrollTop } from '../utils/dom/scroll';
import {
t,
bem,
copyDates,
getNextDay,
compareDay,
compareMonth,
createComponent,
calcDateNum,
ROW_HEIGHT,
} from './utils';
// Components
import Popup from '../popup';
import Button from '../button';
import Toast from '../toast';
import Month from './components/Month';
import Header from './components/Header';
export default createComponent({
props: {
title: String,
color: String,
value: Boolean,
formatter: Function,
confirmText: String,
rangePrompt: String,
defaultDate: [Date, Array],
getContainer: [String, Function],
allowSameDay: Boolean,
closeOnPopstate: Boolean,
confirmDisabledText: String,
type: {
type: String,
default: 'single',
},
minDate: {
type: Date,
validator: isDate,
default: () => new Date(),
},
maxDate: {
type: Date,
validator: isDate,
default() {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 6, now.getDate());
},
},
position: {
type: String,
default: 'bottom',
},
rowHeight: {
type: [Number, String],
default: ROW_HEIGHT,
},
round: {
type: Boolean,
default: true,
},
poppable: {
type: Boolean,
default: true,
},
showMark: {
type: Boolean,
default: true,
},
showTitle: {
type: Boolean,
default: true,
},
showConfirm: {
type: Boolean,
default: true,
},
showSubtitle: {
type: Boolean,
default: true,
},
safeAreaInsetBottom: {
type: Boolean,
default: true,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
maxRange: {
type: [Number, String],
default: null,
},
},
data() {
return {
subtitle: '',
currentDate: this.getInitialDate(),
};
},
computed: {
months() {
const months = [];
const cursor = new Date(this.minDate);
cursor.setDate(1);
do {
months.push(new Date(cursor));
cursor.setMonth(cursor.getMonth() + 1);
} while (compareMonth(cursor, this.maxDate) !== 1);
return months;
},
buttonDisabled() {
const { type, currentDate } = this;
if (type === 'range') {
return !currentDate[0] || !currentDate[1];
}
if (type === 'multiple') {
return !currentDate.length;
}
return !currentDate;
},
},
watch: {
type: 'reset',
value(val) {
if (val) {
this.initRect();
this.scrollIntoView();
}
},
defaultDate(val) {
this.currentDate = val;
this.scrollIntoView();
},
},
mounted() {
if (this.value || !this.poppable) {
this.initRect();
this.scrollIntoView();
}
},
methods: {
// @exposed-api
reset() {
this.currentDate = this.getInitialDate();
this.scrollIntoView();
},
initRect() {
this.$nextTick(() => {
// add Math.floor to avoid decimal height issues
// https://github.com/youzan/vant/issues/5640
this.bodyHeight = Math.floor(
this.$refs.body.getBoundingClientRect().height
);
this.onScroll();
});
},
// scroll to current month
scrollIntoView() {
this.$nextTick(() => {
const { currentDate } = this;
const targetDate =
this.type === 'single' ? currentDate : currentDate[0];
const displayed = this.value || !this.poppable;
/* istanbul ignore if */
if (!targetDate || !displayed) {
return;
}
this.months.some((month, index) => {
if (compareMonth(month, targetDate) === 0) {
this.$refs.months[index].scrollIntoView();
return true;
}
return false;
});
});
},
getInitialDate() {
const { type, defaultDate, minDate } = this;
if (type === 'range') {
const [startDay, endDay] = defaultDate || [];
return [startDay || minDate, endDay || getNextDay(minDate)];
}
if (type === 'multiple') {
return [defaultDate || minDate];
}
return defaultDate || minDate;
},
// calculate the position of the elements
// and find the elements that needs to be rendered
onScroll() {
const { body, months } = this.$refs;
const top = getScrollTop(body);
const bottom = top + this.bodyHeight;
const heights = months.map(item => item.height);
const heightSum = heights.reduce((a, b) => a + b, 0);
// iOS scroll bounce may exceed the range
/* istanbul ignore next */
if (top < 0 || (bottom > heightSum && top > 0)) {
return;
}
let height = 0;
let currentMonth;
for (let i = 0; i < months.length; i++) {
const visible = height <= bottom && height + heights[i] >= top;
if (visible && !currentMonth) {
currentMonth = months[i];
}
months[i].visible = visible;
height += heights[i];
}
/* istanbul ignore else */
if (currentMonth) {
this.subtitle = currentMonth.title;
}
},
onClickDay(item) {
const { date } = item;
const { type, currentDate } = this;
if (type === 'range') {
const [startDay, endDay] = currentDate;
if (startDay && !endDay) {
const compareToStart = compareDay(date, startDay);
if (compareToStart === 1) {
this.select([startDay, date], true);
} else if (compareToStart === -1) {
this.select([date, null]);
} else if (this.allowSameDay) {
this.select([date, date]);
}
} else {
this.select([date, null]);
}
} else if (type === 'multiple') {
let selectedIndex;
const selected = this.currentDate.some((dateItem, index) => {
const equal = compareDay(dateItem, date) === 0;
if (equal) {
selectedIndex = index;
}
return equal;
});
if (selected) {
currentDate.splice(selectedIndex, 1);
} else {
this.select([...currentDate, date]);
}
} else {
this.select(date, true);
}
},
togglePopup(val) {
this.$emit('input', val);
},
select(date, complete) {
this.currentDate = date;
this.$emit('select', copyDates(this.currentDate));
if (complete && this.type === 'range') {
const valid = this.checkRange();
if (!valid) {
return;
}
}
if (complete && !this.showConfirm) {
this.onConfirm();
}
},
checkRange() {
const { maxRange, currentDate, rangePrompt } = this;
if (maxRange && calcDateNum(currentDate) > maxRange) {
Toast(rangePrompt || t('rangePrompt', maxRange));
return false;
}
return true;
},
onConfirm() {
if (this.type === 'range' && !this.checkRange()) {
return;
}
this.$emit('confirm', copyDates(this.currentDate));
},
genMonth(date, index) {
const showMonthTitle = index !== 0 || !this.showSubtitle;
return (
<Month
ref="months"
refInFor
date={date}
type={this.type}
color={this.color}
minDate={this.minDate}
maxDate={this.maxDate}
showMark={this.showMark}
formatter={this.formatter}
rowHeight={this.rowHeight}
currentDate={this.currentDate}
showSubtitle={this.showSubtitle}
allowSameDay={this.allowSameDay}
showMonthTitle={showMonthTitle}
onClick={this.onClickDay}
/>
);
},
genFooterContent() {
const slot = this.slots('footer');
if (slot) {
return slot;
}
if (this.showConfirm) {
const text = this.buttonDisabled
? this.confirmDisabledText
: this.confirmText;
return (
<Button
round
block
type="danger"
color={this.color}
class={bem('confirm')}
disabled={this.buttonDisabled}
nativeType="text"
onClick={this.onConfirm}
>
{text || t('confirm')}
</Button>
);
}
},
genFooter() {
return (
<div
class={bem('footer', {
'safe-area-inset-bottom': this.safeAreaInsetBottom,
})}
>
{this.genFooterContent()}
</div>
);
},
genCalendar() {
return (
<div class={bem()}>
<Header
title={this.title}
showTitle={this.showTitle}
subtitle={this.subtitle}
showSubtitle={this.showSubtitle}
scopedSlots={{
title: () => this.slots('title'),
}}
/>
<div ref="body" class={bem('body')} onScroll={this.onScroll}>
{this.months.map(this.genMonth)}
</div>
{this.genFooter()}
</div>
);
},
},
render() {
if (this.poppable) {
const createListener = name => () => this.$emit(name);
return (
<Popup
round
class={bem('popup')}
value={this.value}
round={this.round}
position={this.position}
closeable={this.showTitle || this.showSubtitle}
getContainer={this.getContainer}
closeOnPopstate={this.closeOnPopstate}
closeOnClickOverlay={this.closeOnClickOverlay}
onInput={this.togglePopup}
onOpen={createListener('open')}
onOpened={createListener('opened')}
onClose={createListener('close')}
onClosed={createListener('closed')}
>
{this.genCalendar()}
</Popup>
);
}
return this.genCalendar();
},
});