PluginProbe
The Innovative Form Builder – IvyForms / 1.1.1
The Innovative Form Builder – IvyForms v1.1.1
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 / utils / styleUtils.ts

styleUtils.ts in The Innovative Form Builder – IvyForms 1.1.1, at frontend/src/utils/styleUtils.ts

564 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import type { Ref } from 'vue'
2 import { watch } from 'vue'
3 import type { FormStyleSettings } from '@/types/form-style'
4
5 /**
6 * Represents a CSS dimension value split into numeric value and unit
7 */
8 export interface DimensionValue {
9 value: string
10 unit: string
11 }
12
13 /**
14 * Represents corner radius settings with linked/unlinked state
15 */
16 export interface CornerSettings {
17 isLinked: boolean
18 value: string
19 topLeft: string
20 topRight: string
21 bottomRight: string
22 bottomLeft: string
23 }
24
25 /**
26 * Form builder store interface for type safety
27 */
28 export interface FormBuilderStore {
29 styleSettings: FormStyleSettings
30 updateStyleSettings: (path: string, value: unknown) => void
31 }
32
33 export type Alignment = 'left' | 'center' | 'right'
34 export type BorderStyle = 'solid' | 'dashed' | 'dotted'
35 export type FontStyle = 'normal' | 'italic'
36
37 const BORDER_STYLE_VALUES: BorderStyle[] = ['solid', 'dashed', 'dotted']
38 const FONT_STYLE_VALUES: FontStyle[] = ['normal', 'italic']
39
40 export const BORDER_STYLE_OPTIONS: Array<{ value: BorderStyle; label: string }> = [
41 { value: 'solid', label: 'Solid' },
42 { value: 'dashed', label: 'Dash' },
43 { value: 'dotted', label: 'Dotted' },
44 ]
45
46 export const FONT_STYLE_OPTIONS: Array<{ value: FontStyle; label: string }> = [
47 { value: 'normal', label: 'Normal' },
48 { value: 'italic', label: 'Italic' },
49 ]
50
51 export function normalizeAlignment(value?: string): Alignment {
52 if (value === 'center' || value === 'right') {
53 return value as Alignment
54 }
55 return 'left'
56 }
57
58 export function resolveBorderStyle(
59 borderStyle: unknown,
60 fallbackValue: BorderStyle = 'solid',
61 ): BorderStyle {
62 if (typeof borderStyle === 'string' && BORDER_STYLE_VALUES.includes(borderStyle as BorderStyle)) {
63 return borderStyle as BorderStyle
64 }
65
66 return fallbackValue
67 }
68
69 export function resolveFontStyle(
70 fontStyle: unknown,
71 fallbackValue: FontStyle = 'normal',
72 ): FontStyle {
73 if (typeof fontStyle === 'string' && FONT_STYLE_VALUES.includes(fontStyle as FontStyle)) {
74 return fontStyle as FontStyle
75 }
76
77 return fallbackValue
78 }
79
80 export function getBorderStyleIcon(borderStyle: BorderStyle): string {
81 if (borderStyle === 'dashed') {
82 return 'border-dash'
83 }
84 if (borderStyle === 'dotted') {
85 return 'border-dotted'
86 }
87
88 return 'border-solid'
89 }
90
91 /**
92 * Configuration for creating a style property watcher
93 */
94 export interface StyleWatcherConfig {
95 ref: Ref<unknown>
96 storePath: string
97 syncPaths?: string[]
98 formBuilderStore: FormBuilderStore
99 isSyncingFromStore: Ref<boolean>
100 }
101
102 /**
103 * Parse a CSS dimension string (e.g., "12px", "1.5rem") into value and unit
104 * @param value - CSS dimension string
105 * @returns Object with separated value and unit
106 */
107 export function parseDimension(value: string): DimensionValue {
108 const match = value?.match(/^([0-9.]+)(.*)$/)
109 if (match) {
110 return { value: match[1], unit: match[2] || 'px' }
111 }
112 return { value: '', unit: 'px' }
113 }
114
115 /**
116 * Parse dimension and return numeric value with unit.
117 * @param value - CSS dimension string
118 * @param fallbackValue - Numeric fallback when parsed value is empty
119 */
120 export function parseDimensionToNumeric(
121 value: string,
122 fallbackValue: number | null = null,
123 ): { value: number | null; unit: string } {
124 const parsedDimension = parseDimension(value)
125
126 return {
127 value: parsedDimension.value !== '' ? Number(parsedDimension.value) : fallbackValue,
128 unit: parsedDimension.unit,
129 }
130 }
131
132 /**
133 * Resolve a style string value from store or fallback to the provided default.
134 * Useful inside syncWithStore to avoid repeated `(storeVal || defaultVal) as string` casts.
135 * @param storeValue - Value from the store (may be undefined)
136 * @param defaultValue - Default value to use when storeValue is falsy
137 */
138 export function resolveSyncStringValue(storeValue?: string, defaultValue?: string): string {
139 return storeValue || defaultValue || ''
140 }
141
142 /**
143 * Parse a dimension stored in the store, falling back to the default value.
144 * Combines resolveSyncStringValue and parseDimensionToNumeric.
145 * @param storeValue - CSS dimension string from store (may be undefined)
146 * @param defaultValue - CSS dimension string from defaults (may be undefined)
147 * @param fallbackValue - Numeric fallback when the resolved string cannot be parsed
148 */
149 export function parseSyncNumericDimension(
150 storeValue?: string,
151 defaultValue?: string,
152 fallbackValue: number | null = null,
153 ): { value: number | null; unit: string } {
154 return parseDimensionToNumeric(resolveSyncStringValue(storeValue, defaultValue), fallbackValue)
155 }
156
157 /**
158 * Parse a dimension stored in the store, falling back to the default value.
159 * Returns a raw { value, unit } string pair (not numeric).
160 * @param storeValue - CSS dimension string from store (may be undefined)
161 * @param defaultValue - CSS dimension string from defaults (may be undefined)
162 */
163 export function parseSyncDimensionValue(
164 storeValue?: string,
165 defaultValue?: string,
166 ): DimensionValue {
167 return parseDimension(resolveSyncStringValue(storeValue, defaultValue))
168 }
169
170 /**
171 * Extract the numeric corner value from a parsed DimensionValue.
172 * Replaces the repeated `Number(parsed.value ?? fallback)` pattern at ref initialization.
173 * Returns 0 when the parsed string value is empty.
174 */
175 export function parseCornerNumericValue(parsed: DimensionValue): number {
176 return parsed.value !== '' ? Number(parsed.value) : 0
177 }
178
179 /**
180 * Resolve corner default value from either a plain dimension string
181 * or a corner settings object containing a `value` field.
182 */
183 export function resolveCornerDefaultValue(cornerSetting: unknown, fallbackValue = '0px'): string {
184 if (typeof cornerSetting === 'string' && cornerSetting !== '') {
185 return cornerSetting
186 }
187
188 if (cornerSetting && typeof cornerSetting === 'object') {
189 const cornerObject = cornerSetting as Record<string, unknown>
190 const cornerValue = cornerObject.value
191
192 if (typeof cornerValue === 'string' && cornerValue !== '') {
193 return cornerValue
194 }
195 }
196
197 return fallbackValue
198 }
199
200 /**
201 * Resolve corner linked state from corner settings object.
202 */
203 export function resolveCornerLinkedState(cornerSetting: unknown, fallbackValue = true): boolean {
204 if (cornerSetting && typeof cornerSetting === 'object') {
205 const cornerObject = cornerSetting as Record<string, unknown>
206
207 return cornerObject.isLinked !== false
208 }
209
210 return fallbackValue
211 }
212
213 /**
214 * Build a CSS dimension string from value and unit
215 * @param value - Numeric or string value
216 * @param unit - CSS unit (px, rem, em, etc.)
217 * @returns Formatted CSS dimension string or empty string if value is empty
218 */
219 export function buildDimensionString(
220 value: number | string | null | undefined,
221 unit: string,
222 ): string {
223 if (value === '' || value === null || value === undefined) {
224 return ''
225 }
226 return `${value}${unit}`
227 }
228
229 /**
230 * Dimension input used by generators and formatters.
231 * Accept either a CSS string (e.g. "16px"), or an object with numeric value and unit.
232 */
233 export type DimensionSpec = string | { value: number | string | null; unit: string } | undefined
234
235 /**
236 * Format a dimension spec into a CSS value string.
237 * - If input is a string, returns it (except 'auto' returns empty string to indicate no explicit padding).
238 * - If input is an object, returns `${value}${unit}` when value is not null/empty.
239 * - Otherwise returns empty string.
240 */
241 export function formatDimensionToCss(input: DimensionSpec): string {
242 if (!input) return ''
243
244 if (typeof input === 'string') {
245 return input === 'auto' ? '' : input
246 }
247
248 if (input.value === null || input.value === '' || input.value === undefined) {
249 return ''
250 }
251
252 return `${input.value}${input.unit || 'px'}`
253 }
254
255 /**
256 * Apply corner settings from store data to component refs
257 * @param cornerSetting - Corner data from store (can be string or object)
258 * @param defaultValue - Fallback dimension string
259 * @param linkedState - Ref for isLinked boolean
260 * @param linkedValue - Ref for linked numeric value
261 * @param linkedUnit - Ref for linked unit
262 * @param topLeftValue - Ref for top-left numeric value
263 * @param topLeftUnit - Ref for top-left unit
264 * @param topRightValue - Ref for top-right numeric value
265 * @param topRightUnit - Ref for top-right unit
266 * @param bottomRightValue - Ref for bottom-right numeric value
267 * @param bottomRightUnit - Ref for bottom-right unit
268 * @param bottomLeftValue - Ref for bottom-left numeric value
269 * @param bottomLeftUnit - Ref for bottom-left unit
270 */
271 export function applyCornerSettings(
272 cornerSetting: unknown,
273 defaultValue: string,
274 linkedState: Ref<boolean>,
275 linkedValue: Ref<number>,
276 linkedUnit: Ref<string>,
277 topLeftValue: Ref<number>,
278 topLeftUnit: Ref<string>,
279 topRightValue: Ref<number>,
280 topRightUnit: Ref<string>,
281 bottomRightValue: Ref<number>,
282 bottomRightUnit: Ref<string>,
283 bottomLeftValue: Ref<number>,
284 bottomLeftUnit: Ref<string>,
285 ): void {
286 // Handle string or missing corner setting - set all to linked
287 if (!cornerSetting || typeof cornerSetting === 'string') {
288 const normalizedValue = parseDimension((cornerSetting as string) || defaultValue)
289 const defaultParsed = parseDimension(defaultValue)
290 linkedState.value = true
291 linkedValue.value =
292 normalizedValue.value !== '' ? Number(normalizedValue.value) : Number(defaultParsed.value)
293 linkedUnit.value = normalizedValue.unit
294 topLeftValue.value = linkedValue.value
295 topLeftUnit.value = linkedUnit.value
296 topRightValue.value = linkedValue.value
297 topRightUnit.value = linkedUnit.value
298 bottomRightValue.value = linkedValue.value
299 bottomRightUnit.value = linkedUnit.value
300 bottomLeftValue.value = linkedValue.value
301 bottomLeftUnit.value = linkedUnit.value
302 return
303 }
304
305 // Handle object corner setting with individual corner values
306 const cornerObject = cornerSetting as Record<string, unknown>
307 const isLinkedValue = cornerObject.isLinked !== false
308 const linkedValueRaw = (cornerObject.value as string) || defaultValue
309 const linkedValueData = parseDimension(linkedValueRaw)
310 const defaultParsed = parseDimension(defaultValue)
311 const topLeftValueData = parseDimension((cornerObject.topLeft as string) || linkedValueRaw)
312 const topRightValueData = parseDimension((cornerObject.topRight as string) || linkedValueRaw)
313 const bottomRightValueData = parseDimension(
314 (cornerObject.bottomRight as string) || linkedValueRaw,
315 )
316 const bottomLeftValueData = parseDimension((cornerObject.bottomLeft as string) || linkedValueRaw)
317
318 linkedState.value = isLinkedValue
319 linkedValue.value =
320 linkedValueData.value !== '' ? Number(linkedValueData.value) : Number(defaultParsed.value)
321 linkedUnit.value = linkedValueData.unit
322
323 const linkedFallback = linkedValue.value
324 topLeftValue.value =
325 topLeftValueData.value !== '' ? Number(topLeftValueData.value) : linkedFallback
326 topLeftUnit.value = topLeftValueData.unit
327 topRightValue.value =
328 topRightValueData.value !== '' ? Number(topRightValueData.value) : linkedFallback
329 topRightUnit.value = topRightValueData.unit
330 bottomRightValue.value =
331 bottomRightValueData.value !== '' ? Number(bottomRightValueData.value) : linkedFallback
332 bottomRightUnit.value = bottomRightValueData.unit
333 bottomLeftValue.value =
334 bottomLeftValueData.value !== '' ? Number(bottomLeftValueData.value) : linkedFallback
335 bottomLeftUnit.value = bottomLeftValueData.unit
336 }
337
338 /**
339 * Build corner settings object from component refs for storing
340 * @param isLinked - Whether corners are linked
341 * @param linkedValue - Linked corner value
342 * @param linkedUnit - Linked corner unit
343 * @param topLeftValue - Top-left corner value
344 * @param topLeftUnit - Top-left corner unit
345 * @param topRightValue - Top-right corner value
346 * @param topRightUnit - Top-right corner unit
347 * @param bottomRightValue - Bottom-right corner value
348 * @param bottomRightUnit - Bottom-right corner unit
349 * @param bottomLeftValue - Bottom-left corner value
350 * @param bottomLeftUnit - Bottom-left corner unit
351 * @returns CornerSettings object ready for store
352 */
353 export function buildCornerSettings(
354 isLinked: boolean,
355 linkedValue: number,
356 linkedUnit: string,
357 topLeftValue: number,
358 topLeftUnit: string,
359 topRightValue: number,
360 topRightUnit: string,
361 bottomRightValue: number,
362 bottomRightUnit: string,
363 bottomLeftValue: number,
364 bottomLeftUnit: string,
365 ): CornerSettings {
366 return {
367 isLinked,
368 value: buildDimensionString(linkedValue, linkedUnit),
369 topLeft: buildDimensionString(topLeftValue, topLeftUnit),
370 topRight: buildDimensionString(topRightValue, topRightUnit),
371 bottomRight: buildDimensionString(bottomRightValue, bottomRightUnit),
372 bottomLeft: buildDimensionString(bottomLeftValue, bottomLeftUnit),
373 }
374 }
375
376 /**
377 * Toggle corner linked state and synchronize values
378 * @param isLinked - Current linked state ref
379 * @param linkedValue - Linked value ref
380 * @param linkedUnit - Linked unit ref
381 * @param topLeftValue - Top-left value ref
382 * @param topLeftUnit - Top-left unit ref
383 * @param topRightValue - Top-right value ref
384 * @param topRightUnit - Top-right unit ref
385 * @param bottomRightValue - Bottom-right value ref
386 * @param bottomRightUnit - Bottom-right unit ref
387 * @param bottomLeftValue - Bottom-left value ref
388 * @param bottomLeftUnit - Bottom-left unit ref
389 */
390 export function toggleCornerLinked(
391 isLinked: Ref<boolean>,
392 linkedValue: Ref<number>,
393 linkedUnit: Ref<string>,
394 topLeftValue: Ref<number>,
395 topLeftUnit: Ref<string>,
396 topRightValue: Ref<number>,
397 topRightUnit: Ref<string>,
398 bottomRightValue: Ref<number>,
399 bottomRightUnit: Ref<string>,
400 bottomLeftValue: Ref<number>,
401 bottomLeftUnit: Ref<string>,
402 ): void {
403 if (isLinked.value) {
404 // Unlinking: copy linked values to individual corners
405 isLinked.value = false
406 topLeftValue.value = linkedValue.value
407 topRightValue.value = linkedValue.value
408 bottomRightValue.value = linkedValue.value
409 bottomLeftValue.value = linkedValue.value
410 topLeftUnit.value = linkedUnit.value
411 topRightUnit.value = linkedUnit.value
412 bottomRightUnit.value = linkedUnit.value
413 bottomLeftUnit.value = linkedUnit.value
414 } else {
415 // Linking: copy top-left to linked value
416 isLinked.value = true
417 linkedValue.value = topLeftValue.value ?? linkedValue.value
418 linkedUnit.value = topLeftUnit.value ?? linkedUnit.value
419 }
420 }
421
422 /**
423 * Safely extract a style value from store with fallback chain
424 * @param storeValue - Value from store (may be undefined)
425 * @param defaultValue - Default theme value (may be undefined)
426 * @param fallback - Final fallback value
427 * @returns The first non-undefined value in the chain, or fallback
428 */
429 export function syncStyleValue<T>(
430 storeValue: T | undefined,
431 defaultValue: T | undefined,
432 fallback: T,
433 ): T {
434 return storeValue ?? defaultValue ?? fallback
435 }
436
437 /**
438 * Get nested property value from object using dot notation path
439 * @param obj - Source object
440 * @param path - Dot-separated path (e.g., 'advancedMode.general.formBackground')
441 * @returns Value at path or undefined
442 */
443 export function getNestedValue(
444 obj: FormStyleSettings | Record<string, unknown>,
445 path: string,
446 ): unknown {
447 return path.split('.').reduce((current: unknown, key: string) => {
448 return current && typeof current === 'object' && key in current
449 ? (current as Record<string, unknown>)[key]
450 : undefined
451 }, obj)
452 }
453
454 /**
455 * Update multiple store paths with the same value
456 * Useful for syncing Quick mode and Advanced mode simultaneously
457 * @param formBuilderStore - The form builder store instance
458 * @param paths - Array of dot-notation paths to update
459 * @param value - Value to set for all paths
460 */
461 export function updateStylePaths(
462 formBuilderStore: FormBuilderStore,
463 paths: string[],
464 value: unknown,
465 ): void {
466 paths.forEach((path) => {
467 formBuilderStore.updateStyleSettings(path, value)
468 })
469 }
470
471 /**
472 * Create a watcher for a style property that automatically syncs with store
473 * Handles the common pattern of checking sync state, comparing values, and updating store
474 * @param config - Watcher configuration object
475 * @returns Cleanup function to stop the watcher
476 */
477 export function createStyleWatcher(config: StyleWatcherConfig): () => void {
478 const { ref: watchRef, storePath, syncPaths, formBuilderStore, isSyncingFromStore } = config
479
480 let timeoutId: ReturnType<typeof setTimeout> | undefined
481
482 const stop = watch(watchRef, (newValue) => {
483 if (isSyncingFromStore.value) return
484
485 if (timeoutId) {
486 clearTimeout(timeoutId)
487 }
488
489 timeoutId = setTimeout(() => {
490 const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath)
491
492 if (newValue !== currentValue) {
493 // If syncPaths provided, update multiple paths; otherwise just the storePath
494 if (syncPaths && syncPaths.length > 0) {
495 updateStylePaths(formBuilderStore, syncPaths, newValue)
496 } else {
497 formBuilderStore.updateStyleSettings(storePath, newValue)
498 }
499 }
500
501 timeoutId = undefined
502 }, 0)
503 })
504
505 return () => {
506 if (timeoutId) {
507 clearTimeout(timeoutId)
508 timeoutId = undefined
509 }
510
511 stop()
512 }
513 }
514
515 /**
516 * Create watchers for dimension value+unit pairs (e.g., borderWidth)
517 * Handles the pattern of watching both value and unit refs together
518 * @param valueRef - Ref containing numeric value
519 * @param unitRef - Ref containing unit string
520 * @param storePath - Primary store path to compare against
521 * @param syncPaths - Array of paths to update (for dual-mode sync)
522 * @param formBuilderStore - Form builder store instance
523 * @param isSyncingFromStore - Flag to prevent circular updates
524 * @returns Cleanup function to stop the watcher
525 */
526 export function createDimensionWatcher(
527 valueRef: Ref<number>,
528 unitRef: Ref<string>,
529 storePath: string,
530 syncPaths: string[],
531 formBuilderStore: FormBuilderStore,
532 isSyncingFromStore: Ref<boolean>,
533 ): () => void {
534 let timeoutId: ReturnType<typeof setTimeout> | undefined
535
536 const stop = watch([valueRef, unitRef], () => {
537 if (isSyncingFromStore.value) return
538
539 if (timeoutId) {
540 clearTimeout(timeoutId)
541 }
542
543 timeoutId = setTimeout(() => {
544 const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath)
545 const newValue = buildDimensionString(valueRef.value, unitRef.value)
546
547 if (newValue !== '' && newValue !== currentValue) {
548 updateStylePaths(formBuilderStore, syncPaths, newValue)
549 }
550
551 timeoutId = undefined
552 }, 0)
553 })
554
555 return () => {
556 if (timeoutId) {
557 clearTimeout(timeoutId)
558 timeoutId = undefined
559 }
560
561 stop()
562 }
563 }
564