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