@stonecrop/aform 0.20.0 → 0.22.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.
- package/dist/aform.d.ts +3 -0
- package/dist/aform.js +888 -783
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/package.json +5 -5
- package/src/components/form/ADate.vue +17 -74
- package/src/components/form/ADateRange.vue +9 -70
- package/src/components/form/ADateSelection.vue +18 -10
- package/src/components/form/ADateTime.vue +118 -254
- package/src/components/form/ADateTimeInput.vue +299 -0
- package/src/components/form/ADuration.vue +6 -1
- package/src/index.ts +3 -0
|
@@ -1,285 +1,149 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@focus="focusInput"
|
|
10
|
-
@blur="confirmTime"
|
|
11
|
-
@keydown.enter.prevent="confirmTime"
|
|
12
|
-
@keydown.up.prevent="tick('hours')"
|
|
13
|
-
@keydown.down.prevent="tick('hours', -1)" />
|
|
14
|
-
<span class="colon">:</span>
|
|
15
|
-
<input
|
|
16
|
-
v-model="timeData.minutes"
|
|
17
|
-
type="text"
|
|
18
|
-
inputmode="numeric"
|
|
19
|
-
@paste="pasteInput"
|
|
20
|
-
@focus="focusInput"
|
|
21
|
-
@blur="confirmTime"
|
|
22
|
-
@keydown.enter.prevent="confirmTime"
|
|
23
|
-
@keydown.up.prevent="tick('minutes')"
|
|
24
|
-
@keydown.down.prevent="tick('minutes', -1)" />
|
|
25
|
-
<span v-if="useSeconds" class="colon">:</span>
|
|
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>
|
|
26
9
|
<input
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
:id="uuid"
|
|
11
|
+
class="aform_input-field"
|
|
29
12
|
type="text"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
v-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
13
|
+
:value="datetimeDisplay"
|
|
14
|
+
placeholder="Select date and time"
|
|
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="adatetime-picker"
|
|
26
|
+
:select-range="false"
|
|
27
|
+
:show-date="true"
|
|
28
|
+
:show-time="true"
|
|
29
|
+
:default-hours="pickerDefaults.hours"
|
|
30
|
+
:default-minutes="pickerDefaults.minutes"
|
|
31
|
+
:default-seconds="pickerDefaults.seconds"
|
|
32
|
+
:default-meridiem="pickerDefaults.meridiem"
|
|
33
|
+
:allow-military-time="allowMilitaryTime"
|
|
34
|
+
:use-seconds="useSeconds"
|
|
35
|
+
@get-date="handleDate"
|
|
36
|
+
@get-time="handleTime" />
|
|
37
|
+
</template>
|
|
47
38
|
</div>
|
|
48
39
|
</template>
|
|
49
40
|
|
|
50
41
|
<script setup lang="ts">
|
|
51
|
-
import { ref,
|
|
42
|
+
import { ref, computed, watch } from 'vue'
|
|
43
|
+
import { onClickOutside } from '@vueuse/core'
|
|
44
|
+
import ADateSelection from './ADateSelection.vue'
|
|
45
|
+
import type { ComponentProps } from '../../types'
|
|
52
46
|
|
|
53
47
|
const {
|
|
48
|
+
label = 'Date & Time',
|
|
49
|
+
mode,
|
|
50
|
+
uuid,
|
|
51
|
+
errors,
|
|
52
|
+
validation = { errorMessage: '' },
|
|
54
53
|
allowMilitaryTime = false,
|
|
55
|
-
defaultHours = 12,
|
|
56
|
-
defaultMinutes = 0,
|
|
57
|
-
defaultSeconds = 0,
|
|
58
|
-
defaultMeridiem = 'AM',
|
|
59
54
|
useSeconds = true,
|
|
60
|
-
} = defineProps<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
useSeconds?: boolean
|
|
67
|
-
}>()
|
|
68
|
-
|
|
69
|
-
const emit = defineEmits<{
|
|
70
|
-
'get-time': [{ hours: number; minutes: number; seconds: number; meridiem: string; militaryTime: number }]
|
|
71
|
-
}>()
|
|
72
|
-
|
|
73
|
-
const meridiemSelector = useTemplateRef<HTMLSelectElement>('meridiem-selector')
|
|
74
|
-
|
|
75
|
-
const timeData = reactive({
|
|
76
|
-
hours: String(defaultHours).padStart(2, '0'),
|
|
77
|
-
minutes: String(defaultMinutes).padStart(2, '0'),
|
|
78
|
-
seconds: String(defaultSeconds).padStart(2, '0'),
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const meridiem = ref(defaultMeridiem == 'AM' ? 'AM' : 'PM')
|
|
82
|
-
|
|
83
|
-
onMounted(() => {
|
|
84
|
-
emitTime()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const confirmTime = () => {
|
|
88
|
-
const maxHours = allowMilitaryTime ? 23 : 12
|
|
89
|
-
const minHours = allowMilitaryTime ? 0 : 1
|
|
90
|
-
let hours = Number(timeData.hours)
|
|
91
|
-
let minutes = Number(timeData.minutes)
|
|
92
|
-
let seconds = Number(timeData.seconds)
|
|
55
|
+
} = defineProps<
|
|
56
|
+
ComponentProps & {
|
|
57
|
+
allowMilitaryTime?: boolean
|
|
58
|
+
useSeconds?: boolean
|
|
59
|
+
}
|
|
60
|
+
>()
|
|
93
61
|
|
|
94
|
-
|
|
95
|
-
if (!allowMilitaryTime && hours < minHours) hours = minHours
|
|
96
|
-
if (isNaN(minutes) || timeData.minutes === '' || minutes > 59) minutes = 59
|
|
97
|
-
if (isNaN(seconds) || timeData.seconds === '' || seconds > 59) seconds = 59
|
|
62
|
+
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
98
63
|
|
|
99
|
-
|
|
100
|
-
timeData.minutes = String(minutes).padStart(2, '0')
|
|
101
|
-
timeData.seconds = String(seconds).padStart(2, '0')
|
|
64
|
+
const modelValue = defineModel<string | Date>()
|
|
102
65
|
|
|
103
|
-
|
|
104
|
-
}
|
|
66
|
+
const currentDateTime = ref<Date>(modelValue.value ? new Date(modelValue.value) : new Date())
|
|
105
67
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const seconds = Number(timeData.seconds)
|
|
110
|
-
emit('get-time', {
|
|
111
|
-
hours,
|
|
112
|
-
minutes,
|
|
113
|
-
seconds,
|
|
114
|
-
meridiem: meridiem.value,
|
|
115
|
-
militaryTime: allowMilitaryTime ? hours : meridiem.value === 'PM' ? (hours === 12 ? 12 : hours + 12) : hours % 12,
|
|
116
|
-
})
|
|
117
|
-
}
|
|
68
|
+
const showPicker = ref(false)
|
|
69
|
+
const pickerRef = ref(null)
|
|
70
|
+
onClickOutside(pickerRef, () => (showPicker.value = false))
|
|
118
71
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (target instanceof HTMLInputElement) {
|
|
122
|
-
target.select()
|
|
123
|
-
}
|
|
72
|
+
const openPicker = () => {
|
|
73
|
+
if (mode !== 'read') showPicker.value = true
|
|
124
74
|
}
|
|
125
75
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (target == 'hours') {
|
|
131
|
-
const oldHours = Number(timeData.hours)
|
|
132
|
-
timeData.hours = String(oldHours + amount)
|
|
133
|
-
if ((oldHours == 11 && Number(timeData.hours) == 12) || (oldHours == 12 && Number(timeData.hours) == 11)) {
|
|
134
|
-
changeMeridiem()
|
|
135
|
-
}
|
|
136
|
-
} else if (target == 'minutes') {
|
|
137
|
-
timeData.minutes = String(Number(timeData.minutes) + amount)
|
|
138
|
-
} else if (target == 'seconds') {
|
|
139
|
-
timeData.seconds = String(Number(timeData.seconds) + amount)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const prevHours = Number(timeData.hours)
|
|
143
|
-
|
|
144
|
-
if (Number(timeData.seconds) < 0) timeData.minutes = String(Number(timeData.minutes) - 1)
|
|
145
|
-
else if (Number(timeData.seconds) > 59) timeData.minutes = String(Number(timeData.minutes) + 1)
|
|
146
|
-
|
|
147
|
-
if (Number(timeData.minutes) < 0) timeData.hours = String(prevHours - 1)
|
|
148
|
-
else if (Number(timeData.minutes) > 59) timeData.hours = String(prevHours + 1)
|
|
76
|
+
const displayValue = computed(() => {
|
|
77
|
+
if (!modelValue.value) return ''
|
|
78
|
+
return currentDateTime.value.toLocaleString()
|
|
79
|
+
})
|
|
149
80
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
81
|
+
const datetimeDisplay = computed(() => displayValue.value)
|
|
82
|
+
|
|
83
|
+
const pickerDefaults = computed(() => {
|
|
84
|
+
const d = currentDateTime.value
|
|
85
|
+
const hours24 = d.getHours()
|
|
86
|
+
const meridiem = hours24 >= 12 ? 'PM' : 'AM'
|
|
87
|
+
const hours12 = hours24 % 12 || 12
|
|
88
|
+
return {
|
|
89
|
+
hours: allowMilitaryTime ? hours24 : hours12,
|
|
90
|
+
minutes: d.getMinutes(),
|
|
91
|
+
seconds: d.getSeconds(),
|
|
92
|
+
meridiem,
|
|
155
93
|
}
|
|
94
|
+
})
|
|
156
95
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
96
|
+
const emitModel = () => {
|
|
97
|
+
modelValue.value = currentDateTime.value.toISOString()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleDate = (data: { selected: Date }) => {
|
|
101
|
+
const next = new Date(currentDateTime.value)
|
|
102
|
+
next.setFullYear(data.selected.getFullYear(), data.selected.getMonth(), data.selected.getDate())
|
|
103
|
+
currentDateTime.value = next
|
|
104
|
+
emitModel()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleTime = (data: {
|
|
108
|
+
hours: number
|
|
109
|
+
minutes: number
|
|
110
|
+
seconds: number
|
|
111
|
+
meridiem: string
|
|
112
|
+
militaryTime?: number
|
|
113
|
+
source?: 'init' | 'user'
|
|
114
|
+
}) => {
|
|
115
|
+
// The widget announces its own starting value as it mounts, and those defaults come from
|
|
116
|
+
// `pickerDefaults` — i.e. straight back out of this component. Writing that echo to the model
|
|
117
|
+
// meant one click on an empty field silently filled it with the current date and time.
|
|
118
|
+
if (data.source === 'init') return
|
|
119
|
+
|
|
120
|
+
const next = new Date(currentDateTime.value)
|
|
121
|
+
const hours = data.militaryTime ?? data.hours
|
|
122
|
+
next.setHours(hours, data.minutes, useSeconds ? data.seconds : 0, 0)
|
|
123
|
+
currentDateTime.value = next
|
|
124
|
+
emitModel()
|
|
125
|
+
// Deliberately does NOT close the picker. `get-time` is the widget's current value, not a
|
|
126
|
+
// commit — it fires on every blur, arrow key and meridiem change — so closing here shut the
|
|
127
|
+
// picker as soon as the user tabbed out of the hours field. Dismissal is the click-outside
|
|
128
|
+
// handler above; closing on `get-date` instead would strand the time half of a datetime.
|
|
160
129
|
}
|
|
161
130
|
|
|
162
131
|
watch(
|
|
163
|
-
() =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
watch(
|
|
169
|
-
() => timeData.minutes,
|
|
170
|
-
(newVal, oldVal) => {
|
|
171
|
-
timeData.minutes = Number(newVal) > 99 ? oldVal : newVal
|
|
172
|
-
}
|
|
173
|
-
)
|
|
174
|
-
watch(
|
|
175
|
-
() => timeData.seconds,
|
|
176
|
-
(newVal, oldVal) => {
|
|
177
|
-
timeData.seconds = Number(newVal) > 99 ? oldVal : newVal
|
|
178
|
-
}
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
const formatTime = (target: number, min: number, max: number): number => {
|
|
182
|
-
if (target > max) return min
|
|
183
|
-
else if (target < min) return max
|
|
184
|
-
return target
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const changeMeridiem = () => {
|
|
188
|
-
meridiem.value = meridiem.value == 'PM' ? 'AM' : 'PM'
|
|
189
|
-
emitTime()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const pasteInput = (event: ClipboardEvent, pasteAllFields = false) => {
|
|
193
|
-
event.stopPropagation()
|
|
194
|
-
event.preventDefault()
|
|
195
|
-
|
|
196
|
-
const clipboardData = event.clipboardData
|
|
197
|
-
if (!clipboardData) return
|
|
198
|
-
let pastedData: string = clipboardData.getData('Text')
|
|
199
|
-
|
|
200
|
-
pastedData = pastedData.replace(/[^0-9]/g, '')
|
|
201
|
-
|
|
202
|
-
if (pasteAllFields) {
|
|
203
|
-
if (pastedData.length % 2 != 0) pastedData = '0' + pastedData
|
|
204
|
-
if (pastedData.length < 3) pastedData += '00'
|
|
205
|
-
if (pastedData.length < 5) pastedData += '00'
|
|
206
|
-
|
|
207
|
-
const timeUnits = pastedData.match(/(..?)/g)
|
|
208
|
-
if (!timeUnits || timeUnits.length < 3) return
|
|
209
|
-
|
|
210
|
-
timeData.seconds = timeUnits[2]
|
|
211
|
-
timeData.minutes = timeUnits[1]
|
|
212
|
-
timeData.hours = timeUnits[0]
|
|
213
|
-
confirmTime()
|
|
214
|
-
if (!allowMilitaryTime) meridiemSelector.value?.focus()
|
|
215
|
-
} else {
|
|
216
|
-
if (pastedData.length > 2) pastedData = pastedData.slice(0, 2)
|
|
217
|
-
const target = event.target
|
|
218
|
-
if (target instanceof HTMLInputElement) {
|
|
219
|
-
target.value = pastedData
|
|
220
|
-
target.dispatchEvent(new Event('input'))
|
|
132
|
+
() => modelValue.value,
|
|
133
|
+
newValue => {
|
|
134
|
+
if (newValue) {
|
|
135
|
+
currentDateTime.value = new Date(newValue)
|
|
221
136
|
}
|
|
222
137
|
}
|
|
223
|
-
|
|
138
|
+
)
|
|
224
139
|
</script>
|
|
225
140
|
|
|
226
141
|
<style scoped>
|
|
227
|
-
.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
.adate_time_fields {
|
|
235
|
-
display: flex;
|
|
236
|
-
align-items: stretch;
|
|
237
|
-
gap: 5px;
|
|
238
|
-
justify-content: flex-start;
|
|
239
|
-
}
|
|
240
|
-
.adate_time_fields > input {
|
|
241
|
-
min-width: 30px;
|
|
242
|
-
padding: 2px;
|
|
243
|
-
text-align: center;
|
|
244
|
-
display: inline-block;
|
|
245
|
-
flex-basis: 0;
|
|
246
|
-
}
|
|
247
|
-
.meridiem-selector {
|
|
248
|
-
cursor: pointer;
|
|
249
|
-
display: inline-block;
|
|
250
|
-
flex-basis: 0;
|
|
251
|
-
padding: 5px;
|
|
252
|
-
user-select: none;
|
|
253
|
-
}
|
|
254
|
-
.meridiem-selector:focus {
|
|
255
|
-
outline: 2px solid black;
|
|
256
|
-
outline-offset: -2px;
|
|
257
|
-
}
|
|
258
|
-
.adate_time_segment {
|
|
259
|
-
display: flex;
|
|
260
|
-
flex-direction: column;
|
|
261
|
-
width: 40px;
|
|
262
|
-
}
|
|
263
|
-
.colon {
|
|
264
|
-
display: flex;
|
|
265
|
-
align-items: normal;
|
|
266
|
-
}
|
|
267
|
-
.aform_form-btn {
|
|
268
|
-
cursor: pointer;
|
|
269
|
-
}
|
|
270
|
-
.aform-select {
|
|
271
|
-
border-radius: 0px;
|
|
272
|
-
border: 1px solid rgb(118, 118, 118);
|
|
273
|
-
font-size: 1rem;
|
|
274
|
-
padding: 0rem;
|
|
275
|
-
margin: 0;
|
|
276
|
-
border-radius: 0;
|
|
277
|
-
box-sizing: border-box;
|
|
278
|
-
min-height: auto;
|
|
279
|
-
position: relative;
|
|
280
|
-
color: var(--sc-cell-text-color);
|
|
281
|
-
}
|
|
282
|
-
.meridiem-selector {
|
|
283
|
-
margin-left: 6px;
|
|
142
|
+
.adatetime-picker {
|
|
143
|
+
position: absolute;
|
|
144
|
+
top: 100%;
|
|
145
|
+
left: 0;
|
|
146
|
+
z-index: 1000;
|
|
147
|
+
margin-top: 0.25rem;
|
|
284
148
|
}
|
|
285
149
|
</style>
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="adate_time">
|
|
3
|
+
<div class="adate_time_fields">
|
|
4
|
+
<input
|
|
5
|
+
v-model="timeData.hours"
|
|
6
|
+
type="text"
|
|
7
|
+
inputmode="numeric"
|
|
8
|
+
@paste="pasteInput($event, true)"
|
|
9
|
+
@focus="focusInput"
|
|
10
|
+
@blur="confirmTime"
|
|
11
|
+
@keydown.enter.prevent="confirmTime"
|
|
12
|
+
@keydown.up.prevent="tick('hours')"
|
|
13
|
+
@keydown.down.prevent="tick('hours', -1)" />
|
|
14
|
+
<span class="colon">:</span>
|
|
15
|
+
<input
|
|
16
|
+
v-model="timeData.minutes"
|
|
17
|
+
type="text"
|
|
18
|
+
inputmode="numeric"
|
|
19
|
+
@paste="pasteInput"
|
|
20
|
+
@focus="focusInput"
|
|
21
|
+
@blur="confirmTime"
|
|
22
|
+
@keydown.enter.prevent="confirmTime"
|
|
23
|
+
@keydown.up.prevent="tick('minutes')"
|
|
24
|
+
@keydown.down.prevent="tick('minutes', -1)" />
|
|
25
|
+
<span v-if="useSeconds" class="colon">:</span>
|
|
26
|
+
<input
|
|
27
|
+
v-if="useSeconds"
|
|
28
|
+
v-model="timeData.seconds"
|
|
29
|
+
type="text"
|
|
30
|
+
inputmode="numeric"
|
|
31
|
+
@paste="pasteInput"
|
|
32
|
+
@focus="focusInput"
|
|
33
|
+
@blur="confirmTime"
|
|
34
|
+
@keydown.enter.prevent="confirmTime"
|
|
35
|
+
@keydown.up.prevent="tick('seconds')"
|
|
36
|
+
@keydown.down.prevent="tick('seconds', -1)" />
|
|
37
|
+
<select
|
|
38
|
+
v-if="!allowMilitaryTime"
|
|
39
|
+
ref="meridiem-selector"
|
|
40
|
+
v-model="meridiem"
|
|
41
|
+
class="aform-select meridiem-selector"
|
|
42
|
+
@change="confirmTime">
|
|
43
|
+
<option value="AM">AM</option>
|
|
44
|
+
<option value="PM">PM</option>
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { ref, reactive, useTemplateRef, watch, onMounted } from 'vue'
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
allowMilitaryTime = false,
|
|
55
|
+
defaultHours = 12,
|
|
56
|
+
defaultMinutes = 0,
|
|
57
|
+
defaultSeconds = 0,
|
|
58
|
+
defaultMeridiem = 'AM',
|
|
59
|
+
useSeconds = true,
|
|
60
|
+
} = defineProps<{
|
|
61
|
+
allowMilitaryTime?: boolean
|
|
62
|
+
defaultHours?: number
|
|
63
|
+
defaultMinutes?: number
|
|
64
|
+
defaultSeconds?: number
|
|
65
|
+
defaultMeridiem?: string
|
|
66
|
+
useSeconds?: boolean
|
|
67
|
+
}>()
|
|
68
|
+
|
|
69
|
+
// `get-time` carries the widget's CURRENT VALUE, not a commit: it fires once as the widget
|
|
70
|
+
// mounts and again on every blur, Enter, arrow key and meridiem change — five times during one
|
|
71
|
+
// ordinary edit. `source` is what lets a parent tell the start-up announcement apart from a real
|
|
72
|
+
// user edit; without it a parent has nothing to key on and must guess from the value alone.
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
'get-time': [
|
|
75
|
+
{
|
|
76
|
+
hours: number
|
|
77
|
+
minutes: number
|
|
78
|
+
seconds: number
|
|
79
|
+
meridiem: string
|
|
80
|
+
militaryTime: number
|
|
81
|
+
source: 'init' | 'user'
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
}>()
|
|
85
|
+
|
|
86
|
+
const meridiemSelector = useTemplateRef<HTMLSelectElement>('meridiem-selector')
|
|
87
|
+
|
|
88
|
+
const timeData = reactive({
|
|
89
|
+
hours: String(defaultHours).padStart(2, '0'),
|
|
90
|
+
minutes: String(defaultMinutes).padStart(2, '0'),
|
|
91
|
+
seconds: String(defaultSeconds).padStart(2, '0'),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const meridiem = ref(defaultMeridiem == 'AM' ? 'AM' : 'PM')
|
|
95
|
+
|
|
96
|
+
onMounted(() => {
|
|
97
|
+
emitTime('init')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const confirmTime = () => {
|
|
101
|
+
const maxHours = allowMilitaryTime ? 23 : 12
|
|
102
|
+
const minHours = allowMilitaryTime ? 0 : 1
|
|
103
|
+
let hours = Number(timeData.hours)
|
|
104
|
+
let minutes = Number(timeData.minutes)
|
|
105
|
+
let seconds = Number(timeData.seconds)
|
|
106
|
+
|
|
107
|
+
if (isNaN(hours) || timeData.hours === '' || hours > maxHours) hours = maxHours
|
|
108
|
+
if (!allowMilitaryTime && hours < minHours) hours = minHours
|
|
109
|
+
if (isNaN(minutes) || timeData.minutes === '' || minutes > 59) minutes = 59
|
|
110
|
+
if (isNaN(seconds) || timeData.seconds === '' || seconds > 59) seconds = 59
|
|
111
|
+
|
|
112
|
+
timeData.hours = String(hours).padStart(2, '0')
|
|
113
|
+
timeData.minutes = String(minutes).padStart(2, '0')
|
|
114
|
+
timeData.seconds = String(seconds).padStart(2, '0')
|
|
115
|
+
|
|
116
|
+
emitTime('user')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const emitTime = (source: 'init' | 'user') => {
|
|
120
|
+
const hours = Number(timeData.hours)
|
|
121
|
+
const minutes = Number(timeData.minutes)
|
|
122
|
+
const seconds = Number(timeData.seconds)
|
|
123
|
+
emit('get-time', {
|
|
124
|
+
hours,
|
|
125
|
+
minutes,
|
|
126
|
+
seconds,
|
|
127
|
+
source,
|
|
128
|
+
meridiem: meridiem.value,
|
|
129
|
+
militaryTime: allowMilitaryTime ? hours : meridiem.value === 'PM' ? (hours === 12 ? 12 : hours + 12) : hours % 12,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const focusInput = (event: FocusEvent) => {
|
|
134
|
+
const target = event.target
|
|
135
|
+
if (target instanceof HTMLInputElement) {
|
|
136
|
+
target.select()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const tick = (target: 'hours' | 'minutes' | 'seconds', amount = 1) => {
|
|
141
|
+
const maxHours = allowMilitaryTime ? 23 : 12
|
|
142
|
+
const minHours = allowMilitaryTime ? 0 : 1
|
|
143
|
+
|
|
144
|
+
if (target == 'hours') {
|
|
145
|
+
const oldHours = Number(timeData.hours)
|
|
146
|
+
timeData.hours = String(oldHours + amount)
|
|
147
|
+
if ((oldHours == 11 && Number(timeData.hours) == 12) || (oldHours == 12 && Number(timeData.hours) == 11)) {
|
|
148
|
+
changeMeridiem()
|
|
149
|
+
}
|
|
150
|
+
} else if (target == 'minutes') {
|
|
151
|
+
timeData.minutes = String(Number(timeData.minutes) + amount)
|
|
152
|
+
} else if (target == 'seconds') {
|
|
153
|
+
timeData.seconds = String(Number(timeData.seconds) + amount)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const prevHours = Number(timeData.hours)
|
|
157
|
+
|
|
158
|
+
if (Number(timeData.seconds) < 0) timeData.minutes = String(Number(timeData.minutes) - 1)
|
|
159
|
+
else if (Number(timeData.seconds) > 59) timeData.minutes = String(Number(timeData.minutes) + 1)
|
|
160
|
+
|
|
161
|
+
if (Number(timeData.minutes) < 0) timeData.hours = String(prevHours - 1)
|
|
162
|
+
else if (Number(timeData.minutes) > 59) timeData.hours = String(prevHours + 1)
|
|
163
|
+
|
|
164
|
+
const newRawHours = Number(timeData.hours)
|
|
165
|
+
if (!allowMilitaryTime && newRawHours !== prevHours) {
|
|
166
|
+
if ((prevHours === 11 && newRawHours === 12) || (prevHours === 12 && newRawHours === 11)) {
|
|
167
|
+
changeMeridiem()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
timeData.hours = String(formatTime(Number(timeData.hours), minHours, maxHours)).padStart(2, '0')
|
|
172
|
+
timeData.minutes = String(formatTime(Number(timeData.minutes), 0, 59)).padStart(2, '0')
|
|
173
|
+
timeData.seconds = String(formatTime(Number(timeData.seconds), 0, 59)).padStart(2, '0')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
watch(
|
|
177
|
+
() => timeData.hours,
|
|
178
|
+
(newVal, oldVal) => {
|
|
179
|
+
timeData.hours = Number(newVal) > 99 ? oldVal : newVal
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
watch(
|
|
183
|
+
() => timeData.minutes,
|
|
184
|
+
(newVal, oldVal) => {
|
|
185
|
+
timeData.minutes = Number(newVal) > 99 ? oldVal : newVal
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
watch(
|
|
189
|
+
() => timeData.seconds,
|
|
190
|
+
(newVal, oldVal) => {
|
|
191
|
+
timeData.seconds = Number(newVal) > 99 ? oldVal : newVal
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const formatTime = (target: number, min: number, max: number): number => {
|
|
196
|
+
if (target > max) return min
|
|
197
|
+
else if (target < min) return max
|
|
198
|
+
return target
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const changeMeridiem = () => {
|
|
202
|
+
meridiem.value = meridiem.value == 'PM' ? 'AM' : 'PM'
|
|
203
|
+
emitTime('user')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const pasteInput = (event: ClipboardEvent, pasteAllFields = false) => {
|
|
207
|
+
event.stopPropagation()
|
|
208
|
+
event.preventDefault()
|
|
209
|
+
|
|
210
|
+
const clipboardData = event.clipboardData
|
|
211
|
+
if (!clipboardData) return
|
|
212
|
+
let pastedData: string = clipboardData.getData('Text')
|
|
213
|
+
|
|
214
|
+
pastedData = pastedData.replace(/[^0-9]/g, '')
|
|
215
|
+
|
|
216
|
+
if (pasteAllFields) {
|
|
217
|
+
if (pastedData.length % 2 != 0) pastedData = '0' + pastedData
|
|
218
|
+
if (pastedData.length < 3) pastedData += '00'
|
|
219
|
+
if (pastedData.length < 5) pastedData += '00'
|
|
220
|
+
|
|
221
|
+
const timeUnits = pastedData.match(/(..?)/g)
|
|
222
|
+
if (!timeUnits || timeUnits.length < 3) return
|
|
223
|
+
|
|
224
|
+
timeData.seconds = timeUnits[2]
|
|
225
|
+
timeData.minutes = timeUnits[1]
|
|
226
|
+
timeData.hours = timeUnits[0]
|
|
227
|
+
confirmTime()
|
|
228
|
+
if (!allowMilitaryTime) meridiemSelector.value?.focus()
|
|
229
|
+
} else {
|
|
230
|
+
if (pastedData.length > 2) pastedData = pastedData.slice(0, 2)
|
|
231
|
+
const target = event.target
|
|
232
|
+
if (target instanceof HTMLInputElement) {
|
|
233
|
+
target.value = pastedData
|
|
234
|
+
target.dispatchEvent(new Event('input'))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<style scoped>
|
|
241
|
+
.adate_time {
|
|
242
|
+
width: auto;
|
|
243
|
+
padding: 10px;
|
|
244
|
+
box-sizing: border-box;
|
|
245
|
+
font-size: 1rem;
|
|
246
|
+
background: var(--sc-gray-10);
|
|
247
|
+
}
|
|
248
|
+
.adate_time_fields {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: stretch;
|
|
251
|
+
gap: 5px;
|
|
252
|
+
justify-content: flex-start;
|
|
253
|
+
}
|
|
254
|
+
.adate_time_fields > input {
|
|
255
|
+
min-width: 30px;
|
|
256
|
+
padding: 2px;
|
|
257
|
+
text-align: center;
|
|
258
|
+
display: inline-block;
|
|
259
|
+
flex-basis: 0;
|
|
260
|
+
}
|
|
261
|
+
.meridiem-selector {
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
display: inline-block;
|
|
264
|
+
flex-basis: 0;
|
|
265
|
+
padding: 5px;
|
|
266
|
+
user-select: none;
|
|
267
|
+
}
|
|
268
|
+
.meridiem-selector:focus {
|
|
269
|
+
outline: 2px solid black;
|
|
270
|
+
outline-offset: -2px;
|
|
271
|
+
}
|
|
272
|
+
.adate_time_segment {
|
|
273
|
+
display: flex;
|
|
274
|
+
flex-direction: column;
|
|
275
|
+
width: 40px;
|
|
276
|
+
}
|
|
277
|
+
.colon {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: normal;
|
|
280
|
+
}
|
|
281
|
+
.aform_form-btn {
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
.aform-select {
|
|
285
|
+
border-radius: 0px;
|
|
286
|
+
border: 1px solid rgb(118, 118, 118);
|
|
287
|
+
font-size: 1rem;
|
|
288
|
+
padding: 0rem;
|
|
289
|
+
margin: 0;
|
|
290
|
+
border-radius: 0;
|
|
291
|
+
box-sizing: border-box;
|
|
292
|
+
min-height: auto;
|
|
293
|
+
position: relative;
|
|
294
|
+
color: var(--sc-cell-text-color);
|
|
295
|
+
}
|
|
296
|
+
.meridiem-selector {
|
|
297
|
+
margin-left: 6px;
|
|
298
|
+
}
|
|
299
|
+
</style>
|