Skip to content

Commit 9848985

Browse files
stinger567stinger567probablykasper
authored
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 8184f52. * 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 <[email protected]> Co-authored-by: Kasper <[email protected]>
1 parent 174fb35 commit 9848985

File tree

7 files changed

+169
-55
lines changed

7 files changed

+169
-55
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## Next
4+
- Add `isDisabledDate` prop (@stinger567)
5+
36
## 2.15.2 - 2025 Mar 19
47
- Fix `timePrecision` not always setting unused values to 0 (@stinger567)
58

src/lib/DateInput.svelte

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { fly } from 'svelte/transition'
33
import { cubicInOut } from 'svelte/easing'
4-
import { toText } from './date-utils.js'
4+
import { toText, cloneDate, toValidDate } from './date-utils.js'
55
import type { Locale } from './locale.js'
66
import { parse, createFormat, type FormatToken } from './parse.js'
77
import DateTimePicker from './DatePicker.svelte'
@@ -16,10 +16,6 @@
1616
/** Default date to display in picker before value is assigned */
1717
const defaultDate = new Date()
1818
19-
function cloneDate(d: Date) {
20-
return new Date(d.getTime())
21-
}
22-
2319
// inner date value store for preventing value updates (and also
2420
// text updates as a result) when date is unchanged
2521
const innerStore = writable(null as Date | null)
@@ -30,7 +26,10 @@
3026
if (date === null || date === undefined) {
3127
innerStore.set(null)
3228
value = date
33-
} else if (date.getTime() !== $innerStore?.getTime()) {
29+
} else if (
30+
date.getTime() !== $innerStore?.getTime() ||
31+
date.getTime() !== value?.getTime()
32+
) {
3433
innerStore.set(cloneDate(date))
3534
value = date
3635
}
@@ -40,7 +39,7 @@
4039
4140
/** Date value */
4241
export let value: Date | null = null
43-
$: store.set(value)
42+
$: store.set(value ? toValidDate(defaultDate, value, min, max, isDisabledDate) : value)
4443
4544
/** The earliest value the user can select */
4645
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
@@ -80,7 +79,7 @@
8079
const result = parse(text, formatTokens, $store)
8180
if (result.date !== null) {
8281
valid = true
83-
store.set(result.date)
82+
store.set(toValidDate(defaultDate, result.date, min, max, isDisabledDate))
8483
} else {
8584
valid = false
8685
}
@@ -105,6 +104,9 @@
105104
/** Show a time picker with the specified precision */
106105
export let timePrecision: 'minute' | 'second' | 'millisecond' | null = null
107106
107+
/** Disallow specific dates */
108+
export let isDisabledDate: ((dateToCheck: Date) => boolean) | null = null
109+
108110
// handle on:focusout for parent element. If the parent element loses
109111
// focus (e.g input element), visible is set to false
110112
function onFocusOut(e: FocusEvent) {
@@ -237,6 +239,7 @@
237239
{locale}
238240
{browseWithoutSelecting}
239241
{timePrecision}
242+
{isDisabledDate}
240243
>
241244
<slot />
242245
</DateTimePicker>

src/lib/DatePicker.svelte

Lines changed: 23 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,29 @@
55
getCalendarDays,
66
type CalendarDay,
77
applyTimePrecision,
8+
clampDate,
9+
clamp,
810
} from './date-utils.js'
911
import { getInnerLocale, type Locale } from './locale.js'
1012
import { createEventDispatcher } from 'svelte'
13+
import { cloneDate, toValidDate } from './date-utils.js'
1114
1215
const dispatch = createEventDispatcher<{
1316
/** Fires when the user selects a new value by clicking on a date or by pressing enter */
1417
select: Date
1518
}>()
1619
17-
function cloneDate(d: Date) {
18-
return new Date(d.getTime())
19-
}
20-
2120
/** Date value. It's `null` if no date is selected */
2221
export let value: Date | null = null
2322
2423
function setValue(d: Date) {
2524
if (d.getTime() !== value?.getTime()) {
26-
browseDate = clamp(d, min, max)
25+
browseDate = toValidDate(value ?? browseDate, d, min, max, isDisabledDate)
2726
applyTimePrecision(browseDate, timePrecision)
2827
value = cloneDate(browseDate)
2928
}
3029
}
3130
32-
function setValueDate(d: Date) {
33-
if (d.getTime() !== value?.getTime()) {
34-
browseDate = clampDate(d, min, max)
35-
value = cloneDate(browseDate)
36-
}
37-
}
38-
3931
/** Set the browseDate */
4032
function browse(d: Date) {
4133
browseDate = clampDate(d, min, max)
@@ -63,35 +55,20 @@
6355
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
6456
/** The latest year the user can select */
6557
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
6666
$: if (value && value > max) {
67-
setValue(max)
67+
setValue(toValidDate(value, max, min, max, isDisabledDate))
6868
} 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))
9572
}
9673
9774
/** The date shown in the popup when none is selected */
@@ -156,14 +133,14 @@
156133
$: calendarDays = getCalendarDays(browseDate, iLocale.weekStartsOn)
157134
158135
function selectDay(calendarDay: CalendarDay) {
159-
if (dayIsInRange(calendarDay, min, max)) {
136+
if (dayIsInRange(calendarDay, min, max) && !handleDisabledDate(calendarDay)) {
160137
browseDate.setFullYear(0)
161138
browseDate.setMonth(0)
162139
browseDate.setDate(1)
163140
browseDate.setFullYear(calendarDay.year)
164141
browseDate.setMonth(calendarDay.month)
165142
browseDate.setDate(calendarDay.number)
166-
setValueDate(browseDate)
143+
setValue(browseDate)
167144
dispatch('select', cloneDate(browseDate))
168145
}
169146
}
@@ -236,16 +213,16 @@
236213
return
237214
} else if (e.key === 'ArrowUp') {
238215
browseDate.setDate(browseDate.getDate() - 7)
239-
setValueDate(browseDate)
216+
setValue(browseDate)
240217
} else if (e.key === 'ArrowDown') {
241218
browseDate.setDate(browseDate.getDate() + 7)
242-
setValueDate(browseDate)
219+
setValue(browseDate)
243220
} else if (e.key === 'ArrowLeft') {
244221
browseDate.setDate(browseDate.getDate() - 1)
245-
setValueDate(browseDate)
222+
setValue(browseDate)
246223
} else if (e.key === 'ArrowRight') {
247224
browseDate.setDate(browseDate.getDate() + 1)
248-
setValueDate(browseDate)
225+
setValue(browseDate)
249226
} else if (e.key === 'Enter') {
250227
setValue(browseDate)
251228
dispatch('select', cloneDate(browseDate))
@@ -355,7 +332,7 @@
355332
<div
356333
class="cell"
357334
on:click={() => selectDay(calendarDay)}
358-
class:disabled={!dayIsInRange(calendarDay, min, max)}
335+
class:disabled={!dayIsInRange(calendarDay, min, max) || handleDisabledDate(calendarDay)}
359336
class:selected={value &&
360337
calendarDay.year === value.getFullYear() &&
361338
calendarDay.month === value.getMonth() &&

src/lib/date-utils.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export function toText(date: Date | null, formatTokens: FormatToken[]): string {
2323
return text
2424
}
2525

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+
2634
export type CalendarDay = {
2735
year: number
2836
month: number
@@ -88,3 +96,85 @@ export function applyTimePrecision(
8896
date.setMilliseconds(0)
8997
}
9098
}
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+
}

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as DatePicker } from './DatePicker.svelte'
22
export { default as DateInput } from './DateInput.svelte'
33

