PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / public / fields / TimeFormField.vue

TimeFormField.vue in The Innovative Form Builder – IvyForms 0.8, at frontend/src/views/public/fields/TimeFormField.vue

427 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div
3 :class="[
4 'ivyforms-field',
5 'ivyforms-field__time',
6 'ivyforms-field__time_' + field.id,
7 field.cssClasses,
8 {
9 'ivyforms-field__time--is-readonly': !!field.readOnly,
10 },
11 ]"
12 >
13 <IvyFormItem
14 v-if="field.timeFieldType === 'time-picker'"
15 :label="field.hideLabel ? '' : field.label"
16 :required="field.required"
17 :error="error"
18 :prop="field.type + '_' + field.fieldIndex"
19 :show-info="!!field.description"
20 :info-text="field.description || ''"
21 :show-info-icon="false"
22 :label-position="field.labelPosition"
23 >
24 <!-- time-picker mode -->
25 <IvyTimePicker
26 :id="fieldID"
27 v-model="localModelValue"
28 :aria-label="field.label"
29 class="ivyforms-field__time-picker"
30 :time-format="field.timeFormat"
31 :placeholder="field.placeholder"
32 :disabled="disabled || field.readOnly"
33 :readonly="field.readOnly"
34 :clearable="true"
35 />
36 </IvyFormItem>
37
38 <!-- free text input mode -->
39 <IvyFormItem
40 v-if="field.timeFieldType === 'input'"
41 :label="field.hideLabel ? '' : field.label"
42 :required="field.required"
43 :error="error"
44 :prop="field.type + '_' + field.fieldIndex"
45 :show-info="!!field.description"
46 :info-text="field.description || ''"
47 :show-info-icon="false"
48 :label-position="field.labelPosition"
49 >
50 <div class="ivyforms-flex ivyforms-gap-12 ivyforms-width-100">
51 <div :class="['ivyforms-flex', 'ivyforms-flex-1', 'ivyforms-flex-direction-column']">
52 <IvyTextInput
53 :id="fieldID"
54 v-model="localModelValue"
55 :aria-label="field.label"
56 class="ivyforms-field__time-input"
57 :class="{ 'has-error': error && !localModelValue }"
58 type="text"
59 :placeholder="field.placeholder"
60 :disabled="disabled || field.readOnly"
61 :readonly="field.readOnly"
62 :required="field.required"
63 />
64 </div>
65 <div
66 v-if="field.timeFormat === 'ampm'"
67 class="ivyforms-flex ivyforms-flex-1 ivyforms-flex-direction-column"
68 >
69 <IvySelectInput
70 id="meridiem-input-select"
71 v-model="selectedInputMeridiem"
72 aria-label="getLabel('am_pm')"
73 :error="!!(error && !selectedInputMeridiem)"
74 :placeholder="getLabel('am_pm')"
75 :disabled="disabled || field.readOnly"
76 field-type="time"
77 @update:model-value="onInputMeridiemChange"
78 >
79 <IvySelectOption value="" :label="getLabel('not_selected')" field-type="time">
80 {{ getLabel('not_selected') }}
81 </IvySelectOption>
82 <IvySelectOption
83 v-for="meridiem in meridiemOptions"
84 :key="meridiem.value"
85 :label="meridiem.label"
86 :value="meridiem.value"
87 field-type="time"
88 >
89 {{ meridiem.label }}
90 </IvySelectOption>
91 </IvySelectInput>
92 </div>
93 </div>
94 </IvyFormItem>
95 <IvyFormItem
96 v-if="field.timeFieldType === 'dropdown'"
97 :label="field.label"
98 :required="field.required"
99 :error="error"
100 :prop="field.type + '_' + field.fieldIndex"
101 :show-info="!!field.description"
102 :info-text="field.description || ''"
103 :show-info-icon="false"
104 :label-position="field.labelPosition"
105 :hidden-label="field.hideLabel"
106 >
107 <!-- dropdown mode -->
108 <div class="ivyforms-flex ivyforms-gap-12 ivyforms-width-100">
109 <div class="ivyforms-flex ivyforms-flex-1 ivyforms-flex-direction-column">
110 <label for="hours-select" class="medium-14 ivyforms-mb-6 ivyforms-field__time__sublabel">
111 {{ getLabel('hours') }}
112 </label>
113 <IvySelectInput
114 id="hours-select"
115 v-model="selectedHour"
116 class="ivyforms-field__time-hour"
117 :error="!!(error && !selectedHour)"
118 placeholder="&#45;&#45;"
119 :disabled="disabled || field.readOnly"
120 field-type="time"
121 @update:model-value="onPartChange"
122 >
123 <IvySelectOption
124 v-for="h in hourOptions"
125 :key="h.value"
126 :label="h.label"
127 :value="h.value"
128 field-type="time"
129 />
130 </IvySelectInput>
131 </div>
132
133 <div class="ivyforms-flex ivyforms-flex-1 ivyforms-flex-direction-column">
134 <label
135 for="minutes-select"
136 class="medium-14 ivyforms-mb-6 ivyforms-field__time__sublabel"
137 >
138 {{ getLabel('minutes') }}
139 </label>
140 <IvySelectInput
141 id="minutes-select"
142 v-model="selectedMinute"
143 class="ivyforms-field__time-minute"
144 :error="!!(error && !selectedMinute)"
145 placeholder="&#45;&#45;"
146 :disabled="disabled || field.readOnly"
147 field-type="time"
148 @update:model-value="onPartChange"
149 >
150 <IvySelectOption
151 v-for="m in minuteOptions"
152 :key="m.value"
153 :label="m.label"
154 :value="m.value"
155 field-type="time"
156 />
157 </IvySelectInput>
158 </div>
159
160 <div
161 v-if="field.timeFormat === 'ampm'"
162 class="ivyforms-flex ivyforms-flex-1 ivyforms-flex-direction-column ivyforms-mt-26"
163 >
164 <IvySelectInput
165 id="meridiem-select"
166 v-model="selectedDropdownMeridiem"
167 :error="!!(error && !selectedDropdownMeridiem)"
168 aria-label="getLabel('am_pm')"
169 :placeholder="getLabel('am_pm')"
170 :disabled="disabled || field.readOnly"
171 field-type="time"
172 @update:model-value="onDropdownMeridiemChange"
173 >
174 <IvySelectOption value="" :label="getLabel('not_selected')" field-type="time">
175 {{ getLabel('not_selected') }}
176 </IvySelectOption>
177 <IvySelectOption
178 v-for="meridiem in meridiemOptions"
179 :key="meridiem.value"
180 :label="meridiem.label"
181 :value="meridiem.value"
182 field-type="time"
183 >
184 {{ meridiem.label }}
185 </IvySelectOption>
186 </IvySelectInput>
187 </div>
188 </div>
189 </IvyFormItem>
190 </div>
191 </template>
192
193 <script setup lang="ts">
194 import { computed, inject, ref, watch } from 'vue'
195 import { useLabels } from '@/composables/useLabels'
196 import { useTimeField } from '@/composables/useTimeField'
197 import type { Field } from '@/types/field'
198 import IvyFormItem from '@/views/_components/form/IvyFormItem.vue'
199
200 const { getLabel } = useLabels()
201
202 interface FieldProps {
203 modelValue: string | null | undefined
204 field: Field
205 disabled?: boolean
206 error?: string
207 }
208
209 const props = withDefaults(defineProps<FieldProps>(), {
210 modelValue: '',
211 disabled: false,
212 error: '',
213 })
214
215 const emit = defineEmits(['update:modelValue'])
216 const clearFieldError = inject<(fieldKey: string) => void>('clearFieldError', () => {})
217 const fieldKey = computed(() => props.field.type + '_' + props.field.fieldIndex)
218 const { parseTimeString } = useTimeField()
219 const pad2 = (number: number): string => (number < 10 ? '0' + number : '' + number)
220 const parsedDefaultValue = computed(() => parseTimeString(props.field.defaultValue || ''))
221 const selectedHour = ref<string>(parsedDefaultValue.value.hours || '')
222 const selectedMinute = ref<string>(parsedDefaultValue.value.minutes || '')
223 const handleInput = (value: string | null | undefined, shouldClearError: boolean = true) => {
224 emit('update:modelValue', value ?? '')
225 if (shouldClearError) {
226 clearFieldError(fieldKey.value)
227 }
228 }
229 const selectedDropdownMeridiem = ref<'AM' | 'PM' | ''>(
230 props.field.timeFieldType === 'dropdown'
231 ? (parsedDefaultValue.value.amPm as 'AM' | 'PM' | '')
232 : '',
233 )
234 const selectedInputMeridiem = ref<'AM' | 'PM' | ''>(
235 props.field.timeFieldType === 'input' ? (parsedDefaultValue.value.amPm as 'AM' | 'PM' | '') : '',
236 )
237 const inputTime12h = ref<string>('')
238 const meridiemOptions = computed(() => [
239 { label: getLabel('am'), value: 'AM' },
240 { label: getLabel('pm'), value: 'PM' },
241 ])
242 const localModelValue = computed({
243 get() {
244 if (props.field.timeFieldType === 'input' && props.field.timeFormat === 'ampm') {
245 return inputTime12h.value
246 }
247 if (
248 props.field.timeFieldType === 'time-picker' &&
249 props.field.timeFormat === 'ampm' &&
250 (!props.modelValue || props.modelValue === '')
251 ) {
252 const parsed = parseTimeString(props.field.defaultValue || '')
253 if (parsed.hours && parsed.minutes && parsed.amPm) {
254 return `${pad2(Number(parsed.hours))}:${pad2(Number(parsed.minutes))} ${parsed.amPm}`
255 }
256 }
257 return props.modelValue != null ? String(props.modelValue) : ''
258 },
259 set(timeValue: string) {
260 if (props.field.timeFieldType === 'input' && props.field.timeFormat === 'ampm') {
261 inputTime12h.value = timeValue
262 if (!timeValue || !selectedInputMeridiem.value) {
263 handleInput('', true)
264 return
265 }
266 const timeMatch = timeValue.match(/^(\d{1,2}):(\d{2})$/)
267 if (timeMatch && selectedInputMeridiem.value) {
268 const hour12 = timeMatch[1]
269 const minutes = timeMatch[2]
270 const meridiem = selectedInputMeridiem.value
271 const timeString = `${hour12}:${minutes} ${meridiem}`
272 handleInput(timeString, true)
273 } else {
274 handleInput('__PARTIAL__', false)
275 }
276 } else if (props.field.timeFieldType === 'time-picker') {
277 if (timeValue) {
278 handleInput(timeValue, true)
279 } else {
280 handleInput('', true)
281 }
282 } else {
283 handleInput(timeValue)
284 }
285 },
286 })
287
288 function onInputMeridiemChange(value: 'AM' | 'PM' | '') {
289 selectedInputMeridiem.value = value
290 if (props.field.timeFieldType === 'input' && inputTime12h.value) {
291 const timeMatch = inputTime12h.value.match(/^(\d{1,2}):(\d{2})$/)
292 if (timeMatch && value) {
293 const hour12 = timeMatch[1]
294 const minutes = timeMatch[2]
295 const timeString = `${hour12}:${minutes} ${value}`
296 handleInput(timeString, true)
297 } else {
298 handleInput('__PARTIAL__', false)
299 }
300 }
301 }
302 function onDropdownMeridiemChange(value: 'AM' | 'PM' | '') {
303 selectedDropdownMeridiem.value = value
304 emitFromParts()
305 }
306 const hourOptions = computed(() => {
307 if (props.field.timeFormat === 'ampm') {
308 return Array.from({ length: 12 }, (_, i) => {
309 const value = pad2(i + 1)
310 return { value, label: value }
311 })
312 } else {
313 return Array.from({ length: 24 }, (_, i) => {
314 const value = pad2(i)
315 return { value, label: value }
316 })
317 }
318 })
319 const minuteOptions = computed(() =>
320 Array.from({ length: 60 }, (_, i) => {
321 const value = pad2(i)
322 return { value, label: value }
323 }),
324 )
325 function emitFromParts() {
326 const hasAnyValue = !!(
327 selectedHour.value ||
328 selectedMinute.value ||
329 (props.field.timeFormat === 'ampm' && selectedDropdownMeridiem.value)
330 )
331 const hasAllValues =
332 selectedHour.value &&
333 selectedMinute.value &&
334 (props.field.timeFormat === '24h' || selectedDropdownMeridiem.value)
335 if (!hasAnyValue) {
336 handleInput('', true)
337 return
338 }
339 if (!hasAllValues) {
340 handleInput('__PARTIAL__', false)
341 return
342 }
343 if (props.field.timeFormat === 'ampm') {
344 const hour12 = parseInt(selectedHour.value, 10)
345 const minute = selectedMinute.value
346 const meridiem = selectedDropdownMeridiem.value
347 const timeValue = pad2(hour12) + ':' + minute + ' ' + meridiem
348 handleInput(timeValue, true)
349 return
350 }
351 const timeValue = selectedHour.value + ':' + selectedMinute.value
352 handleInput(timeValue, true)
353 }
354 function onPartChange() {
355 emitFromParts()
356 }
357 watch(
358 () => [props.modelValue, props.field.timeFormat],
359 () => {
360 const val = props.modelValue != null ? String(props.modelValue) : ''
361 if (props.field.timeFieldType === 'input' && props.field.timeFormat === 'ampm') {
362 if (val && val !== '__PARTIAL__') {
363 const match = val.match(/^(.*?)(?:\s*(AM|PM|am|pm))?$/)
364 inputTime12h.value = match ? match[1].trim() : ''
365 selectedInputMeridiem.value =
366 match && match[2] ? (match[2].toUpperCase() as 'AM' | 'PM') : ''
367 } else if (!val || val.trim() === '') {
368 inputTime12h.value = ''
369 selectedInputMeridiem.value = ''
370 }
371 } else if (props.field.timeFieldType === 'time-picker') {
372 const ampmRegex = /^(?:0?[1-9]|1[0-2]):[0-5]\d\s?(AM|PM|am|pm)$/
373 const twentyFourRegex = /^(?:[01]\d|2[0-3]):[0-5]\d$/
374 if (
375 val &&
376 val !== '__PARTIAL__' &&
377 !twentyFourRegex.test(val) &&
378 !(props.field.timeFormat === 'ampm' && ampmRegex.test(val))
379 ) {
380 handleInput('', true)
381 }
382 } else if (props.field.timeFieldType === 'dropdown') {
383 if (val !== '__PARTIAL__') {
384 const parsed = parseTimeString(val)
385 selectedHour.value = parsed.hours || ''
386 selectedMinute.value = parsed.minutes || ''
387 selectedDropdownMeridiem.value = parsed.amPm as 'AM' | 'PM' | ''
388 emitFromParts()
389 }
390 }
391 },
392 { immediate: true },
393 )
394 const fieldID = computed(() => {
395 return `ivyforms-field__time-input_${props.field.formId || ''}_${props.field.fieldIndex || ''}`
396 })
397 </script>
398
399 <style lang="scss">
400 .ivyforms-field__time {
401 &__asterisk {
402 color: var(--map-status-error-fill-0);
403 }
404
405 &--is-readonly {
406 opacity: 0.7;
407 .el-select__wrapper,
408 .el-select__wrapper:hover,
409 .el-select__wrapper.is-focus,
410 .el-input__wrapper,
411 .el-input__wrapper:hover,
412 .el-input__wrapper.is-focus {
413 border-color: var(--map-base-dusk-stroke--2) !important;
414 box-shadow: none !important;
415 border: 1px solid var(--map-base-dusk-stroke--2) !important;
416 }
417 .el-select:has(> .is-disabled),
418 input[disabled]] {
419 cursor: not-allowed;
420 }
421 }
422 &__sublabel {
423 color: var(--map-base-text-0);
424 }
425 }
426 </style>
427