UNPKG

vuetify

Version:

Vue Material Component Framework

373 lines (367 loc) 14.6 kB
import { createVNode as _createVNode, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, mergeProps as _mergeProps } from "vue"; // Styles import "./VDatePickerMonth.css"; // Components import { VBadge } from "../VBadge/index.js"; import { VBtn } from "../VBtn/index.js"; // Composables import { makeCalendarProps, useCalendar } from "../../composables/calendar.js"; import { useBackgroundColor } from "../../composables/color.js"; import { useDate } from "../../composables/date/date.js"; import { useGridSelection } from "../../composables/gridSelection.js"; import { useLocale } from "../../composables/locale.js"; import { useProxiedModel } from "../../composables/proxiedModel.js"; import { useRangePicker } from "../../composables/rangePicker.js"; import { MaybeTransition } from "../../composables/transition.js"; // Utilities import { computed, nextTick, shallowRef, toRef, useId, watch } from 'vue'; import { chunkArray, genericComponent, omit, propsFactory, useRender, wrapInArray } from "../../util/index.js"; // Types export const makeVDatePickerMonthProps = propsFactory({ color: String, hideWeekdays: Boolean, multiple: [Boolean, Number, String], showWeek: Boolean, readonly: Boolean, transition: { type: String, default: 'picker-transition' }, reverseTransition: { type: String, default: 'picker-reverse-transition' }, events: { type: [Array, Function, Object], default: () => null }, eventColor: { type: [Array, Function, Object, String], default: () => null }, noAutoNavigation: Boolean, previewValue: null, ...omit(makeCalendarProps(), ['displayValue']) }, 'VDatePickerMonth'); export const VDatePickerMonth = genericComponent()({ name: 'VDatePickerMonth', props: makeVDatePickerMonthProps(), emits: { 'update:modelValue': date => true, 'update:month': date => true, 'update:year': date => true, 'update:previewValue': _value => true, 'boundary-navigate': _payload => true }, setup(props, { emit, slots }) { const uid = useId(); const { t } = useLocale(); const { daysInMonth, model, weekNumbers, weekdayLabels } = useCalendar(props); const adapter = useDate(); const isReverse = shallowRef(false); const transition = toRef(() => { return !isReverse.value ? props.transition : props.reverseTransition; }); function compareDays(a, b) { if (adapter.isSameDay(a, b)) return 0; return adapter.isBefore(a, b) ? -1 : 1; } const previewValue = useProxiedModel(props, 'previewValue'); const range = useRangePicker({ multiple: computed(() => { if (props.multiple === 'range') return 'range'; return !!props.multiple; }), model, compare: compareDays, normalizeEnd: value => adapter.endOfDay(value), previewValue }); const selectionColor = toRef(() => props.color || 'surface-variant'); const { backgroundColorClasses: rangeColorClasses, backgroundColorStyles: rangeColorStyles } = useBackgroundColor(selectionColor); const atMax = computed(() => { const max = ['number', 'string'].includes(typeof props.multiple) ? Number(props.multiple) : Infinity; return model.value.length >= max; }); const dayRows = computed(() => { return chunkArray(daysInMonth.value, props.weekdays.length); }); function isSelectedDay(item) { return range.isSelected(item.date); } function isDayDisabled(item) { return item.isDisabled || item.isHidden || atMax.value && !isSelectedDay(item); } function getDateAriaLabel(item) { const fullDate = adapter.format(item.date, 'fullDateWithWeekday'); const localeKey = item.isToday ? 'currentDate' : 'selectDate'; return t(`$vuetify.datePicker.ariaLabel.${localeKey}`, fullDate); } function onClick(value) { range.select(adapter.startOfDay(value)); } function initialFocusDate(current) { const isVisible = d => !d.isAdjacent && !d.isDisabled; // Preserve existing highlight if it's still pointing to a visible day in the current grid if (current != null) { const cur = daysInMonth.value.find(d => d.isoDate === current); if (cur && !cur.isAdjacent) return current; } const selected = daysInMonth.value.find(d => isVisible(d) && model.value.some(m => adapter.isSameDay(m, d.date))); return (selected ?? daysInMonth.value.find(isVisible))?.isoDate; } const { containerProps, containerEl, selectItem, focusItem, clear } = useGridSelection({ items: () => daysInMonth.value.map(d => ({ value: d.isoDate, isDisabled: isDayDisabled(d) })), columns: () => props.weekdays.length, initialValue: initialFocusDate, itemAttribute: 'data-v-date', onSelect: onDaySelect, onNavigation: onNavigationBoundary, onEscape }); function onDaySelect(isoDate) { const item = daysInMonth.value.find(d => d.isoDate === isoDate); if (!item || isDayDisabled(item)) return; onClick(item.date); if (item.isAdjacent) { emit('update:month', adapter.getMonth(item.date)); emit('update:year', adapter.getYear(item.date)); nextTick(() => focusItem(isoDate)); } } function onNavigationBoundary(direction, e, curId) { if (curId == null) return false; const cols = props.weekdays.length; const rtl = getComputedStyle(e.currentTarget).direction === 'rtl'; // stride = array-index delta; calendarDays = actual date offset for boundary crossing let stride; let calendarDays; if (direction === 'left') { stride = rtl ? 1 : -1; calendarDays = stride; } else if (direction === 'right') { stride = rtl ? -1 : 1; calendarDays = stride; } else if (direction === 'up') { stride = -cols; calendarDays = -7; } else { stride = cols; calendarDays = 7; } const all = daysInMonth.value; const curIndex = all.findIndex(d => d.isoDate === curId); if (curIndex < 0) return false; const targetItem = all[curIndex + stride]; // isHidden = isAdjacent && !showAdjacentMonths — no button in DOM for this day if (targetItem && !targetItem.isHidden) return false; e.preventDefault(); let targetIsoDate; if (targetItem) { targetIsoDate = targetItem.isoDate; } else { const step = calendarDays < 0 ? -1 : 1; let candidate = adapter.addDays(adapter.date(curId), calendarDays); while (!props.weekdays.includes(adapter.toJsDate(candidate).getDay())) { candidate = adapter.addDays(candidate, step); } targetIsoDate = adapter.toISO(candidate); } if (props.noAutoNavigation) { emit('boundary-navigate', { direction, targetIsoDate }); return true; } const targetDate = adapter.date(targetIsoDate); emit('update:month', adapter.getMonth(targetDate)); emit('update:year', adapter.getYear(targetDate)); nextTick(() => focusItem(targetIsoDate)); return true; } function onEscape() { const rawTarget = model.value[0] ?? adapter.date(); const targetIso = adapter.toISO(adapter.date(rawTarget)); const inCurrentMonth = daysInMonth.value.find(d => d.isoDate === targetIso && !d.isAdjacent); if (inCurrentMonth) { focusItem(targetIso); return; } const targetDate = adapter.date(rawTarget); emit('update:month', adapter.getMonth(targetDate)); emit('update:year', adapter.getYear(targetDate)); nextTick(() => focusItem(targetIso)); } function onDayClick(item) { if (item.isAdjacent) { onDaySelect(item.isoDate); } else { selectItem(item.isoDate); } } function focusGrid() { containerEl.value?.focus(); } watch(daysInMonth, (val, oldVal) => { if (!oldVal || val[0].isoDate === oldVal[0].isoDate) return; // only clear when the month actually changes isReverse.value = adapter.isBefore(val[0].date, oldVal[0].date); clear(); }); function getEventColors(date) { const { events, eventColor } = props; let eventData; let eventColors = []; if (Array.isArray(events)) { eventData = events.includes(date); } else if (events instanceof Function) { eventData = events(date) || false; } else if (events) { eventData = events[date] || false; } else { eventData = false; } if (!eventData) { return []; } else if (eventData !== true) { eventColors = wrapInArray(eventData); } else if (typeof eventColor === 'string') { eventColors = [eventColor]; } else if (typeof eventColor === 'function') { eventColors = wrapInArray(eventColor(date)); } else if (Array.isArray(eventColor)) { eventColors = eventColor; } else if (typeof eventColor === 'object' && eventColor !== null) { eventColors = wrapInArray(eventColor[date]); } // Fallback to default color if no color is found return !eventColors.length ? ['surface-variant'] : eventColors.filter(Boolean).map(color => typeof color === 'string' ? color : 'surface-variant'); } function genEvents(date) { const eventColors = getEventColors(date); if (!eventColors.length) return null; return _createElementVNode("div", { "class": "v-date-picker-month__events" }, [eventColors.map(color => _createVNode(VBadge, { "dot": true, "color": color }, null))]); } useRender(() => _createElementVNode("div", { "class": "v-date-picker-month", "style": { '--v-date-picker-days-in-week': props.weekdays.length } }, [props.showWeek && _createElementVNode("div", { "key": "weeks", "class": "v-date-picker-month__weeks" }, [!props.hideWeekdays && _createElementVNode("div", { "key": "hide-week-days", "class": "v-date-picker-month__day" }, [_createTextVNode("\xA0")]), weekNumbers.value.map(week => _createElementVNode("div", { "class": _normalizeClass(['v-date-picker-month__day', 'v-date-picker-month__day--adjacent']) }, [week]))]), _createVNode(MaybeTransition, { "name": transition.value }, { default: () => [_createElementVNode("div", _mergeProps({ "key": daysInMonth.value[0].date?.toString(), "class": "v-date-picker-month__days", "role": "grid", "onMouseleave": range.clearPreview }, containerProps.value), [!props.hideWeekdays && _createElementVNode("div", { "key": "weekday-labels", "class": "v-date-picker-month__days-row" }, [weekdayLabels.value.map(weekDay => _createElementVNode("div", { "class": _normalizeClass(['v-date-picker-month__day', 'v-date-picker-month__weekday']) }, [weekDay]))]), dayRows.value.map((row, rowIndex) => _createElementVNode("div", { "class": "v-date-picker-month__days-row", "role": "row" }, [row.map((item, colIndex) => { const i = rowIndex * props.weekdays.length + colIndex; const isSelected = isSelectedDay(item); const disabled = isDayDisabled(item); const rangeStart = range.isRangeStart(item.date); const rangeEnd = range.isRangeEnd(item.date); const rangeMiddle = range.isRangeMiddle(item.date); const previewStart = range.isPreviewStart(item.date); const previewEnd = range.isPreviewEnd(item.date); const previewMiddle = range.isPreviewMiddle(item.date); const slotProps = { props: { class: 'v-date-picker-month__day-btn', color: isSelected && !rangeMiddle || item.isToday ? props.color : undefined, disabled, readonly: props.readonly, icon: true, ripple: false, tabindex: -1, variant: isSelected && !rangeMiddle ? 'flat' : item.isToday ? 'outlined' : 'text', 'aria-label': getDateAriaLabel(item), 'aria-current': item.isToday ? 'date' : undefined, id: `${uid}-day-${item.isoDate}`, 'data-v-date': !disabled ? item.isoDate : undefined, onMousedown: e => e.preventDefault(), // preserve virtual focus onClick: () => onDayClick(item), onMouseenter: () => range.setPreview(item.date), onFocus: () => range.setPreview(item.date), onBlur: range.clearPreview }, item, i }; const hasRangeBg = rangeStart || rangeEnd || rangeMiddle; const hasPreviewBg = previewStart || previewEnd || previewMiddle; return _createElementVNode("div", { "class": _normalizeClass(['v-date-picker-month__day', { 'v-date-picker-month__day--adjacent': item.isAdjacent, 'v-date-picker-month__day--hide-adjacent': item.isHidden, 'v-date-picker-month__day--selected': isSelected, 'v-date-picker-month__day--week-end': item.isWeekEnd, 'v-date-picker-month__day--week-start': item.isWeekStart, 'v-date-picker-month__day--range-start': rangeStart, 'v-date-picker-month__day--range-end': rangeEnd, 'v-date-picker-month__day--range-middle': rangeMiddle, 'v-date-picker-month__day--preview-start': previewStart, 'v-date-picker-month__day--preview-end': previewEnd, 'v-date-picker-month__day--preview-middle': previewMiddle }]), "role": "gridcell" }, [(hasRangeBg || hasPreviewBg) && _createElementVNode("div", { "key": "range-bg", "class": _normalizeClass(['v-date-picker-month__range-bg', hasRangeBg ? 'v-date-picker-month__range-bg--range' : 'v-date-picker-month__range-bg--preview', rangeColorClasses.value]), "style": _normalizeStyle(rangeColorStyles.value) }, null), (props.showAdjacentMonths || !item.isAdjacent) && (slots.day?.(slotProps) ?? _createVNode(VBtn, slotProps.props, { default: () => [item.localized, genEvents(item.isoDate)] }))]); })]))])] })])); return { focusGrid, focusItem }; } }); //# sourceMappingURL=VDatePickerMonth.js.map