@stonecrop/aform 0.21.0 → 0.23.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 +35 -0
- package/dist/aform.js +1197 -996
- package/dist/aform.js.map +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/utils/badge.d.ts +25 -0
- package/dist/src/utils/badge.d.ts.map +1 -0
- package/dist/src/utils/badge.js +71 -0
- package/package.json +5 -5
- package/src/components/form/ABadge.vue +135 -0
- 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/ADropdown.vue +81 -99
- package/src/components/form/ADuration.vue +6 -1
- package/src/index.ts +8 -0
- package/src/utils/badge.ts +94 -0
|
@@ -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>
|
|
@@ -1,50 +1,62 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class="autocomplete-result"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
<div class="aform_form-element">
|
|
3
|
+
<template v-if="mode === 'display'">
|
|
4
|
+
<span v-if="badgeDescriptor" class="aform_display-value" :style="displayAccentStyle">{{
|
|
5
|
+
badgeDescriptor.label
|
|
6
|
+
}}</span>
|
|
7
|
+
<span v-else class="aform_display-value">{{ search ?? '' }}</span>
|
|
8
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
9
|
+
</template>
|
|
10
|
+
<template v-else>
|
|
11
|
+
<div v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
|
|
12
|
+
<input
|
|
13
|
+
v-model="search"
|
|
14
|
+
type="text"
|
|
15
|
+
class="aform_input-field"
|
|
16
|
+
:disabled="mode === 'read'"
|
|
17
|
+
:style="inputAccentStyle"
|
|
18
|
+
@input="filter"
|
|
19
|
+
@focus="openDropdown"
|
|
20
|
+
@keydown.down="selectNextResult"
|
|
21
|
+
@keydown.up="selectPrevResult"
|
|
22
|
+
@keydown.enter="setCurrentResult"
|
|
23
|
+
@keydown.esc="onClickOutside"
|
|
24
|
+
@keydown.tab="onClickOutside" />
|
|
25
|
+
|
|
26
|
+
<ul v-show="dropdown.open" id="autocomplete-results" class="autocomplete-results">
|
|
27
|
+
<li v-if="dropdown.loading" class="loading autocomplete-result">Loading results...</li>
|
|
28
|
+
<li
|
|
29
|
+
v-for="(result, i) in dropdown.results"
|
|
30
|
+
v-else
|
|
31
|
+
:key="result"
|
|
32
|
+
class="autocomplete-result"
|
|
33
|
+
:class="{ 'is-active': i === dropdown.activeItemIndex }"
|
|
34
|
+
@click.stop="setResult(result)">
|
|
35
|
+
{{ result }}
|
|
36
|
+
</li>
|
|
37
|
+
</ul>
|
|
38
|
+
<label class="aform_field-label">{{ label }}</label>
|
|
39
|
+
</div>
|
|
40
|
+
<p v-show="errorText" class="aform_error" v-html="errorText"></p>
|
|
41
|
+
</template>
|
|
36
42
|
</div>
|
|
37
43
|
</template>
|
|
38
44
|
|
|
39
45
|
<script setup lang="ts">
|
|
46
|
+
import type { FieldOptions } from '@stonecrop/schema'
|
|
47
|
+
import { selectChoices } from '@stonecrop/schema'
|
|
40
48
|
import { vOnClickOutside } from '@vueuse/components'
|
|
41
|
-
import { computed, reactive, ref } from 'vue'
|
|
49
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
42
50
|
|
|
43
51
|
import type { ComponentProps } from '../../types'
|
|
52
|
+
import type { BadgeFormatFn } from '../../utils/badge'
|
|
53
|
+
import { badgeInputAccentStyle, resolveFieldBadge } from '../../utils/badge'
|
|
54
|
+
import { deserializeFunction } from '../../utils/deserialize'
|
|
44
55
|
|
|
45
56
|
const {
|
|
46
57
|
label,
|
|
47
58
|
options = [],
|
|
59
|
+
format,
|
|
48
60
|
isAsync = false,
|
|
49
61
|
filterFunction = undefined,
|
|
50
62
|
mode,
|
|
@@ -52,26 +64,46 @@ const {
|
|
|
52
64
|
validation = { errorMessage: '' },
|
|
53
65
|
} = defineProps<
|
|
54
66
|
ComponentProps & {
|
|
55
|
-
options?:
|
|
67
|
+
options?: FieldOptions
|
|
68
|
+
format?: string
|
|
56
69
|
isAsync?: boolean
|
|
57
70
|
filterFunction?: (search: string) => string[] | Promise<string[]>
|
|
58
71
|
}
|
|
59
72
|
>()
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
const choiceList = computed(() => selectChoices(options))
|
|
75
|
+
|
|
76
|
+
// Compile the serialized formatter once per `format` string, not once per keystroke:
|
|
77
|
+
// badgeDescriptor re-evaluates on every input event and deserializeFunction runs the
|
|
78
|
+
// Function constructor.
|
|
79
|
+
const formatFn = computed(() => (format ? deserializeFunction<BadgeFormatFn>(format) : undefined))
|
|
80
|
+
|
|
81
|
+
const badgeDescriptor = computed(() => resolveFieldBadge(search.value, options, formatFn.value))
|
|
82
|
+
|
|
83
|
+
const inputAccentStyle = computed(() => badgeInputAccentStyle(badgeDescriptor.value))
|
|
84
|
+
|
|
85
|
+
const displayAccentStyle = computed(() => badgeInputAccentStyle(badgeDescriptor.value))
|
|
86
|
+
|
|
62
87
|
const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
|
|
63
88
|
const search = defineModel<string>()
|
|
64
89
|
|
|
65
|
-
// tracks the last explicitly-committed value so outside-click reverts instead of clears
|
|
66
90
|
const committedValue = ref(search.value ?? '')
|
|
67
91
|
|
|
68
92
|
const dropdown = reactive({
|
|
69
93
|
activeItemIndex: null as number | null,
|
|
70
94
|
open: false,
|
|
71
95
|
loading: false,
|
|
72
|
-
results:
|
|
96
|
+
results: [] as string[],
|
|
73
97
|
})
|
|
74
98
|
|
|
99
|
+
watch(
|
|
100
|
+
choiceList,
|
|
101
|
+
choices => {
|
|
102
|
+
dropdown.results = choices
|
|
103
|
+
},
|
|
104
|
+
{ immediate: true }
|
|
105
|
+
)
|
|
106
|
+
|
|
75
107
|
const onClickOutside = () => closeDropdown()
|
|
76
108
|
|
|
77
109
|
const filter = async () => {
|
|
@@ -99,26 +131,25 @@ const setResult = (result: string) => {
|
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
const openDropdown = () => {
|
|
102
|
-
const idx =
|
|
134
|
+
const idx = choiceList.value.indexOf(search.value ?? '')
|
|
103
135
|
dropdown.activeItemIndex = isAsync ? null : idx >= 0 ? idx : null
|
|
104
136
|
dropdown.open = true
|
|
105
|
-
|
|
106
|
-
dropdown.results = isAsync ? [] : options
|
|
137
|
+
dropdown.results = isAsync ? [] : choiceList.value
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
const closeDropdown = (result?: string) => {
|
|
110
141
|
dropdown.activeItemIndex = null
|
|
111
142
|
dropdown.open = false
|
|
112
|
-
if (!
|
|
143
|
+
if (!choiceList.value.includes(result || search.value || '')) {
|
|
113
144
|
search.value = committedValue.value
|
|
114
145
|
}
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
const filterResults = () => {
|
|
118
149
|
if (!search.value) {
|
|
119
|
-
dropdown.results =
|
|
150
|
+
dropdown.results = choiceList.value
|
|
120
151
|
} else {
|
|
121
|
-
dropdown.results =
|
|
152
|
+
dropdown.results = choiceList.value.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
155
|
|
|
@@ -157,65 +188,26 @@ const setCurrentResult = () => {
|
|
|
157
188
|
</script>
|
|
158
189
|
|
|
159
190
|
<style scoped>
|
|
160
|
-
/* variables taken from here: https://github.com/frappe/frappe/blob/version-13/frappe/public/scss/common/awesomeplete.scss */
|
|
161
191
|
.autocomplete {
|
|
162
192
|
position: relative;
|
|
163
193
|
}
|
|
164
194
|
|
|
165
|
-
.input-wrapper {
|
|
166
|
-
border: 1px solid transparent;
|
|
167
|
-
padding: 0rem;
|
|
168
|
-
margin: 0rem;
|
|
169
|
-
margin-right: 1ch;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
input {
|
|
173
|
-
width: calc(100% - 1ch);
|
|
174
|
-
outline: 1px solid transparent;
|
|
175
|
-
border: 1px solid var(--sc-input-border-color);
|
|
176
|
-
padding: 1ch 0.5ch 0.5ch 1ch;
|
|
177
|
-
margin: calc(1.15rem / 2) 0 0 0;
|
|
178
|
-
min-height: 1.15rem;
|
|
179
|
-
border-radius: 0.25rem;
|
|
180
|
-
font-family: var(--sc-font-family);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
input:focus {
|
|
184
|
-
border: 1px solid var(--sc-input-active-border-color);
|
|
185
|
-
border-radius: 0.25rem 0.25rem 0 0;
|
|
186
|
-
border-bottom: none;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
label {
|
|
190
|
-
display: block;
|
|
191
|
-
min-height: 1.15rem;
|
|
192
|
-
padding: 0rem;
|
|
193
|
-
margin: 0rem;
|
|
194
|
-
border: 1px solid transparent;
|
|
195
|
-
margin-bottom: 0.25rem;
|
|
196
|
-
z-index: 0;
|
|
197
|
-
font-size: 80%;
|
|
198
|
-
position: absolute;
|
|
199
|
-
background: white;
|
|
200
|
-
margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
|
|
201
|
-
padding: 0 0.25ch 0 0.25ch;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
195
|
.autocomplete-results {
|
|
205
196
|
position: absolute;
|
|
206
|
-
|
|
197
|
+
left: 0;
|
|
198
|
+
right: 0;
|
|
207
199
|
z-index: 100;
|
|
208
200
|
padding: 0;
|
|
209
201
|
margin: 0;
|
|
210
202
|
color: var(--sc-input-active-border-color);
|
|
211
203
|
border: 1px solid var(--sc-input-active-border-color);
|
|
212
|
-
border-radius: 0
|
|
204
|
+
border-radius: 0;
|
|
213
205
|
border-top: none;
|
|
214
|
-
background-color: #fff;
|
|
206
|
+
background-color: var(--sc-input-field-background, #fff);
|
|
207
|
+
list-style: none;
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
.autocomplete-result {
|
|
218
|
-
list-style: none;
|
|
219
211
|
text-align: left;
|
|
220
212
|
padding: 4px 6px;
|
|
221
213
|
cursor: pointer;
|
|
@@ -227,14 +219,4 @@ label {
|
|
|
227
219
|
background-color: var(--sc-row-color-zebra-light);
|
|
228
220
|
color: var(--sc-input-active-border-color);
|
|
229
221
|
}
|
|
230
|
-
|
|
231
|
-
/* Keep the field error in-flow below the control. The shared .aform_error is absolutely
|
|
232
|
-
positioned against a .aform_form-element anchor, which this component does not use. */
|
|
233
|
-
p.aform_error {
|
|
234
|
-
position: static;
|
|
235
|
-
display: block;
|
|
236
|
-
color: var(--sc-brand-danger, red);
|
|
237
|
-
font-size: 0.7rem;
|
|
238
|
-
margin: 0.25rem 0 0;
|
|
239
|
-
}
|
|
240
222
|
</style>
|
|
@@ -57,7 +57,12 @@ watch(duration, newMs => {
|
|
|
57
57
|
modelValue.value = newMs
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
const handleRange = (data: { start: Date; end: Date }) => {
|
|
60
|
+
const handleRange = (data: { start: Date; end: Date; source?: 'init' | 'user' }) => {
|
|
61
|
+
// Both time widgets announce their starting values as they mount, which ADateSelection turns
|
|
62
|
+
// into a range. That is not a range the user picked: acting on it wrote a 0ms duration into the
|
|
63
|
+
// model, and lit the summary strip, on first render.
|
|
64
|
+
if (data.source === 'init') return
|
|
65
|
+
|
|
61
66
|
startDatetime.value = data.start
|
|
62
67
|
endDatetime.value = data.end
|
|
63
68
|
modelValue.value = duration.value
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type * from '@stonecrop/atable/types'
|
|
2
2
|
export { deserializeFunction } from './utils/deserialize'
|
|
3
|
+
export { badgeInputAccentStyle, resolveFieldBadge } from './utils/badge'
|
|
4
|
+
export type { BadgeFormatContext, BadgeFormatFn } from './utils/badge'
|
|
3
5
|
import { install as installATable } from '@stonecrop/atable'
|
|
4
6
|
import type { App } from 'vue'
|
|
5
7
|
|
|
@@ -7,8 +9,10 @@ import ACheckbox from './components/form/ACheckbox.vue'
|
|
|
7
9
|
import ACurrencyInput from './components/form/ACurrencyInput.vue'
|
|
8
10
|
import ADate from './components/form/ADate.vue'
|
|
9
11
|
import ADropdown from './components/form/ADropdown.vue'
|
|
12
|
+
import ABadge from './components/form/ABadge.vue'
|
|
10
13
|
import ADatePicker from './components/form/ADatePicker.vue'
|
|
11
14
|
import ADateTime from './components/form/ADateTime.vue'
|
|
15
|
+
import ADateTimeInput from './components/form/ADateTimeInput.vue'
|
|
12
16
|
import ADateSelection from './components/form/ADateSelection.vue'
|
|
13
17
|
import ADuration from './components/form/ADuration.vue'
|
|
14
18
|
import ADateRange from './components/form/ADateRange.vue'
|
|
@@ -37,8 +41,10 @@ function install(app: App /* options */) {
|
|
|
37
41
|
app.component('ACurrencyInput', ACurrencyInput)
|
|
38
42
|
app.component('ADate', ADate)
|
|
39
43
|
app.component('ADropdown', ADropdown)
|
|
44
|
+
app.component('ABadge', ABadge)
|
|
40
45
|
app.component('ADatePicker', ADatePicker)
|
|
41
46
|
app.component('ADateTime', ADateTime)
|
|
47
|
+
app.component('ADateTimeInput', ADateTimeInput)
|
|
42
48
|
app.component('ADateRange', ADateRange)
|
|
43
49
|
app.component('ADateSelection', ADateSelection)
|
|
44
50
|
app.component('AFieldset', AFieldset)
|
|
@@ -58,11 +64,13 @@ export {
|
|
|
58
64
|
ACurrencyInput,
|
|
59
65
|
ADate,
|
|
60
66
|
ADropdown,
|
|
67
|
+
ABadge,
|
|
61
68
|
ADatePicker,
|
|
62
69
|
ADateRange,
|
|
63
70
|
ADateSelection,
|
|
64
71
|
ADuration,
|
|
65
72
|
ADateTime,
|
|
73
|
+
ADateTimeInput,
|
|
66
74
|
AFieldset,
|
|
67
75
|
AFileAttach,
|
|
68
76
|
AForm,
|