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

Commit cea5e0e

Browse files
committed
feat: Add week-picker mode (resolves #110)
1 parent b41b639 commit cea5e0e

File tree

9 files changed

+82
-20
lines changed

9 files changed

+82
-20
lines changed

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ interface Vue3DatePicker {
194194
multiDatesLimit?: number | string;
195195
reverseYears?: boolean;
196196
keepActionRow?: boolean;
197+
weekPicker?: boolean;
197198
}
198199

199200
interface PublicMethods extends MethodOptions {

src/Vue3DatePicker/Vue3DatePicker.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
multiDatesLimit,
117117
reverseYears,
118118
keepActionRow,
119+
weekPicker,
119120
}"
120121
v-model:internalModelValue="internalModelValue"
121122
@close-picker="closeMenu"
@@ -293,6 +294,7 @@
293294
multiDatesLimit: { type: [Number, String] as PropType<number | string>, default: null },
294295
reverseYears: { type: Boolean as PropType<boolean>, default: false },
295296
keepActionRow: { type: Boolean as PropType<boolean>, default: false },
297+
weekPicker: { type: Boolean as PropType<boolean>, default: false },
296298
});
297299
const slots = useSlots();
298300
const isOpen = ref(false);
@@ -357,6 +359,7 @@
357359
formatLocaleRef,
358360
props.multiDates,
359361
props.utc,
362+
props.weekPicker,
360363
props.textInputOptions,
361364
emit,
362365
);
@@ -380,6 +383,7 @@
380383
props.enableSeconds,
381384
props.monthPicker,
382385
props.timePicker,
386+
props.weekPicker,
383387
props.enableTimePicker,
384388
);
385389
});

src/Vue3DatePicker/components/DatepickerMenu.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
</div>
8888
<div>
8989
<component
90-
v-if="enableTimePicker && !monthPicker"
90+
v-if="enableTimePicker && !monthPicker && !weekPicker"
9191
:is="timePickerComponent ? timePickerComponent : TimePickerCmp"
9292
ref="timePickerRef"
9393
v-bind="{
@@ -299,6 +299,7 @@
299299
multiDatesLimit: { type: [Number, String] as PropType<number | string>, default: null },
300300
reverseYears: { type: Boolean as PropType<boolean>, default: false },
301301
keepActionRow: { type: Boolean as PropType<boolean>, default: false },
302+
weekPicker: { type: Boolean as PropType<boolean>, default: false },
302303
});
303304
const slots = useSlots();
304305
const calendarWrapperRef = ref(null);
@@ -502,7 +503,7 @@
502503
dp__date_hover_start: isHoverDateStartEnd(dateHover, calendarDay, true),
503504
dp__date_hover_end: isHoverDateStartEnd(dateHover, calendarDay, false),
504505
dp__range_between:
505-
props.range &&
506+
(props.range || props.weekPicker) &&
506507
(props.multiCalendars > 0 ? calendarDay.current : true) &&
507508
!disabled &&
508509
!(!calendarDay.current && props.hideOffsetDates) &&

src/Vue3DatePicker/components/composition/calendar.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
subMonths,
1818
} from 'date-fns';
1919

20-
import { ICalendarDay, IMarker, InternalModuleValue, UseCalendar, VueEmit } from '../../interfaces';
20+
import { ICalendarDay, IMarker, InternalModuleValue, UseCalendar, VueEmit, WeekStartNum } from '../../interfaces';
2121
import {
2222
getNextMonthYear,
23+
getWeekFromDate,
2324
isDateAfter,
2425
isDateBefore,
2526
isDateBetween,
@@ -396,6 +397,12 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
396397
return [];
397398
};
398399

