Skip to content
This repository was archived by the owner on Apr 17, 2022. It is now read-only.

Commit d41da66

Browse files
committed
feat: Add support for range in month-picker (resolves #109)
1 parent 223aab8 commit d41da66

File tree

10 files changed

+114
-31
lines changed

10 files changed

+114
-31
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface Vue3DatePicker {
3939
seconds?: number | string;
4040
}[]
4141
| { month: number | string; year: number | string }
42+
| { month: number | string; year: number | string }[]
4243
| null;
4344
locale?: string;
4445
position?: 'left' | 'center' | 'right';

src/Vue3DatePicker/components/DatepickerMenu.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
minDate,
5252
maxDate,
5353
preventMinMaxNavigation,
54+
internalModelValue,
55+
range,
5456
}"
5557
@mount="childMount('monthYearInput')"
5658
@reset-flow="resetFlow"

src/Vue3DatePicker/components/MonthYearInput.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
disabledValues: filters.months,
105105
minValue: minMonth,
106106
maxValue: maxMonth,
107+
multiModelValue,
108+
year,
107109
}"
108110
@update:model-value="onMonthUpdate"
109111
@toggle="toggleMonthPicker"
@@ -176,14 +178,14 @@
176178

177179
<script lang="ts" setup>
178180
import { computed, onMounted, PropType, ref } from 'vue';
181+
import { getMonth, getYear } from 'date-fns';
179182
180183
import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from './Icons';
181184
import SelectionGrid from './SelectionGrid.vue';
182185
183-
import { IDateFilter, IDefaultSelect } from '../interfaces';
186+
import { IDateFilter, IDefaultSelect, InternalModuleValue } from '../interfaces';
184187
import { useMontYearPick } from './composition/month-year';
185188
import { useTransitions } from './composition/transition';
186-
import { getMonth, getYear } from 'date-fns';
187189
188190
const emit = defineEmits(['update:month', 'update:year', 'monthYearSelect', 'mount', 'reset-flow']);
189191
const props = defineProps({
@@ -200,6 +202,8 @@
200202
minDate: { type: [Date, String] as PropType<Date | string>, default: null },
201203
maxDate: { type: [Date, String] as PropType<Date | string>, default: null },
202204
preventMinMaxNavigation: { type: Boolean as PropType<boolean>, default: false },
205+
internalModelValue: { type: [Date, Array] as PropType<InternalModuleValue>, default: null },
206+
range: { type: Boolean as PropType<boolean>, default: false },
203207
});
204208
205209
const { transitionName, showTransition } = useTransitions();
@@ -243,6 +247,10 @@
243247
return null;
244248
});
245249
250+
const multiModelValue = computed(() => {
251+
return props.range && props.internalModelValue && props.monthPicker ? (props.internalModelValue as Date[]) : [];
252+
});
253+
246254
const getGroupedList = (items: IDefaultSelect[]): IDefaultSelect[][] => {
247255
const list = [];
248256

src/Vue3DatePicker/components/SelectionGrid.vue

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
tabindex="0"
2020
@click="onClick(col.value)"
2121
@keydown.enter="onClick(col.value)"
22+
@mouseover="hoverValue = col.value"
2223
>
2324
<div :class="col.className">
2425
<slot v-if="$slots.item" name="item" :item="col" />
@@ -43,25 +44,30 @@
4344

4445
<script lang="ts" setup>
4546
import { computed, inject, onBeforeUpdate, onMounted, PropType, ref } from 'vue';
47+
import { setMonth, setYear } from 'date-fns';
4648
4749
import { IDefaultSelect, DynamicClass } from '../interfaces';
4850
import { getKey, unrefElement } from '../utils/util';
51+
import { isDateBetween, isDateEqual } from '../utils/date-utils';
4952
5053
const emit = defineEmits(['update:modelValue', 'selected', 'toggle', 'reset-flow']);
5154
5255
const props = defineProps({
5356
items: { type: Array as PropType<IDefaultSelect[][]>, default: () => [] },
5457
modelValue: { type: [String, Number] as PropType<string | number>, default: null },
58+
multiModelValue: { type: Array as PropType<Date[]>, default: () => [] },
5559
disabledValues: { type: Array as PropType<number[]>, default: () => [] },
5660
minValue: { type: [Number, String] as PropType<number | string>, default: null },
5761
maxValue: { type: [Number, String] as PropType<number | string>, default: null },
62+
year: { type: Number as PropType<number>, default: 0 },
5863
});
5964
6065
const scrollable = ref(false);
6166
const selectionActiveRef = ref(null);
6267
const gridWrapRef = ref(null);
6368
const autoApply = inject('autoApply', false);
6469
const textInput = inject('textInput', ref(false));
70+
const hoverValue = ref();
6571
6672
onBeforeUpdate(() => {
6773
selectionActiveRef.value = null;
@@ -102,14 +108,21 @@
102108
.map((itemVal) => {
103109
const disabled =
104110
props.disabledValues.some((val) => val === itemVal.value) || checkMinMaxValue(itemVal.value);
111+
const active = props.multiModelValue?.length
112+
? props.multiModelValue?.some((value) =>
113+
isDateEqual(value, setYear(setMonth(new Date(), itemVal.value), props.year)),
114+
)
115+
: itemVal.value === props.modelValue;
116+
105117
return {
106118
...itemVal,
107119
className: {
108-
dp__overlay_cell_active: itemVal.value === props.modelValue,
109-
dp__overlay_cell: itemVal.value !== props.modelValue,
120+
dp__overlay_cell_active: active,
121+
dp__overlay_cell: !active,
110122
dp__overlay_cell_disabled: disabled,
111-
dp__overlay_cell_active_disabled: disabled && itemVal.value === props.modelValue,
123+
dp__overlay_cell_active_disabled: disabled && active,
112124
dp__overlay_cell_pad: true,
125+
dp__cell_in_between: props.multiModelValue?.length ? rangeActive(itemVal.value) : false,
113126
},
114127
};
115128
});
@@ -168,6 +181,14 @@
168181
}
169182
};
170183
184+
const rangeActive = (value: number): boolean => {
185+
return isDateBetween(
186+
props.multiModelValue,
187+
setYear(setMonth(new Date(), hoverValue.value || 0), props.year),
188+
setYear(setMonth(new Date(), value), props.year),
189+
);
190+
};
191+
171192
const toggle = () => {
172193
emit('toggle');
173194
emit('reset-flow');

src/Vue3DatePicker/components/composition/calendar.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getNextMonthYear,
2323
isDateAfter,
2424
isDateBefore,
25+
isDateBetween,
2526
isDateEqual,
2627
sanitizeDate,
2728
setDateMonthOrYear,
@@ -233,21 +234,7 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
233234
* If range mode used, this will check if the calendar day is between 2 active dates
234235
*/
235236
const rangeActive = (calendarDay: ICalendarDay): boolean => {
236-
if (isModelValueRange(modelValue.value) && modelValue.value[0] && modelValue.value[1]) {
237-
return (
238-
isDateAfter(calendarDay.value, modelValue.value[0]) &&
239-
isDateBefore(calendarDay.value, modelValue.value[1])
240-
);
241-
}
242-
if (isModelValueRange(modelValue.value) && modelValue.value[0] && hoveredDate.value) {
243-
return (
244-
(isDateAfter(calendarDay.value, modelValue.value[0]) &&
245-
isDateBefore(calendarDay.value, hoveredDate.value)) ||
246-
(isDateBefore(calendarDay.value, modelValue.value[0]) &&
247-
isDateAfter(calendarDay.value, hoveredDate.value))
248-
);
249-
}
250-
return false;
237+
return isDateBetween(modelValue.value as Date[], hoveredDate.value as Date, calendarDay.value);
251238
};
252239

253240
/**
@@ -337,7 +324,7 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
337324
setDateTime(new Date(), hours.value[1], minutes.value[1], getSecondsValue(false)),
338325
];
339326
}
340-
} else if (props.monthPicker) {
327+
} else if (props.monthPicker && !props.range) {
341328
modelValue.value = setDateMonthOrYear(new Date(), month.value(0), year.value(0));
342329
} else if (props.multiCalendars) {
343330
assignMonthAndYear(new Date());
@@ -541,6 +528,10 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
541528
}
542529
};
543530

531+
const getMonthValue = (instance: number): Date => {
532+
return setDateMonthOrYear(new Date(), month.value(instance), year.value(instance));
533+
};
534+
544535
const updateMonthYear = (instance: number, value: number, isMonth = true): void => {
545536
if (isMonth) {
546537
setCalendarMonth(instance, value);
@@ -551,10 +542,25 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
551542
autoChangeMultiCalendars(instance);
552543
}
553544
if (props.monthPicker) {
554-
if (modelValue.value) {
555-
modelValue.value = setDateMonthOrYear(modelValue.value as Date, month.value(0), year.value(0));
545+
if (props.range) {
546+
if (isMonth) {
547+
let rangeDate = modelValue.value ? (modelValue.value as Date[]).slice() : [];
548+
if (rangeDate.length === 2) {
549+
rangeDate = [];
550+
}
551+
if (!rangeDate.length) {
552+
rangeDate = [getMonthValue(instance)];
553+
} else {
554+
if (isDateBefore(getMonthValue(instance), rangeDate[0])) {
555+
rangeDate.unshift(getMonthValue(instance));
556+
} else {
557+
rangeDate.push(getMonthValue(instance));
558+
}
559+
}
560+
modelValue.value = rangeDate;
561+
}
556562
} else {
557-
modelValue.value = setDateMonthOrYear(new Date(), month.value(0), year.value(0));
563+
modelValue.value = getMonthValue(instance);
558564
}
559565
}
560566
updateFlow();

src/Vue3DatePicker/components/composition/external-internal-mapper.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import {
44
dateToUtc,
55
formatDate,
66
getDefaultPattern,
7+
getMonthValForExternal,
78
getTImeForExternal,
89
isValidDate,
910
setDateMonthOrYear,
1011
setDateTime,
1112
} from '../../utils/date-utils';
1213
import { IFormat, ITextInputOptions, ModelValue, VueEmit } from '../../interfaces';
13-
import { isMonth, isRangeArray, isSingle, isTime, isTimeArray } from '../../utils/type-guard';
14-
import { getMonthVal } from '../../utils/date-utils';
14+
import { isMonth, isMonthArray, isRangeArray, isSingle, isTime, isTimeArray } from '../../utils/type-guard';
1515
import { Locale } from 'date-fns';
1616

1717
interface IExternalInternalMapper {
@@ -66,7 +66,12 @@ export const useExternalInternalMapper = (
6666
mappedDate = setDateTime(null, +value.hours, +value.minutes, +value.seconds);
6767
}
6868
} else if (monthPicker) {
69-
if (isMonth(value) && 'month' in value && 'year' in value) {
69+
if (isMonthArray(value) && 'month' in value[0] && 'year' in value[0]) {
70+
mappedDate = [
71+
setDateMonthOrYear(null, +value[0].month, +value[0].year),
72+
setDateMonthOrYear(null, +value[1].month, +value[1].year),
73+
];
74+
} else if (isMonth(value) && 'month' in value && 'year' in value) {
7075
mappedDate = setDateMonthOrYear(null, +value.month, +value.year);
7176
}
7277
} else if (multiDates && Array.isArray(value)) {
@@ -114,7 +119,7 @@ export const useExternalInternalMapper = (
114119
} else if (timePicker) {
115120
inputValue.value = format(getTImeForExternal(internalModelValue.value));
116121
} else if (monthPicker) {
117-
inputValue.value = format(getMonthVal(internalModelValue.value as Date));
122+
inputValue.value = format(getMonthValForExternal(internalModelValue.value));
118123
} else {
119124
inputValue.value = format(internalModelValue.value);
120125
}
@@ -136,7 +141,7 @@ export const useExternalInternalMapper = (
136141
*/
137142
const emitModelValue = (): void => {
138143
if (monthPicker) {
139-
emit('update:modelValue', getMonthVal(internalModelValue.value as Date));
144+
emit('update:modelValue', getMonthValForExternal(internalModelValue.value));
140145
} else if (timePicker) {
141146
emit('update:modelValue', getTImeForExternal(internalModelValue.value));
142147
} else {

src/Vue3DatePicker/interfaces.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export enum OpenPosition {
1717
right = 'right',
1818
}
1919

20-
export type IFormat = string | ((date: Date | Date[] | ITimeValue | ITimeValue[] | IMonthValue) => string);
20+
export type IFormat =
21+
| string
22+
| ((date: Date | Date[] | ITimeValue | ITimeValue[] | IMonthValue | IMonthValue[]) => string);
2123

2224
export type InternalModuleValue = Date | Date[] | null;
2325

@@ -58,7 +60,16 @@ export interface ITimeValue {
5860
seconds: number | string;
5961
}
6062

61-
export type ModelValue = Date | Date[] | string | string[] | ITimeValue | ITimeValue[] | IMonthValue | null;
63+
export type ModelValue =
64+
| Date
65+
| Date[]
66+
| string
67+
| string[]
68+
| ITimeValue
69+
| ITimeValue[]
70+
| IMonthValue
71+
| IMonthValue[]
72+
| null;
6273

6374
export type WeekStartNum = 0 | 1 | 2 | 3 | 4 | 5 | 6;
6475
export type WeekStartStr = '0' | '1' | '2' | '3' | '4' | '5' | '6';

src/Vue3DatePicker/style/components/_SelectionGrid.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
}
8686
}
8787

88+
.dp__cell_in_between {
89+
background: var(--dp-hover-color);
90+
color: var(--dp-hover-text-color);
91+
}
92+
8893
.dp__overlay_action {
8994
position: sticky;
9095
bottom: 0;

src/Vue3DatePicker/utils/date-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ export const getTimeVal = (date?: Date): ITimeValue => {
127127

128128
export const getMonthVal = (date: Date): IMonthValue => ({ month: getMonth(date), year: getYear(date) });
129129

130+
export const getMonthValForExternal = (date: Date | Date[]): IMonthValue | IMonthValue[] => {
131+
if (Array.isArray(date)) {
132+
return [getMonthVal(date[0]), getMonthVal(date[1])];
133+
}
134+
return getMonthVal(date);
135+
};
136+
130137
export const getTImeForExternal = (date: Date | Date[]): ITimeValue | ITimeValue[] => {
131138
if (Array.isArray(date)) {
132139
return [getTimeVal(date[0]), getTimeVal(date[1])];
@@ -256,3 +263,16 @@ export const dateToUtc = (date: Date): string => {
256263

257264
return new Date(utcDate).toISOString();
258265
};
266+
267+
export const isDateBetween = (range: Date[], hoverDate: Date, dateToCheck: Date): boolean => {
268+
if (range[0] && range[1]) {
269+
return isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, range[1]);
270+
}
271+
if (range[0] && hoverDate) {
272+
return (
273+
(isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, hoverDate)) ||
274+
(isDateBefore(dateToCheck, range[0]) && isDateAfter(dateToCheck, hoverDate))
275+
);
276+
}
277+
return false;
278+
};

src/Vue3DatePicker/utils/type-guard.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const isTimeArray = (value: ModelValue): value is ITimeValue[] => {
1818
return Array.isArray(value) && value.length === 2;
1919
};
2020

21+
export const isMonthArray = (value: ModelValue): value is IMonthValue[] => {
22+
return Array.isArray(value);
23+
};
24+
2125
export const isMonth = (value: ModelValue): value is IMonthValue => {
2226
return typeof value === 'object';
2327
};

0 commit comments

Comments
 (0)