@stonecrop/aform 0.31.0 → 0.33.0

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.
Files changed (92) hide show
  1. package/dist/aform.js +3024 -2256
  2. package/dist/aform.js.map +1 -1
  3. package/dist/assets/index.css +1170 -1
  4. package/dist/src/components/AForm.vue.d.ts +20 -0
  5. package/dist/src/components/AForm.vue.d.ts.map +1 -0
  6. package/dist/src/components/AFormLoading.vue.d.ts +14 -0
  7. package/dist/src/components/AFormLoading.vue.d.ts.map +1 -0
  8. package/dist/src/components/base/CollapseButton.vue.d.ts +7 -0
  9. package/dist/src/components/base/CollapseButton.vue.d.ts.map +1 -0
  10. package/dist/src/components/base/ExpandButton.vue.d.ts +7 -0
  11. package/dist/src/components/base/ExpandButton.vue.d.ts.map +1 -0
  12. package/dist/src/components/form/ABadge.vue.d.ts +14 -0
  13. package/dist/src/components/form/ABadge.vue.d.ts.map +1 -0
  14. package/dist/src/components/form/ACheckbox.vue.d.ts +15 -0
  15. package/dist/src/components/form/ACheckbox.vue.d.ts.map +1 -0
  16. package/dist/src/components/form/ACurrencyInput.vue.d.ts +20 -0
  17. package/dist/src/components/form/ACurrencyInput.vue.d.ts.map +1 -0
  18. package/dist/src/components/form/ADate.vue.d.ts +14 -0
  19. package/dist/src/components/form/ADate.vue.d.ts.map +1 -0
  20. package/dist/src/components/form/ADatePicker.vue.d.ts +28 -0
  21. package/dist/src/components/form/ADatePicker.vue.d.ts.map +1 -0
  22. package/dist/src/components/form/ADateRange.vue.d.ts +18 -0
  23. package/dist/src/components/form/ADateRange.vue.d.ts.map +1 -0
  24. package/dist/src/components/form/ADateSelection.vue.d.ts +52 -0
  25. package/dist/src/components/form/ADateSelection.vue.d.ts.map +1 -0
  26. package/dist/src/components/form/ADateTime.vue.d.ts +17 -0
  27. package/dist/src/components/form/ADateTime.vue.d.ts.map +1 -0
  28. package/dist/src/components/form/ADateTimeInput.vue.d.ts +30 -0
  29. package/dist/src/components/form/ADateTimeInput.vue.d.ts.map +1 -0
  30. package/dist/src/components/form/ADropdown.vue.d.ts +20 -0
  31. package/dist/src/components/form/ADropdown.vue.d.ts.map +1 -0
  32. package/dist/src/components/form/ADuration.vue.d.ts +18 -0
  33. package/dist/src/components/form/ADuration.vue.d.ts.map +1 -0
  34. package/dist/src/components/form/AFieldset.vue.d.ts +28 -0
  35. package/dist/src/components/form/AFieldset.vue.d.ts.map +1 -0
  36. package/dist/src/components/form/AFileAttach.vue.d.ts +5 -0
  37. package/dist/src/components/form/AFileAttach.vue.d.ts.map +1 -0
  38. package/dist/src/components/form/AFormLink.vue.d.ts +40 -0
  39. package/dist/src/components/form/AFormLink.vue.d.ts.map +1 -0
  40. package/dist/src/components/form/ANumericInput.vue.d.ts +14 -0
  41. package/dist/src/components/form/ANumericInput.vue.d.ts.map +1 -0
  42. package/dist/src/components/form/AQuantityInput.vue.d.ts +20 -0
  43. package/dist/src/components/form/AQuantityInput.vue.d.ts.map +1 -0
  44. package/dist/src/components/form/ASegmentedControl.vue.d.ts +23 -0
  45. package/dist/src/components/form/ASegmentedControl.vue.d.ts.map +1 -0
  46. package/dist/src/components/form/ATextInput.vue.d.ts +14 -0
  47. package/dist/src/components/form/ATextInput.vue.d.ts.map +1 -0
  48. package/dist/src/components/form/ATextboxInput.vue.d.ts +21 -0
  49. package/dist/src/components/form/ATextboxInput.vue.d.ts.map +1 -0
  50. package/dist/src/components/utilities/Login.vue.d.ts +14 -0
  51. package/dist/src/components/utilities/Login.vue.d.ts.map +1 -0
  52. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  53. package/package.json +35 -30
  54. package/dist/aform.d.ts +0 -441
  55. package/dist/aform.tsbuildinfo +0 -1
  56. package/dist/src/directives/mask.js +0 -107
  57. package/dist/src/index.js +0 -58
  58. package/dist/src/types/index.js +0 -0
  59. package/dist/src/utils/badge.js +0 -71
  60. package/dist/src/utils/columns.js +0 -33
  61. package/dist/src/utils/deserialize.js +0 -20
  62. package/src/components/AForm.vue +0 -314
  63. package/src/components/AFormLoading.vue +0 -76
  64. package/src/components/base/CollapseButton.vue +0 -36
  65. package/src/components/base/ExpandButton.vue +0 -34
  66. package/src/components/form/ABadge.vue +0 -135
  67. package/src/components/form/ACheckbox.vue +0 -62
  68. package/src/components/form/ACurrencyInput.vue +0 -357
  69. package/src/components/form/ADate.vue +0 -93
  70. package/src/components/form/ADatePicker.vue +0 -470
  71. package/src/components/form/ADateRange.vue +0 -133
  72. package/src/components/form/ADateSelection.vue +0 -174
  73. package/src/components/form/ADateTime.vue +0 -149
  74. package/src/components/form/ADateTimeInput.vue +0 -299
  75. package/src/components/form/ADropdown.vue +0 -222
  76. package/src/components/form/ADuration.vue +0 -124
  77. package/src/components/form/AFieldset.vue +0 -72
  78. package/src/components/form/AFileAttach.vue +0 -97
  79. package/src/components/form/AFormLink.vue +0 -368
  80. package/src/components/form/ANumericInput.vue +0 -32
  81. package/src/components/form/AQuantityInput.vue +0 -349
  82. package/src/components/form/ASegmentedControl.vue +0 -342
  83. package/src/components/form/ATextInput.vue +0 -40
  84. package/src/components/form/ATextboxInput.vue +0 -66
  85. package/src/components/utilities/Login.vue +0 -189
  86. package/src/directives/mask.ts +0 -122
  87. package/src/index.ts +0 -92
  88. package/src/shims-vue.d.ts +0 -5
  89. package/src/types/index.ts +0 -333
  90. package/src/utils/badge.ts +0 -94
  91. package/src/utils/columns.ts +0 -38
  92. package/src/utils/deserialize.ts +0 -20
