Last active
July 18, 2025 08:28
-
-
Save gregg-cbs/5a401f03d6b53c7a8d5587c0269a27be to your computer and use it in GitHub Desktop.
svelte datepicker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export const clickOutside = (node, config = {}) => { | |
| const options = { | |
| include: [], | |
| onClickOutside: () => {}, | |
| ...config | |
| }; | |
| const detect = ({ target }) => { | |
| if (!node.contains(target) || options.include.some((i) => target.isSameNode(i))) { | |
| options.onClickOutside(); | |
| } | |
| }; | |
| document.addEventListener('click', detect, { passive: true, capture: true }); | |
| return { | |
| destroy() { | |
| document.removeEventListener('click', detect); | |
| } | |
| }; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { SvelteComponent } from 'svelte'; | |
| export interface DatePickerProps { | |
| /** | |
| * Represents the start date for a date picker. | |
| * @default null | |
| */ | |
| startDate?: any; | |
| /** | |
| * Represents the end date for a date picker. | |
| * @default null | |
| */ | |
| endDate?: any; | |
| /** | |
| * Represents the start time for the date picker (in HH:mm format). | |
| * @default '00:00' | |
| */ | |
| startDateTime?: string; | |
| /** | |
| * Represents the end time for the date picker (in HH:mm format). | |
| * @default '00:00' | |
| */ | |
| endDateTime?: string; | |
| /** | |
| * Represents the current date. | |
| */ | |
| today?: Date; | |
| /** | |
| * Represents the default year for the date picker. | |
| */ | |
| defaultYear?: number; | |
| /** | |
| * Represents the default month for the date picker. | |
| */ | |
| defaultMonth?: number; | |
| /** | |
| * Represents the start day of the week (0 for Sunday, 1 for Monday, etc.). | |
| */ | |
| startOfWeek?: number; | |
| /** | |
| * Indicates whether the date picker has multiple panes. | |
| */ | |
| isMultipane?: boolean; | |
| /** | |
| * Indicates whether the date picker is in range mode. | |
| */ | |
| isRange?: boolean; | |
| /** | |
| * Indicates whether the date picker is open. | |
| */ | |
| isOpen?: boolean; | |
| /** | |
| * Specifies the alignment of the date picker (e.g., 'left', 'center', 'right'). | |
| */ | |
| align?: string; | |
| /** | |
| * Represents the theme of the date picker. | |
| */ | |
| theme?: string; | |
| /** | |
| * An array of disabled dates. | |
| */ | |
| disabledDates?: string[]; | |
| /** | |
| * An array of enabled dates. | |
| */ | |
| enabledDates?: string[]; | |
| /** | |
| * Callback function to handle when the date change events. | |
| */ | |
| onDateChange?: (event: Object) => void; | |
| /** | |
| * Callback function to handle day click events. | |
| */ | |
| onDayClick?: (event: Object) => void; | |
| /** | |
| * Callback function to handle the navigation click event for months and years | |
| * @type {(event: Object) => void} | |
| */ | |
| onNavigationChange?: (event: Object) => void; | |
| /** | |
| * Indicates whether the date picker should always be shown. | |
| */ | |
| alwaysShow?: boolean; | |
| /** | |
| * Indicates whether year controls are displayed in the date picker. | |
| */ | |
| showYearControls?: boolean; | |
| /** | |
| * Indicates whether preset options are displayed in the date picker. | |
| */ | |
| showPresets?: boolean; | |
| /** | |
| * Indicates whether preset options should only be shown for range pickers. | |
| */ | |
| showPresetsOnly?: boolean; | |
| /** | |
| * Indicates whether the time picker is shown in the date picker. | |
| */ | |
| showTimePicker?: boolean; | |
| /** | |
| * Indicates whether future dates are enabled. | |
| */ | |
| enableFutureDates?: boolean; | |
| /** | |
| * Indicates whether past dates are enabled. | |
| */ | |
| enablePastDates?: boolean; | |
| /** | |
| * An array of preset date range labels. | |
| */ | |
| presetLabels?: string[]; | |
| /** | |
| * An array of preset date ranges with labels and start/end timestamps. | |
| */ | |
| presetRanges?: Object[]; | |
| /** | |
| * An array of day-of-week labels. | |
| */ | |
| dowLabels?: string[]; | |
| /** | |
| * An array of month labels. | |
| */ | |
| monthLabels?: string[]; | |
| /** | |
| * Determines if the default font "Rubik" should be loaded. | |
| */ | |
| includeFont?: boolean; | |
| } | |
| export interface DatePickerEvents { | |
| [key: string]: any; | |
| } | |
| export interface DatePickerSlots { | |
| default?: {}; | |
| } | |
| export default class DatePicker extends SvelteComponent<DatePickerProps, DatePickerEvents, DatePickerSlots> {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script lang="ts"> | |
| // https://github.com/svelte-plugins/datepicker | |
| import { onMount, tick } from 'svelte'; | |
| import { clickOutside } from './actions'; | |
| type DatePickerProps= { | |
| /** | |
| * Represents the start date for a date picker. | |
| * @default null | |
| */ | |
| startDate?: any; | |
| /** | |
| * Represents the end date for a date picker. | |
| * @default null | |
| */ | |
| endDate?: any; | |
| /** | |
| * Represents the start time for the date picker (in HH:mm format). | |
| * @default '00:00' | |
| */ | |
| startDateTime?: string; | |
| /** | |
| * Represents the end time for the date picker (in HH:mm format). | |
| * @default '00:00' | |
| */ | |
| endDateTime?: string; | |
| /** | |
| * Represents the current date. | |
| */ | |
| today?: Date; | |
| /** | |
| * Represents the default year for the date picker. | |
| */ | |
| defaultYear?: number; | |
| /** | |
| * Represents the default month for the date picker. | |
| */ | |
| defaultMonth?: number; | |
| /** | |
| * Represents the start day of the week (0 for Sunday, 1 for Monday, etc.). | |
| */ | |
| startOfWeek?: number; | |
| /** | |
| * Indicates whether the date picker has multiple panes. | |
| */ | |
| isMultipane?: boolean; | |
| /** | |
| * Indicates whether the date picker is in range mode. | |
| */ | |
| isRange?: boolean; | |
| /** | |
| * Indicates whether the date picker is open. | |
| */ | |
| isOpen?: boolean; | |
| /** | |
| * Specifies the alignment of the date picker (e.g., 'left', 'center', 'right'). | |
| */ | |
| align?: string; | |
| /** | |
| * Represents the theme of the date picker. | |
| */ | |
| theme?: string; | |
| /** | |
| * An array of disabled dates. | |
| */ | |
| disabledDates?: string[]; | |
| /** | |
| * An array of enabled dates. | |
| */ | |
| enabledDates?: string[]; | |
| /** | |
| * Callback function to handle when the date change events. | |
| */ | |
| onDateChange?: (event: Object) => void; | |
| /** | |
| * Callback function to handle day click events. | |
| */ | |
| onDayClick?: (event: Object) => void; | |
| /** | |
| * Callback function to handle the navigation click event for months and years | |
| * @type {(event: Object) => void} | |
| */ | |
| onNavigationChange?: (event: Object) => void; | |
| /** | |
| * Indicates whether the date picker should always be shown. | |
| */ | |
| alwaysShow?: boolean; | |
| /** | |
| * Indicates whether year controls are displayed in the date picker. | |
| */ | |
| showYearControls?: boolean; | |
| /** | |
| * Indicates whether preset options are displayed in the date picker. | |
| */ | |
| showPresets?: boolean; | |
| /** | |
| * Indicates whether preset options should only be shown for range pickers. | |
| */ | |
| showPresetsOnly?: boolean; | |
| /** | |
| * Indicates whether the time picker is shown in the date picker. | |
| */ | |
| showTimePicker?: boolean; | |
| /** | |
| * Indicates whether future dates are enabled. | |
| */ | |
| enableFutureDates?: boolean; | |
| /** | |
| * Indicates whether past dates are enabled. | |
| */ | |
| enablePastDates?: boolean; | |
| /** | |
| * An array of preset date range labels. | |
| */ | |
| presetLabels?: string[]; | |
| /** | |
| * An array of preset date ranges with labels and start/end timestamps. | |
| */ | |
| presetRanges?: Object[]; | |
| /** | |
| * An array of day-of-week labels. | |
| */ | |
| dowLabels?: string[]; | |
| /** | |
| * An array of month labels. | |
| */ | |
| monthLabels?: string[]; | |
| /** | |
| * Determines if the default font "Rubik" should be loaded. | |
| */ | |
| includeFont?: boolean; | |
| } | |
| let _today = new Date(); | |
| let { | |
| startDate = _today, | |
| endDate = null, | |
| startDateTime = '00:00', | |
| endDateTime = '00:00', | |
| today = _today, | |
| defaultYear = _today.getFullYear(), | |
| defaultMonth = _today.getMonth(), | |
| startOfWeek = 1, | |
| isMultipane = false, | |
| isRange = false, | |
| isOpen = $bindable(true), | |
| align = 'left', | |
| theme = '', | |
| disabledDates = [], | |
| enabledDates = [], | |
| onDateChange = () => {}, | |
| onDayClick = () => {}, | |
| onNavigationChange = () => {}, | |
| alwaysShow = true, | |
| showYearControls = true, | |
| showPresets = false, | |
| showPresetsOnly = false, | |
| showTimePicker = false, | |
| enableFutureDates = true, | |
| enablePastDates = false, | |
| presetLabels = ['Today', 'Last 7 Days', 'Last 30 Days', 'Last 60 Days', 'Last 90 Days', 'Last Year'], | |
| dowLabels = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], | |
| monthLabels = [ | |
| 'January', 'February', 'March', 'April', 'May', 'June', | |
| 'July', 'August', 'September', 'October', 'November', 'December' | |
| ], | |
| includeFont = true, | |
| presetRanges = [], | |
| }: DatePickerProps = $props() | |
| export const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000 | |
| export const getDateFromToday = (days: number) => { | |
| return Date.now() - days * MILLISECONDS_IN_DAY | |
| } | |
| // Only populate `presetRanges` if not provided | |
| if (presetRanges.length === 0) { | |
| presetRanges = [ | |
| { label: presetLabels[0], start: getDateFromToday(0), end: getDateFromToday(0) }, | |
| { label: presetLabels[1], start: getDateFromToday(6), end: getDateFromToday(0) }, | |
| { label: presetLabels[2], start: getDateFromToday(29), end: getDateFromToday(0) }, | |
| { label: presetLabels[3], start: getDateFromToday(59), end: getDateFromToday(0) }, | |
| { label: presetLabels[4], start: getDateFromToday(89), end: getDateFromToday(0) }, | |
| { label: presetLabels[5], start: getDateFromToday(364), end: getDateFromToday(0) } | |
| ] | |
| } | |
| /** | |
| * Initialization flag to delay some actions | |
| */ | |
| let initialize: boolean = $state(false); | |
| /** | |
| * Stores the possible end date for a date range. | |
| */ | |
| let tempEndDate: any = $state(); | |
| /** | |
| * Stores the start date for any revert operation. | |
| */ | |
| let prevStartDate: any = $state(); | |
| /** | |
| * Stores the end date for any revert operation. | |
| */ | |
| let prevEndDate: any = $state(); | |
| let startDateYear = $state(Number(defaultYear)); | |
| let startDateMonth = $state(Number(defaultMonth)); | |
| /** | |
| * Generates a calendar representation as a two-dimensional array. (Pulled from github.com/lukeed/calendarize) | |
| * | |
| * @param {Date} target - The target date for the calendar (defaults to the current date if not provided). | |
| * @param {number} offset - The offset for the first day of the week (0 for Sunday, 1 for Monday, etc.). | |
| * @returns {Array<Array<number>>} A two-dimensional array representing the calendar. | |
| */ | |
| const calendarize = (target, offset) => { | |
| const out = []; | |
| const date = new Date(target || new Date()); | |
| const year = date.getFullYear(); | |
| const month = date.getMonth(); | |
| const days = new Date(year, month + 1, 0).getDate(); | |
| let first = new Date(year, month, 1 - (offset | 0)).getDay(); | |
| let i = 0; | |
| let j = 0; | |
| let week; | |
| while (i < days) { | |
| for (j = 0, week = Array(7); j < 7; ) { | |
| while (j < first) { | |
| week[j++] = 0; | |
| } | |
| week[j++] = ++i > days ? 0 : i; | |
| first = 0; | |
| } | |
| out.push(week); | |
| } | |
| return out; | |
| }; | |
| /** | |
| * Creates a timestamp for a given year, month, and day. | |
| * | |
| * @param {number} year - The year. | |
| * @param {number} month - The month (0-11, where 0 is January and 11 is December). | |
| * @param {number} day - The day of the month. | |
| * @returns {number} - The timestamp representing the specified date. | |
| */ | |
| const createTimestamp = (year, month, day) => new Date(year, month, day).getTime(); | |
| /** | |
| * Gets the timestamp for a given date. | |
| * | |
| * @param {Date} date - The date. | |
| * @returns {number} - The timestamp of the date. | |
| */ | |
| const getTimestamp = (date) => new Date(date).getTime(); | |
| /** | |
| * Normalizes a timestamp by setting the time to midnight (00:00:00.000). | |
| * | |
| * @param {any} timestamp - The timestamp to normalize. | |
| * @returns {number} - The normalized timestamp. | |
| */ | |
| const normalizeTimestamp = (timestamp) => { | |
| const date = new Date(timestamp); | |
| date.setHours(0, 0, 0, 0); | |
| return date.getTime(); | |
| }; | |
| /** | |
| * Handles the click outside event of the date picker. | |
| */ | |
| const onClickOutside = () => { | |
| if (alwaysShow) { | |
| return; | |
| } | |
| if (prevStartDate && prevEndDate && startDate && !endDate) { | |
| startDate = prevStartDate; | |
| endDate = prevEndDate; | |
| prevStartDate = null; | |
| prevEndDate = null; | |
| } | |
| isOpen = false; | |
| }; | |
| const updateCalendars = () => { | |
| startDateCalendar = startDateCalendar; | |
| endDateCalendar = endDateCalendar; | |
| }; | |
| /** | |
| * Handles the navigation click event for months and years | |
| * @param {string} direction - The direction of the navigation (previous or next). | |
| * @param {string} type - The type of navigation (month or year). | |
| */ | |
| const onNavigation = async (direction, type) => { | |
| await tick(); | |
| const initial = new Date(defaultYear, defaultMonth); | |
| const initialDayOffMonth = '01'; | |
| let current = new Date(startDateYear, startDateMonth); | |
| let month = startDateMonth + 1; | |
| const calendar = isMultipane ? endDateCalendar : startDateCalendar; | |
| const lastWeekOfMonth = calendar[calendar.length - 1].filter(Boolean); | |
| const lastDayOfMonth = lastWeekOfMonth[lastWeekOfMonth.length - 1]; | |
| const currentPeriod = { | |
| start: `${startDateYear}-${month >= 10 ? month : `0${month}`}-${initialDayOffMonth}`, | |
| end: `${startDateYear}-${month >= 10 ? month : `0${month}`}-${lastDayOfMonth}` | |
| }; | |
| if (isMultipane) { | |
| month += 1; | |
| if (month > 11) { | |
| month = 1; | |
| } | |
| currentPeriod.end = `${endDateYear}-${month >= 10 ? month : `0${month}`}-${lastDayOfMonth}`; | |
| current = new Date(endDateYear, endDateMonth); | |
| } | |
| onNavigationChange({ | |
| direction, | |
| type, | |
| currentPeriod, | |
| isPastPeriod: current < initial | |
| }); | |
| }; | |
| /** | |
| * Handles the "to previous month" action in the date picker. | |
| */ | |
| const toPrev = () => { | |
| [startDateCalendar, next] = [prev, startDateCalendar]; | |
| if (--startDateMonth < 0) { | |
| startDateMonth = 11; | |
| startDateYear--; | |
| } | |
| onNavigation('previous', 'month'); | |
| }; | |
| /** | |
| * Handles the "to previous year" action in the date picker. | |
| */ | |
| const toPrevYear = () => { | |
| startDateYear--; | |
| onNavigation('previous', 'year'); | |
| }; | |
| /** | |
| * Handles the "to next month" action in the date picker. | |
| */ | |
| const toNext = () => { | |
| if (++startDateMonth > 11) { | |
| startDateMonth = 0; | |
| startDateYear++; | |
| } | |
| onNavigation('next', 'month'); | |
| }; | |
| /** | |
| * Handles the "to next year" action in the date picker. | |
| */ | |
| const toNextYear = () => { | |
| startDateYear++; | |
| onNavigation('next', 'year'); | |
| }; | |
| /** | |
| * Checks if a given date is today. | |
| * | |
| * @param {number} day - The day of the date. | |
| * @param {number} month - The month of the date (0-11). | |
| * @param {number} year - The year of the date. | |
| * @returns {boolean} - True if the date is today; otherwise, false. | |
| */ | |
| const isToday = (day, month, year) => { | |
| return today && todayMonth === month && todayDay === day && todayYear === year; | |
| }; | |
| /** | |
| * Handles the selection of a single date in the date picker. | |
| * | |
| * @param {number} timestamp - The timestamp of the selected date. | |
| */ | |
| const handleSingleDate = (timestamp) => { | |
| startDate = updateTime('start', timestamp); | |
| if (!alwaysShow) { | |
| isOpen = false; | |
| } | |
| }; | |
| /** | |
| * Handles the selection of a date range in the date picker. | |
| * | |
| * @param {number} timestamp - The timestamp of the selected date. | |
| */ | |
| const handleDateRange = (timestamp) => { | |
| if (startDate === null || (startDate !== null && endDate !== null)) { | |
| startDate = updateTime('start', timestamp); | |
| endDate = null; | |
| } else { | |
| endDate = updateTime('end', timestamp); | |
| if (startDate > endDate) { | |
| [startDate, endDate] = [endDate, startDate]; | |
| } | |
| if (!alwaysShow) { | |
| isOpen = false; | |
| } | |
| } | |
| }; | |
| /** | |
| * Gets an array of dates within a specified range. | |
| * | |
| * @param {number} startDate - The timestamp of the start date. | |
| * @param {number} endDate - The timestamp of the end date. | |
| * @returns {string[]} - An array of dates within the specified range. | |
| */ | |
| const getDatesInRange = (startDate, endDate) => { | |
| const dateRangeStart = new Date(startDate); | |
| const dateRangeEnd = new Date(endDate); | |
| const datesInRange = []; | |
| for (; dateRangeStart <= dateRangeEnd; dateRangeStart.setDate(dateRangeStart.getDate() + 1)) { | |
| const formattedDate = `${ | |
| dateRangeStart.getMonth() + 1 | |
| }/${dateRangeStart.getDate()}/${dateRangeStart.getFullYear()}`; | |
| if ( | |
| (!enabled && !disabled) || | |
| (enabled.length && enabled.includes(formattedDate)) || | |
| !disabled.includes(formattedDate) | |
| ) { | |
| datesInRange.push(formattedDate); | |
| } | |
| } | |
| return datesInRange; | |
| }; | |
| /** | |
| * Handles the click event on a day in the date picker. | |
| * | |
| * @param {Event} e - The click event. | |
| * @param {number} day - The day of the clicked date. | |
| * @param {number} month - The month of the clicked date. | |
| * @param {number} year - The year of the clicked date. | |
| */ | |
| const onClick = function (e, day, month, year) { | |
| const classes = e.target?.closest('.calendar-date').className; | |
| if (classes.includes('future') || classes.includes('past') || classes.includes('disabled')) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| const timestamp = createTimestamp(year, month, day); | |
| if (isRange) { | |
| prevStartDate = startDate; | |
| prevEndDate = endDate; | |
| handleDateRange(timestamp); | |
| } else { | |
| handleSingleDate(timestamp); | |
| } | |
| const event = { | |
| startDate, | |
| startDateTime, | |
| ...(isRange && { | |
| endDate, | |
| endDateTime, | |
| rangeDates: getDatesInRange(startDate, endDate) | |
| }) | |
| }; | |
| onDayClick(event); | |
| if ((isRange && startDate && endDate) || (!isRange && startDate)) { | |
| onDateChange(event); | |
| } | |
| }; | |
| /** | |
| * Checks if a date is within a specified range. | |
| * | |
| * @param {any} start - The start date of the range. | |
| * @param {any} end - The end date of the range. | |
| * @param {any} selected - The date to check. | |
| * @returns {boolean} - True if the date is within the range, false otherwise. | |
| */ | |
| const isDateInRange = (start, end, selected) => { | |
| const startCompare = normalizeTimestamp(start); | |
| const endCompare = normalizeTimestamp(end); | |
| const selectedCompare = normalizeTimestamp(selected); | |
| return selectedCompare >= startCompare && selectedCompare <= endCompare; | |
| }; | |
| /** | |
| * Checks if a given date is in the selected date range. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is in the selected range, false otherwise. | |
| */ | |
| const inRange = (day, month, year) => { | |
| const selectedDateTimestamp = createTimestamp(year, month, day); | |
| if (normalizeTimestamp(startDate) === selectedDateTimestamp) { | |
| return true; | |
| } | |
| return isRange ? isDateInRange(startDate, endDate, selectedDateTimestamp) : startDate === selectedDateTimestamp; | |
| }; | |
| /** | |
| * Checks if a given date is the first date in the selected range. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is the first date in the selected range, false otherwise. | |
| */ | |
| const isFirstInRange = (day, month, year) => { | |
| const currentTimestamp = createTimestamp(year, month, day); | |
| const startCompare = normalizeTimestamp(startDate); | |
| const tempEndCompare = normalizeTimestamp(tempEndDate); | |
| const currentCompare = normalizeTimestamp(currentTimestamp); | |
| if ((!isRange && startCompare) || (isRange && !endDate && tempEndDate)) { | |
| return startCompare === currentCompare; | |
| } | |
| return isRange ? startCompare === currentCompare : tempEndCompare === currentCompare; | |
| }; | |
| /** | |
| * Checks if a given date is the last date in the selected range. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is the last date in the selected range, false otherwise. | |
| */ | |
| const isLastInRange = (day, month, year) => { | |
| const currentTimestamp = createTimestamp(year, month, day); | |
| const endCompare = normalizeTimestamp(endDate); | |
| const startCompare = normalizeTimestamp(startDate); | |
| const currentCompare = normalizeTimestamp(currentTimestamp); | |
| const tempEndCompare = normalizeTimestamp(tempEndDate); | |
| if (isRange && startDate && !endDate && tempEndDate) { | |
| return tempEndCompare === startCompare; | |
| } | |
| return isRange ? endCompare === currentCompare : startCompare === currentCompare; | |
| }; | |
| /** | |
| * Checks if a given date is disabled. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is disabled, false otherwise. | |
| */ | |
| const isDisabled = (day, month, year) => { | |
| const selectedDateTimestamp = createTimestamp(year, month, day); | |
| return ( | |
| (!enabled && !disabled) || | |
| (enabled.length && !enabled.map((d) => new Date(d).getTime()).includes(selectedDateTimestamp)) | |
| ); | |
| }; | |
| /** | |
| * Checks if a given date is in the future. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is in the future, false otherwise. | |
| */ | |
| const isFutureDate = (day, month, year) => { | |
| if (enableFutureDates) { | |
| return false; | |
| } | |
| const selectedDateTimestamp = createTimestamp(year, month, day); | |
| const todayCompare = normalizeTimestamp(new Date()); | |
| const selectedCompare = normalizeTimestamp(selectedDateTimestamp); | |
| return todayCompare < selectedCompare; | |
| }; | |
| /** | |
| * Checks if a given date is in the past. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is in the past, false otherwise. | |
| */ | |
| const isPastDate = (day, month, year) => { | |
| if (enablePastDates) { | |
| return false; | |
| } | |
| const selectedDateTimestamp = createTimestamp(year, month, day); | |
| const todayCompare = normalizeTimestamp(new Date()); | |
| const selectedCompare = normalizeTimestamp(selectedDateTimestamp); | |
| return todayCompare > selectedCompare; | |
| }; | |
| /** | |
| * Checks if a given day is the first day of the month. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @returns {boolean} - True if it's the first day of the month, false otherwise. | |
| */ | |
| const isFirstDayOfMonth = (day) => { | |
| return day === 1; | |
| }; | |
| /** | |
| * Checks if a given day is the last day of the month. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {Array<number>} calendar - The calendar array. | |
| * @returns {boolean} - True if it's the last day of the month, false otherwise. | |
| */ | |
| const isLastDayOfMonth = (day, calendar) => { | |
| return day === Math.max(...calendar.flat(10)); | |
| }; | |
| /** | |
| * Handles the mouse enter event for a day in the date picker. | |
| * | |
| * @param {Event} e - The mouse enter event. | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| */ | |
| const onMouseEnter = function (e, day, month, year) { | |
| if (startDate && endDate) { | |
| tempEndDate = null; | |
| return; | |
| } | |
| const { className: classes } = e.target || {}; | |
| if (classes.includes('future') || classes.includes('past') || classes.includes('disabled')) { | |
| return; | |
| } | |
| tempEndDate = createTimestamp(year, month, day); | |
| }; | |
| /** | |
| * Handles the mouse leave event for a day in the date picker. | |
| */ | |
| const onMouseLeave = () => { | |
| if (startDate && endDate) { | |
| tempEndDate = null; | |
| return; | |
| } | |
| tempEndDate = normalizeTimestamp(startDate); | |
| }; | |
| /** | |
| * Checks if a given date is in the range during hover. | |
| * | |
| * @param {number} day - The day of the month. | |
| * @param {number} month - The month (0-11). | |
| * @param {number} year - The year. | |
| * @returns {boolean} - True if the date is in the range during hover, false otherwise. | |
| */ | |
| const inRangeHover = (day, month, year) => { | |
| if (!isRange || endDate || !startDate || !tempEndDate) { | |
| return false; | |
| } | |
| const dateString = createTimestamp(year, month, day); | |
| const startCompare = normalizeTimestamp(startDate); | |
| const tempEndCompare = normalizeTimestamp(tempEndDate); | |
| const selectedCompare = normalizeTimestamp(dateString); | |
| const minDate = startCompare < tempEndCompare ? startCompare : tempEndCompare; | |
| const maxDate = startCompare > tempEndCompare ? startCompare : tempEndCompare; | |
| return selectedCompare >= minDate && selectedCompare <= maxDate; | |
| }; | |
| /** | |
| * Handles a click event on a preset date range. | |
| * | |
| * @param {Object} preset - The preset date range object with start and end dates. | |
| * @param {number} preset.start - The start date of the preset range. | |
| * @param {number} preset.end - The end date of the preset range. | |
| */ | |
| const onPresetClick = ({ start, startTime, end, endTime }) => { | |
| startDate = start; | |
| endDate = end; | |
| if (startTime && endTime) { | |
| startDateTime = startTime; | |
| endDateTime = endTime; | |
| } | |
| if (isRange && startDate && endDate) { | |
| onDateChange({ | |
| startDate, | |
| startDateTime, | |
| endDate, | |
| endDateTime, | |
| rangeDates: getDatesInRange(startDate, endDate) | |
| }); | |
| } | |
| if (!alwaysShow) { | |
| isOpen = false; | |
| } | |
| }; | |
| /** | |
| * Updates the time for a date and returns the timestamp. | |
| * | |
| * @param {string} which - 'start' or 'end' to indicate which date to update. | |
| * @param {number} timestamp - The timestamp to set. | |
| * @returns {number} - The updated timestamp. | |
| */ | |
| const updateTime = (which, timestamp) => { | |
| const date = new Date(timestamp); | |
| if (!showTimePicker) { | |
| return date.getTime(); | |
| } | |
| const [hours = 0, minutes = 0, seconds = 0] = (which === 'start' ? startDateTime : endDateTime).split(':'); | |
| date.setHours(hours); | |
| date.setMinutes(minutes); | |
| date.setSeconds(seconds); | |
| return date.getTime(); | |
| }; | |
| /** | |
| * Gets the hours and minutes from a given date. | |
| * | |
| * @param {Date} date - The date to extract hours and minutes from. | |
| * @returns {string} - A string in HH:mm format representing hours and minutes. | |
| */ | |
| const getHoursAndMinutes = (date) => { | |
| date = new Date(date); | |
| if (!date) { | |
| return '00:00'; | |
| } | |
| let hours = date.getHours(); | |
| let minutes = date.getMinutes(); | |
| if (hours < 10) { | |
| hours = `0${hours}`; | |
| } | |
| if (minutes < 10) { | |
| minutes = `0${minutes}`; | |
| } | |
| return `${hours}:${minutes}`; | |
| }; | |
| /** | |
| * Returns an array of timestamps from an array of date strings, considering leap years. | |
| * @param {string[]} collection - An array of date strings. | |
| * @returns {number[]} - An array of timestamps. | |
| */ | |
| const getDatesFromArray = (collection) => { | |
| return collection.reduce((acc, date) => { | |
| let newDates = []; | |
| if (typeof date === 'string') { | |
| if (date.includes(':')) { | |
| const [rangeStart, rangeEnd] = date.split(':').map((d) => new Date(d)); | |
| let currentDate = new Date(rangeStart); | |
| while (currentDate <= rangeEnd) { | |
| newDates.push(normalizeTimestamp(currentDate.getTime())); | |
| currentDate.setDate(currentDate.getDate() + 1); | |
| } | |
| } else { | |
| newDates.push(normalizeTimestamp(new Date(date).getTime())); | |
| } | |
| } | |
| return [...acc, ...newDates]; | |
| }, []); | |
| }; | |
| if (typeof startOfWeek === 'string') { | |
| startOfWeek = parseInt(startOfWeek, 10); | |
| } | |
| let todayMonth = $derived(today && today.getMonth()); | |
| let todayDay = $derived(today && today.getDate()); | |
| let todayYear = $derived(today && today.getFullYear()); | |
| let prev = calendarize(new Date(startDateYear, startDateMonth - 1), startOfWeek); | |
| let startDateCalendar = $derived(calendarize(new Date(startDateYear, startDateMonth), startOfWeek)); | |
| let next = $derived(calendarize(new Date(startDateYear, startDateMonth + 1), startOfWeek)); | |
| let endDateMonth = $derived(startDateMonth === 11 ? 0 : startDateMonth + 1); | |
| let endDateYear = $derived(endDateMonth === 0 ? startDateYear + 1 : startDateYear); | |
| let endDateCalendar = $derived(calendarize(new Date(endDateYear, endDateMonth), startOfWeek)); | |
| let disabled = $derived(getDatesFromArray(disabledDates)); | |
| let enabled = $derived(getDatesFromArray(enabledDates, true)); | |
| $effect(()=> { | |
| startDate = startDate ? getTimestamp(startDate) : null | |
| }) | |
| $effect(()=> { | |
| endDate = endDate ? getTimestamp(endDate) : null | |
| }) | |
| // $effect(()=> { | |
| // if (!isRange) { | |
| // endDate = null | |
| // } | |
| // }) | |
| $effect(()=> { | |
| if (!startDate && !endDate) { | |
| startDateYear = Number(defaultYear); | |
| startDateMonth = Number(defaultMonth); | |
| } | |
| }) | |
| $effect(() => { | |
| if (isRange !== null || (startDate && tempEndDate !== null) || !isOpen) { | |
| updateCalendars(); | |
| } | |
| }); | |
| $effect(() => { | |
| if (isOpen) { | |
| if (startDate) { | |
| const date = new Date(startDate); | |
| startDateYear = date.getFullYear(); | |
| startDateMonth = date.getMonth(); | |
| } | |
| } | |
| }); | |
| $effect(() => { | |
| if (showTimePicker && !initialize) { | |
| startDateTime = getHoursAndMinutes(startDate); | |
| endDateTime = getHoursAndMinutes(endDate); | |
| initialize = true; | |
| } | |
| }); | |
| /* this is to stop the range from showing when the start and end date is the same date */ | |
| onMount(()=> { | |
| if (isOpen) { | |
| if (startDate && !endDate) { | |
| endDate = startDate; | |
| } | |
| } | |
| }) | |
| </script> | |
| <div class="datepicker" data-picker-theme={theme} use:clickOutside={{ onClickOutside }}> | |
| <div | |
| class="calendars-container" | |
| class:right={align === 'right'} | |
| class:range={isRange && isMultipane} | |
| class:presets={isRange && showPresets} | |
| class:show={isOpen} | |
| > | |
| {#if isRange && showPresets} | |
| <div class="calendar-presets" class:presets-only={showPresetsOnly}> | |
| {#each presetRanges as option} | |
| <button | |
| type="button" | |
| class:active={normalizeTimestamp(startDate) === normalizeTimestamp(option.start) && | |
| normalizeTimestamp(endDate) === normalizeTimestamp(option.end)} | |
| onclick={() => onPresetClick({ ...option })} | |
| > | |
| {option.label} | |
| </button> | |
| {/each} | |
| </div> | |
| {/if} | |
| <div class="calendar" class:presets-only={isRange && showPresetsOnly}> | |
| <header class="calendar-header" class:has-timepicker={showTimePicker}> | |
| <button class="calendar-month-button calendar-month-prev-button" type="button" onclick={toPrev} aria-label="Previous month"> | |
| <div class="icon-previous-month"></div> | |
| </button> | |
| <span class="calendar-month-year"> | |
| <div class="calendar-month-year-label">{monthLabels[startDateMonth]} {startDateYear}</div> | |
| {#if showYearControls} | |
| <div class="calendar-years-buttons"> | |
| <button class="calendar-year-button calendar-year-next-button" type="button" onclick={toNextYear} aria-label="Next year"></button> | |
| <button class="calendar-year-button calendar-year-previous-button" type="button" onclick={toPrevYear} aria-label="Previous year"></button> | |
| </div> | |
| {/if} | |
| </span> | |
| <button class="calendar-month-button calendar-month-next-button" type="button" onclick={toNext} class:hide={!(!isRange || (isRange && !isMultipane))} aria-label="Next month"> | |
| <div class="icon-next-month"></div> | |
| </button> | |
| </header> | |
| {#if showTimePicker} | |
| <div class="calendar-timepicker" class:show={isRange && !isMultipane}> | |
| <input type="time" bind:value={startDateTime} oninput={() => (startDate = updateTime('start', startDate))} /> | |
| {#if isRange} | |
| <input | |
| type="time" | |
| class="end-time" | |
| bind:value={endDateTime} | |
| oninput={() => (endDate = updateTime('end', endDate))} | |
| /> | |
| {/if} | |
| </div> | |
| {/if} | |
| <div class="calendar-days"> | |
| {#each dowLabels as text, labelIndex (text)} | |
| <span class="calendar-day-of-week">{dowLabels[(labelIndex + startOfWeek) % 7]}</span> | |
| {/each} | |
| {#each { length: 6 } as week, weekIndex (weekIndex)} | |
| {#if startDateCalendar[weekIndex]} | |
| {#each { length: 7 } as d, dayIndex (dayIndex)} | |
| {#if startDateCalendar[weekIndex][dayIndex] !== 0} | |
| <button | |
| type="button" | |
| class="calendar-date" | |
| class:today={isToday(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:start={isFirstInRange(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:end={isLastInRange(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:range={inRange(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:rangehover={inRangeHover(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:past={isPastDate(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:future={isFutureDate(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:first={isFirstDayOfMonth(startDateCalendar[weekIndex][dayIndex])} | |
| class:last={isLastDayOfMonth(startDateCalendar[weekIndex][dayIndex], startDateCalendar)} | |
| class:disabled={isDisabled(startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| onmouseenter={(e) => | |
| onMouseEnter(e, startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| onmouseleave={onMouseLeave} | |
| onclick={(e) => | |
| onClick(e, startDateCalendar[weekIndex][dayIndex], startDateMonth, startDateYear)} | |
| class:norange={isRange && tempEndDate === startDate} | |
| > | |
| <span class="calendar-day-number"> | |
| <span>{startDateCalendar[weekIndex][dayIndex]}</span> | |
| </span> | |
| </button> | |
| {:else} | |
| <div class="date other"> </div> | |
| {/if} | |
| {/each} | |
| {/if} | |
| {/each} | |
| </div> | |
| </div> | |
| {#if isRange && isMultipane} | |
| <div class="calendar" class:presets-only={showPresetsOnly}> | |
| <header class:has-timepicker={showTimePicker}> | |
| <button class="calendar-month-previous-button" type="button" onclick={toPrev} class:hide={!(!isRange || (isRange && !isMultipane))} aria-label="Previous month"> | |
| <div class="icon-previous-month"></div> | |
| </button> | |
| <span class="calendar-month-year-label"> | |
| <div>{monthLabels[endDateMonth]} {endDateYear}</div> | |
| {#if showYearControls} | |
| <div class="calendar-year-buttons"> | |
| <button class="calendar-year-next-button" type="button" onclick={toNextYear} aria-label="Next year"> | |
| <i class="icon-next-year"></i> | |
| </button> | |
| <button class="calendar-year-previous-button" type="button" onclick={toPrevYear} aria-label="Previous year"> | |
| <i class="icon-previous-year"></i> | |
| </button> | |
| </div> | |
| {/if} | |
| </span> | |
| <button class="calendar-month-next-button" type="button" onclick={toNext} aria-label="Next month"> | |
| <div class="icon-next-month"></div> | |
| </button> | |
| </header> | |
| {#if showTimePicker} | |
| <div class="calendar-timepicker"> | |
| <input type="time" bind:value={endDateTime} oninput={() => (endDate = updateTime('end', endDate))} /> | |
| </div> | |
| {/if} | |
| <div class="calendar-days"> | |
| {#each dowLabels as text, labelIndex (text)} | |
| <span class="dow">{dowLabels[(labelIndex + startOfWeek) % 7]}</span> | |
| {/each} | |
| {#each { length: 6 } as week, weekIndex (weekIndex)} | |
| {#if endDateCalendar[weekIndex]} | |
| {#each { length: 7 } as d, dayIndex (dayIndex)} | |
| {#if endDateCalendar[weekIndex][dayIndex] !== 0} | |
| <button | |
| type="button" | |
| class=date | |
| class:today={isToday(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:range={inRange(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:rangehover={inRangeHover(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:start={isFirstInRange(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:end={isLastInRange(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:past={isPastDate(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:future={isFutureDate(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:first={isFirstDayOfMonth(endDateCalendar[weekIndex][dayIndex])} | |
| class:last={isLastDayOfMonth(endDateCalendar[weekIndex][dayIndex], endDateCalendar)} | |
| class:disabled={isDisabled(endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| onmouseenter={(e) => | |
| onMouseEnter(e, endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| onmouseleave={onMouseLeave} | |
| onclick={(e) => | |
| onClick(e, endDateCalendar[weekIndex][dayIndex], endDateMonth, endDateYear)} | |
| class:norange={isRange && tempEndDate === startDate} | |
| > | |
| <span class="calendar-day-number"> | |
| <span>{endDateCalendar[weekIndex][dayIndex]}</span> | |
| </span> | |
| </button> | |
| {:else} | |
| <div class="date other"> </div> | |
| {/if} | |
| {/each} | |
| {/if} | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <svelte:head> | |
| {#if includeFont} | |
| <link | |
| rel="stylesheet" | |
| href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700;800;900&display=swap" | |
| /> | |
| {/if} | |
| </svelte:head> | |
| <style lang="scss"> | |
| :root { | |
| --background-range: #e7f7fc; | |
| --background-date: #0087ff; | |
| --background-date-hover: #f5f5f5; | |
| --background-range-on-range: #d7ebf1; | |
| } | |
| .calendar { | |
| color: #21333d; | |
| font-family: 'Rubik', sans-serif; | |
| font-size: 14px; | |
| @media only screen and (min-width: 600px) { | |
| font-size: 16px; | |
| } | |
| } | |
| .calendar-header { | |
| align-items: center; | |
| display: flex; | |
| font-size: 1.125em; | |
| justify-content: space-between; | |
| margin: 0; | |
| padding: 8px; | |
| user-select: none; | |
| .calendar-month-year { | |
| display: flex; | |
| .calendar-month-year-label { | |
| font-weight: 500; | |
| font-size: 1.125rem; | |
| } | |
| .calendar-years-buttons { | |
| display: flex; | |
| flex-direction: column; | |
| padding-left: 4px; | |
| .calendar-year-button { | |
| width: 18px; | |
| height: 12px; | |
| } | |
| .calendar-year-next-button { | |
| background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABuSURBVHgB7c7BCYAwDIXhBy7gKB2hm9Vx3UJzqCASRWOTHvo+yDG8HyAiGt2Ef7LcLLeigyK31SsIdh4Pj9DGwyKu40u9kAht/OAe8TTuHvFm3C3iy3jziGQYv4vIMMjGcS0iwSjBWN/on4hoADu88UW4KXFVfgAAAABJRU5ErkJggg==') no-repeat center center; | |
| background-size: contain; | |
| } | |
| .calendar-year-previous-button { | |
| background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAB3SURBVHgB7dTRCYAwDATQAxdwlI6QzZpx3UIrKJSC1aS2fngP7kvi3VcBIqK/m26+S8qcssBHWu5Dynokwi5m9wIHyX5gHRGL2wAndYwoyxWN1DDi9XLLiG7lT0Z0L6+NGFZ+NWJoeW2EYjD9svy0PzACIiJqsAHF2EaCcjFGaQAAAABJRU5ErkJggg==') no-repeat center center; | |
| background-size: contain; | |
| } | |
| } | |
| } | |
| .calendar-month-button { | |
| appearance: none; | |
| background-color: transparent; | |
| border: 0; | |
| border-radius: 100%; | |
| aspect-ratio: 1/1; | |
| cursor: pointer; | |
| margin: 0; | |
| padding: 8px; | |
| &:hover { | |
| background: #f5f5f5; | |
| } | |
| } | |
| .icon-next-month { | |
| background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACLSURBVHgB7ZTLCYAwEERHbcASUpIlaAd2YDoxlmIX3ixFEwwYQQL5kCWwD94ph5mwywIMUzmLlYRBe1lXENBrT+oSgktwiepLNJ63EWkl3AOltBMCkHh/kEv5F9SCGN8IzKntEYfAdwQb0kYaHO4uoUJBBIdzOAoiKMMNQ47wDvEceA7Zrp3BMLVyA56LVFYQOkngAAAAAElFTkSuQmCC') | |
| no-repeat center center; | |
| background-size: 16px 16px; | |
| filter: invert(0); | |
| height: 18px; | |
| margin: auto; | |
| width: 18px; | |
| } | |
| .icon-previous-month { | |
| background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgB7ZbBDYAgDEW/xgEcgZHcQDYRJ5ER3EhHcAPtAQMHQwIiSNKXvAMH+CUNDQDDVM5kLMJCnsYBmXHDN1IgIxzO4QIZ+Ty8gT9cOuuZ3BHHQa4hGxTszVOpnoJaFMbXAk2OzvpNC+7zojYVewFcBBdRVRE9CqCR4EvWIR4JO5iC5jzD/IoLU/FXPXheCj0AAAAASUVORK5CYII=') | |
| no-repeat center center; | |
| background-size: 16px 16px; | |
| filter: invert(0); | |
| height: 18px; | |
| margin: auto; | |
| width: 18px; | |
| } | |
| } | |
| .calendar-timepicker { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0px 0px; | |
| padding-bottom: 20px; | |
| gap: 30%; | |
| input { | |
| border: 1px solid lightgray; | |
| border-radius: 6px; | |
| padding: 4px 8px; | |
| } | |
| } | |
| .calendar-days { | |
| display: grid; | |
| gap: 0px; | |
| grid-template-columns: repeat(7, 1fr); | |
| .calendar-day-of-week { | |
| color: #8b9198; | |
| font-size: 14px; | |
| font-weight: 500; | |
| text-align: center; | |
| } | |
| .calendar-date { | |
| aspect-ratio: 1/1; | |
| &.past { | |
| color: lightgray; | |
| } | |
| .calendar-day-number { | |
| display: block; | |
| border-radius: 100%; | |
| aspect-ratio: 1/1; | |
| text-align: center; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| &:hover.range .calendar-day-number { | |
| background: var(--background-range-on-range); | |
| } | |
| &:hover:not(.range) .calendar-day-number { | |
| background: var(--background-date-hover); | |
| } | |
| &.start:not(.norange):not(.past.end), | |
| &.end:not(.norange):not(.past.end):not(.end + .end), | |
| &.rangehover:not(:has(+ .rangehover)), | |
| &.rangehover:not(+ .rangehover), | |
| & + .rangehover:not(.rangehover + .rangehover) { | |
| background: var(--background-range); | |
| .calendar-day-number { | |
| background: var(--background-date); | |
| color: #fff; | |
| } | |
| } | |
| // range | |
| &.start:not(.norange):not(.rangehover + .start), | |
| & + .rangehover:not(.rangehover + .rangehover) { | |
| border-top-left-radius: 100%; | |
| border-bottom-left-radius: 100%; | |
| } | |
| .rangehover + .start { | |
| border-top-left-radius: 0; | |
| border-bottom-left-radius: 0; | |
| } | |
| &.end:not(.norange), | |
| &.rangehover:not(:has(+ .rangehover)) { | |
| border-top-right-radius: 100%; | |
| border-bottom-right-radius: 100%; | |
| } | |
| &.range:not(.start):not(.end), | |
| &.rangehover:not(.start) { | |
| background: var(--background-range); | |
| } | |
| } | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment