@stonecrop/aform 0.13.3 → 0.13.5
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 +2656 -2561
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/src/directives/mask.d.ts.map +1 -1
- package/dist/src/directives/mask.js +7 -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/dist/src/utils/deserialize.d.ts.map +1 -1
- package/dist/src/utils/deserialize.js +3 -1
- package/package.json +12 -15
- package/src/components/form/ADate.vue +51 -16
- package/src/components/form/ADatePicker.vue +274 -60
- package/src/components/form/ADateRange.vue +191 -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/components/form/AFileAttach.vue +1 -1
- package/src/components/form/AFormLink.vue +0 -2
- package/src/directives/mask.ts +8 -2
- package/src/index.ts +12 -0
- package/src/types/index.ts +3 -0
- package/src/utils/deserialize.ts +3 -1
|
@@ -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 = (dateValue: Date | null) => {
|
|
134
|
+
if (!validateDate(dateValue)) return ''
|
|
135
|
+
return dateValue.getMonth() + 1 + '/' + dateValue.getDate() + '/' + dateValue.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,139 @@ 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 = (dateValue: unknown): dateValue is Date => {
|
|
268
|
+
return dateValue instanceof Date && !isNaN(dateValue.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
|
+
// 'keydown.pageup': previousMonth,
|
|
329
|
+
// 'keydown.shift.pageup': previousYear,
|
|
330
|
+
// 'keydown.pagedown': nextMonth,
|
|
331
|
+
// 'keydown.shift.pagedown': nextYear,
|
|
332
|
+
// // TODO: this is a hack to override the stonecrop enter handler;
|
|
333
|
+
// // store context inside the component so that handlers can be setup consistently
|
|
334
|
+
|
|
335
|
+
// 'keydown.enter': () => {}, // select this date
|
|
336
|
+
// },
|
|
337
|
+
// },
|
|
338
|
+
// ])
|
|
339
|
+
|
|
340
|
+
/*******************
|
|
341
|
+
Watchers
|
|
342
|
+
*******************/
|
|
343
|
+
|
|
344
|
+
watch([currentMonth, currentYear], populateMonth)
|
|
345
|
+
|
|
346
|
+
/*******************
|
|
347
|
+
Expose
|
|
348
|
+
*******************/
|
|
166
349
|
|
|
167
350
|
defineExpose({ currentMonth, currentYear, selectedDate })
|
|
168
351
|
</script>
|
|
@@ -175,6 +358,7 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
175
358
|
color: var(--sc-cell-text-color);
|
|
176
359
|
outline: none;
|
|
177
360
|
border-collapse: collapse;
|
|
361
|
+
margin-bottom: 10px;
|
|
178
362
|
/* width: calc(100% - 4px); */
|
|
179
363
|
}
|
|
180
364
|
|
|
@@ -190,26 +374,45 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
190
374
|
outline: 2px solid transparent;
|
|
191
375
|
min-width: 3ch;
|
|
192
376
|
max-width: 3ch;
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
}
|
|
379
|
+
.adatepicker td.date-cell:hover {
|
|
380
|
+
background: var(--sc-gray-10);
|
|
193
381
|
}
|
|
194
382
|
|
|
195
383
|
.adatepicker td:focus,
|
|
196
384
|
.adatepicker td:focus-within {
|
|
197
|
-
outline: 1px dashed black;
|
|
385
|
+
/* outline: 1px dashed black; */
|
|
198
386
|
box-shadow: none;
|
|
199
387
|
overflow: hidden;
|
|
200
388
|
min-height: 1.15em;
|
|
201
389
|
max-height: 1.15em;
|
|
202
390
|
overflow: hidden;
|
|
203
391
|
}
|
|
204
|
-
.adatepicker .selectedDate
|
|
205
|
-
|
|
392
|
+
.adatepicker .selectedDate,
|
|
393
|
+
.adatepicker .startDate,
|
|
394
|
+
.adatepicker .endDate {
|
|
395
|
+
/* outline: 1px solid black; */
|
|
206
396
|
background: var(--sc-gray-20);
|
|
207
397
|
font-weight: bolder;
|
|
208
398
|
}
|
|
399
|
+
.adatepicker .startDate {
|
|
400
|
+
/* border-radius: 5px 0px 0px 5px; */
|
|
401
|
+
border-left: 1px solid var(--sc-gray-50);
|
|
402
|
+
background: var(--sc-gray-20) !important;
|
|
403
|
+
}
|
|
404
|
+
.adatepicker .endDate {
|
|
405
|
+
border-right: 1px solid var(--sc-gray-50);
|
|
406
|
+
/* border-radius: 0px 5px 5px 0px; */
|
|
407
|
+
background: var(--sc-gray-20) !important;
|
|
408
|
+
}
|
|
409
|
+
.adatepicker .withinRange {
|
|
410
|
+
background: var(--sc-gray-5);
|
|
411
|
+
}
|
|
209
412
|
|
|
210
413
|
.adatepicker .todaysDate {
|
|
211
414
|
font-weight: bolder;
|
|
212
|
-
text-decoration: underline;
|
|
415
|
+
/* text-decoration: underline; */
|
|
213
416
|
color: black;
|
|
214
417
|
}
|
|
215
418
|
.days-header > td {
|
|
@@ -218,4 +421,15 @@ defineExpose({ currentMonth, currentYear, selectedDate })
|
|
|
218
421
|
.prev-date {
|
|
219
422
|
color: var(--sc-gray-20);
|
|
220
423
|
}
|
|
424
|
+
|
|
425
|
+
.adatepicker .date-input {
|
|
426
|
+
display: flex;
|
|
427
|
+
width: 100%;
|
|
428
|
+
gap: 5px;
|
|
429
|
+
align-items: center;
|
|
430
|
+
}
|
|
431
|
+
.adatepicker .date-input > input {
|
|
432
|
+
width: 50%;
|
|
433
|
+
padding: 2px;
|
|
434
|
+
}
|
|
221
435
|
</style>
|
|
@@ -0,0 +1,191 @@
|
|
|
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 fmt = (d: string) => new Date(d).toLocaleDateString()
|
|
40
|
+
|
|
41
|
+
const { label = 'Date Range', mode, uuid, validation = { errorMessage: ' ' } } = defineProps<ComponentProps>()
|
|
42
|
+
|
|
43
|
+
export interface DateRangeValue {
|
|
44
|
+
start_date: string | null
|
|
45
|
+
end_date: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const modelValue = defineModel<DateRangeValue>({
|
|
49
|
+
default: () => ({ start_date: null, end_date: null }),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const startDate = ref<Date | null>(modelValue.value.start_date ? new Date(modelValue.value.start_date) : null)
|
|
53
|
+
const endDate = ref<Date | null>(modelValue.value.end_date ? new Date(modelValue.value.end_date) : null)
|
|
54
|
+
|
|
55
|
+
const showPicker = ref(false)
|
|
56
|
+
const pickerRef = ref(null)
|
|
57
|
+
onClickOutside(pickerRef, () => (showPicker.value = false))
|
|
58
|
+
|
|
59
|
+
const openPicker = () => {
|
|
60
|
+
if (mode !== 'read') showPicker.value = true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const formatDate = (d: Date | null): string => {
|
|
64
|
+
if (!d) return ''
|
|
65
|
+
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rangeDisplay = computed(() => {
|
|
69
|
+
const s = formatDate(startDate.value)
|
|
70
|
+
const e = formatDate(endDate.value)
|
|
71
|
+
if (s && e) return `${s} — ${e}`
|
|
72
|
+
if (s) return `${s} — ...`
|
|
73
|
+
return ''
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const displayValue = computed(() => {
|
|
77
|
+
const s = modelValue.value.start_date
|
|
78
|
+
const e = modelValue.value.end_date
|
|
79
|
+
if (!s && !e) return ''
|
|
80
|
+
if (s && e) return `${fmt(s)} — ${fmt(e)}`
|
|
81
|
+
if (s) return `From ${fmt(s)}`
|
|
82
|
+
return `Until ${fmt(e!)}`
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const ensureOrder = () => {
|
|
86
|
+
const s = startDate.value
|
|
87
|
+
const e = endDate.value
|
|
88
|
+
if (s && e && e.getTime() < s.getTime()) {
|
|
89
|
+
;[startDate.value, endDate.value] = [e, s]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const toISODate = (d: Date | null): string | null => (d ? d.toISOString().split('T')[0] : null)
|
|
94
|
+
|
|
95
|
+
const emitModel = () => {
|
|
96
|
+
modelValue.value = {
|
|
97
|
+
start_date: toISODate(startDate.value),
|
|
98
|
+
end_date: toISODate(endDate.value),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handlePickerDate = (data: { selected: Date; start?: Date | null; end?: Date | null }) => {
|
|
103
|
+
if (data.start) startDate.value = data.start
|
|
104
|
+
if (data.end) {
|
|
105
|
+
endDate.value = data.end
|
|
106
|
+
ensureOrder()
|
|
107
|
+
showPicker.value = false
|
|
108
|
+
}
|
|
109
|
+
emitModel()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
watch(
|
|
113
|
+
() => modelValue.value,
|
|
114
|
+
newVal => {
|
|
115
|
+
startDate.value = newVal.start_date ? new Date(newVal.start_date) : null
|
|
116
|
+
endDate.value = newVal.end_date ? new Date(newVal.end_date) : null
|
|
117
|
+
},
|
|
118
|
+
{ deep: true }
|
|
119
|
+
)
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.adaterange {
|
|
124
|
+
min-width: 40ch;
|
|
125
|
+
width: 100%;
|
|
126
|
+
box-sizing: border-box;
|
|
127
|
+
border: 1px solid transparent;
|
|
128
|
+
padding: 0;
|
|
129
|
+
margin: 0;
|
|
130
|
+
margin-right: 1ch;
|
|
131
|
+
position: relative;
|
|
132
|
+
overflow: visible;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.adate-input {
|
|
136
|
+
width: calc(100% - 1ch);
|
|
137
|
+
box-sizing: border-box;
|
|
138
|
+
outline: 1px solid transparent;
|
|
139
|
+
border: 1px solid var(--sc-input-border-color);
|
|
140
|
+
padding: 1ch 0.5ch 0.5ch 1ch;
|
|
141
|
+
margin: calc(1.15rem / 2) 0 0 0;
|
|
142
|
+
min-height: 1.15rem;
|
|
143
|
+
border-radius: 0.25rem;
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
background: white;
|
|
146
|
+
font-size: 1rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.adate-input:focus {
|
|
150
|
+
border: 1px solid var(--sc-input-active-border-color);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.adate-input:focus + label {
|
|
154
|
+
color: var(--sc-input-active-label-color);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
p,
|
|
158
|
+
label {
|
|
159
|
+
color: var(--sc-input-label-color);
|
|
160
|
+
display: block;
|
|
161
|
+
min-height: 1.15rem;
|
|
162
|
+
padding: 0;
|
|
163
|
+
margin: 0;
|
|
164
|
+
border: 1px solid transparent;
|
|
165
|
+
margin-bottom: 0.25rem;
|
|
166
|
+
box-sizing: border-box;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
p {
|
|
170
|
+
width: 100%;
|
|
171
|
+
color: red;
|
|
172
|
+
font-size: 85%;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
label {
|
|
176
|
+
z-index: 0;
|
|
177
|
+
font-size: 80%;
|
|
178
|
+
position: absolute;
|
|
179
|
+
background: white;
|
|
180
|
+
margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
|
|
181
|
+
padding: 0 0.25ch 0 0.25ch;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.picker {
|
|
186
|
+
position: absolute;
|
|
187
|
+
top: 50px;
|
|
188
|
+
left: 0;
|
|
189
|
+
z-index: 1000;
|
|
190
|
+
}
|
|
191
|
+
</style>
|