Files
vant/packages/vant/src/calendar/CalendarMonth.tsx
T
9ffacac25f feat(Calendar): add click-disabled-date event (#12274)
* feat(Calendar): add click-disabled-date event

* refactor(Calendar): refactor event name

* test(Calendar): add click-disabled-date event test

---------

Co-authored-by: 骆沛 <luopei@11.com>
2023-09-13 08:15:20 +08:00

292 lines
7.3 KiB
TypeScript

import {
ref,
computed,
defineComponent,
type PropType,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
pick,
addUnit,
numericProp,
setScrollTop,
createNamespace,
makeRequiredProp,
} from '../utils';
import { getMonthEndDay } from '../date-picker/utils';
import {
t,
bem,
compareDay,
getPrevDay,
getNextDay,
formatMonthTitle,
} from './utils';
// Composables
import { useRect, useToggle } from '@vant/use';
import { useExpose } from '../composables/use-expose';
import { useHeight } from '../composables/use-height';
// Components
import CalendarDay from './CalendarDay';
// Types
import type { CalendarType, CalendarDayItem, CalendarDayType } from './types';
const [name] = createNamespace('calendar-month');
const calendarMonthProps = {
date: makeRequiredProp(Date),
type: String as PropType<CalendarType>,
color: String,
minDate: makeRequiredProp(Date),
maxDate: makeRequiredProp(Date),
showMark: Boolean,
rowHeight: numericProp,
formatter: Function as PropType<(item: CalendarDayItem) => CalendarDayItem>,
lazyRender: Boolean,
currentDate: [Date, Array] as PropType<Date | Date[] | null>,
allowSameDay: Boolean,
showSubtitle: Boolean,
showMonthTitle: Boolean,
firstDayOfWeek: Number,
};
export type CalendarMonthProps = ExtractPropTypes<typeof calendarMonthProps>;
export default defineComponent({
name,
props: calendarMonthProps,
emits: ['click', 'clickDisabledDate'],
setup(props, { emit, slots }) {
const [visible, setVisible] = useToggle();
const daysRef = ref<HTMLElement>();
const monthRef = ref<HTMLElement>();
const height = useHeight(monthRef);
const title = computed(() => formatMonthTitle(props.date));
const rowHeight = computed(() => addUnit(props.rowHeight));
const offset = computed(() => {
const realDay = props.date.getDay();
if (props.firstDayOfWeek) {
return (realDay + 7 - props.firstDayOfWeek) % 7;
}
return realDay;
});
const totalDay = computed(() =>
getMonthEndDay(props.date.getFullYear(), props.date.getMonth() + 1),
);
const shouldRender = computed(() => visible.value || !props.lazyRender);
const getTitle = () => title.value;
const getMultipleDayType = (day: Date) => {
const isSelected = (date: Date) =>
(props.currentDate as Date[]).some(
(item) => compareDay(item, date) === 0,
);
if (isSelected(day)) {
const prevDay = getPrevDay(day);
const nextDay = getNextDay(day);
const prevSelected = isSelected(prevDay);
const nextSelected = isSelected(nextDay);
if (prevSelected && nextSelected) {
return 'multiple-middle';
}
if (prevSelected) {
return 'end';
}
if (nextSelected) {
return 'start';
}
return 'multiple-selected';
}
return '';
};
const getRangeDayType = (day: Date) => {
const [startDay, endDay] = props.currentDate as Date[];
if (!startDay) {
return '';
}
const compareToStart = compareDay(day, startDay);
if (!endDay) {
return compareToStart === 0 ? 'start' : '';
}
const compareToEnd = compareDay(day, endDay);
if (props.allowSameDay && compareToStart === 0 && compareToEnd === 0) {
return 'start-end';
}
if (compareToStart === 0) {
return 'start';
}
if (compareToEnd === 0) {
return 'end';
}
if (compareToStart > 0 && compareToEnd < 0) {
return 'middle';
}
return '';
};
const getDayType = (day: Date): CalendarDayType => {
const { type, minDate, maxDate, currentDate } = props;
if (compareDay(day, minDate) < 0 || compareDay(day, maxDate) > 0) {
return 'disabled';
}
if (currentDate === null) {
return '';
}
if (Array.isArray(currentDate)) {
if (type === 'multiple') {
return getMultipleDayType(day);
}
if (type === 'range') {
return getRangeDayType(day);
}
} else if (type === 'single') {
return compareDay(day, currentDate as Date) === 0 ? 'selected' : '';
}
return '';
};
const getBottomInfo = (dayType: CalendarDayType) => {
if (props.type === 'range') {
if (dayType === 'start' || dayType === 'end') {
return t(dayType);
}
if (dayType === 'start-end') {
return `${t('start')}/${t('end')}`;
}
}
};
const renderTitle = () => {
if (props.showMonthTitle) {
return (
<div class={bem('month-title')}>
{slots['month-title']
? slots['month-title']({
date: props.date,
text: title.value,
})
: title.value}
</div>
);
}
};
const renderMark = () => {
if (props.showMark && shouldRender.value) {
return <div class={bem('month-mark')}>{props.date.getMonth() + 1}</div>;
}
};
const placeholders = computed<CalendarDayItem[]>(() => {
const count = Math.ceil((totalDay.value + offset.value) / 7);
return Array(count).fill({ type: 'placeholder' });
});
const days = computed(() => {
const days: CalendarDayItem[] = [];
const year = props.date.getFullYear();
const month = props.date.getMonth();
for (let day = 1; day <= totalDay.value; day++) {
const date = new Date(year, month, day);
const type = getDayType(date);
let config: CalendarDayItem = {
date,
type,
text: day,
bottomInfo: getBottomInfo(type),
};
if (props.formatter) {
config = props.formatter(config);
}
days.push(config);
}
return days;
});
const disabledDays = computed(() =>
days.value.filter((day) => day.type === 'disabled'),
);
const scrollToDate = (body: Element, targetDate: Date) => {
if (daysRef.value) {
const daysRect = useRect(daysRef.value);
const totalRows = placeholders.value.length;
const currentRow = Math.ceil((targetDate.getDate() + offset.value) / 7);
const rowOffset = ((currentRow - 1) * daysRect.height) / totalRows;
setScrollTop(
body,
daysRect.top + rowOffset + body.scrollTop - useRect(body).top,
);
}
};
const renderDay = (item: CalendarDayItem, index: number) => (
<CalendarDay
v-slots={pick(slots, ['top-info', 'bottom-info'])}
item={item}
index={index}
color={props.color}
offset={offset.value}
rowHeight={rowHeight.value}
onClick={(item) => emit('click', item)}
onClickDisabledDate={(item) => emit('clickDisabledDate', item)}
/>
);
const renderDays = () => (
<div ref={daysRef} role="grid" class={bem('days')}>
{renderMark()}
{(shouldRender.value ? days : placeholders).value.map(renderDay)}
</div>
);
useExpose({
getTitle,
getHeight: () => height.value,
setVisible,
scrollToDate,
disabledDays,
});
return () => (
<div class={bem('month')} ref={monthRef}>
{renderTitle()}
{renderDays()}
</div>
);
},
});