@@ -1,470 +0,0 @@
1
- <template>
2
- <template v-if="mode === 'display' || mode === 'read'">
3
- <span class="aform_display-value">{{ date ? new Date(date).toLocaleDateString() : '' }}</span>
4
- <label v-if="label">{{ label }}</label>
5
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
6
- </template>
7
- <template v-else>
8
- <div ref="datepicker" class="adatepicker" tabindex="0">
9
- <table @mousedown="preventCellSelection">
10
- <tbody>
11
- <tr>
12
- <td id="previous-month-btn" :tabindex="-1" @click="previousMonth">&lt;</td>
13
- <th colspan="5" :tabindex="-1">{{ monthAndYear }}</th>
14
- <td id="next-month-btn" :tabindex="-1" @click="nextMonth">&gt;</td>
15
- </tr>
16
- <tr v-if="selectRange">
17
- <td colspan="7">
18
- <div class="date-input">
19
- <input
20
- ref="start-date-input"
21
- :value="getStartDate"
22
- class="date-input-start aform_input-field"
23
- type="text"
24
- placeholder="start date"
25
- @blur="enterInputDate()"
26
- @keydown="enterDate" />
27
- <div>-</div>
28
- <input
29
- ref="end-date-input"
30
- :value="getEndDate"
31
- class="date-input-end aform_input-field"
32
- type="text"
33
- placeholder="end date"
34
- @blur="enterInputDate()"
35
- @keydown="enterDate" />
36
- </div>
37
- <!-- {{ formattedDateRange }} -->
38
- </td>
39
- </tr>
40
- <tr class="days-header">
41
- <td>M</td>
42
- <td>T</td>
43
- <td>W</td>
44
- <td>T</td>
45
- <td>F</td>
46
- <td>S</td>
47
- <td>S</td>
48
- </tr>
49
- <tr v-for="rowNo in numberOfRows" :key="rowNo">
50
- <!-- the 'ref' key is currently only used for test references -->
51
- <td
52
- v-for="colNo in numberOfColumns"
53
- ref="celldate"
54
- :key="getCurrentCell(rowNo, colNo)"
55
- class="date-cell"
56
- :contenteditable="false"
57
- :spellcheck="false"
58
- :tabindex="0"
59
- :class="{
60
- todaysDate: isTodaysDate(getCurrentDate(rowNo, colNo)),
61
- selectedDate: isSelectedDate(getCurrentDate(rowNo, colNo)),
62
- withinRange: selectRange ? isInDateRange(getCurrentDate(rowNo, colNo)) : false,
63
- startDate: selectRange ? isStartDate(getCurrentDate(rowNo, colNo)) : false,
64
- endDate: selectRange ? isEndDate(getCurrentDate(rowNo, colNo)) : false,
65
- }"
66
- @click.prevent.stop="selectDate(getCurrentCell(rowNo, colNo))"
67
- @keydown.enter="selectDate(getCurrentCell(rowNo, colNo))"
68
- @mouseover="hoverDate(getCurrentCell(rowNo, colNo))">
69
- {{ new Date(getCurrentDate(rowNo, colNo)).getDate() }}
70
- </td>
71
- </tr>
72
- </tbody>
73
- </table>
74
- </div>
75
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
76
- </template>
77
- </template>
78
-
79
- <script setup lang="ts">
80
- /* removed keyboard nav temportarily since it interfered with user experience navigating input fields */
81
- // import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
82
- import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
83
-
84
- import type { ComponentProps } from '../../types'
85
-
86
- const numberOfRows = 6
87
- const numberOfColumns = 7
88
-
89
- const { mode, label, selectRange = false, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
90
-
91
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
92
- const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
93
-
94
- const date = defineModel<number | Date>({ default: () => new Date() })
95
- const selectedDate = ref(new Date(date.value))
96
- const currentMonth = ref<number>(selectedDate.value.getMonth())
97
- const currentYear = ref<number>(selectedDate.value.getFullYear())
98
- const currentDates = ref<number[]>([])
99
-
100
- /* needed for keyboard navigation. uncomment if implementing */
101
- // const datepickerRef = useTemplateRef<HTMLDivElement>('datepicker')
102
-
103
- const hoveredDate = ref(new Date(date.value))
104
- const start_date = ref<Date | null>(null)
105
- const end_date = ref<Date | null>(null)
106
- const startDateInput = useTemplateRef<HTMLInputElement>('start-date-input')
107
- const endDateInput = useTemplateRef<HTMLInputElement>('end-date-input')
108
-
109
- /*******************
110
- Emits
111
- *******************/
112
-
113
- const emit = defineEmits<{
114
- 'get-date': [{ start: Date | null; end: Date | null; selected: Date }]
115
- }>()
116
-
117
- /*******************
118
- Computed
119
- *******************/
120
-
121
- const monthAndYear = computed(() => {
122
- return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
123
- year: 'numeric',
124
- month: 'long',
125
- })
126
- })
127
-
128
- const getStartDate = computed(() => {
129
- return start_date.value != null ? parseDateToString(start_date.value) : ''
130
- })
131
-
132
- const getEndDate = computed(() => {
133
- return end_date.value != null ? parseDateToString(end_date.value) : ''
134
- })
135
-
136
- /*******************
137
- Functions
138
- *******************/
139
-
140
- const parseDateToString = (dateValue: Date | null) => {
141
- if (!validateDate(dateValue)) return ''
142
- return dateValue.getMonth() + 1 + '/' + dateValue.getDate() + '/' + dateValue.getFullYear()
143
- }
144
-
145
- const isTodaysDate = (day: string | number | Date): boolean => {
146
- const todaysDate = new Date()
147
- if (currentMonth.value !== todaysDate.getMonth()) return false
148
- return todaysDate.toDateString() === new Date(day).toDateString()
149
- }
150
-
151
- const isSelectedDate = (day: string | number | Date) => {
152
- return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
153
- }
154
-
155
- const isStartDate = (day: string | number | Date) => {
156
- const start = start_date.value
157
- if (!validateDate(start)) return false
158
- return new Date(day).toDateString() === start.toDateString()
159
- }
160
-
161
- const isEndDate = (day: string | number | Date) => {
162
- const end = end_date.value
163
- if (!validateDate(end)) return false
164
- return new Date(day).toDateString() === end.toDateString()
165
- }
166
-
167
- const getCurrentCell = (rowNo: number, colNo: number) => {
168
- return (rowNo - 1) * numberOfColumns + colNo
169
- }
170
-
171
- const isInDateRange = (day: string | number | Date) => {
172
- const start = start_date.value
173
- if (!validateDate(start)) return false
174
- const this_date = new Date(day)
175
-
176
- //the end is either the selected end date or wherever the user is hovering
177
- const end = end_date.value
178
- const temp_end_date = validateDate(end) ? end : new Date(hoveredDate.value)
179
-
180
- return this_date.getTime() > start.getTime() && this_date.getTime() < temp_end_date.getTime()
181
- }
182
-
183
- const getCurrentDate = (rowNo: number, colNo: number) => {
184
- return currentDates.value[getCurrentCell(rowNo, colNo)]
185
- }
186
-
187
- const hoverDate = (currentIndex: number) => {
188
- hoveredDate.value = new Date(currentDates.value[currentIndex])
189
- }
190
-
191
- // browsers (notably Firefox) allow drag-selecting text across table cells even with
192
- // `user-select: none` on the cells; blocking mousedown is the reliable cross-browser fix.
193
- // the start/end-date inputs must keep native mousedown behavior so they stay focusable/typable.
194
- const preventCellSelection = (event: MouseEvent) => {
195
- if ((event.target as HTMLElement)?.tagName !== 'INPUT') {
196
- event.preventDefault()
197
- }
198
- }
199
-
200
- const populateMonth = () => {
201
- currentDates.value = []
202
- const firstOfMonth = new Date(currentYear.value, currentMonth.value, 1)
203
- const monthStartWeekday = firstOfMonth.getDay()
204
- const calendarStartDay = firstOfMonth.setDate(firstOfMonth.getDate() - monthStartWeekday)
205
-
206
- // assume midnight for all dates while building the calendar
207
- for (const dayIndex of Array(43).keys()) {
208
- currentDates.value.push(calendarStartDay + dayIndex * 86400000)
209
- }
210
- }
211
- const previousYear = () => (currentYear.value -= 1)
212
- const nextYear = () => (currentYear.value += 1)
213
-
214
- const previousMonth = () => {
215
- if (currentMonth.value == 0) {
216
- currentMonth.value = 11
217
- previousYear()
218
- } else {
219
- currentMonth.value -= 1
220
- }
221
- }
222
-
223
- const nextMonth = () => {
224
- if (currentMonth.value == 11) {
225
- currentMonth.value = 0
226
- nextYear()
227
- } else {
228
- currentMonth.value += 1
229
- }
230
- }
231
-
232
- const enterDate = (event: KeyboardEvent) => {
233
- if (event.key === 'Enter') enterInputDate()
234
- }
235
-
236
- // useKeyboardNav([
237
- // {
238
- // parent: datepickerRef,
239
- // selectors: 'td',
240
- // handlers: {
241
- // ...defaultKeypressHandlers,
242
- // ...{
243
- // 'keydown.pageup': previousMonth,
244
- // 'keydown.shift.pageup': previousYear,
245
- // 'keydown.pagedown': nextMonth,
246
- // 'keydown.shift.pagedown': nextYear,
247
- // // TODO: this is a hack to override the stonecrop enter handler;
248
- // // store context inside the component so that handlers can be setup consistently
249
- // // eslint-disable-next-line @typescript-eslint/no-empty-function
250
- // 'keydown.enter': () => {}, // select this date
251
- // },
252
- // },
253
- // },
254
- // ])
255
-
256
- const selectDate = (currentIndex: number) => {
257
- date.value = selectedDate.value = new Date(currentDates.value[currentIndex])
258
-
259
- if (selectRange) {
260
- const start = start_date.value
261
- if (start == null || end_date.value != null) {
262
- start_date.value = date.value
263
- end_date.value = null
264
- } else if (validateDate(start) && selectedDate.value.getTime() < start.getTime()) {
265
- end_date.value = null
266
- start_date.value = date.value
267
- } else {
268
- end_date.value = date.value
269
- }
270
- if (startDateInput.value) startDateInput.value.value = parseDateToString(start_date.value) ?? ''
271
- if (endDateInput.value) endDateInput.value.value = parseDateToString(end_date.value) ?? ''
272
- }
273
- emitData()
274
- }
275
-
276
- const testDateOrder = () => {
277
- const start = start_date.value
278
- const end = end_date.value
279
- if (validateDate(end) && validateDate(start) && end.getTime() < start.getTime())
280
- [start_date.value, end_date.value] = [end, start]
281
- }
282
-
283
- const validateDate = (dateValue: unknown): dateValue is Date => {
284
- return dateValue instanceof Date && !isNaN(dateValue.getTime())
285
- }
286
-
287
- const enterInputDate = () => {
288
- if (startDateInput.value?.value == '') {
289
- start_date.value = null
290
- } else if (startDateInput.value) {
291
- const start = new Date(startDateInput.value.value)
292
- start_date.value = validateDate(start) ? start : null
293
- }
294
-
295
- if (endDateInput.value?.value == '') {
296
- end_date.value = null
297
- } else if (endDateInput.value) {
298
- const end = new Date(endDateInput.value.value)
299
- end_date.value = validateDate(end) ? end : null
300
- }
301
-
302
- if (validateDate(start_date.value)) {
303
- if (validateDate(end_date.value)) testDateOrder()
304
- selectedDate.value = start_date.value
305
- }
306
-
307
- emitData()
308
- }
309
-
310
- const emitData = () => {
311
- emit('get-date', {
312
- start: selectRange ? start_date.value : null,
313
- end: selectRange ? end_date.value : null,
314
- selected: selectedDate.value,
315
- })
316
- }
317
-
318
- /*******************
319
- Hooks
320
- *******************/
321
-
322
- onMounted(async () => {
323
- populateMonth()
324
- // required to allow the elements to be focused in the next step
325
- await nextTick()
326
- const $selectedDate = document.getElementsByClassName('selectedDate')
327
- if ($selectedDate.length > 0) {
328
- ;($selectedDate[0] as HTMLElement).focus()
329
- } else {
330
- const $todaysDate = document.getElementsByClassName('todaysDate')
331
- if ($todaysDate.length > 0) {
332
- ;($todaysDate[0] as HTMLElement).focus()
333
- }
334
- }
335
- })
336
-
337
- // setup keyboard navigation
338
- // useKeyboardNav([
339
- // {
340
- // parent: datepickerRef,
341
- // selectors: 'td',
342
- // handlers: {
343
- // ...defaultKeypressHandlers,
344
- // 'keydown.pageup': previousMonth,
345
- // 'keydown.shift.pageup': previousYear,
346
- // 'keydown.pagedown': nextMonth,
347
- // 'keydown.shift.pagedown': nextYear,
348
- // // TODO: this is a hack to override the stonecrop enter handler;
349
- // // store context inside the component so that handlers can be setup consistently
350
-
351
- // 'keydown.enter': () => {}, // select this date
352
- // },
353
- // },
354
- // ])
355
-
356
- /*******************
357
- Watchers
358
- *******************/
359
-
360
- watch([currentMonth, currentYear], populateMonth)
361
-
362
- /*******************
363
- Expose
364
- *******************/
365
-
366
- defineExpose({ currentMonth, currentYear, selectedDate })
367
- </script>
368
-
369
- <style scoped>
370
- .adatepicker,
371
- .adatepicker table,
372
- .adatepicker tr,
373
- .adatepicker td,
374
- .adatepicker th {
375
- -webkit-user-select: none;
376
- -moz-user-select: none;
377
- user-select: none;
378
- }
379
-
380
- .adatepicker {
381
- font-size: var(--sc-table-font-size);
382
- display: inline-table;
383
- color: var(--sc-cell-text-color);
384
- outline: none;
385
- border-collapse: collapse;
386
- margin-bottom: 10px;
387
- /* width: calc(100% - 4px); */
388
- }
389
-
390
- .adatepicker tr {
391
- height: 1.15rem;
392
- height: 1.15rem;
393
- text-align: center;
394
- vertical-align: middle;
395
- }
396
-
397
- .adatepicker td {
398
- border: 2px solid transparent;
399
- outline: 2px solid transparent;
400
- min-width: 3ch;
401
- max-width: 3ch;
402
- cursor: pointer;
403
- }
404
- .adatepicker td.date-cell:hover {
405
- background: var(--sc-gray-10);
406
- }
407
-
408
- .adatepicker td:focus,
409
- .adatepicker td:focus-within {
410
- /* outline: 1px dashed black; */
411
- box-shadow: none;
412
- overflow: hidden;
413
- min-height: 1.15em;
414
- max-height: 1.15em;
415
- overflow: hidden;
416
- }
417
- .adatepicker .selectedDate,
418
- .adatepicker .startDate,
419
- .adatepicker .endDate {
420
- /* outline: 1px solid black; */
421
- background: var(--sc-gray-20);
422
- font-weight: bolder;
423
- }
424
- .adatepicker .startDate {
425
- /* border-radius: 5px 0px 0px 5px; */
426
- border-left: 1px solid var(--sc-gray-50);
427
- background: var(--sc-gray-20) !important;
428
- }
429
- .adatepicker .endDate {
430
- border-right: 1px solid var(--sc-gray-50);
431
- /* border-radius: 0px 5px 5px 0px; */
432
- background: var(--sc-gray-20) !important;
433
- }
434
- .adatepicker .withinRange {
435
- background: var(--sc-gray-5);
436
- }
437
-
438
- .adatepicker .todaysDate {
439
- font-weight: bolder;
440
- /* text-decoration: underline; */
441
- color: black;
442
- }
443
- .days-header > td {
444
- font-weight: bold;
445
- }
446
- .prev-date {
447
- color: var(--sc-gray-20);
448
- }
449
-
450
- .adatepicker .date-input {
451
- display: flex;
452
- width: 100%;
453
- gap: 5px;
454
- align-items: center;
455
- }
456
- .adatepicker .date-input > input {
457
- width: 50%;
458
- padding: 2px;
459
- }
460
-
461
- /* Keep the field error in-flow below the calendar. The shared .aform_error is absolutely
462
- positioned against a .aform_form-element anchor, which this grid component does not use. */
463
- p.aform_error {
464
- position: static;
465
- display: block;
466
- color: var(--sc-brand-danger);
467
- font-size: 0.7rem;
468
- margin: 0.25rem 0 0;
469
- }
470
- </style>
@@ -1,133 +0,0 @@
1
- <template>
2
- <div class="aform_form-element">
3
- <template v-if="mode === 'display'">
4
- <span class="aform_display-value">{{ displayValue }}</span>
5
- <label class="aform_field-label">{{ label }}</label>
6
- </template>
7
-
8
- <template v-else>
9
- <input
10
- :id="uuid"
11
- class="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 class="aform_field-label" :for="uuid">{{ label }}</label>
19
-
20
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
21
-
22
- <ADateSelection
23
- v-if="showPicker"
24
- ref="pickerRef"
25
- class="adaterange-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, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
42
-
43
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
44
- const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
45
-
46
- export interface DateRangeValue {
47
- start_date: string | null
48
- end_date: string | null
49
- }
50
-
51
- const modelValue = defineModel<DateRangeValue>({
52
- default: () => ({ start_date: null, end_date: null }),
53
- })
54
-
55
- const startDate = ref<Date | null>(modelValue.value.start_date ? new Date(modelValue.value.start_date) : null)
56
- const endDate = ref<Date | null>(modelValue.value.end_date ? new Date(modelValue.value.end_date) : null)
57
-
58
- const showPicker = ref(false)
59
- const pickerRef = ref(null)
60
- onClickOutside(pickerRef, () => (showPicker.value = false))
61
-
62
- const openPicker = () => {
63
- if (mode !== 'read') showPicker.value = true
64
- }
65
-
66
- const formatDate = (d: Date | null): string => {
67
- if (!d) return ''
68
- return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`
69
- }
70
-
71
- const rangeDisplay = computed(() => {
72
- const s = formatDate(startDate.value)
73
- const e = formatDate(endDate.value)
74
- if (s && e) return `${s} — ${e}`
75
- if (s) return `${s} — ...`
76
- return ''
77
- })
78
-
79
- const displayValue = computed(() => {
80
- const s = modelValue.value.start_date
81
- const e = modelValue.value.end_date
82
- if (!s && !e) return ''
83
- if (s && e) return `${fmt(s)} — ${fmt(e)}`
84
- if (s) return `From ${fmt(s)}`
85
- return `Until ${fmt(e!)}`
86
- })
87
-
88
- const ensureOrder = () => {
89
- const s = startDate.value
90
- const e = endDate.value
91
- if (s && e && e.getTime() < s.getTime()) {
92
- ;[startDate.value, endDate.value] = [e, s]
93
- }
94
- }
95
-
96
- const toISODate = (d: Date | null): string | null => (d ? d.toISOString().split('T')[0] : null)
97
-
98
- const emitModel = () => {
99
- modelValue.value = {
100
- start_date: toISODate(startDate.value),
101
- end_date: toISODate(endDate.value),
102
- }
103
- }
104
-
105
- const handlePickerDate = (data: { selected: Date; start?: Date | null; end?: Date | null }) => {
106
- if (data.start) startDate.value = data.start
107
- if (data.end) {
108
- endDate.value = data.end
109
- ensureOrder()
110
- showPicker.value = false
111
- }
112
- emitModel()
113
- }
114
-
115
- watch(
116
- () => modelValue.value,
117
- newVal => {
118
- startDate.value = newVal.start_date ? new Date(newVal.start_date) : null
119
- endDate.value = newVal.end_date ? new Date(newVal.end_date) : null
120
- },
121
- { deep: true }
122
- )
123
- </script>
124
-
125
- <style scoped>
126
- .adaterange-picker {
127
- position: absolute;
128
- top: 100%;
129
- left: 0;
130
- z-index: 1000;
131
- margin-top: 0.25rem;
132
- }
133
- </style>