PluginProbe
The Innovative Form Builder – IvyForms / 0.9
The Innovative Form Builder – IvyForms v0.9
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 / composables / useTimeField.ts

useTimeField.ts in The Innovative Form Builder – IvyForms 0.9, at frontend/src/composables/useTimeField.ts

223 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Composable function for time field logic
3 */
4 export function useTimeField() {
5 function parseTimeString(str: string) {
6 if (!str) return { hours: '', minutes: '', amPm: '' }
7 const fullMatch = str.match(/^(\d{2}):?(\d{2})?(?:\s*(AM|PM))?$/)
8 const minutesAmPmMatch = str.match(/^:(\d{2})\s*(AM|PM)$/)
9 const hoursMatch = str.match(/^\d{2}$/)
10 const minutesMatch = str.match(/^:(\d{2})$/)
11 const ampmMatch = str.match(/^(AM|PM)$/)
12 if (fullMatch) {
13 return {
14 hours: fullMatch[1] || '',
15 minutes: fullMatch[2] || '',
16 amPm: fullMatch[3] || '',
17 }
18 }
19 if (minutesAmPmMatch) {
20 return {
21 hours: '',
22 minutes: minutesAmPmMatch[1],
23 amPm: minutesAmPmMatch[2],
24 }
25 }
26 if (hoursMatch) {
27 return {
28 hours: str,
29 minutes: '',
30 amPm: '',
31 }
32 }
33 if (minutesMatch) {
34 return {
35 hours: '',
36 minutes: minutesMatch[1],
37 amPm: '',
38 }
39 }
40 if (ampmMatch) {
41 return {
42 hours: '',
43 minutes: '',
44 amPm: ampmMatch[1],
45 }
46 }
47 return { hours: '', minutes: '', amPm: '' }
48 }
49
50 /**
51 * Parse time string to total minutes since midnight
52 * Handles both 24h and 12h (AM/PM) formats
53 * Returns null if time string is malformed
54 *
55 * @param timeStr - Time string in format "HH:MM" or "HH:MM AM/PM"
56 * @returns Total minutes since midnight, or null if parsing fails
57 */
58 function parseTimeToMinutes(timeStr: string): number | null {
59 if (!timeStr || typeof timeStr !== 'string') {
60 return null
61 }
62
63 let hours = 0
64 let minutes = 0
65
66 // Handle AM/PM format
67 const ampmMatch = timeStr.trim().match(/^(\d{1,2}):(\d{2})\s?(AM|PM|am|pm)$/i)
68 if (ampmMatch) {
69 hours = parseInt(ampmMatch[1], 10)
70 minutes = parseInt(ampmMatch[2], 10)
71 const meridiem = ampmMatch[3].toUpperCase()
72
73 // Validate hour range for 12h format
74 if (hours < 1 || hours > 12) {
75 return null
76 }
77
78 // Convert to 24h format
79 if (meridiem === 'PM' && hours !== 12) {
80 hours += 12
81 } else if (meridiem === 'AM' && hours === 12) {
82 hours = 0
83 }
84 } else {
85 // Handle 24h format
86 const match24h = timeStr.trim().match(/^(\d{1,2}):(\d{2})$/)
87 if (match24h) {
88 hours = parseInt(match24h[1], 10)
89 minutes = parseInt(match24h[2], 10)
90 } else {
91 return null // Malformed time string
92 }
93 }
94
95 // Validate hour and minute ranges
96 if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
97 return null
98 }
99
100 return hours * 60 + minutes
101 }
102
103 /**
104 * Validate time format based on time format and field type
105 * Returns true if time matches the expected format
106 */
107 function isValidTimeFormat(timeStr: string, timeFormat: string, timeFieldType?: string): boolean {
108 if (!timeStr || typeof timeStr !== 'string') {
109 return false
110 }
111
112 const trimmed = timeStr.trim()
113
114 // Dropdown mode already ensures format, so only validate non-dropdown
115 if (timeFieldType === 'dropdown') {
116 if (timeFormat === 'ampm') {
117 return /^(0?[1-9]|1[0-2]):[0-5]\d ?([AP]M|[ap]m)$/.test(trimmed)
118 } else {
119 return /^(?:[01]?\d|2[0-3]):[0-5]\d$/.test(trimmed)
120 }
121 }
122
123 // Time-picker mode
124 if (timeFieldType === 'time-picker') {
125 return /^(?:[01]\d|2[0-3]):[0-5]\d(?:\s?(?:AM|PM|am|pm))?$/.test(trimmed)
126 }
127
128 // Input mode
129 if (timeFormat === 'ampm') {
130 return /^(0?[1-9]|1[0-2]):[0-5]\d (AM|PM|am|pm)$/.test(trimmed)
131 } else {
132 return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(trimmed)
133 }
134 }
135 function isTimeInValidRange(
136 timeStr: string,
137 minTimeValue?: string | null,
138 maxTimeValue?: string | null,
139 ): { isInRange: boolean; error?: string } {
140 // Empty or partial values are allowed
141 if (!timeStr || timeStr === '__PARTIAL__') {
142 return { isInRange: true }
143 }
144
145 const currentTimeMinutes = parseTimeToMinutes(timeStr)
146 if (currentTimeMinutes === null) {
147 return { isInRange: false, error: 'Invalid time format' }
148 }
149
150 const hasMin = minTimeValue !== null && minTimeValue !== undefined && minTimeValue !== ''
151 const hasMax = maxTimeValue !== null && maxTimeValue !== undefined && maxTimeValue !== ''
152
153 if (!hasMin && !hasMax) {
154 return { isInRange: true }
155 }
156
157 // Parse min time
158 let minTimeMinutes: number | null = null
159 if (hasMin) {
160 minTimeMinutes = parseTimeToMinutes(minTimeValue!)
161 if (minTimeMinutes === null) {
162 return { isInRange: false, error: `Malformed minimum time value: ${minTimeValue}` }
163 }
164 }
165
166 // Parse max time
167 let maxTimeMinutes: number | null = null
168 if (hasMax) {
169 maxTimeMinutes = parseTimeToMinutes(maxTimeValue!)
170 if (maxTimeMinutes === null) {
171 return { isInRange: false, error: `Malformed maximum time value: ${maxTimeValue}` }
172 }
173 }
174
175 // Check range
176 if (minTimeMinutes !== null && currentTimeMinutes < minTimeMinutes) {
177 return { isInRange: false, error: `Time must be at or after ${minTimeValue}` }
178 }
179
180 if (maxTimeMinutes !== null && currentTimeMinutes > maxTimeMinutes) {
181 return { isInRange: false, error: `Time must be at or before ${maxTimeValue}` }
182 }
183
184 return { isInRange: true }
185 }
186
187 function formatTimeString(hours: string, minutes: string, amPm: string) {
188 if (!hours && !minutes && amPm) return amPm
189 if (!hours && minutes && amPm) return `:${minutes} ${amPm}`
190 if (hours && minutes) return amPm ? `${hours}:${minutes} ${amPm}` : `${hours}:${minutes}`
191 if (hours) return hours
192 if (minutes) return `:${minutes}`
193 return ''
194 }
195
196 function getAmPmFromHours(hours: string): 'AM' | 'PM' {
197 const h = parseInt(hours, 10)
198 return h >= 12 ? 'PM' : 'AM'
199 }
200
201 function to24hFormat(time: string): string {
202 if (!time) return ''
203 const match = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i)
204 if (!match) return time
205 let hours = parseInt(match[1], 10)
206 const minutes = match[2]
207 const ampm = match[3]?.toUpperCase()
208 if (ampm === 'PM' && hours < 12) hours += 12
209 if (ampm === 'AM' && hours === 12) hours = 0
210 return `${hours.toString().padStart(2, '0')}:${minutes}`
211 }
212
213 return {
214 parseTimeString,
215 parseTimeToMinutes,
216 isValidTimeFormat,
217 isTimeInValidRange,
218 formatTimeString,
219 getAmPmFromHours,
220 to24hFormat,
221 }
222 }
223