[READ-ONLY] Mirror of https://github.com/probablykasper/date-picker-svelte. Date and time picker for Svelte date-picker-svelte.kasper.space
calendar date date-picker date-time-picker datepicker package svelte time
0

Configure Feed

Select the types of activity you want to include in your feed.

add isDisabledDate (#113)

* added the ability to enable and disable dates, using isDisabledDate function

* reworked to use isDisabledDate prop

* fixed docs

* ran prettier

* Update comments

* Use optional chaining

* Update docs example

* Revert demo

* Fix lints

* Create disableddate dev page

* Format

* added checks to prevent selecting a disabled date by typing or from arrow keys

* Small adjustments

* Update CHANGELOG.md

* updated-fallback-to-browseDate

* fixed-typo

* Make toValidDate args non-null

* Debug set date button

* Svelte 3 test

* Revert "Svelte 3 test"

This reverts commit 8184f5235b9d45061448a88b606d134f59a99738.

* added toValidDate to both DateInput and DatePicker

* Fix time changing when switching calendar dates

* Also run toValidDate in textUpdate

* Fix double-setting to invalid date not being fixed

---------

Co-authored-by: stinger567 <jdsteck567@gmail.com>
Co-authored-by: Kasper <kasperkh.kh@gmail.com>

authored by

stinger567
stinger567
Kasper
and committed by
GitHub
(Apr 2, 2025, 7:21 AM +0200) 98489853 174fb358

+169 -55
+3
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## Next 4 + - Add `isDisabledDate` prop (@stinger567) 5 + 3 6 ## 2.15.2 - 2025 Mar 19 4 7 - Fix `timePrecision` not always setting unused values to 0 (@stinger567) 5 8
+11 -8
src/lib/DateInput.svelte
··· 1 1 <script lang="ts"> 2 2 import { fly } from 'svelte/transition' 3 3 import { cubicInOut } from 'svelte/easing' 4 - import { toText } from './date-utils.js' 4 + import { toText, cloneDate, toValidDate } from './date-utils.js' 5 5 import type { Locale } from './locale.js' 6 6 import { parse, createFormat, type FormatToken } from './parse.js' 7 7 import DateTimePicker from './DatePicker.svelte' ··· 16 16 /** Default date to display in picker before value is assigned */ 17 17 const defaultDate = new Date() 18 18 19 - function cloneDate(d: Date) { 20 - return new Date(d.getTime()) 21 - } 22 - 23 19 // inner date value store for preventing value updates (and also 24 20 // text updates as a result) when date is unchanged 25 21 const innerStore = writable(null as Date | null) ··· 30 26 if (date === null || date === undefined) { 31 27 innerStore.set(null) 32 28 value = date 33 - } else if (date.getTime() !== $innerStore?.getTime()) { 29 + } else if ( 30 + date.getTime() !== $innerStore?.getTime() || 31 + date.getTime() !== value?.getTime() 32 + ) { 34 33 innerStore.set(cloneDate(date)) 35 34 value = date 36 35 } ··· 40 39 41 40 /** Date value */ 42 41 export let value: Date | null = null 43 - $: store.set(value) 42 + $: store.set(value ? toValidDate(defaultDate, value, min, max, isDisabledDate) : value) 44 43 45 44 /** The earliest value the user can select */ 46 45 export let min = new Date(defaultDate.getFullYear() - 20, 0, 1) ··· 80 79 const result = parse(text, formatTokens, $store) 81 80 if (result.date !== null) { 82 81 valid = true 83 - store.set(result.date) 82 + store.set(toValidDate(defaultDate, result.date, min, max, isDisabledDate)) 84 83 } else { 85 84 valid = false 86 85 } ··· 104 103 105 104 /** Show a time picker with the specified precision */ 106 105 export let timePrecision: 'minute' | 'second' | 'millisecond' | null = null 106 + 107 + /** Disallow specific dates */ 108 + export let isDisabledDate: ((dateToCheck: Date) => boolean) | null = null 107 109 108 110 // handle on:focusout for parent element. If the parent element loses 109 111 // focus (e.g input element), visible is set to false ··· 237 239 {locale} 238 240 {browseWithoutSelecting} 239 241 {timePrecision} 242 + {isDisabledDate} 240 243 > 241 244 <slot /> 242 245 </DateTimePicker>
+23 -46
src/lib/DatePicker.svelte
··· 5 5 getCalendarDays, 6 6 type CalendarDay, 7 7 applyTimePrecision, 8 + clampDate, 9 + clamp, 8 10 } from './date-utils.js' 9 11 import { getInnerLocale, type Locale } from './locale.js' 10 12 import { createEventDispatcher } from 'svelte' 13 + import { cloneDate, toValidDate } from './date-utils.js' 11 14 12 15 const dispatch = createEventDispatcher<{ 13 16 /** Fires when the user selects a new value by clicking on a date or by pressing enter */ 14 17 select: Date 15 18 }>() 16 19 17 - function cloneDate(d: Date) { 18 - return new Date(d.getTime()) 19 - } 20 - 21 20 /** Date value. It's `null` if no date is selected */ 22 21 export let value: Date | null = null 23 22 24 23 function setValue(d: Date) { 25 24 if (d.getTime() !== value?.getTime()) { 26 - browseDate = clamp(d, min, max) 25 + browseDate = toValidDate(value ?? browseDate, d, min, max, isDisabledDate) 27 26 applyTimePrecision(browseDate, timePrecision) 28 - value = cloneDate(browseDate) 29 - } 30 - } 31 - 32 - function setValueDate(d: Date) { 33 - if (d.getTime() !== value?.getTime()) { 34 - browseDate = clampDate(d, min, max) 35 27 value = cloneDate(browseDate) 36 28 } 37 29 } ··· 63 55 export let min = new Date(defaultDate.getFullYear() - 20, 0, 1) 64 56 /** The latest year the user can select */ 65 57 export let max = new Date(defaultDate.getFullYear(), 11, 31, 23, 59, 59, 999) 58 + /** Disallow specific dates */ 59 + export let isDisabledDate: ((dateToCheck: Date) => boolean) | null = null 60 + 61 + function handleDisabledDate(date: CalendarDay) { 62 + return isDisabledDate?.(new Date(date.year, date.month, date.number)) 63 + } 64 + 65 + // Prevents a invalid date from being typed into the Dateinput text box 66 66 $: if (value && value > max) { 67 - setValue(max) 67 + setValue(toValidDate(value, max, min, max, isDisabledDate)) 68 68 } else if (value && value < min) { 69 - setValue(min) 70 - } 71 - function clamp(d: Date, min: Date, max: Date) { 72 - if (d > max) { 73 - return cloneDate(max) 74 - } else if (d < min) { 75 - return cloneDate(min) 76 - } else { 77 - return cloneDate(d) 78 - } 79 - } 80 - function clampDate(d: Date, min: Date, max: Date) { 81 - const limit = clamp(d, min, max) 82 - if (limit.getTime() !== d.getTime()) { 83 - d = new Date( 84 - limit.getFullYear(), 85 - limit.getMonth(), 86 - limit.getDate(), 87 - d.getHours(), 88 - d.getMinutes(), 89 - d.getSeconds(), 90 - d.getMilliseconds(), 91 - ) 92 - d = clamp(d, min, max) 93 - } 94 - return d 69 + setValue(toValidDate(value, min, min, max, isDisabledDate)) 70 + } else if (value && isDisabledDate?.(value)) { 71 + setValue(toValidDate(browseDate, value, min, max, isDisabledDate)) 95 72 } 96 73 97 74 /** The date shown in the popup when none is selected */ ··· 156 133 $: calendarDays = getCalendarDays(browseDate, iLocale.weekStartsOn) 157 134 158 135 function selectDay(calendarDay: CalendarDay) { 159 - if (dayIsInRange(calendarDay, min, max)) { 136 + if (dayIsInRange(calendarDay, min, max) && !handleDisabledDate(calendarDay)) { 160 137 browseDate.setFullYear(0) 161 138 browseDate.setMonth(0) 162 139 browseDate.setDate(1) 163 140 browseDate.setFullYear(calendarDay.year) 164 141 browseDate.setMonth(calendarDay.month) 165 142 browseDate.setDate(calendarDay.number) 166 - setValueDate(browseDate) 143 + setValue(browseDate) 167 144 dispatch('select', cloneDate(browseDate)) 168 145 } 169 146 } ··· 236 213 return 237 214 } else if (e.key === 'ArrowUp') { 238 215 browseDate.setDate(browseDate.getDate() - 7) 239 - setValueDate(browseDate) 216 + setValue(browseDate) 240 217 } else if (e.key === 'ArrowDown') { 241 218 browseDate.setDate(browseDate.getDate() + 7) 242 - setValueDate(browseDate) 219 + setValue(browseDate) 243 220 } else if (e.key === 'ArrowLeft') { 244 221 browseDate.setDate(browseDate.getDate() - 1) 245 - setValueDate(browseDate) 222 + setValue(browseDate) 246 223 } else if (e.key === 'ArrowRight') { 247 224 browseDate.setDate(browseDate.getDate() + 1) 248 - setValueDate(browseDate) 225 + setValue(browseDate) 249 226 } else if (e.key === 'Enter') { 250 227 setValue(browseDate) 251 228 dispatch('select', cloneDate(browseDate)) ··· 355 332 <div 356 333 class="cell" 357 334 on:click={() => selectDay(calendarDay)} 358 - class:disabled={!dayIsInRange(calendarDay, min, max)} 335 + class:disabled={!dayIsInRange(calendarDay, min, max) || handleDisabledDate(calendarDay)} 359 336 class:selected={value && 360 337 calendarDay.year === value.getFullYear() && 361 338 calendarDay.month === value.getMonth() &&
+90
src/lib/date-utils.ts
··· 23 23 return text 24 24 } 25 25 26 + export function isSameDate(date1: Date, date2: Date) { 27 + return ( 28 + date1.getFullYear() === date2.getFullYear() && 29 + date1.getMonth() === date2.getMonth() && 30 + date1.getDate() === date2.getDate() 31 + ) 32 + } 33 + 26 34 export type CalendarDay = { 27 35 year: number 28 36 month: number ··· 88 96 date.setMilliseconds(0) 89 97 } 90 98 } 99 + 100 + export function cloneDate(d: Date) { 101 + return new Date(d) 102 + } 103 + 104 + export function toValidDate( 105 + oldDate: Date, 106 + newDate: Date, 107 + minDate: Date, 108 + maxDate: Date, 109 + isDisabledDate: ((date: Date) => boolean) | null, 110 + ): Date { 111 + // Don't mutate the original newDate to avoid unintended side effects 112 + let adjustedDate = cloneDate(newDate) 113 + 114 + if (oldDate > newDate) { 115 + adjustDate(adjustedDate, -1, minDate, maxDate, isDisabledDate) 116 + if (adjustedDate < minDate) { 117 + adjustedDate = clampDate(adjustedDate, minDate, maxDate) 118 + // Adjusts the date one more time if the min date is disabled, to ensure a valid, enabled date is selected 119 + adjustDate(adjustedDate, 1, minDate, maxDate, isDisabledDate) 120 + } 121 + } else if (adjustedDate >= oldDate) { 122 + adjustDate(adjustedDate, 1, minDate, maxDate, isDisabledDate) 123 + if (adjustedDate > maxDate) { 124 + adjustedDate = clampDate(adjustedDate, minDate, maxDate) 125 + // Adjusts the date one more time if the max date is disabled, to ensure a valid, enabled date is selected 126 + adjustDate(adjustedDate, -1, minDate, maxDate, isDisabledDate) 127 + } 128 + } 129 + // Finally, clamp the time 130 + if (adjustedDate < minDate || adjustedDate > maxDate) { 131 + adjustedDate = clamp(adjustedDate, minDate, maxDate) 132 + } 133 + return adjustedDate 134 + } 135 + 136 + function adjustDate( 137 + date: Date, 138 + increment: number, 139 + minDate: Date, 140 + maxDate: Date, 141 + isDisabledDate: ((date: Date) => boolean) | null, 142 + ) { 143 + // Prevents accidental infinite loops 144 + const MAXLOOPS = 36525 // ~100 years, should be large enough 145 + let loopCount = 0 146 + 147 + while (isDisabledDate?.(date) && date >= minDate && date <= maxDate && loopCount <= MAXLOOPS) { 148 + date.setDate(date.getDate() + increment) 149 + loopCount++ 150 + } 151 + } 152 + 153 + export function clamp(value: Date, min: Date, max: Date) { 154 + if (value > max) { 155 + return cloneDate(max) 156 + } else if (value < min) { 157 + return cloneDate(min) 158 + } else { 159 + return cloneDate(value) 160 + } 161 + } 162 + export function clampDate(value: Date, min: Date, max: Date) { 163 + const limit = clamp(value, min, max) 164 + value = new Date( 165 + limit.getFullYear(), 166 + limit.getMonth(), 167 + limit.getDate(), 168 + value.getHours(), 169 + value.getMinutes(), 170 + value.getSeconds(), 171 + value.getMilliseconds(), 172 + ) 173 + if (value > max) { 174 + value.setDate(max.getDate()) 175 + } 176 + if (value < min) { 177 + value.setDate(min.getDate()) 178 + } 179 + return value 180 + }
+1
src/lib/index.ts
··· 2 2 export { default as DateInput } from './DateInput.svelte' 3 3 4 4 export { localeFromDateFnsLocale, type Locale } from './locale.js' 5 + export { isSameDate } from './date-utils.js'
+22
src/routes/bug/isdisableddate/+page.svelte
··· 1 + <script lang="ts"> 2 + import DateInput from '$lib/DateInput.svelte' 3 + 4 + let min = new Date(2024, 1, 26, 17, 30) 5 + let value: Date | undefined 6 + </script> 7 + 8 + <DateInput 9 + timePrecision="minute" 10 + {min} 11 + bind:value 12 + isDisabledDate={(date) => { 13 + return date.getDate() === 15 || date.getDate() === 16 14 + }} 15 + /> 16 + <button 17 + on:click={() => { 18 + value = new Date(2024, 10, 15) 19 + }}>Set to 2024-10-15</button 20 + > 21 + 22 + {value}
+19 -1
src/routes/docs/+page.md
··· 46 46 | `browseWithoutSelecting` | bool | Wait with updating the date until a value is selected | 47 47 | `dynamicPositioning` | bool | Dynamically postions the date popup to best fit on the screen | 48 48 | `locale` | Locale | Locale object for internationalization | 49 + | `isDisabledDate` | ((dateToCheck: Date) => boolean) \| null | Disallow specific dates | 49 50 50 51 <h4 id="format-string">Format string</h4> 51 52 ··· 76 77 | `timePrecision` | "minute" \| "second" \| "millisecond" \| null | Show a time picker with the specified precision | 77 78 | `locale` | Locale | Locale object for internationalization | 78 79 | `browseWithoutSelecting` | bool | Wait with updating the date until a date is selected | 80 + | `isDisabledDate` | ((dateToCheck: Date) => boolean) \| null | Disallow specific dates | 81 + 82 + <h2 id="isDisabledDate">Date disabling example</h2> 83 + 84 + Example usage of the `isDisabledDate` prop: 85 + 86 + ```svelte 87 + <script> 88 + const disabledDate = new Date() 89 + </script> 90 + 91 + <DatePicker 92 + isDisabledDate={(dateToCheck) => { 93 + return isSameDate(dateToCheck, disabledDate) 94 + }} 95 + /> 96 + ``` 79 97 80 98 <h2 id="internationalization">Internationalization</h2> 81 99 ··· 91 109 92 110 If you use [date-fns](https://date-fns.org/), you can create a Locale object by passing a date-fns locale to this function: 93 111 94 - ```js 112 + ```svelte 95 113 <script> 96 114 import { DatePicker, localeFromDateFnsLocale } from 'date-picker-svelte' 97 115 import { hy } from 'date-fns/locale'