@stonecrop/aform 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aform.d.ts +13 -0
- package/dist/aform.js +2450 -2370
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -1
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/form/ADate.vue +51 -16
- package/src/components/form/ADatePicker.vue +276 -60
- package/src/components/form/ADateRange.vue +190 -0
- package/src/components/form/ADateSelection.vue +166 -0
- package/src/components/form/ADateTime.vue +285 -0
- package/src/components/form/ADuration.vue +119 -0
- package/src/index.ts +12 -0
- package/src/types/index.ts +3 -0
|
@@ -11,6 +11,30 @@
|
|
|
11
11
|
<th colspan="5" :tabindex="-1">{{ monthAndYear }}</th>
|
|
12
12
|
<td id="next-month-btn" :tabindex="-1" @click="nextMonth">></td>
|
|
13
13
|
</tr>
|
|
14
|
+
<tr v-if="selectRange">
|
|
15
|
+
<td colspan="7">
|
|
16
|
+
<div class="date-input">
|
|
17
|
+
<input
|
|
18
|
+
ref="start-date-input"
|
|
19
|
+
:value="getStartDate"
|
|
20
|
+
class="date-input-start aform_input-field"
|
|
21
|
+
type="text"
|
|
22
|
+
placeholder="start date"
|
|
23
|
+
@blur="enterInputDate()"
|
|
24
|
+
@keydown="enterDate" />
|
|
25
|
+
<div>-</div>
|
|
26
|
+
<input
|
|
27
|
+
ref="end-date-input"
|
|
28
|
+
:value="getEndDate"
|
|
29
|
+
class="date-input-end aform_input-field"
|
|
30
|
+
type="text"
|
|
31
|
+
placeholder="end date"
|
|
32
|
+
@blur="enterInputDate()"
|
|
33
|
+
@keydown="enterDate" />
|
|
34
|
+
</div>
|
|
35
|
+
<!-- {{ formattedDateRange }} -->
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
14
38
|
<tr class="days-header">
|
|
15
39
|
<td>M</td>
|
|
16
40
|
<td>T</td>
|
|
@@ -26,15 +50,20 @@
|
|
|
26
50
|
v-for="colNo in numberOfColumns"
|
|
27
51
|
ref="celldate"
|
|
28
52
|
:key="getCurrentCell(rowNo, colNo)"
|
|
53
|
+
class="date-cell"
|
|
29
54
|
:contenteditable="false"
|
|
30
55
|
:spellcheck="false"
|
|
31
56
|
:tabindex="0"
|
|
32
57
|
:class="{
|
|
33
58
|
todaysDate: isTodaysDate(getCurrentDate(rowNo, colNo)),
|
|
34
59
|
selectedDate: isSelectedDate(getCurrentDate(rowNo, colNo)),
|
|
60
|
+
withinRange: selectRange ? isInDateRange(getCurrentDate(rowNo, colNo)) : false,
|
|
61
|
+
startDate: selectRange ? isStartDate(getCurrentDate(rowNo, colNo)) : false,
|
|
62
|
+
endDate: selectRange ? isEndDate(getCurrentDate(rowNo, colNo)) : false,
|
|
35
63
|
}"
|
|
36
64
|
@click.prevent.stop="selectDate(getCurrentCell(rowNo, colNo))"
|
|
37
|
-
@keydown.enter="selectDate(getCurrentCell(rowNo, colNo))"
|
|
65
|
+
@keydown.enter="selectDate(getCurrentCell(rowNo, colNo))"
|
|
66
|
+
@mouseover="hoverDate(getCurrentCell(rowNo, colNo))">
|
|
38
67
|
{{ new Date(getCurrentDate(rowNo, colNo)).getDate() }}
|
|
39
68
|
</td>
|
|
40
69
|
</tr>
|
|
@@ -44,7 +73,8 @@
|
|
|
44
73
|
</template>
|
|
45
74
|
|
|
46
75
|
<script setup lang="ts">
|
|
47
|
-
|
|
76
|
+
/* removed keyboard nav temportarily since it interfered with user experience navigating input fields */
|
|
77
|
+
// import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
|
|
48
78
|
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
|
|
49
79
|
|
|
50
80
|
import type { ComponentProps } from '../../types'
|
|
@@ -52,32 +82,105 @@ import type { ComponentProps } from '../../types'
|
|
|
52
82
|
const numberOfRows = 6
|
|
53
83
|
const numberOfColumns = 7
|
|
54
84
|
|
|
55
|
-
const { mode, label } = defineProps<ComponentProps>()
|
|
85
|
+
const { mode, label, selectRange = false } = defineProps<ComponentProps>()
|
|
56
86
|
|
|
57
87
|
const date = defineModel<number | Date>({ default: new Date() })
|
|
58
88
|
const selectedDate = ref(new Date(date.value))
|
|
59
89
|
const currentMonth = ref<number>(selectedDate.value.getMonth())
|
|
60
90
|
const currentYear = ref<number>(selectedDate.value.getFullYear())
|
|
61
91
|
const currentDates = ref<number[]>([])
|
|
62
|
-
const datepickerRef = useTemplateRef<HTMLDivElement>('datepicker')
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
/* needed for keyboard navigation. uncomment if implementing */
|
|
94
|
+
// const datepickerRef = useTemplateRef<HTMLDivElement>('datepicker')
|
|
66
95
|
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
const hoveredDate = ref(new Date(date.value))
|
|
97
|
+
const start_date = ref<Date | null>(null)
|
|
98
|
+
const end_date = ref<Date | null>(null)
|
|
99
|
+
const startDateInput = useTemplateRef<HTMLInputElement>('start-date-input')
|
|
100
|
+
const endDateInput = useTemplateRef<HTMLInputElement>('end-date-input')
|
|
69
101
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
/*******************
|
|
103
|
+
Emits
|
|
104
|
+
*******************/
|
|
105
|
+
|
|
106
|
+
const emit = defineEmits<{
|
|
107
|
+
'get-date': [{ start: Date | null; end: Date | null; selected: Date }]
|
|
108
|
+
}>()
|
|
109
|
+
|
|
110
|
+
/*******************
|
|
111
|
+
Computed
|
|
112
|
+
*******************/
|
|
113
|
+
|
|
114
|
+
const monthAndYear = computed(() => {
|
|
115
|
+
return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
|
|
116
|
+
year: 'numeric',
|
|
117
|
+
month: 'long',
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const getStartDate = computed(() => {
|
|
122
|
+
return start_date.value != null ? parseDateToString(start_date.value) : ''
|
|
79
123
|
})
|
|
80
124
|
|
|
125
|
+
const getEndDate = computed(() => {
|
|
126
|
+
return end_date.value != null ? parseDateToString(end_date.value) : ''
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/*******************
|
|
130
|
+
Functions
|
|
131
|
+
*******************/
|
|
132
|
+
|
|
133
|
+
const parseDateToString = (date: Date | null) => {
|
|
134
|
+
if (!validateDate(date)) return ''
|
|
135
|
+
return date.getMonth() + 1 + '/' + date.getDate() + '/' + date.getFullYear()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isTodaysDate = (day: string | number | Date): boolean => {
|
|
139
|
+
const todaysDate = new Date()
|
|
140
|
+
if (currentMonth.value !== todaysDate.getMonth()) return false
|
|
141
|
+
return todaysDate.toDateString() === new Date(day).toDateString()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isSelectedDate = (day: string | number | Date) => {
|
|
145
|
+
return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isStartDate = (day: string | number | Date) => {
|
|
149
|
+
const start = start_date.value
|
|
150
|
+
if (!validateDate(start)) return false
|
|
151
|
+
return new Date(day).toDateString() === start.toDateString()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const isEndDate = (day: string | number | Date) => {
|
|
155
|
+
const end = end_date.value
|
|
156
|
+
if (!validateDate(end)) return false
|
|
157
|
+
return new Date(day).toDateString() === end.toDateString()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const getCurrentCell = (rowNo: number, colNo: number) => {
|
|
161
|
+
return (rowNo - 1) * numberOfColumns + colNo
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isInDateRange = (day: string | number | Date) => {
|
|
165
|
+
const start = start_date.value
|
|
166
|
+
if (!validateDate(start)) return false
|
|
167
|
+
const this_date = new Date(day)
|
|
168
|
+
|
|
169
|
+
//the end is either the selected end date or wherever the user is hovering
|
|
170
|
+
const end = end_date.value
|
|
171
|
+
const temp_end_date = validateDate(end) ? end : new Date(hoveredDate.value)
|
|
172
|
+
|
|
173
|
+
return this_date.getTime() > start.getTime() && this_date.getTime() < temp_end_date.getTime()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const getCurrentDate = (rowNo: number, colNo: number) => {
|
|
177
|
+
return currentDates.value[getCurrentCell(rowNo, colNo)]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hoverDate = (currentIndex: number) => {
|
|
181
|
+
hoveredDate.value = new Date(currentDates.value[currentIndex])
|
|
182
|
+
}
|
|
183
|
+
|
|
81
184
|
const populateMonth = () => {
|
|
82
185
|
currentDates.value = []
|
|
83
186
|
const firstOfMonth = new Date(currentYear.value, currentMonth.value, 1)
|
|
@@ -89,8 +192,6 @@ const populateMonth = () => {
|
|
|
89
192
|
currentDates.value.push(calendarStartDay + dayIndex * 86400000)
|
|
90
193
|
}
|
|
91
194
|
}
|
|
92
|
-
|
|
93
|
-
watch([currentMonth, currentYear], populateMonth)
|
|
94
195
|
const previousYear = () => (currentYear.value -= 1)
|
|
95
196
|
const nextYear = () => (currentYear.value += 1)
|
|
96
197
|
|
|
@@ -112,57 +213,141 @@ const nextMonth = () => {
|
|
|
112
213
|
}
|
|
113
214
|
}
|
|
114
215
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
if (currentMonth.value !== todaysDate.getMonth()) {
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
return todaysDate.toDateString() === new Date(day).toDateString()
|
|
216
|
+
const enterDate = (event: KeyboardEvent) => {
|
|
217
|
+
if (event.key === 'Enter') enterInputDate()
|
|
121
218
|
}
|
|
122
219
|
|
|
123
|
-
|
|
124
|
-
|
|
220
|
+
// useKeyboardNav([
|
|
221
|
+
// {
|
|
222
|
+
// parent: datepickerRef,
|
|
223
|
+
// selectors: 'td',
|
|
224
|
+
// handlers: {
|
|
225
|
+
// ...defaultKeypressHandlers,
|
|
226
|
+
// ...{
|
|
227
|
+
// 'keydown.pageup': previousMonth,
|
|
228
|
+
// 'keydown.shift.pageup': previousYear,
|
|
229
|
+
// 'keydown.pagedown': nextMonth,
|
|
230
|
+
// 'keydown.shift.pagedown': nextYear,
|
|
231
|
+
// // TODO: this is a hack to override the stonecrop enter handler;
|
|
232
|
+
// // store context inside the component so that handlers can be setup consistently
|
|
233
|
+
// // eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
234
|
+
// 'keydown.enter': () => {}, // select this date
|
|
235
|
+
// },
|
|
236
|
+
// },
|
|
237
|
+
// },
|
|
238
|
+
// ])
|
|
239
|
+
|
|
240
|
+
const selectDate = (currentIndex: number) => {
|
|
241
|
+
date.value = selectedDate.value = new Date(currentDates.value[currentIndex])
|
|
242
|
+
|
|
243
|
+
if (selectRange) {
|
|
244
|
+
const start = start_date.value
|
|
245
|
+
if (start == null || end_date.value != null) {
|
|
246
|
+
start_date.value = date.value
|
|
247
|
+
end_date.value = null
|
|
248
|
+
} else if (validateDate(start) && selectedDate.value.getTime() < start.getTime()) {
|
|
249
|
+
end_date.value = null
|
|
250
|
+
start_date.value = date.value
|
|
251
|
+
} else {
|
|
252
|
+
end_date.value = date.value
|
|
253
|
+
}
|
|
254
|
+
if (startDateInput.value) startDateInput.value.value = parseDateToString(start_date.value) ?? ''
|
|
255
|
+
if (endDateInput.value) endDateInput.value.value = parseDateToString(end_date.value) ?? ''
|
|
256
|
+
}
|
|
257
|
+
emitData()
|
|
125
258
|
}
|
|
126
259
|
|
|
127
|
-
const
|
|
128
|
-
|
|
260
|
+
const testDateOrder = () => {
|
|
261
|
+
const start = start_date.value
|
|
262
|
+
const end = end_date.value
|
|
263
|
+
if (validateDate(end) && validateDate(start) && end.getTime() < start.getTime())
|
|
264
|
+
[start_date.value, end_date.value] = [end, start]
|
|
129
265
|
}
|
|
130
266
|
|
|
131
|
-
const
|
|
132
|
-
return
|
|
267
|
+
const validateDate = (date: unknown): date is Date => {
|
|
268
|
+
return date instanceof Date && !isNaN(date.getTime())
|
|
133
269
|
}
|
|
134
270
|
|
|
135
|
-
const
|
|
136
|
-
|
|
271
|
+
const enterInputDate = () => {
|
|
272
|
+
if (startDateInput.value?.value == '') {
|
|
273
|
+
start_date.value = null
|
|
274
|
+
} else if (startDateInput.value) {
|
|
275
|
+
const start = new Date(startDateInput.value.value)
|
|
276
|
+
start_date.value = validateDate(start) ? start : null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (endDateInput.value?.value == '') {
|
|
280
|
+
end_date.value = null
|
|
281
|
+
} else if (endDateInput.value) {
|
|
282
|
+
const end = new Date(endDateInput.value.value)
|
|
283
|
+
end_date.value = validateDate(end) ? end : null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (validateDate(start_date.value)) {
|
|
287
|
+
if (validateDate(end_date.value)) testDateOrder()
|
|
288
|
+
selectedDate.value = start_date.value
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
emitData()
|
|
137
292
|
}
|
|
138
293
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
294
|
+
const emitData = () => {
|
|
295
|
+
emit('get-date', {
|
|
296
|
+
start: selectRange ? start_date.value : null,
|
|
297
|
+
end: selectRange ? end_date.value : null,
|
|
298
|
+
selected: selectedDate.value,
|
|
143
299
|
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/*******************
|
|
303
|
+
Hooks
|
|
304
|
+
*******************/
|
|
305
|
+
|
|
306
|
+
onMounted(async () => {
|
|
307
|
+
populateMonth()
|
|
308
|
+
// required to allow the elements to be focused in the next step
|
|
309
|
+
await nextTick()
|
|
310
|
+
const $selectedDate = document.getElementsByClassName('selectedDate')
|
|
311
|
+
if ($selectedDate.length > 0) {
|
|
312
|
+
;($selectedDate[0] as HTMLElement).focus()
|
|
313
|
+
} else {
|
|
314
|
+
const $todaysDate = document.getElementsByClassName('todaysDate')
|
|
315
|
+
if ($todaysDate.length > 0) {
|
|
316
|
+
;($todaysDate[0] as HTMLElement).focus()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
144
319
|
})
|
|
145
320
|
|
|
146
321
|
// setup keyboard navigation
|
|
147
|
-
useKeyboardNav([
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
])
|
|
322
|
+
// useKeyboardNav([
|
|
323
|
+
// {
|
|
324
|
+
// parent: datepickerRef,
|
|
325
|
+
// selectors: 'td',
|
|
326
|
+
// handlers: {
|
|
327
|
+
// ...defaultKeypressHandlers,
|
|
328
|
+
// ...{
|
|
329
|
+
// 'keydown.pageup': previousMonth,
|
|
330
|
+
// 'keydown.shift.pageup': previousYear,
|
|
331
|
+
// 'keydown.pagedown': nextMonth,
|
|
332
|
+
// 'keydown.shift.pagedown': nextYear,
|
|
333
|
+
// // TODO: this is a hack to override the stonecrop enter handler;
|
|
334
|
+
// // store context inside the component so that handlers can be setup consistently
|
|
335
|
+
|
|
336
|
+
// 'keydown.enter': () => {}, // select this date
|
|
337
|
+
// },
|
|
338
|
+
// },
|
|
339
|
+
// },
|
|
340
|
+
// ])
|
|
341
|
+
|
|
342
|
+
/*******************
|
|
343
|
+
Watchers
|
|
344
|
+
*******************/
|
|
345
|
+
|
|
346
|
+
watch([currentMonth, currentYear], populateMonth)
|
|
347
|
+
|
|
348
|
+
/*******************
|
|
349
|
+
Expose
|
|
350
|
+
*******************/
|
|
166
351
|
|
|
167
352
|
defineExpose({ currentMonth, currentYear, selectedDate })
|
|
168
353
|
</script>
|
|
@@ -175,6 +360,7 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
175
360
|
color: var(--sc-cell-text-color);
|
|
176
361
|
outline: none;
|
|
177
362
|
border-collapse: collapse;
|
|
363
|
+
margin-bottom: 10px;
|
|
178
364
|
/* width: calc(100% - 4px); */
|
|
179
365
|
}
|
|
180
366
|
|
|
@@ -190,26 +376,45 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
190
376
|
outline: 2px solid transparent;
|
|
191
377
|
min-width: 3ch;
|
|
192
378
|
max-width: 3ch;
|
|
379
|
+
cursor: pointer;
|
|
380
|
+
}
|
|
381
|
+
.adatepicker td.date-cell:hover {
|
|
382
|
+
background: var(--sc-gray-10);
|
|
193
383
|
}
|
|
194
384
|
|
|
195
385
|
.adatepicker td:focus,
|
|
196
386
|
.adatepicker td:focus-within {
|
|
197
|
-
outline: 1px dashed black;
|
|
387
|
+
/* outline: 1px dashed black; */
|
|
198
388
|
box-shadow: none;
|
|
199
389
|
overflow: hidden;
|
|
200
390
|
min-height: 1.15em;
|
|
201
391
|
max-height: 1.15em;
|
|
202
392
|
overflow: hidden;
|
|
203
393
|
}
|
|
204
|
-
.adatepicker .selectedDate
|
|
205
|
-
|
|
394
|
+
.adatepicker .selectedDate,
|
|
395
|
+
.adatepicker .startDate,
|
|
396
|
+
.adatepicker .endDate {
|
|
397
|
+
/* outline: 1px solid black; */
|
|
206
398
|
background: var(--sc-gray-20);
|
|
207
399
|
font-weight: bolder;
|
|
208
400
|
}
|
|
401
|
+
.adatepicker .startDate {
|
|
402
|
+
/* border-radius: 5px 0px 0px 5px; */
|
|
403
|
+
border-left: 1px solid var(--sc-gray-50);
|
|
404
|
+
background: var(--sc-gray-20) !important;
|
|
405
|
+
}
|
|
406
|
+
.adatepicker .endDate {
|
|
407
|
+
border-right: 1px solid var(--sc-gray-50);
|
|
408
|
+
/* border-radius: 0px 5px 5px 0px; */
|
|
409
|
+
background: var(--sc-gray-20) !important;
|
|
410
|
+
}
|
|
411
|
+
.adatepicker .withinRange {
|
|
412
|
+
background: var(--sc-gray-5);
|
|
413
|
+
}
|
|
209
414
|
|
|
210
415
|
.adatepicker .todaysDate {
|
|
211
416
|
font-weight: bolder;
|
|
212
|
-
text-decoration: underline;
|
|
417
|
+
/* text-decoration: underline; */
|
|
213
418
|
color: black;
|
|
214
419
|
}
|
|
215
420
|
.days-header > td {
|
|
@@ -218,4 +423,15 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
218
423
|
.prev-date {
|
|
219
424
|
color: var(--sc-gray-20);
|
|
220
425
|
}
|
|
426
|
+
|
|
427
|
+
.adatepicker .date-input {
|
|
428
|
+
display: flex;
|
|
429
|
+
width: 100%;
|
|
430
|
+
gap: 5px;
|
|
431
|
+
align-items: center;
|
|
432
|
+
}
|
|
433
|
+
.adatepicker .date-input > input {
|
|
434
|
+
width: 50%;
|
|
435
|
+
padding: 2px;
|
|
436
|
+
}
|
|
221
437
|
</style>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="adaterange">
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span class="aform_display-value">{{ displayValue }}</span>
|
|
5
|
+
<label>{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<template v-else>
|
|
9
|
+
<input
|
|
10
|
+
:id="uuid"
|
|
11
|
+
class="adate-input aform_input-field"
|
|
12
|
+
type="text"
|
|
13
|
+
:value="rangeDisplay"
|
|
14
|
+
placeholder="Select date range"
|
|
15
|
+
:disabled="mode === 'read'"
|
|
16
|
+
readonly
|
|
17
|
+
@click="openPicker" />
|
|
18
|
+
<label :for="uuid">{{ label }}</label>
|
|
19
|
+
|
|
20
|
+
<p v-show="validation.errorMessage" v-html="validation.errorMessage"></p>
|
|
21
|
+
|
|
22
|
+
<ADateSelection
|
|
23
|
+
v-if="showPicker"
|
|
24
|
+
ref="pickerRef"
|
|
25
|
+
class="picker"
|
|
26
|
+
:select-range="true"
|
|
27
|
+
:show-time="false"
|
|
28
|
+
@get-date="handlePickerDate" />
|
|
29
|
+
</template>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref, computed, watch } from 'vue'
|
|
35
|
+
import { onClickOutside } from '@vueuse/core'
|
|
36
|
+
import ADateSelection from './ADateSelection.vue'
|
|
37
|
+
import type { ComponentProps } from '../../types'
|
|
38
|
+
|
|
39
|
+
const { label = 'Date Range', mode, uuid, validation = { errorMessage: ' ' } } = defineProps<ComponentProps>()
|
|
40
|
+
|
|
41
|
+
export interface DateRangeValue {
|
|
42
|
+
start_date: string | null
|
|
43
|
+
end_date: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const modelValue = defineModel<DateRangeValue>({
|
|
47
|
+
default: () => ({ start_date: null, end_date: null }),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const startDate = ref<Date | null>(modelValue.value.start_date ? new Date(modelValue.value.start_date) : null)
|
|
51
|
+
const endDate = ref<Date | null>(modelValue.value.end_date ? new Date(modelValue.value.end_date) : null)
|
|
52
|
+
|
|
53
|
+
const showPicker = ref(false)
|
|
54
|
+
const pickerRef = ref(null)
|
|
55
|
+
onClickOutside(pickerRef, () => (showPicker.value = false))
|
|
56
|
+
|
|
57
|
+
const openPicker = () => {
|
|
58
|
+
if (mode !== 'read') showPicker.value = true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const formatDate = (d: Date | null): string => {
|
|
62
|
+
if (!d) return ''
|
|
63
|
+
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rangeDisplay = computed(() => {
|
|
67
|
+
const s = formatDate(startDate.value)
|
|
68
|
+
const e = formatDate(endDate.value)
|
|
69
|
+
if (s && e) return `${s} — ${e}`
|
|
70
|
+
if (s) return `${s} — ...`
|
|
71
|
+
return ''
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const displayValue = computed(() => {
|
|
75
|
+
const s = modelValue.value.start_date
|
|
76
|
+
const e = modelValue.value.end_date
|
|
77
|
+
if (!s && !e) return ''
|
|
78
|
+
const fmt = (d: string) => new Date(d).toLocaleDateString()
|
|
79
|
+
if (s && e) return `${fmt(s)} — ${fmt(e)}`
|
|
80
|
+
if (s) return `From ${fmt(s)}`
|
|
81
|
+
return `Until ${fmt(e!)}`
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const ensureOrder = () => {
|
|
85
|
+
const s = startDate.value
|
|
86
|
+
const e = endDate.value
|
|
87
|
+
if (s && e && e.getTime() < s.getTime()) {
|
|
88
|
+
;[startDate.value, endDate.value] = [e, s]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const toISODate = (d: Date | null): string | null => (d ? d.toISOString().split('T')[0] : null)
|
|
93
|
+
|
|
94
|
+
const emitModel = () => {
|
|
95
|
+
modelValue.value = {
|
|
96
|
+
start_date: toISODate(startDate.value),
|
|
97
|
+
end_date: toISODate(endDate.value),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handlePickerDate = (data: { selected: Date; start?: Date | null; end?: Date | null }) => {
|
|
102
|
+
if (data.start) startDate.value = data.start
|
|
103
|
+
if (data.end) {
|
|
104
|
+
endDate.value = data.end
|
|
105
|
+
ensureOrder()
|
|
106
|
+
showPicker.value = false
|
|
107
|
+
}
|
|
108
|
+
emitModel()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
watch(
|
|
112
|
+
() => modelValue.value,
|
|
113
|
+
newVal => {
|
|
114
|
+
startDate.value = newVal.start_date ? new Date(newVal.start_date) : null
|
|
115
|
+
endDate.value = newVal.end_date ? new Date(newVal.end_date) : null
|
|
116
|
+
},
|
|
117
|
+
{ deep: true }
|
|
118
|
+
)
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.adaterange {
|
|
123
|
+
min-width: 40ch;
|
|
124
|
+
width: 100%;
|
|
125
|
+
box-sizing: border-box;
|
|
126
|
+
border: 1px solid transparent;
|
|
127
|
+
padding: 0;
|
|
128
|
+
margin: 0;
|
|
129
|
+
margin-right: 1ch;
|
|
130
|
+
position: relative;
|
|
131
|
+
overflow: visible;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.adate-input {
|
|
135
|
+
width: calc(100% - 1ch);
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
outline: 1px solid transparent;
|
|
138
|
+
border: 1px solid var(--sc-input-border-color);
|
|
139
|
+
padding: 1ch 0.5ch 0.5ch 1ch;
|
|
140
|
+
margin: calc(1.15rem / 2) 0 0 0;
|
|
141
|
+
min-height: 1.15rem;
|
|
142
|
+
border-radius: 0.25rem;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
background: white;
|
|
145
|
+
font-size: 1rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.adate-input:focus {
|
|
149
|
+
border: 1px solid var(--sc-input-active-border-color);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.adate-input:focus + label {
|
|
153
|
+
color: var(--sc-input-active-label-color);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
p,
|
|
157
|
+
label {
|
|
158
|
+
color: var(--sc-input-label-color);
|
|
159
|
+
display: block;
|
|
160
|
+
min-height: 1.15rem;
|
|
161
|
+
padding: 0;
|
|
162
|
+
margin: 0;
|
|
163
|
+
border: 1px solid transparent;
|
|
164
|
+
margin-bottom: 0.25rem;
|
|
165
|
+
box-sizing: border-box;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
p {
|
|
169
|
+
width: 100%;
|
|
170
|
+
color: red;
|
|
171
|
+
font-size: 85%;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
label {
|
|
175
|
+
z-index: 0;
|
|
176
|
+
font-size: 80%;
|
|
177
|
+
position: absolute;
|
|
178
|
+
background: white;
|
|
179
|
+
margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
|
|
180
|
+
padding: 0 0.25ch 0 0.25ch;
|
|
181
|
+
box-sizing: border-box;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.picker {
|
|
185
|
+
position: absolute;
|
|
186
|
+
top: 50px;
|
|
187
|
+
left: 0;
|
|
188
|
+
z-index: 1000;
|
|
189
|
+
}
|
|
190
|
+
</style>
|