Skip to content

add isDisabledDate #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
68e7e60
added the ability to enable and disable dates, using isDisabledDate f…
Mar 14, 2025
496ee3e
reworked to use isDisabledDate prop
Mar 17, 2025
1af6343
fixed docs
Mar 17, 2025
f1dbb3a
ran prettier
Mar 17, 2025
3b19076
Update comments
probablykasper Mar 19, 2025
140ec54
Use optional chaining
probablykasper Mar 19, 2025
5e4dbdd
Update docs example
probablykasper Mar 19, 2025
af95b34
Revert demo
probablykasper Mar 19, 2025
2b33957
Fix lints
probablykasper Mar 19, 2025
69bc152
Create disableddate dev page
probablykasper Mar 19, 2025
a628b2a
Format
probablykasper Mar 19, 2025
c1f5a2f
Merge branch 'master' into add-enabledDates-disabledDates
Mar 19, 2025
aa89838
added checks to prevent selecting a disabled date by typing or from a…
Mar 20, 2025
29a5bad
Small adjustments
probablykasper Mar 31, 2025
4df2ce3
Update CHANGELOG.md
probablykasper Mar 31, 2025
b10e4f1
updated-fallback-to-browseDate
stinger567 Mar 31, 2025
fc5e44c
fixed-typo
stinger567 Mar 31, 2025
03a8ac3
Make toValidDate args non-null
probablykasper Mar 31, 2025
8365dea
Debug set date button
probablykasper Mar 31, 2025
8184f52
Svelte 3 test
probablykasper Mar 31, 2025
0d19aa9
Revert "Svelte 3 test"
probablykasper Mar 31, 2025
3c32eff
added toValidDate to both DateInput and DatePicker
Apr 1, 2025
0948566
Merge remote-tracking branch 'refs/remotes/origin/add-enabledDates-di…
Apr 1, 2025
76e8d0a
Fix time changing when switching calendar dates
probablykasper Apr 2, 2025
ee83a7a
Also run toValidDate in textUpdate
probablykasper Apr 2, 2025
bebfcd2
Fix double-setting to invalid date not being fixed
probablykasper Apr 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## Next
- Add `isDisabledDate` prop (@stinger567)

## 2.15.2 - 2025 Mar 19
- Fix `timePrecision` not always setting unused values to 0 (@stinger567)

Expand Down
19 changes: 11 additions & 8 deletions src/lib/DateInput.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { cubicInOut } from 'svelte/easing'
import { toText } from './date-utils.js'
import { toText, cloneDate, toValidDate } from './date-utils.js'
import type { Locale } from './locale.js'
import { parse, createFormat, type FormatToken } from './parse.js'
import DateTimePicker from './DatePicker.svelte'
Expand All @@ -16,10 +16,6 @@
/** Default date to display in picker before value is assigned */
const defaultDate = new Date()

function cloneDate(d: Date) {
return new Date(d.getTime())
}

// inner date value store for preventing value updates (and also
// text updates as a result) when date is unchanged
const innerStore = writable(null as Date | null)
Expand All @@ -30,7 +26,10 @@
if (date === null || date === undefined) {
innerStore.set(null)
value = date
} else if (date.getTime() !== $innerStore?.getTime()) {
} else if (
date.getTime() !== $innerStore?.getTime() ||
date.getTime() !== value?.getTime()
) {
innerStore.set(cloneDate(date))
value = date
}
Expand All @@ -40,7 +39,7 @@

/** Date value */
export let value: Date | null = null
$: store.set(value)
$: store.set(value ? toValidDate(defaultDate, value, min, max, isDisabledDate) : value)

/** The earliest value the user can select */
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
Expand Down Expand Up @@ -80,7 +79,7 @@
const result = parse(text, formatTokens, $store)
if (result.date !== null) {
valid = true
store.set(result.date)
store.set(toValidDate(defaultDate, result.date, min, max, isDisabledDate))
} else {
valid = false
}
Expand All @@ -105,6 +104,9 @@
/** Show a time picker with the specified precision */
export let timePrecision: 'minute' | 'second' | 'millisecond' | null = null

/** Disallow specific dates */
export let isDisabledDate: ((dateToCheck: Date) => boolean) | null = null

// handle on:focusout for parent element. If the parent element loses
// focus (e.g input element), visible is set to false
function onFocusOut(e: FocusEvent) {
Expand Down Expand Up @@ -237,6 +239,7 @@
{locale}
{browseWithoutSelecting}
{timePrecision}
{isDisabledDate}
>
<slot />
</DateTimePicker>
Expand Down
69 changes: 23 additions & 46 deletions src/lib/DatePicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,29 @@
getCalendarDays,
type CalendarDay,
applyTimePrecision,
clampDate,
clamp,
} from './date-utils.js'
import { getInnerLocale, type Locale } from './locale.js'
import { createEventDispatcher } from 'svelte'
import { cloneDate, toValidDate } from './date-utils.js'