400+
const autoApply = (): void => {
401+
if (props.autoApply) {
402+
emit('autoApply');
403+
}
404+
};
405+
399406
/**
400407
* Called when the date in the calendar is clicked
401408
* Do a necessary formatting and assign value to internal
@@ -407,6 +414,10 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
407414
if (!day.current && props.hideOffsetDates) {
408415
return;
409416
}
417+
if (props.weekPicker) {
418+
modelValue.value = getWeekFromDate(new Date(day.value), +props.weekStart as WeekStartNum);
419+
return autoApply();
420+
}
410421
if (!props.range && !isNumberArray(hours.value) && !isNumberArray(minutes.value)) {
411422
const date = setDateTime(new Date(day.value), hours.value, minutes.value, getSecondsValue());
412423
if (props.multiDates) {
@@ -415,9 +426,7 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
415426
modelValue.value = date;
416427
}
417428
updateFlow();
418-
if (props.autoApply) {
419-
emit('autoApply');
420-
}
429+
autoApply();
421430
} else if (isNumberArray(hours.value) && isNumberArray(minutes.value) && !props.multiDates) {
422431
let rangeDate = modelValue.value ? (modelValue.value as Date[]).slice() : [];
423432
if (rangeDate.length === 2 && !(props.fixedStart || props.fixedEnd)) {
@@ -480,11 +489,14 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
480489
* Check if range ends on the given day
481490
*/
482491
const isHoverRangeEnd = (day: UnwrapRef<ICalendarDay>): boolean => {
483-
if (props.autoRange) {
492+
if (props.autoRange || props.weekPicker) {
484493
if (hoveredDate.value) {
485494
if (props.hideOffsetDates && !day.current) return false;
486495
const rangeEnd = addDays(hoveredDate.value, +props.autoRange);
487-
return isDateEqual(rangeEnd, new Date(day.value));
496+
const range = getWeekFromDate(new Date(hoveredDate.value), +props.weekStart as WeekStartNum);
497+
return props.weekPicker
498+
? isDateEqual(range[1], new Date(day.value))
499+
: isDateEqual(rangeEnd, new Date(day.value));
488500
}
489501
return false;
490502
}
@@ -495,22 +507,26 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
495507
* Check if date in auto range preview is in between
496508
*/
497509
const isAutoRangeInBetween = (day: UnwrapRef<ICalendarDay>): boolean => {
498-
if (props.autoRange) {
510+
if (props.autoRange || props.weekPicker) {
499511
if (hoveredDate.value) {
500512
const rangeEnd = addDays(hoveredDate.value, +props.autoRange);
501513
if (props.hideOffsetDates && !day.current) return false;
502-
return isDateAfter(day.value, hoveredDate.value) && isDateBefore(day.value, rangeEnd);
514+
const range = getWeekFromDate(new Date(hoveredDate.value), +props.weekStart as WeekStartNum);
515+
return props.weekPicker
516+
? isDateAfter(day.value, range[0]) && isDateBefore(day.value, range[1])
517+
: isDateAfter(day.value, hoveredDate.value) && isDateBefore(day.value, rangeEnd);
503518
}
504519
return false;
505520
}
506521
return false;
507522
};
508523

509524
const isAutoRangeStart = (day: UnwrapRef<ICalendarDay>): boolean => {
510-
if (props.autoRange) {
525+
if (props.autoRange || props.weekPicker) {
511526
if (hoveredDate.value) {
512527
if (props.hideOffsetDates && !day.current) return false;
513-
return isDateEqual(hoveredDate.value, day.value);
528+
const range = getWeekFromDate(new Date(hoveredDate.value), +props.weekStart as WeekStartNum);
529+
return props.weekPicker ? isDateEqual(range[0], day.value) : isDateEqual(hoveredDate.value, day.value);
514530
}
515531
return false;
516532
}
@@ -659,7 +675,7 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
659675
* Check when to add a proper active start/end date class on range picker
660676
*/
661677
const rangeActiveStartEnd = (day: UnwrapRef<ICalendarDay>, isStart = true): boolean => {
662-
if (props.range && isRange(modelValue.value)) {
678+
if ((props.range || props.weekPicker) && isRange(modelValue.value)) {
663679
if (props.hideOffsetDates && !day.current) return false;
664680
return isDateEqual(new Date(day.value), modelValue.value[isStart ? 0 : 1]);
665681
} else if (props.range) {
@@ -687,7 +703,7 @@ export const useCalendar = (props: UseCalendar, emit: VueEmit, updateFlow: () =>
687703
};
688704

689705
const isHoverDate = (disabled: boolean, calendarDay: ICalendarDay) => {
690-
return Array.isArray(props.internalModelValue) && props.internalModelValue.length
706+
return (Array.isArray(props.internalModelValue) && props.internalModelValue.length) || props.weekPicker
691707
? false
692708
: !disabled &&
693709
!isActiveDate(calendarDay) &&

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const useExternalInternalMapper = (
3838
formatLocale: ComputedRef<Locale>,
3939
multiDates: boolean,
4040
utc: boolean,
41+
weekPicker: boolean,
4142
textInputOptions: ITextInputOptions,
4243
emit: VueEmit,
4344
): IExternalInternalMapper => {
@@ -76,6 +77,8 @@ export const useExternalInternalMapper = (
7677
}
7778
} else if (multiDates && Array.isArray(value)) {
7879
mappedDate = value.map((date) => new Date(date as string));
80+
} else if (weekPicker && Array.isArray(value)) {
81+
mappedDate = [new Date(value[0] as string), new Date(value[1] as string)];
7982
} else if (range) {
8083
if (isRangeArray(value, partialRange)) {
8184
mappedDate = [new Date(value[0]), value[1] ? new Date(value[1]) : (null as unknown as Date)];
@@ -103,7 +106,15 @@ export const useExternalInternalMapper = (
103106
if (!internalModelValue.value) {
104107
inputValue.value = '';
105108
} else if (!format || typeof format === 'string') {
106-
const pattern = getDefaultPattern(format, is24, enableSeconds, monthPicker, timePicker, enableTimePicker);
109+
const pattern = getDefaultPattern(
110+
format,
111+
is24,
112+
enableSeconds,
113+
monthPicker,
114+
timePicker,
115+
weekPicker,
116+
enableTimePicker,
117+
);
107118
if (Array.isArray(internalModelValue.value) && multiDates) {
108119
inputValue.value = internalModelValue.value
109120
.map((date) => formatDate(date, pattern, formatLocale?.value))
@@ -144,6 +155,8 @@ export const useExternalInternalMapper = (
144155
emit('update:modelValue', getMonthValForExternal(internalModelValue.value));
145156
} else if (timePicker) {
146157
emit('update:modelValue', getTImeForExternal(internalModelValue.value));
158+
} else if (weekPicker) {
159+
emit('update:modelValue', internalModelValue.value);
147160
} else {
148161
if (internalModelValue.value && range && partialRange && internalModelValue.value.length === 1) {
149162
internalModelValue.value.push(null);

src/Vue3DatePicker/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export type UseCalendar = {
103103
fixedStart: boolean;
104104
fixedEnd: boolean;
105105
multiDatesLimit: number | string;
106+
weekPicker: boolean;
107+
weekStart: WeekStartNum | WeekStartStr;
106108
} & { [key: string]: unknown };
107109

108110
export interface UseMonthYearPick {

src/Vue3DatePicker/utils/date-utils.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import {
2222
add,
2323
sub,
2424
Locale,
25+
startOfWeek,
26+
endOfWeek,
2527
} from 'date-fns';
2628

27-
import { IMonthValue, InternalModuleValue, ITimeValue } from '../interfaces';
29+
import { IMonthValue, InternalModuleValue, ITimeValue, WeekStartNum } from '../interfaces';
2830

2931
export const sanitizeDate = (date: Date) => {
3032
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
@@ -102,6 +104,7 @@ export const getDefaultPattern = (
102104
enableSeconds: boolean,
103105
monthPicker: boolean,
104106
timePicker: boolean,
107+
weekPicker: boolean,
105108
enableTimePicker: boolean,
106109
): string => {
107110
if (pattern) {
@@ -113,6 +116,9 @@ export const getDefaultPattern = (
113116
if (timePicker) {
114117
return getTimeFormat(is24, enableSeconds);
115118
}
119+
if (weekPicker) {
120+
return 'MM/dd/yyyy';
121+
}
116122
return enableTimePicker ? `MM/dd/yyyy, ${getTimeFormat(is24, enableSeconds)}` : 'MM/dd/yyyy';
117123
};
118124

@@ -265,14 +271,20 @@ export const dateToUtc = (date: Date): string => {
265271
};
266272

267273
export const isDateBetween = (range: Date[], hoverDate: Date, dateToCheck: Date): boolean => {
268-
if (range[0] && range[1]) {
274+
if (range && range[0] && range[1]) {
269275
return isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, range[1]);
270276
}
271-
if (range[0] && hoverDate) {
277+
if (range && range[0] && hoverDate) {
272278
return (
273279
(isDateAfter(dateToCheck, range[0]) && isDateBefore(dateToCheck, hoverDate)) ||
274280
(isDateBefore(dateToCheck, range[0]) && isDateAfter(dateToCheck, hoverDate))
275281
);
276282
}
277283
return false;
278284
};
285+
286+
export const getWeekFromDate = (date: Date, weekStartsOn: WeekStartNum): [Date, Date] => {
287+
const start = startOfWeek(date, { weekStartsOn });
288+
const end = endOfWeek(date, { weekStartsOn });
289+
return [start, end];
290+
};

tests/unit/logic.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import MonthYearInput from '../../src/Vue3DatePicker/components/MonthYearInput.v
1111
import ActionRow from '../../src/Vue3DatePicker/components/ActionRow.vue';
1212

1313
import { setMilliseconds, setSeconds } from 'date-fns';
14+
import { getWeekFromDate } from '../../src/Vue3DatePicker/utils/date-utils';
1415

1516
const format = (date: Date): string => {
1617
return `Selected year is ${date.getFullYear()}`;
@@ -242,4 +243,16 @@ describe('Logic connection', () => {
242243
expect(dp.vm.internalModelValue).toEqual(range);
243244
dp.unmount();
244245
});
246+
247+
it('Should select week', async () => {
248+
const today = new Date();
249+
const weekRange = getWeekFromDate(today, 1);
250+
const { dp, menu } = await mountDatepicker({ modelValue: null, weekPicker: true });
251+
252+
const calendar = menu.findComponent(Calendar);
253+
calendar.vm.$emit('selectDate', { value: today, current: true });
254+
255+
expect(dp.vm.internalModelValue).toHaveLength(2);
256+
expect(dp.vm.internalModelValue).toEqual(weekRange);
257+
});
245258
});

tests/unit/utils.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ describe('Utils and date utils formatting', () => {
6363
});
6464

6565
it('Should get default pattern', () => {
66-
const patternDef = getDefaultPattern(null, true, false, false, false, true);
67-
const patternMonthPicker = getDefaultPattern(null, true, false, true, false, false);
66+
const patternDef = getDefaultPattern(null, true, false, false, false, false, true);
67+
const patternMonthPicker = getDefaultPattern(null, true, false, true, false, false, false);
6868

6969
expect(patternDef).toEqual('MM/dd/yyyy, HH:mm');
7070
expect(patternMonthPicker).toEqual('MM/yyyy');

0 commit comments

Comments
 (0)