@stonecrop/aform 0.2.11 → 0.2.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/aform",
3
- "version": "0.2.11",
3
+ "version": "0.2.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": {
@@ -33,8 +33,8 @@
33
33
  "dependencies": {
34
34
  "uuid": "^9.0.0",
35
35
  "vue": "^3.4.23",
36
- "@stonecrop/themes": "0.2.11",
37
- "@stonecrop/utilities": "0.2.11"
36
+ "@stonecrop/themes": "0.2.16",
37
+ "@stonecrop/utilities": "0.2.16"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@histoire/plugin-vue": "^0.17.17",
@@ -55,10 +55,10 @@
55
55
  "vite": "^5.2.9",
56
56
  "vitest": "^1.5.0",
57
57
  "vue-router": "^4",
58
- "@stonecrop/atable": "0.2.11"
58
+ "@stonecrop/atable": "0.2.16"
59
59
  },
60
60
  "peerDependencies": {
61
- "@stonecrop/atable": "0.2.11"
61
+ "@stonecrop/atable": "0.2.16"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"
@@ -1,277 +1,94 @@
1
1
  <template>
2
- <div
3
- v-if="!readonly"
4
- :event="event"
5
- :colIndex="colIndex"
6
- :rowIndex="rowIndex"
7
- :tableid="tableid"
8
- class="adate"
9
- tabindex="0"
10
- ref="adatepicker">
11
- <table>
12
- <tr>
13
- <td @click="previousMonth" :tabindex="-1">&lt;</td>
14
- <th colspan="5">{{ monthAndYear }}</th>
15
- <td @click="nextMonth" :tabindex="-1">&gt;</td>
16
- </tr>
17
- <tr v-for="rowNo in numberOfRows" :key="rowNo">
18
- <!-- TODO: (style) remove inline styling and replace with theme package -->
19
- <td
20
- v-for="colNo in numberOfColumns"
21
- :key="(rowNo - 1) * numberOfColumns + colNo"
22
- :contenteditable="false"
23
- :spellcheck="false"
24
- :tabindex="0"
25
- :style="{
26
- border: isSelectedDate(currentDates[(rowNo - 1) * numberOfColumns + colNo])
27
- ? '2px solid var(--focus-cell-outline)'
28
- : 'none',
29
- borderBottomColor: isTodaysDate(currentDates[(rowNo - 1) * numberOfColumns + colNo])
30
- ? 'var(--focus-cell-outline)'
31
- : 'none',
32
- }"
33
- @click.prevent.stop="selectDate($event, (rowNo - 1) * numberOfColumns + colNo)"
34
- :class="{
35
- todaysdate: isTodaysDate(currentDates[(rowNo - 1) * numberOfColumns + colNo]),
36
- selecteddate: isSelectedDate(currentDates[(rowNo - 1) * numberOfColumns + colNo]),
37
- }">
38
- {{ new Date(currentDates[(rowNo - 1) * numberOfColumns + colNo]).getDate() }}
39
- </td>
40
- </tr>
41
- </table>
2
+ <div>
3
+ <input
4
+ ref="dateRef"
5
+ type="date"
6
+ :id="uuid"
7
+ :disabled="readonly"
8
+ :required="required"
9
+ :value="inputDate"
10
+ @click="showPicker" />
11
+ <label :for="uuid">{{ label }}</label>
12
+ <p v-show="validation.errorMessage" v-html="validation.errorMessage"></p>
42
13
  </div>
43
14
  </template>
44
15
 
45
16
  <script setup lang="ts">