44
export { localeFromDateFnsLocale, type Locale } from './locale.js'
5+
export { isSameDate } from './date-utils.js'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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}

src/routes/docs/+page.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The component will not assign a date value until a specific date is selected in
4646
| `browseWithoutSelecting` | bool | Wait with updating the date until a value is selected |
4747
| `dynamicPositioning` | bool | Dynamically postions the date popup to best fit on the screen |
4848
| `locale` | Locale | Locale object for internationalization |
49+
| `isDisabledDate` | ((dateToCheck: Date) => boolean) \| null | Disallow specific dates |
4950

5051
<h4 id="format-string">Format string</h4>
5152

@@ -76,6 +77,23 @@ The component will not assign a date value until a specific date is selected in
7677
| `timePrecision` | "minute" \| "second" \| "millisecond" \| null | Show a time picker with the specified precision |
7778
| `locale` | Locale | Locale object for internationalization |
7879
| `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+
```
7997

8098
<h2 id="internationalization">Internationalization</h2>
8199

@@ -91,7 +109,7 @@ Object to support internationalization. Properties (all are optional):
91109

92110
If you use [date-fns](https://date-fns.org/), you can create a Locale object by passing a date-fns locale to this function:
93111

94-
```js
112+
```svelte
95113
<script>
96114
import { DatePicker, localeFromDateFnsLocale } from 'date-picker-svelte'
97115
import { hy } from 'date-fns/locale'

0 commit comments

Comments
 (0)