@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<!-- This serves as a wrapper component for ADatePicker and ADateTime in one component -->
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<div class="adate-selection">
|
|
5
|
+
<ADatePicker v-if="showDate" :select-range="selectRange" @get-date="handleDate" />
|
|
6
|
+
|
|
7
|
+
<ADateTime
|
|
8
|
+
v-if="showTime"
|
|
9
|
+
:allow-military-time="allowMilitaryTime"
|
|
10
|
+
:default-hours="defaultHours"
|
|
11
|
+
:default-minutes="defaultMinutes"
|
|
12
|
+
:default-seconds="defaultSeconds"
|
|
13
|
+
:default-meridiem="defaultMeridiem"
|
|
14
|
+
:use-seconds="useSeconds"
|
|
15
|
+
@get-time="handleStartTime" />
|
|
16
|
+
|
|
17
|
+
<template v-if="selectRange && showTime && showEndTime">
|
|
18
|
+
<div class="adate-selection__end-label">End time</div>
|
|
19
|
+
<ADateTime
|
|
20
|
+
:allow-military-time="allowMilitaryTime"
|
|
21
|
+
:default-hours="defaultHours"
|
|
22
|
+
:default-minutes="defaultMinutes"
|
|
23
|
+
:default-seconds="defaultSeconds"
|
|
24
|
+
:default-meridiem="defaultMeridiem"
|
|
25
|
+
:use-seconds="useSeconds"
|
|
26
|
+
@get-time="handleEndTime" />
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<p v-if="!showTime && !showDate" class="empty">empty</p>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { provide, ref } from 'vue'
|
|
35
|
+
import ADatePicker from './ADatePicker.vue'
|
|
36
|
+
import ADateTime from './ADateTime.vue'
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
showDate = true,
|
|
40
|
+
showTime = true,
|
|
41
|
+
selectRange = true,
|
|
42
|
+
showEndTime = false,
|
|
43
|
+
allowMilitaryTime = false,
|
|
44
|
+
defaultHours = 12,
|
|
45
|
+
defaultMinutes = 0,
|
|
46
|
+
defaultSeconds = 0,
|
|
47
|
+
defaultMeridiem = 'AM',
|
|
48
|
+
useSeconds = true,
|
|
49
|
+
} = defineProps<{
|
|
50
|
+
showDate?: boolean
|
|
51
|
+
showTime?: boolean
|
|
52
|
+
selectRange?: boolean
|
|
53
|
+
showEndTime?: boolean
|
|
54
|
+
allowMilitaryTime?: boolean
|
|
55
|
+
defaultHours?: number
|
|
56
|
+
defaultMinutes?: number
|
|
57
|
+
defaultSeconds?: number
|
|
58
|
+
defaultMeridiem?: string
|
|
59
|
+
useSeconds?: boolean
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits<{
|
|
63
|
+
'get-date': [{ selected: Date; start?: Date | null; end?: Date | null }]
|
|
64
|
+
'get-time': [{ hours: number; minutes: number; seconds: number; meridiem: string }]
|
|
65
|
+
'get-range': [{ start: Date; end: Date }]
|
|
66
|
+
}>()
|
|
67
|
+
|
|
68
|
+
provide('select-range', selectRange)
|
|
69
|
+
|
|
70
|
+
const today = new Date()
|
|
71
|
+
const pickerStart = ref<Date>(today)
|
|
72
|
+
const pickerEnd = ref<Date>(today)
|
|
73
|
+
const startTimeMs = ref<number>(0)
|
|
74
|
+
const endTimeMs = ref<number>(0)
|
|
75
|
+
|
|
76
|
+
const mergeDateTime = (date: Date, timeMs: number): Date => {
|
|
77
|
+
const d = new Date(date)
|
|
78
|
+
d.setHours(0, 0, 0, 0)
|
|
79
|
+
return new Date(d.getTime() + timeMs)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const timePayloadToMs = (payload: {
|
|
83
|
+
hours: number
|
|
84
|
+
minutes: number
|
|
85
|
+
seconds: number
|
|
86
|
+
meridiem: string
|
|
87
|
+
militaryTime?: number
|
|
88
|
+
}): number => {
|
|
89
|
+
let h: number
|
|
90
|
+
if (payload.militaryTime !== undefined) {
|
|
91
|
+
h = payload.militaryTime
|
|
92
|
+
} else {
|
|
93
|
+
const hrs = payload.hours % 12
|
|
94
|
+
h = payload.meridiem === 'PM' ? hrs + 12 : hrs
|
|
95
|
+
}
|
|
96
|
+
return (h * 3600 + payload.minutes * 60 + (payload.seconds ?? 0)) * 1000
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tryEmitRange = () => {
|
|
100
|
+
if (!selectRange || !showTime || !showEndTime) return
|
|
101
|
+
emit('get-range', {
|
|
102
|
+
start: mergeDateTime(pickerStart.value, startTimeMs.value),
|
|
103
|
+
end: mergeDateTime(pickerEnd.value, endTimeMs.value),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleDate = (data: { start: Date | null; end: Date | null; selected: Date }) => {
|
|
108
|
+
emit('get-date', data)
|
|
109
|
+
if (selectRange) {
|
|
110
|
+
pickerStart.value = data.start ?? data.selected
|
|
111
|
+
pickerEnd.value = data.end ?? data.selected
|
|
112
|
+
tryEmitRange()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleStartTime = (data: {
|
|
117
|
+
hours: number
|
|
118
|
+
minutes: number
|
|
119
|
+
seconds: number
|
|
120
|
+
meridiem: string
|
|
121
|
+
militaryTime?: number
|
|
122
|
+
}) => {
|
|
123
|
+
startTimeMs.value = timePayloadToMs(data)
|
|
124
|
+
if (showEndTime) {
|
|
125
|
+
tryEmitRange()
|
|
126
|
+
} else {
|
|
127
|
+
emit('get-time', data)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const handleEndTime = (data: {
|
|
132
|
+
hours: number
|
|
133
|
+
minutes: number
|
|
134
|
+
seconds: number
|
|
135
|
+
meridiem: string
|
|
136
|
+
militaryTime?: number
|
|
137
|
+
}) => {
|
|
138
|
+
endTimeMs.value = timePayloadToMs(data)
|
|
139
|
+
tryEmitRange()
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<style scoped>
|
|
144
|
+
.adate-selection {
|
|
145
|
+
display: inline-block;
|
|
146
|
+
border: 1px solid var(--sc-gray-80);
|
|
147
|
+
padding: 10px;
|
|
148
|
+
background: var(--sc-form-background);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.adate-selection__end-label {
|
|
152
|
+
font-size: 0.8em;
|
|
153
|
+
font-weight: bold;
|
|
154
|
+
color: var(--sc-input-active-label-color, #555);
|
|
155
|
+
margin-top: 8px;
|
|
156
|
+
margin-bottom: 2px;
|
|
157
|
+
padding-left: 2px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.empty {
|
|
161
|
+
color: #ccc;
|
|
162
|
+
padding: 10px;
|
|
163
|
+
text-align: center;
|
|
164
|
+
border: 1px solid #ccc;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,285 @@
|
|
|
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
|
+
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)
|
|
93
|
+
|
|
94
|
+
if (isNaN(hours) || timeData.hours === '' || hours > maxHours) hours = maxHours
|
|
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
|
|
98
|
+
|
|
99
|
+
timeData.hours = String(hours).padStart(2, '0')
|
|
100
|
+
timeData.minutes = String(minutes).padStart(2, '0')
|
|
101
|
+
timeData.seconds = String(seconds).padStart(2, '0')
|
|
102
|
+
|
|
103
|
+
emitTime()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const emitTime = () => {
|
|
107
|
+
const hours = Number(timeData.hours)
|
|
108
|
+
const minutes = Number(timeData.minutes)
|
|
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
|
+
}
|
|
118
|
+
|
|
119
|
+
const focusInput = (event: FocusEvent) => {
|
|
120
|
+
const target = event.target
|
|
121
|
+
if (target instanceof HTMLInputElement) {
|
|
122
|
+
target.select()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tick = (target: 'hours' | 'minutes' | 'seconds', amount = 1) => {
|
|
127
|
+
const maxHours = allowMilitaryTime ? 23 : 12
|
|
128
|
+
const minHours = allowMilitaryTime ? 0 : 1
|
|
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)
|
|
149
|
+
|
|
150
|
+
const newRawHours = Number(timeData.hours)
|
|
151
|
+
if (!allowMilitaryTime && newRawHours !== prevHours) {
|
|
152
|
+
if ((prevHours === 11 && newRawHours === 12) || (prevHours === 12 && newRawHours === 11)) {
|
|
153
|
+
changeMeridiem()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
timeData.hours = String(formatTime(Number(timeData.hours), minHours, maxHours)).padStart(2, '0')
|
|
158
|
+
timeData.minutes = String(formatTime(Number(timeData.minutes), 0, 59)).padStart(2, '0')
|
|
159
|
+
timeData.seconds = String(formatTime(Number(timeData.seconds), 0, 59)).padStart(2, '0')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
watch(
|
|
163
|
+
() => timeData.hours,
|
|
164
|
+
(newVal, oldVal) => {
|
|
165
|
+
timeData.hours = Number(newVal) > 99 ? oldVal : newVal
|
|
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'))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<style scoped>
|
|
227
|
+
.adate_time {
|
|
228
|
+
width: auto;
|
|
229
|
+
padding: 10px;
|
|
230
|
+
box-sizing: border-box;
|
|
231
|
+
font-size: 1rem;
|
|
232
|
+
background: var(--sc-gray-10);
|
|
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;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="aduration">
|
|
3
|
+
<template v-if="mode === 'display' || mode === 'read'">
|
|
4
|
+
<span class="aform_display-value">{{ displayValue }}</span>
|
|
5
|
+
<label>{{ label }}</label>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<template v-else>
|
|
9
|
+
<ADateSelection
|
|
10
|
+
ref="selectionRef"
|
|
11
|
+
:select-range="true"
|
|
12
|
+
:show-time="true"
|
|
13
|
+
:show-end-time="true"
|
|
14
|
+
:allow-military-time="allowMilitaryTime"
|
|
15
|
+
:use-seconds="useSeconds"
|
|
16
|
+
@get-range="handleRange" />
|
|
17
|
+
<div class="aduration__footer">
|
|
18
|
+
<label>{{ label }}</label>
|
|
19
|
+
<div v-if="startDatetime && endDatetime" class="aduration__summary">
|
|
20
|
+
<span class="aduration__label">Duration:</span>
|
|
21
|
+
<span class="aduration__value">{{ humanDuration }}</span>
|
|
22
|
+
<span class="aduration__ms">({{ modelValue ?? 0 }} ms)</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { ref, computed, watch } from 'vue'
|
|
31
|
+
import ADateSelection from './ADateSelection.vue'
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
label = 'Duration',
|
|
35
|
+
mode = 'edit',
|
|
36
|
+
allowMilitaryTime = false,
|
|
37
|
+
useSeconds = false,
|
|
38
|
+
} = defineProps<{
|
|
39
|
+
label?: string
|
|
40
|
+
mode?: string
|
|
41
|
+
allowMilitaryTime?: boolean
|
|
42
|
+
useSeconds?: boolean
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const modelValue = defineModel<number>()
|
|
46
|
+
|
|
47
|
+
const startDatetime = ref<Date | null>(null)
|
|
48
|
+
const endDatetime = ref<Date | null>(null)
|
|
49
|
+
|
|
50
|
+
const duration = computed<number>(() => {
|
|
51
|
+
if (!startDatetime.value || !endDatetime.value) return 0
|
|
52
|
+
const ms = endDatetime.value.getTime() - startDatetime.value.getTime()
|
|
53
|
+
return ms > 0 ? ms : 0
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
watch(duration, newMs => {
|
|
57
|
+
modelValue.value = newMs
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const handleRange = (data: { start: Date; end: Date }) => {
|
|
61
|
+
startDatetime.value = data.start
|
|
62
|
+
endDatetime.value = data.end
|
|
63
|
+
modelValue.value = duration.value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const humanDuration = computed(() => {
|
|
67
|
+
const ms = duration.value
|
|
68
|
+
if (ms === 0) return '0s'
|
|
69
|
+
const s = Math.floor(ms / 1000) % 60
|
|
70
|
+
const m = Math.floor(ms / 60000) % 60
|
|
71
|
+
const h = Math.floor(ms / 3600000) % 24
|
|
72
|
+
const d = Math.floor(ms / 86400000)
|
|
73
|
+
return [d && `${d}d`, h && `${h}h`, m && `${m}m`, s && `${s}s`].filter(Boolean).join(' ') || '0s'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const displayValue = computed(() => {
|
|
77
|
+
const ms = modelValue.value
|
|
78
|
+
if (!ms) return '—'
|
|
79
|
+
const s = Math.floor(ms / 1000) % 60
|
|
80
|
+
const m = Math.floor(ms / 60000) % 60
|
|
81
|
+
const h = Math.floor(ms / 3600000) % 24
|
|
82
|
+
const d = Math.floor(ms / 86400000)
|
|
83
|
+
return [d && `${d}d`, h && `${h}h`, m && `${m}m`, s && `${s}s`].filter(Boolean).join(' ') || '0s'
|
|
84
|
+
})
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.aduration {
|
|
89
|
+
position: relative;
|
|
90
|
+
min-width: 40ch;
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.aduration__summary {
|
|
95
|
+
display: flex;
|
|
96
|
+
gap: 6px;
|
|
97
|
+
align-items: baseline;
|
|
98
|
+
margin-top: 6px;
|
|
99
|
+
font-size: 0.9em;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.aduration__label {
|
|
103
|
+
font-weight: bold;
|
|
104
|
+
color: var(--sc-input-active-label-color, #555);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.aduration__value {
|
|
108
|
+
color: var(--sc-cell-text-color, #000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.aduration__ms {
|
|
112
|
+
color: var(--sc-gray-50, #888);
|
|
113
|
+
font-size: 0.85em;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.aduration__footer {
|
|
117
|
+
margin-top: 8px;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -122,7 +122,6 @@ watch(
|
|
|
122
122
|
const results = await fn(String(id))
|
|
123
123
|
displayText = results.find(r => String(r.id) === String(id))?.displayText
|
|
124
124
|
if (displayText === undefined) {
|
|
125
|
-
// eslint-disable-next-line no-console
|
|
126
125
|
console.warn(
|
|
127
126
|
`[AFormLink] filterFunction returned no matching result for id "${id}". ` +
|
|
128
127
|
`The function must return an item whose \`id\` exactly matches the search string.`
|
|
@@ -146,7 +145,6 @@ const handleNavigate = () => {
|
|
|
146
145
|
if (navigator && doctype) {
|
|
147
146
|
navigator.navigate(doctype, modelValue.value.id)
|
|
148
147
|
} else if (navigator && !doctype) {
|
|
149
|
-
// eslint-disable-next-line no-console
|
|
150
148
|
console.warn(
|
|
151
149
|
`[AFormLink] Navigation requested but \`doctype\` prop is missing. ` +
|
|
152
150
|
`Add \`options\` (or \`doctype\`) to the Link field definition to enable navigation.`
|
package/src/directives/mask.ts
CHANGED
|
@@ -28,7 +28,11 @@ function getMask(binding: DirectiveBinding<string>) {
|
|
|
28
28
|
if (maskFn) {
|
|
29
29
|
// TODO: (state) replace with state management;
|
|
30
30
|
// pass the entire form/table data to the function
|
|
31
|
-
|
|
31
|
+
// Vue directive reads arbitrary fields off host component instance; type is unknown at this layer.
|
|
32
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
33
|
+
const instance = binding.instance as Record<string, unknown> | null | undefined
|
|
34
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
35
|
+
const locale = instance?.['locale'] as string | undefined
|
|
32
36
|
return maskFn(locale)
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -104,7 +108,9 @@ export function useStringMask(el: HTMLInputElement, binding: DirectiveBinding<st
|
|
|
104
108
|
// most likely fixed with state management;
|
|
105
109
|
// a better way could be to emit back to instance;
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
// Vue directive reads/writes arbitrary fields on host component instance.
|
|
112
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
113
|
+
const instance = binding.instance as Record<string, unknown> | null | undefined
|
|
108
114
|
if (instance?.['maskFilled']) {
|
|
109
115
|
instance['maskFilled'] = !replacement.includes(maskToken)
|
|
110
116
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,10 @@ import AComboBox from './components/form/AComboBox.vue'
|
|
|
8
8
|
import ADate from './components/form/ADate.vue'
|
|
9
9
|
import ADropdown from './components/form/ADropdown.vue'
|
|
10
10
|
import ADatePicker from './components/form/ADatePicker.vue'
|
|
11
|
+
import ADateTime from './components/form/ADateTime.vue'
|
|
12
|
+
import ADateSelection from './components/form/ADateSelection.vue'
|
|
13
|
+
import ADuration from './components/form/ADuration.vue'
|
|
14
|
+
import ADateRange from './components/form/ADateRange.vue'
|
|
11
15
|
import AFieldset from './components/form/AFieldset.vue'
|
|
12
16
|
import AFileAttach from './components/form/AFileAttach.vue'
|
|
13
17
|
import AForm from './components/AForm.vue'
|
|
@@ -30,12 +34,16 @@ function install(app: App /* options */) {
|
|
|
30
34
|
app.component('ADate', ADate)
|
|
31
35
|
app.component('ADropdown', ADropdown)
|
|
32
36
|
app.component('ADatePicker', ADatePicker)
|
|
37
|
+
app.component('ADateTime', ADateTime)
|
|
38
|
+
app.component('ADateRange', ADateRange)
|
|
39
|
+
app.component('ADateSelection', ADateSelection)
|
|
33
40
|
app.component('AFieldset', AFieldset)
|
|
34
41
|
app.component('AFileAttach', AFileAttach)
|
|
35
42
|
app.component('AForm', AForm)
|
|
36
43
|
app.component('AFormLink', AFormLink)
|
|
37
44
|
app.component('ANumericInput', ANumericInput)
|
|
38
45
|
app.component('ATextInput', ATextInput)
|
|
46
|
+
app.component('ADuration', ADuration)
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export {
|
|
@@ -44,6 +52,10 @@ export {
|
|
|
44
52
|
ADate,
|
|
45
53
|
ADropdown,
|
|
46
54
|
ADatePicker,
|
|
55
|
+
ADateRange,
|
|
56
|
+
ADateSelection,
|
|
57
|
+
ADuration,
|
|
58
|
+
ADateTime,
|
|
47
59
|
AFieldset,
|
|
48
60
|
AFileAttach,
|
|
49
61
|
AForm,
|
package/src/types/index.ts
CHANGED
|
@@ -24,6 +24,9 @@ export type ComponentProps = {
|
|
|
24
24
|
*/
|
|
25
25
|
label?: string
|
|
26
26
|
|
|
27
|
+
// TODO: add docstring
|
|
28
|
+
selectRange?: boolean
|
|
29
|
+
|
|
27
30
|
/**
|
|
28
31
|
* The mask to apply to inputs inside the component. Accepts either a plain
|
|
29
32
|
* mask string (e.g. `"(###) ###-####"`) or a stringified arrow function that
|
package/src/utils/deserialize.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* ```
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
+
// T is intentionally a caller-supplied return type — this utility is unsafe by design (Function constructor).
|
|
16
|
+
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters
|
|
15
17
|
export function deserializeFunction<T extends (...args: any[]) => any>(source: string): T {
|
|
16
|
-
//
|
|
18
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion, typescript/no-implied-eval
|
|
17
19
|
return Function(`"use strict"; return (${source})`)() as T
|
|
18
20
|
}
|