46
- import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
47
-
48
- import { TableDataStore } from '@stonecrop/atable'
49
- import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
50
-
51
- const props = defineProps<{
52
- colIndex?: number
53
- rowIndex?: number
54
- tableid?: string
55
- event?: Event
56
- indent?: number
57
- readonly?: boolean
58
- }>()
59
-
60
- const tableData = inject<TableDataStore>(props.tableid)
61
-
62
- const numberOfRows = 6
63
- const numberOfColumns = 7
64
- const todaysDate = new Date()
65
-
66
- const selectedDate = ref<Date>()
67
- const currentMonth = ref<number>()
68
- const currentYear = ref<number>()
69
- const currentDates = ref<number[]>([])
70
- // const width = ref('')
71
-
72
- onMounted(async () => {
73
- let cellDate = tableData.cellData<string | number | Date>(props.colIndex, props.rowIndex)
74
- if (cellDate) {
75
- if (!(cellDate instanceof Date)) {
76
- cellDate = new Date(cellDate)
77
- }
78
-
79
- selectedDate.value = cellDate
80
- currentMonth.value = selectedDate.value.getMonth()
81
- currentYear.value = selectedDate.value.getFullYear()
82
- } else {
83
- currentMonth.value = todaysDate.getMonth()
84
- currentYear.value = todaysDate.getFullYear()
85
- }
86
-
87
- renderMonth()
88
- await nextTick()
89
-
90
- const $selectedDate = document.getElementsByClassName('selecteddate')
91
- if ($selectedDate.length > 0) {
92
- ;($selectedDate[0] as HTMLElement).focus()
93
- } else {
94
- const $todaysDate = document.getElementsByClassName('todaysdate')
95
- if ($todaysDate.length > 0) {
96
- ;($todaysDate[0] as HTMLElement).focus()
97
- }
98
- }
99
- })
100
-
101
- watch([currentMonth, currentYear], () => {
102
- renderMonth()
103
- })
104
-
105
- const renderMonth = () => {
106
- currentDates.value = []
107
- const firstOfMonth = new Date(currentYear.value, currentMonth.value, 1)
108
- const monthStartWeekday = firstOfMonth.getDay()
109
- const calendarStartDay = firstOfMonth.setDate(firstOfMonth.getDate() - monthStartWeekday)
110
- for (let dayIndex of Array(43).keys()) {
111
- currentDates.value.push(calendarStartDay + dayIndex * 86400000)
112
- }
113
- }
114
-
115
- const previousYear = () => {
116
- currentYear.value -= 1
117
- }
118
-
119
- const nextYear = () => {
120
- currentYear.value += 1
121
- }
122
-
123
- const previousMonth = () => {
124
- if (currentMonth.value == 0) {
125
- currentMonth.value = 11
126
- previousYear()
127
- } else {
128
- currentMonth.value -= 1
17
+ import { ref } from 'vue'
18
+
19
+ withDefaults(
20
+ defineProps<{
21
+ label: string
22
+ required?: boolean
23
+ readonly?: boolean
24
+ uuid?: string
25
+ validation?: Record<string, any>
26
+ }>(),
27
+ {
28
+ validation: () => ({ errorMessage: '&nbsp;' }),
129
29
  }
130
- }
30
+ )
131
31
 
132
- const nextMonth = () => {
133
- if (currentMonth.value == 11) {
134
- currentMonth.value = 0
135
- nextYear()
136
- } else {
137
- currentMonth.value += 1
138
- }
139
- }
32
+ const inputDate = defineModel<string | number | Date>()
33
+ const dateRef = ref<HTMLInputElement | null>(null)
140
34
 
141
- const isTodaysDate = (day: string | number | Date) => {
142
- if (currentMonth.value !== todaysDate.getMonth()) {
143
- return
35
+ const showPicker = () => {
36
+ if (dateRef.value) {
37
+ dateRef.value.showPicker()
144
38
  }
145
- return todaysDate.toDateString() === new Date(day).toDateString()
146
39
  }
147
-
148
- const isSelectedDate = (day: string | number | Date) => {
149
- return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
150
- }
151
-
152
- const selectDate = (event: Event, currentIndex: number) => {
153
- selectedDate.value = new Date(currentDates.value[currentIndex])
154
- updateData()
155
- // TODO: (typing) figure out a way to close datepicker
156
- // context.refs.adatepicker.destroy()
157
- }
158
-
159
- const updateData = () => {
160
- // TODO: check proper date format to feed back (assuming number for now)
161
- tableData.setCellData(props.rowIndex, props.colIndex, selectedDate.value.getTime())
162
- }
163
-
164
- // const dayWidth = computed(() => {
165
- // const widthValue = Number(width.value.replace('px', ''))
166
- // return `${widthValue / (numberOfColumns - 1)}px`
167
- // })
168
-
169
- const monthAndYear = computed(() => {
170
- return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
171
- year: 'numeric',
172
- month: 'long',
173
- })
174
- })
175
-
176
- // setup keyboard navigation
177
- useKeyboardNav([
178
- {
179
- parent: 'table.adate',
180
- selectors: 'td',
181
- handlers: {
182
- ...defaultKeypressHandlers,
183
- ...{
184
- 'keydown.pageup': previousMonth,
185
- 'keydown.shift.pageup': previousYear,
186
- 'keydown.pagedown': nextMonth,
187
- 'keydown.shift.pagedown': nextYear,
188
- },
189
- },
190
- },
191
- ])
192
40
  </script>
193
41
 
194
42
  <style scoped>
195
- @import '@/theme/aform.css';
196
-
197
- .adate {
198
- border: 2px solid var(--focus-cell-outline);
43
+ div {
44
+ min-width: 40ch;
45
+ border: 1px solid transparent;
46
+ padding: 0rem;
47
+ margin: 0rem;
48
+ margin-right: 1ch;
49
+ }
50
+
51
+ input {
52
+ width: calc(100% - 1ch);
53
+ outline: 1px solid transparent;
54
+ border: 1px solid var(--input-border-color);
55
+ padding: 1ch 0.5ch 0.5ch 1ch;
56
+ margin: calc(1.15rem / 2) 0 0 0;
57
+ min-height: 1.15rem;
58
+ border-radius: 0.25rem;
59
+ }
60
+
61
+ p,
62
+ label {
63
+ color: var(--input-label-color);
64
+ display: block;
65
+ min-height: 1.15rem;
66
+ padding: 0rem;
67
+ margin: 0rem;
68
+ border: 1px solid transparent;
69
+ margin-bottom: 0.25rem;
70
+ }
71
+
72
+ p {
73
+ width: 100%;
74
+ color: red;
75
+ font-size: 85%;
76
+ }
77
+
78
+ label {
79
+ z-index: 2;
80
+ font-size: 80%;
199
81
  position: absolute;
200
- z-index: 100;
201
- font-size: var(--table-font-size);
202
- display: inline-table;
203
- background-color: var(--row-color-zebra-light);
204
- color: var(--cell-text-color);
205
- outline: none;
206
- width: calc(100% - 4px);
207
- }
208
-
209
- .adate tr {
210
- height: 1.15rem;
211
- text-align: center;
212
- vertical-align: middle;
213
- }
214
-
215
- .adate td {
216
- border: 2px solid transparent;
217
- min-width: 2.25ch; /* this doesn't zoom correctly */
218
- }
219
-
220
- .adate td:hover {
221
- border: 2px solid var(--focus-cell-outline);
222
- }
223
-
224
- .adate td {
225
- border: 1px;
226
- border-style: solid;
227
- border-color: var(--cell-border-color);
228
- border-radius: 0px;
229
- box-sizing: border-box;
230
- margin: 0px;
231
- outline: none;
232
- box-shadow: none;
233
- color: var(--cell-text-color);
234
- text-overflow: ellipsis;
235
- overflow: hidden;
236
- padding-left: 0.5ch;
237
- padding-right: 0.5ch;
238
- }
239
-
240
- .adate td:focus,
241
- .adate td:focus-within {
242
- background-color: var(--focus-cell-background);
243
- outline-width: 2px;
244
- outline-style: solid;
245
- outline-color: var(--focus-cell-outline);
246
- box-shadow: none;
247
- overflow: hidden;
248
- min-height: 1.15em;
249
- max-height: 1.15em;
250
- overflow: hidden;
251
- }
252
-
253
- button {
254
- background-color: var(--row-color-zebra-light);
255
- border: none;
256
- padding: 0px;
257
- margin: 0px;
258
- color: var(--cell-text-color);
259
- outline: none;
260
- font-size: var(--table-font-size);
261
- }
262
-
263
- .dateheader {
264
- font-weight: 700;
265
- display: flex;
266
- align-items: center;
267
- justify-content: space-between;
82
+ background: white;
83
+ margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
84
+ padding: 0 0.25ch 0 0.25ch;
268
85
  }
269
86
 
270
- .adate .todaysdate {
271
- border-bottom-color: var(--focus-cell-outline);
87
+ input:focus {
88
+ border: 1px solid var(--input-active-border-color);
272
89
  }
273
90
 
274
- .adate .selecteddate {
275
- border: 2px solid var(--focus-cell-outline);
91
+ input:focus + label {
92
+ color: var(--input-active-label-color);
276
93
  }
277
94
  </style>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <div class="adatepicker" tabindex="0" ref="adatepicker">
3
+ <table>
4
+ <tr>
5
+ <td @click="previousMonth" :tabindex="-1">&lt;</td>
6
+ <th colspan="5" :tabindex="-1">{{ monthAndYear }}</th>
7
+ <td @click="nextMonth" :tabindex="-1">&gt;</td>
8
+ </tr>
9
+ <tr class="days-header">
10
+ <td>M</td>
11
+ <td>T</td>
12
+ <td>W</td>
13
+ <td>T</td>
14
+ <td>F</td>
15
+ <td>S</td>
16
+ <td>S</td>
17
+ </tr>
18
+ <tr v-for="rowNo in numberOfRows" :key="rowNo">
19
+ <td
20
+ v-for="colNo in numberOfColumns"
21
+ :key="getCurrentCell(rowNo, colNo)"
22
+ :contenteditable="false"
23
+ :spellcheck="false"
24
+ :tabindex="0"
25
+ @click.prevent.stop="selectDate(getCurrentCell(rowNo, colNo))"
26
+ @keydown.enter="selectDate(getCurrentCell(rowNo, colNo))"
27
+ :class="{
28
+ todaysDate: isTodaysDate(getCurrentDate(rowNo, colNo)),
29
+ selectedDate: isSelectedDate(getCurrentDate(rowNo, colNo)),
30
+ }">
31
+ {{ new Date(getCurrentDate(rowNo, colNo)).getDate() }}
32
+ </td>
33
+ </tr>
34
+ </table>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
40
+ import { computed, nextTick, onMounted, ref, watch } from 'vue'
41
+
42
+ const numberOfRows = 6
43
+ const numberOfColumns = 7
44
+
45
+ const date = defineModel<number | Date>({ default: new Date() })
46
+ const selectedDate = ref(new Date(date.value))
47
+ const currentMonth = ref<number>(selectedDate.value.getMonth())
48
+ const currentYear = ref<number>(selectedDate.value.getFullYear())
49
+ const currentDates = ref<number[]>([])
50
+ const adatepicker = ref<HTMLElement | null>(null)
51
+
52
+ onMounted(async () => {
53
+ populateMonth()
54
+
55
+ // required to allow the elements to be focused in the next step
56
+ await nextTick()
57
+
58
+ const $selectedDate = document.getElementsByClassName('selectedDate')
59
+ if ($selectedDate.length > 0) {
60
+ ;($selectedDate[0] as HTMLElement).focus()
61
+ } else {
62
+ const $todaysDate = document.getElementsByClassName('todaysDate')
63
+ if ($todaysDate.length > 0) {
64
+ ;($todaysDate[0] as HTMLElement).focus()
65
+ }
66
+ }
67
+ })
68
+
69
+ const populateMonth = () => {
70
+ currentDates.value = []
71
+ const firstOfMonth = new Date(currentYear.value, currentMonth.value, 1)
72
+ const monthStartWeekday = firstOfMonth.getDay()
73
+ const calendarStartDay = firstOfMonth.setDate(firstOfMonth.getDate() - monthStartWeekday)
74
+
75
+ // assume midnight for all dates while building the calendar
76
+ for (const dayIndex of Array(43).keys()) {
77
+ currentDates.value.push(calendarStartDay + dayIndex * 86400000)
78
+ }
79
+ }
80
+
81
+ watch([currentMonth, currentYear], populateMonth)
82
+ const previousYear = () => (currentYear.value -= 1)
83
+ const nextYear = () => (currentYear.value += 1)
84
+
85
+ const previousMonth = () => {
86
+ if (currentMonth.value == 0) {
87
+ currentMonth.value = 11
88
+ previousYear()
89
+ } else {
90
+ currentMonth.value -= 1
91
+ }
92
+ }
93
+
94
+ const nextMonth = () => {
95
+ if (currentMonth.value == 11) {
96
+ currentMonth.value = 0
97
+ nextYear()
98
+ } else {
99
+ currentMonth.value += 1
100
+ }
101
+ }
102
+
103
+ const isTodaysDate = (day: string | number | Date) => {
104
+ const todaysDate = new Date()
105
+ if (currentMonth.value !== todaysDate.getMonth()) {
106
+ return
107
+ }
108
+ return todaysDate.toDateString() === new Date(day).toDateString()
109
+ }
110
+
111
+ const isSelectedDate = (day: string | number | Date) => {
112
+ return new Date(day).toDateString() === new Date(selectedDate.value).toDateString()
113
+ }
114
+
115
+ const getCurrentCell = (rowNo: number, colNo: number) => {
116
+ return (rowNo - 1) * numberOfColumns + colNo
117
+ }
118
+
119
+ const getCurrentDate = (rowNo: number, colNo: number) => {
120
+ return currentDates.value[getCurrentCell(rowNo, colNo)]
121
+ }
122
+
123
+ const selectDate = (currentIndex: number) => {
124
+ date.value = selectedDate.value = new Date(currentDates.value[currentIndex])
125
+ }
126
+
127
+ const monthAndYear = computed(() => {
128
+ return new Date(currentYear.value, currentMonth.value, 1).toLocaleDateString(undefined, {
129
+ year: 'numeric',
130
+ month: 'long',
131
+ })
132
+ })
133
+
134
+ // setup keyboard navigation
135
+ useKeyboardNav([
136
+ {
137
+ parent: adatepicker,
138
+ selectors: 'td',
139
+ handlers: {
140
+ ...defaultKeypressHandlers,
141
+ ...{
142
+ 'keydown.pageup': previousMonth,
143
+ 'keydown.shift.pageup': previousYear,
144
+ 'keydown.pagedown': nextMonth,
145
+ 'keydown.shift.pagedown': nextYear,
146
+ // TODO: this is a hack to override the stonecrop enter handler;
147
+ // store context inside the component so that handlers can be setup consistently
148
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
149
+ 'keydown.enter': () => {}, // select this date
150
+ },
151
+ },
152
+ },
153
+ ])
154
+ </script>
155
+
156
+ <style>
157
+ @import url('@stonecrop/themes/default/default.css');
158
+ @import url('@/theme/adate.css');
159
+ @import url('@/theme/aform.css');
160
+ </style>
@@ -1,6 +1,8 @@
1
1
  import { defineSetupVue3 } from '@histoire/plugin-vue'
2
2
 
3
3
  import ACheckbox from '@/components/form/ACheckbox.vue'
4
+ import ADate from '@/components/form/ADate.vue'
5
+ import ADatePicker from '@/components/form/ADatePicker.vue'
4
6
  import AFieldset from '@/components/form/AFieldset.vue'
5
7
  import AForm from '@/components/AForm.vue'
6
8
  import ANumericInput from '@/components/form/ANumericInput.vue'
@@ -12,6 +14,8 @@ import '@stonecrop/atable/styles'
12
14
  export const setupVue3 = defineSetupVue3(({ app }) => {
13
15
  // TODO: (typing) add typing for ATable components
14
16
  app.component('ACheckbox', ACheckbox)
17
+ app.component('ADate', ADate)
18
+ app.component('ADatePicker', ADatePicker)
15
19
  app.component('AFieldset', AFieldset)
16
20
  app.component('AForm', AForm)
17
21
  app.component('ANumericInput', ANumericInput)
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import ACheckbox from '@/components/form/ACheckbox.vue'
4
4
  import AComboBox from '@/components/form/AComboBox.vue'
5
5
  import ADate from '@/components/form/ADate.vue'
6
6
  import ADropdown from '@/components/form/ADropdown.vue'
7
+ import ADatePicker from '@/components/form/ADatePicker.vue'
7
8
  import AFieldset from '@/components/form/AFieldset.vue'
8
9
  import AForm from '@/components/AForm.vue'
9
10
  import ANumericInput from '@/components/form/ANumericInput.vue'
@@ -16,6 +17,7 @@ function install(app: App /* options */) {
16
17
  app.component('ACombobox', AComboBox)
17
18
  app.component('ADate', ADate)
18
19
  app.component('ADropdown', ADropdown)
20
+ app.component('ADatePicker', ADatePicker)
19
21
  app.component('AFieldset', AFieldset)
20
22
  app.component('AForm', AForm)
21
23
  app.component('ANumericInput', ANumericInput)
@@ -24,4 +26,4 @@ function install(app: App /* options */) {
24
26
  // app.component('AQuantity', AQuantity)
25
27
  }
26
28
 
27
- export { ACheckbox, AComboBox, ADate, ADropdown, AFieldset, AForm, ANumericInput, ATextInput, install }
29
+ export { ACheckbox, AComboBox, ADate, ADropdown, ADatePicker, AFieldset, AForm, ANumericInput, ATextInput, install }
@@ -0,0 +1,49 @@
1
+ .adatepicker {
2
+ font-size: var(--table-font-size);
3
+ display: inline-table;
4
+ color: var(--cell-text-color);
5
+ outline: none;
6
+ border-collapse: collapse;
7
+ /* width: calc(100% - 4px); */
8
+ }
9
+
10
+ .adatepicker tr {
11
+ height: 1.15rem;
12
+ height: 1.15rem;
13
+ text-align: center;
14
+ vertical-align: middle;
15
+ }
16
+
17
+ .adatepicker td {
18
+ border: 2px solid transparent;
19
+ outline: 2px solid transparent;
20
+ min-width: 3ch;
21
+ max-width: 3ch;
22
+ }
23
+
24
+ .adatepicker td:focus,
25
+ .adatepicker td:focus-within {
26
+ outline: 1px dashed black;
27
+ box-shadow: none;
28
+ overflow: hidden;
29
+ min-height: 1.15em;
30
+ max-height: 1.15em;
31
+ overflow: hidden;
32
+ }
33
+ .adatepicker .selectedDate {
34
+ outline: 1px solid black;
35
+ background: var(--gray-20);
36
+ font-weight: bolder;
37
+ }
38
+
39
+ .adatepicker .todaysDate {
40
+ font-weight: bolder;
41
+ text-decoration: underline;
42
+ color: black;
43
+ }
44
+ .days-header > td {
45
+ font-weight: bold;
46
+ }
47
+ .prev-date {
48
+ color: var(--gray-20);
49
+ }