|
7 | 7 |
|
8 | 8 | const dispatch = createEventDispatcher<{ select: undefined }>() |
9 | 9 |
|
| 10 | + function cloneDate(d: Date) { |
| 11 | + return new Date(d.getTime()) |
| 12 | + } |
| 13 | +
|
10 | 14 | /** Date value. It's `null` if no date is selected */ |
11 | 15 | export let value: Date | null = null |
| 16 | +
|
12 | 17 | function setValue(d: Date) { |
13 | 18 | if (d.getTime() !== value?.getTime()) { |
14 | | - value = d |
| 19 | + browseDate = clamp(d, min, max) |
| 20 | + value = cloneDate(browseDate) |
15 | 21 | } |
16 | 22 | } |
17 | | - function updateValue(updater: (date: Date) => Date) { |
18 | | - const newValue = updater(new Date(shownDate.getTime())) |
19 | | - setValue(newValue) |
| 23 | + function browse(d: Date) { |
| 24 | + browseDate = clamp(d, min, max) |
| 25 | + if (!browseWithoutSelecting && value) { |
| 26 | + setValue(browseDate) |
| 27 | + } |
20 | 28 | } |
21 | 29 |
|
22 | 30 | /** Default Date to use */ |
23 | 31 | const defaultDate = new Date() |
24 | 32 |
|
25 | | - /** The date shown in the popup, for when `value` is null */ |
26 | | - let shownDate = value ?? defaultDate |
27 | | - $: if (value) shownDate = value |
28 | | -
|
29 | | - /** Update the shownDate. The date is only selected if a date is already selected */ |
30 | | - function updateShownDate(updater: (date: Date) => Date) { |
31 | | - shownDate = updater(new Date(shownDate.getTime())) |
32 | | - if (value && shownDate.getTime() !== value.getTime()) { |
33 | | - setValue(shownDate) |
34 | | - } |
35 | | - } |
| 33 | + /** The date shown in the popup when none is selected */ |
| 34 | + let browseDate = value ? cloneDate(value) : cloneDate(defaultDate) |
36 | 35 |
|
37 | 36 | /** The earliest year the user can select */ |
38 | 37 | export let min = new Date(defaultDate.getFullYear() - 20, 0, 1) |
39 | 38 | /** The latest year the user can select */ |
40 | 39 | export let max = new Date(defaultDate.getFullYear(), 11, 31, 23, 59, 59, 999) |
| 40 | + $: if (value && value > max) { |
| 41 | + setValue(max) |
| 42 | + } else if (value && value < min) { |
| 43 | + setValue(min) |
| 44 | + } |
| 45 | + function clamp(d: Date, min: Date, max: Date) { |
| 46 | + if (browseDate > max) { |
| 47 | + return cloneDate(max) |
| 48 | + } else if (browseDate < min) { |
| 49 | + return cloneDate(min) |
| 50 | + } else { |
| 51 | + return cloneDate(d) |
| 52 | + } |
| 53 | + } |
| 54 | +
|
41 | 55 | let years = getYears(min, max) |
42 | 56 | $: years = getYears(min, max) |
43 | 57 | function getYears(min: Date, max: Date) { |
|
48 | 62 | return years |
49 | 63 | } |
50 | 64 |
|
51 | | - $: if (shownDate > max) { |
52 | | - updateShownDate(() => max) |
53 | | - } else if (shownDate < min) { |
54 | | - updateShownDate(() => min) |
55 | | - } |
56 | | -
|
57 | 65 | /** Locale object for internationalization */ |
58 | 66 | export let locale: Locale = {} |
59 | 67 | $: iLocale = getInnerLocale(locale) |
| 68 | + /** Wait with updating the date until a date is selected */ |
| 69 | + export let browseWithoutSelecting = false |
60 | 70 |
|
61 | | - let year = shownDate.getFullYear() |
62 | | - const getYear = (tmpPickerDate: Date) => (year = tmpPickerDate.getFullYear()) |
63 | | - function setYear(year: number) { |
64 | | - updateShownDate((tmpPickerDate) => { |
65 | | - tmpPickerDate.setFullYear(year) |
66 | | - return tmpPickerDate |
67 | | - }) |
| 71 | + let browseYear = browseDate.getFullYear() |
| 72 | + function getBrowseYear(d: Date) { |
| 73 | + browseYear = d.getFullYear() |
68 | 74 | } |
69 | | - $: getYear(shownDate) |
70 | | - $: setYear(year) |
| 75 | + $: getBrowseYear(browseDate) |
| 76 | + function setYear(newYear: number) { |
| 77 | + browseDate.setFullYear(newYear) |
| 78 | + browseDate = browseDate |
| 79 | + browse(browseDate) |
| 80 | + } |
| 81 | + $: setYear(browseYear) |
71 | 82 |
|
72 | | - let month = shownDate.getMonth() |
73 | | - const getMonth = (tmpPickerDate: Date) => (month = tmpPickerDate.getMonth()) |
74 | | - function setMonth(month: number) { |
75 | | - let newYear = year |
76 | | - let newMonth = month |
77 | | - if (month === 12) { |
| 83 | + let browseMonth = browseDate.getMonth() |
| 84 | + function getBrowseMonth(d: Date) { |
| 85 | + browseMonth = d.getMonth() |
| 86 | + } |
| 87 | + $: getBrowseMonth(browseDate) |
| 88 | + function setMonth(newMonth: number) { |
| 89 | + let newYear = browseDate.getFullYear() |
| 90 | + if (newMonth === 12) { |
78 | 91 | newMonth = 0 |
79 | 92 | newYear++ |
80 | | - } else if (month === -1) { |
| 93 | + } else if (newMonth === -1) { |
81 | 94 | newMonth = 11 |
82 | 95 | newYear-- |
83 | 96 | } |
84 | 97 |
|
85 | 98 | const maxDate = getMonthLength(newYear, newMonth) |
86 | | - const newDate = Math.min(shownDate.getDate(), maxDate) |
87 | | - updateShownDate((date) => { |
88 | | - return new Date( |
| 99 | + const newDate = Math.min(browseDate.getDate(), maxDate) |
| 100 | + browse( |
| 101 | + new Date( |
89 | 102 | newYear, |
90 | 103 | newMonth, |
91 | 104 | newDate, |
92 | | - date.getHours(), |
93 | | - date.getMinutes(), |
94 | | - date.getSeconds(), |
95 | | - date.getMilliseconds() |
| 105 | + browseDate.getHours(), |
| 106 | + browseDate.getMinutes(), |
| 107 | + browseDate.getSeconds(), |
| 108 | + browseDate.getMilliseconds() |
96 | 109 | ) |
97 | | - }) |
| 110 | + ) |
98 | 111 | } |
99 | | - $: getMonth(shownDate) |
100 | | - $: setMonth(month) |
| 112 | + $: setMonth(browseMonth) |
101 | 113 |
|
102 | | - let dayOfMonth = value?.getDate() || null |
103 | | - $: dayOfMonth = value?.getDate() || null |
| 114 | + $: calendarDays = getCalendarDays(browseDate, iLocale.weekStartsOn) |
104 | 115 |
|
105 | | - $: calendarDays = getCalendarDays(shownDate, iLocale.weekStartsOn) |
106 | | -
|
107 | | - function setDay(calendarDay: CalendarDay) { |
| 116 | + function selectDay(calendarDay: CalendarDay) { |
108 | 117 | if (dayIsInRange(calendarDay, min, max)) { |
109 | | - updateValue((value) => { |
110 | | - value.setFullYear(0) |
111 | | - value.setMonth(0) |
112 | | - value.setDate(1) |
113 | | - value.setFullYear(calendarDay.year) |
114 | | - value.setMonth(calendarDay.month) |
115 | | - value.setDate(calendarDay.number) |
116 | | - return value |
117 | | - }) |
| 118 | + browseDate.setFullYear(0) |
| 119 | + browseDate.setMonth(0) |
| 120 | + browseDate.setDate(1) |
| 121 | + browseDate.setFullYear(calendarDay.year) |
| 122 | + browseDate.setMonth(calendarDay.month) |
| 123 | + browseDate.setDate(calendarDay.number) |
| 124 | + setValue(browseDate) |
| 125 | + dispatch('select') |
118 | 126 | } |
119 | 127 | } |
120 | | - function selectDay(calendarDay: CalendarDay) { |
121 | | - setDay(calendarDay) |
122 | | - dispatch('select') |
123 | | - } |
124 | 128 | function dayIsInRange(calendarDay: CalendarDay, min: Date, max: Date) { |
125 | 129 | const date = new Date(calendarDay.year, calendarDay.month, calendarDay.number) |
126 | 130 | const minDate = new Date(min.getFullYear(), min.getMonth(), min.getDate()) |
|
129 | 133 | } |
130 | 134 | function shiftKeydown(e: KeyboardEvent) { |
131 | 135 | if (e.shiftKey && e.key === 'ArrowUp') { |
132 | | - setYear(year - 1) |
| 136 | + setYear(browseDate.getFullYear() - 1) |
133 | 137 | } else if (e.shiftKey && e.key === 'ArrowDown') { |
134 | | - setYear(year + 1) |
| 138 | + setYear(browseDate.getFullYear() + 1) |
135 | 139 | } else if (e.shiftKey && e.key === 'ArrowLeft') { |
136 | | - setMonth(month - 1) |
| 140 | + setMonth(browseDate.getMonth() - 1) |
137 | 141 | } else if (e.shiftKey && e.key === 'ArrowRight') { |
138 | | - setMonth(month + 1) |
| 142 | + setMonth(browseDate.getMonth() + 1) |
139 | 143 | } else { |
140 | 144 | return false |
141 | 145 | } |
|
148 | 152 | shiftKeydown(e) |
149 | 153 | return |
150 | 154 | } else if (e.key === 'ArrowUp') { |
151 | | - setYear(year - 1) |
| 155 | + setYear(browseDate.getFullYear() - 1) |
152 | 156 | } else if (e.key === 'ArrowDown') { |
153 | | - setYear(year + 1) |
| 157 | + setYear(browseDate.getFullYear() + 1) |
154 | 158 | } else if (e.key === 'ArrowLeft') { |
155 | | - setMonth(month - 1) |
| 159 | + setMonth(browseDate.getMonth() - 1) |
156 | 160 | } else if (e.key === 'ArrowRight') { |
157 | | - setMonth(month + 1) |
| 161 | + setMonth(browseDate.getMonth() + 1) |
158 | 162 | } else { |
159 | 163 | shiftKeydown(e) |
160 | 164 | return |
|
167 | 171 | shiftKeydown(e) |
168 | 172 | return |
169 | 173 | } else if (e.key === 'ArrowUp') { |
170 | | - setMonth(month - 1) |
| 174 | + setMonth(browseDate.getFullYear() - 1) |
171 | 175 | } else if (e.key === 'ArrowDown') { |
172 | | - setMonth(month + 1) |
| 176 | + setMonth(browseDate.getFullYear() + 1) |
173 | 177 | } else if (e.key === 'ArrowLeft') { |
174 | | - setMonth(month - 1) |
| 178 | + setMonth(browseDate.getFullYear() - 1) |
175 | 179 | } else if (e.key === 'ArrowRight') { |
176 | | - setMonth(month + 1) |
| 180 | + setMonth(browseDate.getFullYear() + 1) |
177 | 181 | } else { |
178 | 182 | shiftKeydown(e) |
179 | 183 | return |
|
189 | 193 | shiftKeydown(e) |
190 | 194 | return |
191 | 195 | } else if (e.key === 'ArrowUp') { |
192 | | - updateValue((value) => { |
193 | | - value.setDate(value.getDate() - 7) |
194 | | - return value |
195 | | - }) |
| 196 | + browseDate.setDate(browseDate.getDate() - 7) |
| 197 | + setValue(browseDate) |
196 | 198 | } else if (e.key === 'ArrowDown') { |
197 | | - updateValue((value) => { |
198 | | - value.setDate(value.getDate() + 7) |
199 | | - return value |
200 | | - }) |
| 199 | + browseDate.setDate(browseDate.getDate() + 7) |
| 200 | + setValue(browseDate) |
201 | 201 | } else if (e.key === 'ArrowLeft') { |
202 | | - updateValue((value) => { |
203 | | - value.setDate(value.getDate() - 1) |
204 | | - return value |
205 | | - }) |
| 202 | + browseDate.setDate(browseDate.getDate() - 1) |
| 203 | + setValue(browseDate) |
206 | 204 | } else if (e.key === 'ArrowRight') { |
207 | | - updateValue((value) => { |
208 | | - value.setDate(value.getDate() + 1) |
209 | | - return value |
210 | | - }) |
| 205 | + browseDate.setDate(browseDate.getDate() + 1) |
| 206 | + setValue(browseDate) |
| 207 | + } else if (e.key === 'Enter') { |
| 208 | + setValue(browseDate) |
| 209 | + dispatch('select') |
211 | 210 | } else { |
212 | 211 | return |
213 | 212 | } |
|
218 | 217 | <div class="date-time-picker" on:focusout tabindex="0" on:keydown={keydown}> |
219 | 218 | <div class="tab-container" tabindex="-1"> |
220 | 219 | <div class="top"> |
221 | | - <div class="page-button" tabindex="-1" on:click={() => setMonth(month - 1)}> |
| 220 | + <div class="page-button" tabindex="-1" on:click={() => setMonth(browseDate.getMonth() - 1)}> |
222 | 221 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" |
223 | 222 | ><path |
224 | 223 | d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z" |
|
227 | 226 | > |
228 | 227 | </div> |
229 | 228 | <div class="dropdown month"> |
230 | | - <select bind:value={month} on:keydown={monthKeydown}> |
| 229 | + <select |
| 230 | + bind:value={browseMonth} |
| 231 | + on:input={() => { |
| 232 | + setMonth(browseMonth) |
| 233 | + }} |
| 234 | + on:keydown={monthKeydown} |
| 235 | + > |
231 | 236 | {#each iLocale.months as monthName, i} |
232 | 237 | <option |
233 | | - disabled={new Date(year, i, getMonthLength(year, i), 23, 59, 59, 999) < min || |
234 | | - new Date(year, i) > max} |
| 238 | + disabled={new Date(browseYear, i, getMonthLength(browseYear, i), 23, 59, 59, 999) < |
| 239 | + min || new Date(browseYear, i) > max} |
235 | 240 | value={i}>{monthName}</option |
236 | 241 | > |
237 | 242 | {/each} |
|
245 | 250 | --> |
246 | 251 | <select class="dummy-select" tabindex="-1"> |
247 | 252 | {#each iLocale.months as monthName, i} |
248 | | - <option value={i} selected={i === month}>{monthName}</option> |
| 253 | + <option value={i} selected={i === browseMonth}>{monthName}</option> |
249 | 254 | {/each} |
250 | 255 | </select> |
251 | 256 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" |
252 | 257 | ><path d="M6 0l12 12-12 12z" transform="rotate(90, 12, 12)" /></svg |
253 | 258 | > |
254 | 259 | </div> |
255 | 260 | <div class="dropdown year"> |
256 | | - <select bind:value={year} on:keydown={yearKeydown}> |
| 261 | + <select bind:value={browseYear} on:keydown={yearKeydown}> |
257 | 262 | {#each years as v} |
258 | 263 | <option value={v}>{v}</option> |
259 | 264 | {/each} |
260 | 265 | </select> |
261 | 266 | <!-- style <select> button without affecting menu popup --> |
262 | 267 | <select class="dummy-select" tabindex="-1"> |
263 | 268 | {#each years as v} |
264 | | - <option value={v} selected={v === year}>{v}</option> |
| 269 | + <option value={v} selected={v === browseDate.getFullYear()}>{v}</option> |
265 | 270 | {/each} |
266 | 271 | </select> |
267 | 272 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" |
268 | 273 | ><path d="M6 0l12 12-12 12z" transform="rotate(90, 12, 12)" /></svg |
269 | 274 | > |
270 | 275 | </div> |
271 | | - <div class="page-button" tabindex="-1" on:click={() => setMonth(month + 1)}> |
| 276 | + <div class="page-button" tabindex="-1" on:click={() => setMonth(browseDate.getMonth() + 1)}> |
272 | 277 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" |
273 | 278 | ><path d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z" /></svg |
274 | 279 | > |
|
290 | 295 | class="cell" |
291 | 296 | on:click={() => selectDay(calendarDay)} |
292 | 297 | class:disabled={!dayIsInRange(calendarDay, min, max)} |
293 | | - class:selected={calendarDay.month === month && calendarDay.number === dayOfMonth} |
294 | | - class:other-month={calendarDay.month !== month} |
| 298 | + class:selected={calendarDay.year === value?.getFullYear() && |
| 299 | + calendarDay.month === value?.getMonth() && |
| 300 | + calendarDay.number === value.getDate()} |
| 301 | + class:other-month={calendarDay.month !== browseMonth} |
295 | 302 | > |
296 | 303 | <span>{calendarDay.number}</span> |
297 | 304 | </div> |
|
0 commit comments