1122 lines
40 KiB
Vue
1122 lines
40 KiB
Vue
<template>
|
|
<div
|
|
class="date-picker text-center"
|
|
:class="[calendarViewDirection, { 'has-error': errors.length > 0 }]">
|
|
<fieldset
|
|
id="date-picker-fieldset"
|
|
ref="datePickerFieldset">
|
|
<legend class="sr-only">
|
|
Select a day and time
|
|
</legend>
|
|
<div
|
|
v-for="month in months"
|
|
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
|
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
|
class="calendar-grid-container"
|
|
:class="[
|
|
hideSomeDaysForInitialView
|
|
? 'partial-month-initial-view'
|
|
: '',
|
|
month.monthClass
|
|
]">
|
|
<div
|
|
class="month-year body-small d-flex align-items-center small">
|
|
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
|
|
</div>
|
|
<div
|
|
v-if="calendarViewDirection === 'future'"
|
|
class="legend caption d-flex align-items-center justify-content-end">
|
|
<span class="legend-circle me-1"></span> = Available
|
|
</div>
|
|
<div class="separator-line"></div>
|
|
<div class="nav-back ps-3">
|
|
<button></button>
|
|
</div>
|
|
<div class="nav-forward pe-3">
|
|
<button></button>
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Sunday</span>S
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Monday</span>M
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Tuesday</span>T
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Wednesday</span>W
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Thursday</span>T
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Friday</span>F
|
|
</div>
|
|
<div class="grid-item caption">
|
|
<span class="sr-only">Saturday</span>S
|
|
</div>
|
|
<div
|
|
v-for="date in month.dates"
|
|
:id="date.inputValue"
|
|
:key="date.inputValue"
|
|
class="grid-item radio-wrapper"
|
|
:class="[
|
|
date.dateNum === 1
|
|
? 'first-day-' + month.startDateDayIndex
|
|
: '',
|
|
date.dayClasses,
|
|
date.isSelectable ? 'selectable-day' : ''
|
|
]">
|
|
<input
|
|
:id="`${month.monthLabel}-${date.dateNum.toString()}`"
|
|
v-model="selectedDate"
|
|
:disabled="!date.isSelectable"
|
|
type="radio"
|
|
name="day-of-month"
|
|
:value="date.inputValue"
|
|
@click="fireDateSelectedEvent"
|
|
@keypress.enter="fireDateSelectedEvent" />
|
|
<label
|
|
:aria-label="getDateLabel(date, month)"
|
|
:for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
|
<span aria-hidden="true">{{ date.dateNum.toString() }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="date-of-month-error"
|
|
class="row form-test-error">
|
|
<ErrorMessage
|
|
:name="customComponentId"
|
|
class="small mt-1">
|
|
</ErrorMessage>
|
|
</div>
|
|
<button
|
|
v-if="
|
|
calendarViewDirection === 'future' &&
|
|
!disableViewMoreDatesButton
|
|
"
|
|
id="viewMoreDates"
|
|
type="button"
|
|
class="btn btn-link"
|
|
:disabled="isLoading"
|
|
@click="showAnotherMonth">
|
|
View more dates
|
|
</button>
|
|
</fieldset>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// Supporting files
|
|
import { useMainStore } from '@/store';
|
|
import { useField, ErrorMessage } from 'vee-validate';
|
|
import { deepClone } from '@/helpers/object-helper';
|
|
import {
|
|
convertDateStringToDate,
|
|
convertDateToDateString
|
|
} from '@/helpers/date-helper';
|
|
import {
|
|
TIMINGFUNC_MAP,
|
|
BUFFER_OFFSET,
|
|
MONTHS_OF_YEAR
|
|
} from './mixins/constants';
|
|
import { selectableDaysOptions, requiredParameter } from './mixins/helpers';
|
|
|
|
export default {
|
|
name: 'date-picker',
|
|
components: {
|
|
ErrorMessage
|
|
},
|
|
props: {
|
|
customComponentId: String,
|
|
selectableDatesSetting: {
|
|
type: String,
|
|
validator(value) {
|
|
return Object.values(selectableDaysOptions).includes(value);
|
|
},
|
|
default: selectableDaysOptions.PAST
|
|
},
|
|
modelValue: {
|
|
type: Object
|
|
},
|
|
todayOverrideDateString: {
|
|
// keep for use in unit tests to override today's date
|
|
type: String,
|
|
default: null
|
|
},
|
|
customSelectableDatesCallback: {
|
|
type: Function,
|
|
default() {
|
|
return [];
|
|
}
|
|
},
|
|
validationRules: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
emits: ['update:modelValue', 'date-clicked'],
|
|
setup(props) {
|
|
const uuid = crypto.randomUUID();
|
|
const componentId = !props.customComponentId
|
|
? `component-${uuid}`
|
|
: props.customComponentId;
|
|
|
|
const mainStore = useMainStore();
|
|
|
|
const { modelValue } = deepClone(props);
|
|
const initialValue = modelValue;
|
|
|
|
const fieldOptions = {
|
|
value: modelValue,
|
|
initialValue
|
|
};
|
|
|
|
const {
|
|
errorMessage,
|
|
handleBlur,
|
|
handleChange,
|
|
meta,
|
|
validate,
|
|
errors,
|
|
resetField
|
|
} = useField(componentId, props.validationRules, fieldOptions);
|
|
|
|
return {
|
|
componentId,
|
|
mainStore,
|
|
errorMessage,
|
|
handleBlur,
|
|
handleChange,
|
|
validate,
|
|
meta,
|
|
errors,
|
|
resetField
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
isLoading: true,
|
|
months: null,
|
|
disableViewMoreDatesButton: false,
|
|
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
|
hideSomeDaysForInitialView: null
|
|
};
|
|
},
|
|
computed: {
|
|
todayString() {
|
|
return (
|
|
this.todayOverrideDateString
|
|
|| convertDateToDateString(new Date())
|
|
);
|
|
},
|
|
todayDayIndex() {
|
|
return convertDateStringToDate(this.todayString).getDay();
|
|
},
|
|
todayDateNum() {
|
|
return convertDateStringToDate(this.todayString).getDate();
|
|
},
|
|
currentWeekStartDateNum() {
|
|
return this.todayDayIndex >= this.todayDateNum
|
|
? 1
|
|
: this.todayDateNum - this.todayDayIndex;
|
|
},
|
|
currentWeekEndDateNum() {
|
|
return this.todayDateNum + (6 - this.todayDayIndex);
|
|
},
|
|
calendarViewDirection() {
|
|
if (this.selectableDatesSetting === 'custom') return 'future';
|
|
return 'past';
|
|
},
|
|
selectedDate: {
|
|
get() {
|
|
return this.modelValue;
|
|
},
|
|
set(newSelectedDate) {
|
|
this.$emit('update:modelValue', newSelectedDate);
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
modelValue(newValue, oldValue) {
|
|
this.resetField({
|
|
value: newValue
|
|
});
|
|
},
|
|
errorMessage(newValue, oldValue) {
|
|
// Check for changing from no error message to some error message.
|
|
if (newValue && !oldValue) {
|
|
this.scrollToElement('date-of-month-error');
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
initializeComponent(initialData) {
|
|
this.setCalendarData(initialData);
|
|
},
|
|
fireDateSelectedEvent(event) {
|
|
// Ignore if arrow key selected radioButton
|
|
if (event.screenX === 0 && event.screenY === 0) {
|
|
return;
|
|
}
|
|
this.$emit('date-clicked');
|
|
},
|
|
getWeekStartDate(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
// Subtract the day of the week from date to get the date of Sunday
|
|
const sunday = new Date(date);
|
|
sunday.setDate(sunday.getDate() - dayOfWeek);
|
|
return convertDateToDateString(sunday);
|
|
},
|
|
getWeekEndDate(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
|
// Clone the given date and add the remaining days until Saturday
|
|
const saturday = new Date(date);
|
|
saturday.setDate(date.getDate() + daysUntilSaturday);
|
|
return convertDateToDateString(saturday);
|
|
},
|
|
getNextWeekSunday(dateString) {
|
|
const date = convertDateStringToDate(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
|
// Clone the given date and add the remaining days until Sunday
|
|
const nextSunday = new Date(date);
|
|
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
|
return convertDateToDateString(nextSunday);
|
|
},
|
|
getMonthEnd(dateStr) {
|
|
// convert string to date, do date calc, then return a string back
|
|
const date = new Date(
|
|
dateStr.split('-')[0],
|
|
parseInt(dateStr.split('-')[1], 10),
|
|
0
|
|
);
|
|
return convertDateToDateString(date);
|
|
},
|
|
getInitialViewWeeks(
|
|
todayString,
|
|
initialViewRowsToShow,
|
|
preSelectedDateString
|
|
) {
|
|
// TODO: this only is for future direction; need to create logic for past direction
|
|
const weeks = [];
|
|
let weekStartDate = this.getWeekStartDate(todayString);
|
|
let weekEndDate = this.getWeekEndDate(todayString);
|
|
|
|
if (preSelectedDateString) {
|
|
const preSelectedDateMonthEnd = this.getMonthEnd(preSelectedDateString);
|
|
let weekIncludesPreSelectedMonthEnd = false;
|
|
let i = 0;
|
|
while (!weekIncludesPreSelectedMonthEnd) {
|
|
if (i > 0) {
|
|
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
|
weekEndDate = this.getWeekEndDate(weekStartDate);
|
|
|
|
if (
|
|
(preSelectedDateMonthEnd > weekStartDate
|
|
&& preSelectedDateMonthEnd < weekEndDate)
|
|
|| preSelectedDateMonthEnd === weekStartDate
|
|
|| preSelectedDateMonthEnd === weekEndDate
|
|
) {
|
|
weekEndDate = preSelectedDateMonthEnd;
|
|
weekIncludesPreSelectedMonthEnd = true;
|
|
}
|
|
}
|
|
weeks.push({
|
|
weekNum: i + 1,
|
|
weekStartDate,
|
|
weekEndDate
|
|
});
|
|
i++;
|
|
}
|
|
} else {
|
|
for (let i = 0; i < initialViewRowsToShow; i++) {
|
|
if (i > 0) {
|
|
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
|
weekEndDate = this.getWeekEndDate(weekStartDate);
|
|
}
|
|
weeks.push({
|
|
weekNum: i + 1,
|
|
weekStartDate,
|
|
weekEndDate
|
|
});
|
|
}
|
|
// If any of these weeks is split between two months, then make them 2 separate "weeks"
|
|
// (a week split between two months is considered 2 weeks per business requirements)
|
|
const hasSplitWeek = (week) =>
|
|
week.weekStartDate.split('-')[1]
|
|
!== week.weekEndDate.split('-')[1];
|
|
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
|
|
|
if (splitWeekIndex > -1) {
|
|
const week1 = [];
|
|
const week2 = [];
|
|
let switchToWeek2 = false;
|
|
|
|
for (let j = 0; j < 7; j++) {
|
|
const newDate = convertDateStringToDate(weeks[splitWeekIndex].weekStartDate);
|
|
newDate.setDate(newDate.getDate() + j);
|
|
if (newDate.getDate() === 1) switchToWeek2 = true;
|
|
if (switchToWeek2) {
|
|
week2.push(convertDateToDateString(newDate));
|
|
} else {
|
|
week1.push(convertDateToDateString(newDate));
|
|
}
|
|
}
|
|
|
|
const week1EndDate = week1[week1.length - 1];
|
|
const week2StartDate = week2[0];
|
|
|
|
if (week1EndDate < todayString) {
|
|
// replace week 1 with week 2
|
|
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
|
} else {
|
|
const newWeek = {
|
|
weekNum: weeks[splitWeekIndex].weekNum,
|
|
weekStartDate: week2StartDate,
|
|
weekEndDate: weeks[splitWeekIndex].weekEndDate
|
|
};
|
|
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
|
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
|
weeks.pop();
|
|
weeks.forEach((item, index) => {
|
|
if (index > splitWeekIndex) {
|
|
item.weekNum += 1;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return weeks;
|
|
},
|
|
async loadInitialData(config) {
|
|
/*
|
|
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
|
|
*/
|
|
let todayDateString;
|
|
let calendarViewDirection = 'none';
|
|
let hideSomeDaysForInitialView = false;
|
|
let hideSecondMonth = false;
|
|
|
|
if (this.todayString) {
|
|
todayDateString = this.todayString;
|
|
} else if (config.todayOverrideDateString) {
|
|
todayDateString = config.todayOverrideDateString;
|
|
} else {
|
|
todayDateString = convertDateToDateString(new Date());
|
|
}
|
|
if (config.selectableDatesSetting === 'past') calendarViewDirection = 'past';
|
|
if (config.selectableDatesSetting === 'custom') calendarViewDirection = 'future';
|
|
|
|
const currentMonthEnd = this.getMonthEnd(todayDateString);
|
|
// TODO - set up currentMonthStart if direction is PAST:
|
|
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
|
const initialViewWeeks = this.getInitialViewWeeks(
|
|
todayDateString,
|
|
config.initialViewRowsToShow,
|
|
config.preSelectedDate
|
|
);
|
|
const initialViewStartDate = todayDateString;
|
|
const initialViewEndDate =
|
|
initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
|
|
|
if (!config.preSelectedDate) {
|
|
const firstSaturdayMonth =
|
|
initialViewWeeks[0].weekEndDate.split('-')[1];
|
|
const lastSundayMonth =
|
|
initialViewWeeks[
|
|
initialViewWeeks.length - 1
|
|
].weekStartDate.split('-')[1];
|
|
|
|
if (calendarViewDirection === 'future') {
|
|
if (firstSaturdayMonth !== lastSundayMonth) {
|
|
hideSomeDaysForInitialView = true;
|
|
}
|
|
if (
|
|
initialViewStartDate.split('-')[1] === lastSundayMonth
|
|
) {
|
|
hideSecondMonth = true;
|
|
if (currentMonthEnd > initialViewEndDate) {
|
|
// should part of 1st month be hidden?
|
|
hideSomeDaysForInitialView = true;
|
|
}
|
|
}
|
|
}
|
|
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE THE ABOVE ^ ^ ^
|
|
}
|
|
|
|
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
|
const response = config.customSelectableDatesCallback(
|
|
initialViewStartDate,
|
|
initialViewEndDate,
|
|
useMainStore().order.serviceLocation.appointmentType,
|
|
useMainStore().order.serviceLocation.provider.providerNumber
|
|
);
|
|
resolve(response);
|
|
});
|
|
|
|
return loadInitialDataPromise.then((response) => {
|
|
const initialData = {
|
|
todayDate: todayDateString,
|
|
initialViewStartDate,
|
|
initialViewEndDate,
|
|
calendarViewDirection,
|
|
initialShopTimeSlotsResponse: response,
|
|
hideSomeDaysForInitialView,
|
|
hideSecondMonth,
|
|
preSelectedDate: config.preSelectedDate
|
|
};
|
|
return initialData;
|
|
});
|
|
},
|
|
async setCalendarData(config = {}) {
|
|
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
|
const { hideSecondMonth } = config;
|
|
const direction = config.calendarViewDirection;
|
|
const monthsAfterToLoadOffset = 6;
|
|
const monthsBeforeToLoadOffset = 36;
|
|
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
|
this.selectableDatesData.push(selectableDate);
|
|
});
|
|
|
|
// GENERATE MONTHS AND PUSH THEM INTO ARRAY
|
|
const months = [];
|
|
const options = {
|
|
calendarViewDirection: direction,
|
|
monthsBeforeToLoadOffset,
|
|
monthsAfterToLoadOffset,
|
|
initialViewStartDate: config.initialViewStartDate,
|
|
initialViewEndDate: config.initialViewEndDate,
|
|
hideSecondMonth,
|
|
preSelectedDate: config.preSelectedDate
|
|
};
|
|
if (direction === 'future') {
|
|
// first 0, then 1
|
|
for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
|
|
months.push(await this.getMonthData(i, options));
|
|
}
|
|
} else if (direction === 'past') {
|
|
// first 0, then -1
|
|
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
|
|
months.unshift(await this.getMonthData(i, options));
|
|
}
|
|
} else {
|
|
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
|
|
// for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {
|
|
// months.push(this.getMonthDataPAST(i));
|
|
// }
|
|
}
|
|
this.months = months;
|
|
this.isLoading = false;
|
|
|
|
if (config.preSelectedDate) {
|
|
this.$nextTick(() => {
|
|
// Advance to month
|
|
const monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes('month-preselected'));
|
|
if (
|
|
monthToShow.monthClass.includes('month-preselected')
|
|
&& monthToShow.monthClass.includes('last-available-month')
|
|
) {
|
|
// disable View More dates button if the preSelectedDate in the last available month
|
|
this.disableViewMoreDatesButton = true;
|
|
}
|
|
this.scrollToElement(monthToShow.monthString);
|
|
});
|
|
}
|
|
},
|
|
async getMonthData(offset = requiredParameter(), options) {
|
|
/* options will contain:
|
|
calendarViewDirection (string)
|
|
initialViewStartDateNum (number)
|
|
initialViewEndDateNum (number)
|
|
monthsBeforeToLoadOffset (number),
|
|
monthsAfterToLoadOffset (number),
|
|
hideSecondMonth (boolean),
|
|
preSelectedDate (string)
|
|
|
|
data used:
|
|
todayDate (date object)
|
|
this.hideSomeDaysForInitialView (string)
|
|
*/
|
|
|
|
let monthNum =
|
|
convertDateStringToDate(this.todayString).getMonth()
|
|
+ offset
|
|
+ 1;
|
|
let yearNum = convertDateStringToDate(this.todayString).getFullYear();
|
|
const { calendarViewDirection } = options;
|
|
if (calendarViewDirection === 'future' && offset > 0) {
|
|
while (monthNum > 12) {
|
|
monthNum -= 12;
|
|
yearNum++;
|
|
}
|
|
} else if (calendarViewDirection === 'past' && offset < 0) {
|
|
while (monthNum < 1) {
|
|
monthNum = 12 + monthNum;
|
|
yearNum--;
|
|
}
|
|
}
|
|
const { initialViewEndDate } = options;
|
|
// const initialViewStartDate = options.initialViewStartDate; // TODO: to be used for past calendarViewDirection
|
|
const { hideSecondMonth } = options;
|
|
const preSelectedDateObj = options.preSelectedDate
|
|
? convertDateStringToDate(options.preSelectedDate)
|
|
: null;
|
|
const dates = [];
|
|
const monthEndDate = new Date(yearNum, monthNum, 0);
|
|
const monthStartDateNum =
|
|
offset === 0 && calendarViewDirection === 'future'
|
|
? this.currentWeekStartDateNum
|
|
: 1;
|
|
const monthStartDate = new Date(
|
|
yearNum,
|
|
monthNum - 1,
|
|
monthStartDateNum
|
|
);
|
|
const startDateDayIndex = monthStartDate.getDay();
|
|
|
|
let monthClass = '';
|
|
let isMonthThatHidesSomeDaysForInitialView;
|
|
let monthEndDateNum = monthEndDate.getDate();
|
|
|
|
if (
|
|
offset === 0
|
|
&& calendarViewDirection === 'past'
|
|
&& monthEndDateNum > this.currentWeekEndDateNum
|
|
) {
|
|
monthEndDateNum = this.currentWeekEndDateNum;
|
|
}
|
|
|
|
if (options.preSelectedDate) {
|
|
if (
|
|
monthStartDate.getFullYear()
|
|
=== preSelectedDateObj.getFullYear()
|
|
&& monthStartDate.getMonth() === preSelectedDateObj.getMonth()
|
|
) {
|
|
monthClass += ' month-preselected';
|
|
} else if (monthStartDate > preSelectedDateObj) {
|
|
monthClass += ' month-hidden';
|
|
}
|
|
} else if (Math.abs(offset) === 1 && hideSecondMonth) {
|
|
monthClass = `${monthClass} month-hidden`;
|
|
} else if (Math.abs(offset) > 1) {
|
|
monthClass = `${monthClass} month-hidden`;
|
|
}
|
|
|
|
if (
|
|
Math.abs(offset) === options.monthsAfterToLoadOffset
|
|
&& calendarViewDirection === 'future'
|
|
) {
|
|
monthClass += ' last-available-month';
|
|
}
|
|
if (
|
|
Math.abs(offset) === options.monthsBeforeToLoadOffset
|
|
&& calendarViewDirection === 'past'
|
|
) {
|
|
// TODO - re-check this logic if past direction
|
|
monthClass += ' last-available-month';
|
|
}
|
|
|
|
// populate dates array
|
|
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
|
|
let dayClasses = '';
|
|
const dateString = `${yearNum.toString()}-${`0${monthNum}`.slice(-2)}-${`0${i}`.slice(-2)}`;
|
|
|
|
if (offset === 0 && i === this.todayDateNum) {
|
|
dayClasses += ' current-day';
|
|
}
|
|
if (
|
|
offset === 0
|
|
&& i < this.todayDateNum
|
|
&& calendarViewDirection === 'future'
|
|
) {
|
|
dayClasses += ' unavailable-day';
|
|
}
|
|
if (
|
|
offset === 0
|
|
&& i > this.todayDateNum
|
|
&& calendarViewDirection === 'past'
|
|
) {
|
|
dayClasses += ' unavailable-day';
|
|
}
|
|
if (convertDateStringToDate(dateString).getDay() === 0) {
|
|
dayClasses += ' sunday';
|
|
}
|
|
if (
|
|
this.hideSomeDaysForInitialView
|
|
&& convertDateStringToDate(initialViewEndDate).getMonth()
|
|
+ 1
|
|
=== monthNum
|
|
&& convertDateStringToDate(initialViewEndDate).getDate() < i
|
|
) {
|
|
dayClasses += ' day-hidden';
|
|
isMonthThatHidesSomeDaysForInitialView = true;
|
|
}
|
|
const dateObject = {
|
|
dateNum: i,
|
|
dayClasses,
|
|
inputValue: dateString,
|
|
isSelectable:
|
|
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
|
};
|
|
dates.push(dateObject);
|
|
}
|
|
|
|
const monthToAdd = {
|
|
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
|
|
monthIndex: monthNum,
|
|
monthString: `${
|
|
MONTHS_OF_YEAR[monthNum - 1]
|
|
}-${yearNum?.toString()}`,
|
|
yearNum,
|
|
dates,
|
|
startDateDayIndex,
|
|
monthClass,
|
|
isMonthThatHidesSomeDaysForInitialView
|
|
};
|
|
return monthToAdd;
|
|
},
|
|
async showAnotherMonth() {
|
|
this.isLoading = true;
|
|
let monthToShow;
|
|
let monthStartDateNum = 0;
|
|
|
|
if (this.hideSomeDaysForInitialView) {
|
|
monthToShow = this.months.find(({ isMonthThatHidesSomeDaysForInitialView }) =>
|
|
isMonthThatHidesSomeDaysForInitialView);
|
|
// find the first day-hidden to become the next api call start date
|
|
monthStartDateNum =
|
|
monthToShow.dates.find(({ dayClasses }) =>
|
|
dayClasses.includes('day-hidden')).dateNum - 1;
|
|
} else {
|
|
if (this.calendarViewDirection === 'future') {
|
|
monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes('month-hidden'));
|
|
}
|
|
if (this.calendarViewDirection === 'past') {
|
|
// TODO: UPDATE THIS WITH CORRECT PAST LOOKING LOGIC
|
|
monthToShow = this.months.find((month) =>
|
|
month.monthClass.includes('month-hidden'));
|
|
}
|
|
}
|
|
if (monthToShow) {
|
|
// make new API call with this month's start and end dates
|
|
await this.updateSelectableDates(
|
|
monthToShow.dates[monthStartDateNum].inputValue,
|
|
monthToShow.dates[monthToShow.dates.length - 1].inputValue
|
|
);
|
|
this.isLoading = false;
|
|
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
|
|
monthToShow.monthClass = monthToShow.monthClass.replace(
|
|
' month-hidden',
|
|
''
|
|
);
|
|
this.scrollToElement(monthToShow.monthString);
|
|
|
|
if (monthToShow.monthClass.includes('last-available-month')) this.disableViewMoreDatesButton = true;
|
|
}
|
|
},
|
|
async updateSelectableDates(monthStart, monthEnd) {
|
|
const moreSelectableDates =
|
|
await this.customSelectableDatesCallback(
|
|
monthStart,
|
|
monthEnd,
|
|
this.mainStore.order.serviceLocation.appointmentType,
|
|
this.mainStore.order.serviceLocation.provider.providerNumber
|
|
);
|
|
|
|
moreSelectableDates.days.forEach((selectableDate) => {
|
|
const index = this.selectableDatesData.findIndex((dateObj) => dateObj.date === selectableDate.date);
|
|
if (index === -1) this.selectableDatesData.push(selectableDate);
|
|
this.months.forEach((month) => {
|
|
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
|
|
month.dates.forEach((date) => {
|
|
if (date.inputValue === selectableDate.date) {
|
|
date.isSelectable = true;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
},
|
|
scrollToElement(elementId, duration = 800, easing = 'ease-in-out') {
|
|
const wrapper = document.querySelector('.fade-on-route-transition');
|
|
const target = document.getElementById(elementId);
|
|
const initY = wrapper.scrollTop;
|
|
const wrapperRect = wrapper.getBoundingClientRect();
|
|
const targetRect = target.getBoundingClientRect();
|
|
const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET;
|
|
const timingFunc = TIMINGFUNC_MAP[easing];
|
|
let start = null;
|
|
const step = (timestamp) => {
|
|
start = start || timestamp;
|
|
const time = Math.min(1, (timestamp - start) / duration);
|
|
const percentageNew = timingFunc(time);
|
|
const distanceToGo = targetY;
|
|
const thisDistance = percentageNew * distanceToGo;
|
|
wrapper.scrollTo(0, initY + thisDistance);
|
|
if (percentageNew < 1) {
|
|
window.requestAnimationFrame(step);
|
|
}
|
|
};
|
|
window.requestAnimationFrame(step);
|
|
},
|
|
getDateLabel(date, month) {
|
|
const dateStr = `${month.yearNum?.toString()}-${month.monthIndex?.toString()}-${date.dateNum.toString()}`;
|
|
const dateObj = convertDateStringToDate(dateStr);
|
|
const labelOptions = {
|
|
weekday: 'long',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
};
|
|
let label = dateObj.toLocaleDateString('en-US', labelOptions);
|
|
date.isSelectable ? label += ', available' : label += ', unavailable';
|
|
return label;
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/styles/ux-variables-svg-strings.scss';
|
|
|
|
.date-picker-hidden {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
}
|
|
.date-picker {
|
|
position: relative;
|
|
flex-grow: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
fieldset {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
}
|
|
.calendar-grid-container {
|
|
margin: 0 auto 2rem auto;
|
|
max-width: 414px;
|
|
position: relative;
|
|
transition: height ease 2s, opacity ease 2s;
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 0 0.75rem;
|
|
opacity: 1;
|
|
|
|
.grid-item {
|
|
text-align: center;
|
|
margin: 10px 3px;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
|
|
&.first-day-,
|
|
&.first-day-0 {
|
|
grid-column-start: 1;
|
|
}
|
|
&.first-day-1 {
|
|
grid-column-start: 2;
|
|
}
|
|
&.first-day-2 {
|
|
grid-column-start: 3;
|
|
}
|
|
&.first-day-3 {
|
|
grid-column-start: 4;
|
|
}
|
|
&.first-day-4 {
|
|
grid-column-start: 5;
|
|
}
|
|
&.first-day-5 {
|
|
grid-column-start: 6;
|
|
}
|
|
&.first-day-6 {
|
|
grid-column-start: 7;
|
|
}
|
|
}
|
|
|
|
.separator-line {
|
|
grid-area: 2/1/2/8;
|
|
border-top: 1px solid $gray-500;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.month-year {
|
|
grid-area: 1 / 1 / 2 / 5;
|
|
text-transform: uppercase;
|
|
font-weight: 300;
|
|
}
|
|
.legend {
|
|
grid-area: 1 / 5 / 2 / 8;
|
|
.legend-circle {
|
|
border-radius: 50%;
|
|
width: 16px;
|
|
height: 16px;
|
|
background-color: $blue-100;
|
|
border: 1px solid $blue;
|
|
}
|
|
}
|
|
.nav-back,
|
|
.nav-forward {
|
|
display: none;
|
|
}
|
|
|
|
.radio-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
outline: none;
|
|
height: 1.5rem;
|
|
opacity: 1;
|
|
transition: height ease 250ms, opacity ease 250ms;
|
|
|
|
input[type='radio'] {
|
|
position: absolute; //override bootstrap
|
|
height: 0;
|
|
opacity: 0;
|
|
|
|
&:focus-visible + label {
|
|
box-shadow: 0 0 0 2.5px $blue;
|
|
}
|
|
|
|
&:focus + label,
|
|
&:checked:focus + label {
|
|
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
|
|
background-color: $blue;
|
|
color: $white;
|
|
&.current-day {
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
|
|
&:checked + label {
|
|
color: $white;
|
|
background: $blue;
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
|
|
.first-day {
|
|
position: absolute;
|
|
font-size: 9px;
|
|
font-weight: 500;
|
|
color: $gray-600;
|
|
top: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
label {
|
|
position: relative;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-width: 2.5rem;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
|
|
span {
|
|
&.small {
|
|
font-size: 0.75rem;
|
|
color: $gray-550;
|
|
}
|
|
}
|
|
|
|
&:hover,
|
|
&:checked {
|
|
@include media-breakpoint-up(sm) {
|
|
box-shadow: 0 0 0 4px transparent;
|
|
background-color: $blue;
|
|
color: $white;
|
|
&:after {
|
|
background-color: $white;
|
|
}
|
|
}
|
|
cursor: pointer;
|
|
&.current-day {
|
|
+ .first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
.first-day {
|
|
color: $white;
|
|
}
|
|
}
|
|
+ p {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
&.selectable-day {
|
|
label {
|
|
color: $blue;
|
|
background-color: $blue-100;
|
|
border: 1px solid $blue;
|
|
min-width: 2.5rem;
|
|
width: 2.5rem;
|
|
border-radius: 50%;
|
|
}
|
|
}
|
|
&.unavailable-day {
|
|
label {
|
|
color: $gray-500;
|
|
background-color: $gray-100;
|
|
border: none;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
&.unavailable-day:not(&.sunday) {
|
|
label {
|
|
&::before {
|
|
content: '';
|
|
display: inline-block;
|
|
position: absolute;
|
|
left: -1rem;
|
|
width: 1rem;
|
|
height: 2.5rem;
|
|
background: $gray-100;
|
|
}
|
|
}
|
|
}
|
|
&.current-day {
|
|
label {
|
|
font-weight: 500;
|
|
color: $black;
|
|
|
|
&:after {
|
|
content: '';
|
|
width: 0.25rem;
|
|
height: 0.25rem;
|
|
border-radius: 50%;
|
|
background-color: $black;
|
|
position: absolute;
|
|
top: 28px;
|
|
}
|
|
.first-day {
|
|
color: $blue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&.partial-month-initial-view {
|
|
.day-hidden {
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
margin: 0;
|
|
max-height: 0;
|
|
}
|
|
}
|
|
&.month-hidden {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
margin-bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
.btn-link {
|
|
font-weight: 500;
|
|
text-underline-offset: 4px;
|
|
flex-grow: 0;
|
|
&:focus {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
.past {
|
|
.calendar-grid-container {
|
|
.month-year {
|
|
grid-area: 1 / 1 / 2 / 6;
|
|
}
|
|
.nav-forward {
|
|
grid-area: 1 / 7 / 2 / 8;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
button {
|
|
&:after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 7px;
|
|
height: 12px;
|
|
background-image: url($svg-date-picker-forward-button);
|
|
}
|
|
}
|
|
}
|
|
.nav-back {
|
|
grid-area: 1 / 6 / 2 / 7;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
button {
|
|
&:after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 7px;
|
|
height: 12px;
|
|
background-image: url($svg-date-picker-nav-back-button);
|
|
}
|
|
}
|
|
}
|
|
.nav-back,
|
|
.nav-forward {
|
|
button {
|
|
position: relative;
|
|
border-radius: 50%;
|
|
width: 28px;
|
|
height: 28px;
|
|
background-color: $blue-100;
|
|
border: 1px solid $blue;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
&:focus,
|
|
&:focus-visible {
|
|
border: 2.5px solid $blue;
|
|
box-shadow: none;
|
|
background-color: $blue-100;
|
|
color: $white;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.btn-link {
|
|
display: block;
|
|
position: relative;
|
|
height: 2rem;
|
|
width: 100%;
|
|
justify-content: center;
|
|
background: transparent;
|
|
border: none;
|
|
font-weight: 500;
|
|
letter-spacing: inherit;
|
|
text-underline-offset: 4px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.form-test-error {
|
|
margin: 0 auto;
|
|
max-width: 414px;
|
|
text-align: left;
|
|
}
|
|
</style>
|