Skip to content

Instantly share code, notes, and snippets.

@gregg-cbs
Last active July 18, 2025 08:28
Show Gist options
  • Select an option

  • Save gregg-cbs/5a401f03d6b53c7a8d5587c0269a27be to your computer and use it in GitHub Desktop.

Select an option

Save gregg-cbs/5a401f03d6b53c7a8d5587c0269a27be to your computer and use it in GitHub Desktop.
svelte datepicker
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);
}
};
};
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> {}
<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">&nbsp;</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">&nbsp;</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