const dispatch = createEventDispatcher<{
/** Fires when the user selects a new value by clicking on a date or by pressing enter */
select: Date
}>()

function cloneDate(d: Date) {
return new Date(d.getTime())
}

/** Date value. It's `null` if no date is selected */
export let value: Date | null = null

function setValue(d: Date) {
if (d.getTime() !== value?.getTime()) {
browseDate = clamp(d, min, max)
browseDate = toValidDate(value ?? browseDate, d, min, max, isDisabledDate)
applyTimePrecision(browseDate, timePrecision)
value = cloneDate(browseDate)
}
}

function setValueDate(d: Date) {
if (d.getTime() !== value?.getTime()) {
browseDate = clampDate(d, min, max)
value = cloneDate(browseDate)
}
}

/** Set the browseDate */
function browse(d: Date) {
browseDate = clampDate(d, min, max)
Expand Down Expand Up @@ -63,35 +55,20 @@
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
/** The latest year the user can select */
export let max = new Date(defaultDate.getFullYear(), 11, 31, 23, 59, 59, 999)
/** Disallow specific dates */
export let isDisabledDate: ((dateToCheck: Date) => boolean) | null = null

function handleDisabledDate(date: CalendarDay) {
return isDisabledDate?.(new Date(date.year, date.month, date.number))
}

// Prevents a invalid date from being typed into the Dateinput text box
$: if (value && value > max) {
setValue(max)
setValue(toValidDate(value, max, min, max, isDisabledDate))
} else if (value && value < min) {
setValue(min)
}
function clamp(d: Date, min: Date, max: Date) {
if (d > max) {
return cloneDate(max)
} else if (d < min) {
return cloneDate(min)
} else {
return cloneDate(d)
}
}
function clampDate(d: Date, min: Date, max: Date) {
const limit = clamp(d, min, max)
if (limit.getTime() !== d.getTime()) {
d = new Date(
limit.getFullYear(),
limit.getMonth(),
limit.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds(),
d.getMilliseconds(),
)
d = clamp(d, min, max)
}
return d
setValue(toValidDate(value, min, min, max, isDisabledDate))
} else if (value && isDisabledDate?.(value)) {
setValue(toValidDate(browseDate, value, min, max, isDisabledDate))
}

/** The date shown in the popup when none is selected */
Expand Down Expand Up @@ -156,14 +133,14 @@
$: calendarDays = getCalendarDays(browseDate, iLocale.weekStartsOn)

function selectDay(calendarDay: CalendarDay) {
if (dayIsInRange(calendarDay, min, max)) {
if (dayIsInRange(calendarDay, min, max) && !handleDisabledDate(calendarDay)) {
browseDate.setFullYear(0)
browseDate.setMonth(0)
browseDate.setDate(1)
browseDate.setFullYear(calendarDay.year)
browseDate.setMonth(calendarDay.month)
browseDate.setDate(calendarDay.number)
setValueDate(browseDate)
setValue(browseDate)
dispatch('select', cloneDate(browseDate))
}
}
Expand Down Expand Up @@ -236,16 +213,16 @@
return
} else if (e.key === 'ArrowUp') {
browseDate.setDate(browseDate.getDate() - 7)
setValueDate(browseDate)
setValue(browseDate)
} else if (e.key === 'ArrowDown') {
browseDate.setDate(browseDate.getDate() + 7)
setValueDate(browseDate)
setValue(browseDate)
} else if (e.key === 'ArrowLeft') {
browseDate.setDate(browseDate.getDate() - 1)
setValueDate(browseDate)
setValue(browseDate)
} else if (e.key === 'ArrowRight') {
browseDate.setDate(browseDate.getDate() + 1)
setValueDate(browseDate)
setValue(browseDate)
} else if (e.key === 'Enter') {
setValue(browseDate)
dispatch('select', cloneDate(browseDate))
Expand Down Expand Up @@ -355,7 +332,7 @@
<div
class="cell"
on:click={() => selectDay(calendarDay)}
class:disabled={!dayIsInRange(calendarDay, min, max)}
class:disabled={!dayIsInRange(calendarDay, min, max) || handleDisabledDate(calendarDay)}
class:selected={value &&
calendarDay.year === value.getFullYear() &&
calendarDay.month === value.getMonth() &&
Expand Down
90 changes: 90 additions & 0 deletions src/lib/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export function toText(date: Date | null, formatTokens: FormatToken[]): string {
return text
}

export function isSameDate(date1: Date, date2: Date) {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
)
}

export type CalendarDay = {
year: number
month: number
Expand Down Expand Up @@ -88,3 +96,85 @@ export function applyTimePrecision(
date.setMilliseconds(0)
}
}

export function cloneDate(d: Date) {
return new Date(d)
}

export function toValidDate(
oldDate: Date,
newDate: Date,
minDate: Date,
maxDate: Date,
isDisabledDate: ((date: Date) => boolean) | null,
): Date {
// Don't mutate the original newDate to avoid unintended side effects
let adjustedDate = cloneDate(newDate)

if (oldDate > newDate) {
adjustDate(adjustedDate, -1, minDate, maxDate, isDisabledDate)
if (adjustedDate < minDate) {
adjustedDate = clampDate(adjustedDate, minDate, maxDate)
// Adjusts the date one more time if the min date is disabled, to ensure a valid, enabled date is selected
adjustDate(adjustedDate, 1, minDate, maxDate, isDisabledDate)
}
} else if (adjustedDate >= oldDate) {
adjustDate(adjustedDate, 1, minDate, maxDate, isDisabledDate)
if (adjustedDate > maxDate) {
adjustedDate = clampDate(adjustedDate, minDate, maxDate)
// Adjusts the date one more time if the max date is disabled, to ensure a valid, enabled date is selected
adjustDate(adjustedDate, -1, minDate, maxDate, isDisabledDate)
}
}
// Finally, clamp the time
if (adjustedDate < minDate || adjustedDate > maxDate) {
adjustedDate = clamp(adjustedDate, minDate, maxDate)
}
return adjustedDate
}

function adjustDate(
date: Date,
increment: number,
minDate: Date,
maxDate: Date,
isDisabledDate: ((date: Date) => boolean) | null,
) {
// Prevents accidental infinite loops
const MAXLOOPS = 36525 // ~100 years, should be large enough
let loopCount = 0

while (isDisabledDate?.(date) && date >= minDate && date <= maxDate && loopCount <= MAXLOOPS) {
date.setDate(date.getDate() + increment)
loopCount++
}
}

export function clamp(value: Date, min: Date, max: Date) {
if (value > max) {
return cloneDate(max)
} else if (value < min) {
return cloneDate(min)
} else {
return cloneDate(value)
}
}
export function clampDate(value: Date, min: Date, max: Date) {
const limit = clamp(value, min, max)
value = new Date(
limit.getFullYear(),
limit.getMonth(),
limit.getDate(),
value.getHours(),
value.getMinutes(),
value.getSeconds(),
value.getMilliseconds(),
)
if (value > max) {
value.setDate(max.getDate())
}
if (value < min) {
value.setDate(min.getDate())
}
return value
}
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as DatePicker } from './DatePicker.svelte'
export { default as DateInput } from './DateInput.svelte'

export { localeFromDateFnsLocale, type Locale } from './locale.js'
export { isSameDate } from './date-utils.js'
22 changes: 22 additions & 0 deletions src/routes/bug/isdisableddate/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
import DateInput from '$lib/DateInput.svelte'

let min = new Date(2024, 1, 26, 17, 30)
let value: Date | undefined
</script>

<DateInput
timePrecision="minute"
{min}
bind:value
isDisabledDate={(date) => {
return date.getDate() === 15 || date.getDate() === 16
}}
/>
<button
on:click={() => {
value = new Date(2024, 10, 15)
}}>Set to 2024-10-15</button
>

{value}
20 changes: 19 additions & 1 deletion src/routes/docs/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The component will not assign a date value until a specific date is selected in
| `browseWithoutSelecting` | bool | Wait with updating the date until a value is selected |
| `dynamicPositioning` | bool | Dynamically postions the date popup to best fit on the screen |
| `locale` | Locale | Locale object for internationalization |
| `isDisabledDate` | ((dateToCheck: Date) => boolean) \| null | Disallow specific dates |

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

Expand Down Expand Up @@ -76,6 +77,23 @@ The component will not assign a date value until a specific date is selected in
| `timePrecision` | "minute" \| "second" \| "millisecond" \| null | Show a time picker with the specified precision |
| `locale` | Locale | Locale object for internationalization |
| `browseWithoutSelecting` | bool | Wait with updating the date until a date is selected |
| `isDisabledDate` | ((dateToCheck: Date) => boolean) \| null | Disallow specific dates |

<h2 id="isDisabledDate">Date disabling example</h2>

Example usage of the `isDisabledDate` prop:

```svelte
<script>
const disabledDate = new Date()
</script>

<DatePicker
isDisabledDate={(dateToCheck) => {
return isSameDate(dateToCheck, disabledDate)
}}
/>
```

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

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

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

```js
```svelte
<script>
import { DatePicker, localeFromDateFnsLocale } from 'date-picker-svelte'
import { hy } from 'date-fns/locale'
Expand Down