vuetify
Version:
Vue Material Component Framework
352 lines (351 loc) • 14.8 kB
JavaScript
import { createVNode as _createVNode, Fragment as _Fragment, createElementVNode as _createElementVNode, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, mergeProps as _mergeProps } from "vue";
// Styles
import "./VMonthPicker.css";
// Components
import { VFadeTransition } from "../../components/transitions/index.js";
import { VBtn } from "../../components/VBtn/index.js";
import { VDatePickerHeader } from "../../components/VDatePicker/VDatePickerHeader.js";
import { VDatePickerYears } from "../../components/VDatePicker/VDatePickerYears.js";
import { VDefaultsProvider } from "../../components/VDefaultsProvider/index.js";
import { makeVPickerProps, VPicker } from "../../components/VPicker/VPicker.js"; // Composables
import { useMonthPicker } from "./month-picker.js";
import { useBackgroundColor } from "../../composables/color.js";
import { useDate } from "../../composables/date/index.js";
import { useGridSelection } from "../../composables/gridSelection.js";
import { IconValue } from "../../composables/icons.js";
import { useLocale } from "../../composables/locale.js";
import { useProxiedModel } from "../../composables/proxiedModel.js";
import { MaybeTransition } from "../../composables/transition.js"; // Utilities
import { computed, nextTick, shallowRef, toRef, watch } from 'vue';
import { chunkArray, createRange, genericComponent, omit, propsFactory, useRender, wrapInArray } from "../../util/index.js"; // Types
export const makeVMonthPickerProps = propsFactory({
disabled: Boolean,
readonly: Boolean,
selectedIcon: {
type: IconValue,
default: '$complete'
},
nextIcon: {
type: IconValue,
default: '$next'
},
prevIcon: {
type: IconValue,
default: '$prev'
},
modeIcon: {
type: IconValue,
default: '$subgroup'
},
modelValue: null,
headerColor: String,
min: String,
max: String,
multiple: [Boolean, String],
allowedMonths: [Array, Function],
allowedYears: [Array, Function],
monthsColumns: {
type: [Number, String],
default: 4
},
yearsColumns: {
type: [Number, String],
default: 3
},
transition: {
type: String,
default: 'picker-transition'
},
reverseTransition: {
type: String,
default: 'picker-reverse-transition'
},
...omit(makeVPickerProps({
title: '$vuetify.monthPicker.title'
}), ['landscape'])
}, 'VMonthPicker');
export const VMonthPicker = genericComponent()({
name: 'VMonthPicker',
props: makeVMonthPickerProps(),
emits: {
'update:modelValue': value => true
},
setup(props, {
slots
}) {
const model = useProxiedModel(props, 'modelValue', undefined, v => wrapInArray(v), v => props.multiple ? v : v[0]);
const adapter = useDate();
const {
t
} = useLocale();
const now = adapter.date();
const currentYear = adapter.getYear(now);
const currentMonth = adapter.getMonth(now);
const isReverse = shallowRef(false);
const transition = toRef(() => {
return !isReverse.value ? props.transition : props.reverseTransition;
});
const {
year,
disablePrevYear,
disableNextYear,
prevYear,
nextYear,
selectMonth,
getMonthValue,
range
} = useMonthPicker(props, model);
const viewMode = shallowRef('months');
function toggleViewMode() {
viewMode.value = viewMode.value === 'years' ? 'months' : 'years';
}
function onUpdateYear(v) {
year.value = v;
// Brief delay so the year-grid click feedback is visible before the view swaps
setTimeout(() => {
viewMode.value = 'months';
}, 100);
}
const headerText = computed(() => {
const values = model.value;
if (values.length === 0) {
return props.multiple === 'range' ? t('$vuetify.monthPicker.range.title') : t('$vuetify.monthPicker.header');
}
if (props.multiple === 'range' && values.length === 2) {
const startDate = adapter.parseISO(`${values[0]}-01`);
const endDate = adapter.parseISO(`${values[1]}-01`);
const start = `${adapter.format(startDate, 'monthShort')} ${adapter.format(startDate, 'year')}`;
const end = `${adapter.format(endDate, 'monthShort')} ${adapter.format(endDate, 'year')}`;
return `${start} – ${end}`;
}
if (props.multiple && values.length > 1) {
return t('$vuetify.monthPicker.itemsSelected', values.length);
}
const last = values[values.length - 1];
return adapter.format(adapter.parseISO(`${last}-01`), 'monthAndYear');
});
const headerColor = toRef(() => props.headerColor ?? props.color);
const {
backgroundColorClasses: rangeColorClasses,
backgroundColorStyles: rangeColorStyles
} = useBackgroundColor(() => props.color);
const headerTransition = toRef(() => `date-picker-header${isReverse.value ? '-reverse' : ''}-transition`);
watch(year, (newVal, oldVal) => {
isReverse.value = newVal < oldVal;
});
const months = computed(() => {
let date = adapter.startOfYear(adapter.date());
date = adapter.setYear(date, year.value);
return createRange(12).map(i => {
const text = adapter.format(date, 'monthShort');
const label = adapter.format(date, 'month');
const isDisabled = !!(!isMonthAllowed(i) || props.min && adapter.isAfter(adapter.startOfMonth(adapter.date(props.min + '-01')), date) || props.max && adapter.isAfter(date, adapter.startOfMonth(adapter.date(props.max + '-01'))));
date = adapter.getNextMonth(date);
return {
isCurrent: year.value === currentYear && i === currentMonth,
isDisabled,
text,
label,
value: i
};
});
});
const isListView = computed(() => Number(props.monthsColumns) === 1);
const monthRows = computed(() => {
const cols = Number(props.monthsColumns) || 4;
return chunkArray(months.value, cols);
});
function isMonthAllowed(month) {
if (Array.isArray(props.allowedMonths) && props.allowedMonths.length) {
return props.allowedMonths.includes(month);
}
if (typeof props.allowedMonths === 'function') {
return props.allowedMonths(month);
}
return true;
}
function initialFocusMonth(current) {
const isVisible = m => !m.isDisabled;
if (current != null) {
const cur = months.value.find(m => m.value === current);
if (cur && !cur.isDisabled) return current;
}
const selected = months.value.find(m => isVisible(m) && model.value.some(v => v === getMonthValue(m.value)));
if (selected) return selected.value;
if (year.value === currentYear) {
const today = months.value.find(m => m.value === currentMonth && isVisible(m));
if (today) return today.value;
}
return months.value.find(isVisible)?.value;
}
function onMonthSelect(index) {
selectMonth(index);
}
const {
containerProps,
selectItem,
focusItem,
clear
} = useGridSelection({
items: () => months.value,
columns: () => Number(props.monthsColumns) || 4,
initialValue: initialFocusMonth,
itemAttribute: 'data-v-month',
onSelect: onMonthSelect,
onNavigation: (...args) => onMonthNavigation(...args),
onHighlight: value => range.setPreview(value != null ? getMonthValue(value) : undefined)
});
function onMonthNavigation(direction, e, curIndex) {
if (curIndex == null) return false;
if (direction === 'left' && curIndex === 0 && !disablePrevYear.value) {
e.preventDefault();
prevYear();
nextTick(() => focusItem(11));
return true;
}
if (direction === 'right' && curIndex === 11 && !disableNextYear.value) {
e.preventDefault();
nextYear();
nextTick(() => focusItem(0));
return true;
}
return false;
}
watch(year, () => clear());
useRender(() => {
const pickerProps = VPicker.filterProps(props);
return _createVNode(VPicker, _mergeProps(pickerProps, {
"class": ['v-month-picker', {
'v-month-picker--list': isListView.value,
'v-month-picker--years': viewMode.value === 'years'
}, props.class],
"color": headerColor.value,
"title": t(props.title),
"style": [{
'--v-month-picker-months-columns': props.monthsColumns,
'--v-month-picker-years-columns': props.yearsColumns
}, props.style]
}), {
header: () => _createVNode(VDatePickerHeader, {
"color": headerColor.value,
"header": headerText.value,
"transition": headerTransition.value
}, null),
default: () => _createElementVNode(_Fragment, null, [_createVNode(VDefaultsProvider, {
"defaults": {
VBtn: {
variant: 'text'
}
}
}, {
default: () => [_createElementVNode("div", {
"class": "v-month-picker__controls"
}, [_createVNode(VBtn, {
"disabled": props.disabled || viewMode.value === 'years' || disablePrevYear.value,
"icon": props.prevIcon,
"aria-label": t('$vuetify.monthPicker.ariaLabel.previousYear'),
"onClick": prevYear
}, null), _createVNode(VBtn, {
"disabled": props.disabled,
"text": year.value,
"appendIcon": props.modeIcon,
"rounded": true,
"aria-label": t('$vuetify.monthPicker.ariaLabel.selectYear'),
"onClick": toggleViewMode
}, null), _createVNode(VBtn, {
"disabled": props.disabled || viewMode.value === 'years' || disableNextYear.value,
"icon": props.nextIcon,
"aria-label": t('$vuetify.monthPicker.ariaLabel.nextYear'),
"onClick": nextYear
}, null)])]
}), _createVNode(VFadeTransition, {
"hideOnLeave": true
}, {
default: () => [viewMode.value === 'years' ? _createVNode(VDatePickerYears, {
"key": "years",
"color": props.color,
"modelValue": year.value,
"min": props.min ? `${props.min}-01` : undefined,
"max": props.max ? `${props.max}-01` : undefined,
"allowedYears": props.allowedYears,
"onUpdate:modelValue": onUpdateYear
}, null) : _createElementVNode("div", {
"key": "months",
"class": "v-month-picker__months flex-grow-1"
}, [_createVNode(MaybeTransition, {
"name": transition.value
}, {
default: () => [_createElementVNode("div", _mergeProps({
"key": year.value,
"class": "v-month-picker__months-content",
"onMouseleave": range.clearPreview
}, containerProps.value), [monthRows.value.map((row, rowIndex) => {
const cols = Number(props.monthsColumns) || 4;
return _createElementVNode("div", {
"class": "v-month-picker__months-row"
}, [row.map((month, colIndex) => {
const index = rowIndex * cols + colIndex;
const monthValue = getMonthValue(index);
const rangeStart = range.isRangeStart(monthValue);
const rangeEnd = range.isRangeEnd(monthValue);
const rangeMiddle = range.isRangeMiddle(monthValue);
const previewStart = range.isPreviewStart(monthValue);
const previewEnd = range.isPreviewEnd(monthValue);
const previewMiddle = range.isPreviewMiddle(monthValue);
const previewed = range.isInPreviewRange(monthValue);
const selected = range.isSelected(monthValue) && !rangeMiddle;
const variant = isListView.value ? selected ? 'tonal' : 'text' : selected ? 'flat' : month.isCurrent ? 'outlined' : 'text';
const icon = isListView.value && selected ? props.selectedIcon : undefined;
const btnProps = {
color: selected || month.isCurrent && !isListView.value ? props.color : undefined,
disabled: props.disabled || month.isDisabled,
readonly: props.readonly,
rounded: isListView.value ? 0 : true,
text: isListView.value ? month.label : month.text,
prependIcon: icon,
variant,
ripple: false,
tabindex: -1,
'aria-label': month.isCurrent ? t('$vuetify.monthPicker.ariaLabel.currentMonth', month.label) : month.label,
'aria-current': month.isCurrent ? 'date' : undefined,
'aria-selected': selected,
'data-v-month': month.isDisabled ? undefined : month.value,
onMousedown: e => e.preventDefault(),
// preserve virtual focus
onClick: () => selectItem(index),
onMouseenter: () => range.setPreview(monthValue)
};
const hasRangeBg = rangeStart || rangeEnd || rangeMiddle;
const hasPreviewBg = previewStart || previewEnd || previewMiddle;
return _createElementVNode("div", {
"class": _normalizeClass(['v-month-picker__month', {
'v-month-picker__month--current': month.isCurrent,
'v-month-picker__month--selected': selected,
'v-month-picker__month--range-start': rangeStart,
'v-month-picker__month--range-end': rangeEnd,
'v-month-picker__month--range-middle': rangeMiddle,
'v-month-picker__month--preview-start': previewStart,
'v-month-picker__month--preview-end': previewEnd,
'v-month-picker__month--preview-middle': previewMiddle,
'v-month-picker__month--previewed': previewed
}])
}, [(hasRangeBg || hasPreviewBg) && _createElementVNode("div", {
"key": "range-background",
"class": _normalizeClass(['v-month-picker__range-bg', hasRangeBg ? 'v-month-picker__range-bg--range' : 'v-month-picker__range-bg--preview', rangeColorClasses.value]),
"style": _normalizeStyle(rangeColorStyles.value)
}, null), slots.month?.({
month,
i: index,
props: btnProps
}) ?? _createVNode(VBtn, btnProps, null)]);
})]);
})])]
})])]
})]),
actions: slots.actions
});
});
return {};
}
});
//# sourceMappingURL=VMonthPicker.js.map