PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
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 / useFieldWidth.ts

useFieldWidth.ts in The Innovative Form Builder – IvyForms 1.0, at frontend/src/composables/useFieldWidth.ts

194 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import type { Field } from '@/types/field'
2
3 /**
4 * Composable for calculating field widths in grid layout
5 *
6 * Supports:
7 * - Auto width based on fields per row (1=100%, 2=50%, 3=33%, 4=25%, 5=20%)
8 * - Custom widths from manual resize
9 * - Maximum 5 fields per row
10 */
11 export function useFieldWidth() {
12 /**
13 * Calculate width percentage based on number of fields in row
14 * Returns values with one decimal place for precision
15 * Maximum fields per row is 5 (minimum 20% each)
16 * CSS will handle the gap spacing separately
17 */
18 const getAutoWidth = (fieldsInRow: number): number => {
19 if (fieldsInRow === 1) return 100
20 if (fieldsInRow >= 5) return 20 // Max 5 fields = 20% each
21
22 // Divide 100% evenly and round to 1 decimal place
23 return Math.round((100 / fieldsInRow) * 10) / 10
24 }
25
26 /**
27 * Group fields by row index
28 */
29 const groupFieldsByRow = (fields: Field[]): Map<number, Field[]> => {
30 const rows = new Map<number, Field[]>()
31
32 fields.forEach((field) => {
33 const rowIndex = field.rowIndex ?? 0
34 if (!rows.has(rowIndex)) {
35 rows.set(rowIndex, [])
36 }
37 rows.get(rowIndex)!.push(field)
38 })
39
40 // Sort fields within each row by columnIndex
41 rows.forEach((rowFields) => {
42 rowFields.sort((a, b) => (a.columnIndex ?? 0) - (b.columnIndex ?? 0))
43 })
44
45 return rows
46 }
47
48 /**
49 * Calculate width for a field
50 * Returns custom width if set, otherwise calculates based on fields in row
51 */
52 const getFieldWidth = (field: Field, fieldsInRow: Field[]): number => {
53 // If custom width is set, use it
54 if (field.width && field.width >= 20 && field.width <= 100) {
55 return field.width
56 }
57
58 // Otherwise calculate based on number of fields in row
59 return getAutoWidth(fieldsInRow.length)
60 }
61
62 /**
63 * Get CSS width style for a field
64 * Uses calc() to account for gap between fields. Gap can be any valid CSS
65 * length value (px/rem/em/etc.) so spacing controls keep their units.
66 *
67 * @param field - current field
68 * @param fieldsInRow - all fields present in the same row
69 * @param gap - horizontal gap CSS value (defaults to 12px)
70 */
71 const getFieldWidthStyle = (field: Field, fieldsInRow: Field[], gap: string = '12px'): string => {
72 const width = getFieldWidth(field, fieldsInRow)
73 const numFields = fieldsInRow.length
74
75 // For single field, use full width
76 if (numFields === 1) {
77 return '100%'
78 }
79
80 // Calculate width accounting for gaps with preserved CSS units.
81 // Formula: calc(width% - (gap * ((numFields - 1) / numFields))).
82 const gapFactor = (numFields - 1) / numFields
83 return `calc(${width}% - (${gap} * ${gapFactor}))`
84 }
85
86 /**
87 * Check if row can accept more fields (max 5 per row)
88 */
89 const canAddToRow = (fieldsInRow: Field[]): boolean => {
90 return fieldsInRow.length < 5
91 }
92
93 /**
94 * Get next available column index in row
95 */
96 const getNextColumnIndex = (fieldsInRow: Field[]): number => {
97 if (fieldsInRow.length === 0) return 0
98
99 // Find the maximum columnIndex and add 1
100 const maxColumn = Math.max(...fieldsInRow.map((f) => f.columnIndex ?? 0))
101 return maxColumn + 1
102 }
103
104 /**
105 * Distribute widths intelligently when adding a new field to a row
106 * - New field gets equal share (1/n where n is total fields after adding)
107 * - Existing fields are proportionally scaled to fit remaining space
108 *
109 * Example: Row has 2 fields at 40% and 60%, adding 3rd field:
110 * - New field gets 33.33% (1/3)
111 * - Remaining space: 100% - 33.33% = 66.67%
112 * - Existing fields scaled proportionally:
113 * - Field 1: 40/(40+60) * 66.67% = 26.67%
114 * - Field 2: 60/(40+60) * 66.67% = 40%
115 */
116 const distributeWidthsEvenly = (fields: Field[]): Field[] => {
117 if (fields.length === 0) return []
118 if (fields.length === 1) return fields.map((f) => ({ ...f, width: 100 }))
119
120 // For rows with gaps (12px each), we need to account for gap space
121 // Each field should take up width that, when combined with gaps, totals 100%
122 // Formula: width% = 100 / fieldCount
123 // The calc() in CSS will handle subtracting the gap portion
124 const width = Math.round((100 / fields.length) * 10) / 10
125
126 return fields.map((field) => ({
127 ...field,
128 width: width,
129 }))
130 }
131
132 /**
133 * Distribute widths proportionally when adding a new field to existing resized fields
134 * Maintains the relative proportions of existing fields
135 */
136 const distributeWidthsProportionally = (
137 existingFields: Field[],
138 newFieldCount: number = 1,
139 ): Field[] => {
140 if (existingFields.length === 0) return []
141
142 const totalFieldsAfter = existingFields.length + newFieldCount
143 const newFieldWidth = getAutoWidth(totalFieldsAfter)
144 const remainingSpace = 100 - newFieldWidth * newFieldCount
145
146 // Calculate total width of existing fields
147 const existingTotalWidth = existingFields.reduce((sum, f) => sum + (f.width ?? 100), 0)
148
149 // Scale existing fields proportionally to fit remaining space
150 return existingFields.map((field) => {
151 const currentWidth = field.width ?? 100
152 const proportion =
153 existingTotalWidth > 0 ? currentWidth / existingTotalWidth : 1 / existingFields.length
154 const newWidth = remainingSpace * proportion
155
156 return {
157 ...field,
158 width: Math.max(10, Math.min(100, newWidth)), // Clamp between 10% and 100%
159 }
160 })
161 }
162
163 /**
164 * Validate and normalize widths in a row
165 * Ensures total doesn't exceed 100% and all fields have valid widths
166 */
167 const normalizeRowWidths = (fields: Field[]): Field[] => {
168 const totalWidth = fields.reduce((sum, f) => sum + (f.width ?? 100), 0)
169
170 // If total exceeds 100%, distribute evenly
171 if (totalWidth > 100) {
172 return distributeWidthsEvenly(fields)
173 }
174
175 // Ensure all fields have width set
176 return fields.map((field) => ({
177 ...field,
178 width: field.width ?? getAutoWidth(fields.length),
179 }))
180 }
181
182 return {
183 getAutoWidth,
184 groupFieldsByRow,
185 getFieldWidth,
186 getFieldWidthStyle,
187 canAddToRow,
188 getNextColumnIndex,
189 distributeWidthsEvenly,
190 distributeWidthsProportionally,
191 normalizeRowWidths,
192 }
193 }
194