@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.
@@ -11,6 +11,30 @@
11
11
  <th colspan="5" :tabindex="-1">{{ monthAndYear }}</th>
12
12
  <td id="next-month-btn" :tabindex="-1" @click="nextMonth">&gt;</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
- import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
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
- onMounted(async () => {
65
- populateMonth()
93
+ /* needed for keyboard navigation. uncomment if implementing */
94
+ // const datepickerRef = useTemplateRef<HTMLDivElement>('datepicker')
66
95
 
67
- // required to allow the elements to be focused in the next step
68
- await nextTick()
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
- const $selectedDate = document.getElementsByClassName('selectedDate')
71
- if ($selectedDate.length > 0) {
72
- ;($selectedDate[0] as HTMLElement).focus()
73
- } else {
74
- const $todaysDate = document.getElementsByClassName('todaysDate')
75
- if ($todaysDate.length > 0) {
76
- ;($todaysDate[0] as HTMLElement).focus()
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 isTodaysDate = (day: string | number | Date) => {
116
- const todaysDate = new Date()
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
- const isSelectedDate = (day: string | number | Date) => {
124
- return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
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 getCurrentCell = (rowNo: number, colNo: number) => {
128
- return (rowNo - 1) * numberOfColumns + colNo
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 getCurrentDate = (rowNo: number, colNo: number) => {
132
- return currentDates.value[getCurrentCell(rowNo, colNo)]
267
+ const validateDate = (dateValue: unknown): dateValue is Date => {
268
+ return dateValue instanceof Date && !isNaN(dateValue.getTime())
133
269
  }
134
270
 
135
- const selectDate = (currentIndex: number) => {
136
- date.value = selectedDate.value = new Date(currentDates.value[currentIndex])
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 monthAndYear = computed(() => {
140
- return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
141
- year: 'numeric',
142
- month: 'long',
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
- parent: datepickerRef,
150
- selectors: 'td',
151
- handlers: {
152
- ...defaultKeypressHandlers,
153
- ...{
154
- 'keydown.pageup': previousMonth,
155
- 'keydown.shift.pageup': previousYear,
156
- 'keydown.pagedown': nextMonth,
157
- 'keydown.shift.pagedown': nextYear,
158
- // TODO: this is a hack to override the stonecrop enter handler;
159
- // store context inside the component so that handlers can be setup consistently
160
-
161
- 'keydown.enter': () => {}, // select this date
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
- outline: 1px solid black;
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: '&nbsp;' } } = 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>