import type { Ref } from 'vue' import { watch } from 'vue' import type { FormStyleSettings } from '@/types/form-style' /** * Represents a CSS dimension value split into numeric value and unit */ export interface DimensionValue { value: string unit: string } /** * Represents corner radius settings with linked/unlinked state */ export interface CornerSettings { isLinked: boolean value: string topLeft: string topRight: string bottomRight: string bottomLeft: string } /** * Form builder store interface for type safety */ export interface FormBuilderStore { styleSettings: FormStyleSettings updateStyleSettings: (path: string, value: unknown) => void } export type Alignment = 'left' | 'center' | 'right' export type BorderStyle = 'solid' | 'dashed' | 'dotted' export type FontStyle = 'normal' | 'italic' const BORDER_STYLE_VALUES: BorderStyle[] = ['solid', 'dashed', 'dotted'] const FONT_STYLE_VALUES: FontStyle[] = ['normal', 'italic'] export const BORDER_STYLE_OPTIONS: Array<{ value: BorderStyle; label: string }> = [ { value: 'solid', label: 'Solid' }, { value: 'dashed', label: 'Dash' }, { value: 'dotted', label: 'Dotted' }, ] export const FONT_STYLE_OPTIONS: Array<{ value: FontStyle; label: string }> = [ { value: 'normal', label: 'Normal' }, { value: 'italic', label: 'Italic' }, ] export function normalizeAlignment(value?: string): Alignment { if (value === 'center' || value === 'right') { return value as Alignment } return 'left' } export function resolveBorderStyle( borderStyle: unknown, fallbackValue: BorderStyle = 'solid', ): BorderStyle { if (typeof borderStyle === 'string' && BORDER_STYLE_VALUES.includes(borderStyle as BorderStyle)) { return borderStyle as BorderStyle } return fallbackValue } export function resolveFontStyle( fontStyle: unknown, fallbackValue: FontStyle = 'normal', ): FontStyle { if (typeof fontStyle === 'string' && FONT_STYLE_VALUES.includes(fontStyle as FontStyle)) { return fontStyle as FontStyle } return fallbackValue } export function getBorderStyleIcon(borderStyle: BorderStyle): string { if (borderStyle === 'dashed') { return 'border-dash' } if (borderStyle === 'dotted') { return 'border-dotted' } return 'border-solid' } /** * Configuration for creating a style property watcher */ export interface StyleWatcherConfig { ref: Ref storePath: string syncPaths?: string[] formBuilderStore: FormBuilderStore isSyncingFromStore: Ref } /** * Parse a CSS dimension string (e.g., "12px", "1.5rem") into value and unit * @param value - CSS dimension string * @returns Object with separated value and unit */ export function parseDimension(value: string): DimensionValue { const match = value?.match(/^([0-9.]+)(.*)$/) if (match) { return { value: match[1], unit: match[2] || 'px' } } return { value: '', unit: 'px' } } /** * Parse dimension and return numeric value with unit. * @param value - CSS dimension string * @param fallbackValue - Numeric fallback when parsed value is empty */ export function parseDimensionToNumeric( value: string, fallbackValue: number | null = null, ): { value: number | null; unit: string } { const parsedDimension = parseDimension(value) return { value: parsedDimension.value !== '' ? Number(parsedDimension.value) : fallbackValue, unit: parsedDimension.unit, } } /** * Resolve a style string value from store or fallback to the provided default. * Useful inside syncWithStore to avoid repeated `(storeVal || defaultVal) as string` casts. * @param storeValue - Value from the store (may be undefined) * @param defaultValue - Default value to use when storeValue is falsy */ export function resolveSyncStringValue(storeValue?: string, defaultValue?: string): string { return storeValue || defaultValue || '' } /** * Parse a dimension stored in the store, falling back to the default value. * Combines resolveSyncStringValue and parseDimensionToNumeric. * @param storeValue - CSS dimension string from store (may be undefined) * @param defaultValue - CSS dimension string from defaults (may be undefined) * @param fallbackValue - Numeric fallback when the resolved string cannot be parsed */ export function parseSyncNumericDimension( storeValue?: string, defaultValue?: string, fallbackValue: number | null = null, ): { value: number | null; unit: string } { return parseDimensionToNumeric(resolveSyncStringValue(storeValue, defaultValue), fallbackValue) } /** * Parse a dimension stored in the store, falling back to the default value. * Returns a raw { value, unit } string pair (not numeric). * @param storeValue - CSS dimension string from store (may be undefined) * @param defaultValue - CSS dimension string from defaults (may be undefined) */ export function parseSyncDimensionValue( storeValue?: string, defaultValue?: string, ): DimensionValue { return parseDimension(resolveSyncStringValue(storeValue, defaultValue)) } /** * Extract the numeric corner value from a parsed DimensionValue. * Replaces the repeated `Number(parsed.value ?? fallback)` pattern at ref initialization. * Returns 0 when the parsed string value is empty. */ export function parseCornerNumericValue(parsed: DimensionValue): number { return parsed.value !== '' ? Number(parsed.value) : 0 } /** * Resolve corner default value from either a plain dimension string * or a corner settings object containing a `value` field. */ export function resolveCornerDefaultValue(cornerSetting: unknown, fallbackValue = '0px'): string { if (typeof cornerSetting === 'string' && cornerSetting !== '') { return cornerSetting } if (cornerSetting && typeof cornerSetting === 'object') { const cornerObject = cornerSetting as Record const cornerValue = cornerObject.value if (typeof cornerValue === 'string' && cornerValue !== '') { return cornerValue } } return fallbackValue } /** * Resolve corner linked state from corner settings object. */ export function resolveCornerLinkedState(cornerSetting: unknown, fallbackValue = true): boolean { if (cornerSetting && typeof cornerSetting === 'object') { const cornerObject = cornerSetting as Record return cornerObject.isLinked !== false } return fallbackValue } /** * Build a CSS dimension string from value and unit * @param value - Numeric or string value * @param unit - CSS unit (px, rem, em, etc.) * @returns Formatted CSS dimension string or empty string if value is empty */ export function buildDimensionString( value: number | string | null | undefined, unit: string, ): string { if (value === '' || value === null || value === undefined) { return '' } return `${value}${unit}` } /** * Dimension input used by generators and formatters. * Accept either a CSS string (e.g. "16px"), or an object with numeric value and unit. */ export type DimensionSpec = string | { value: number | string | null; unit: string } | undefined /** * Format a dimension spec into a CSS value string. * - If input is a string, returns it (except 'auto' returns empty string to indicate no explicit padding). * - If input is an object, returns `${value}${unit}` when value is not null/empty. * - Otherwise returns empty string. */ export function formatDimensionToCss(input: DimensionSpec): string { if (!input) return '' if (typeof input === 'string') { return input === 'auto' ? '' : input } if (input.value === null || input.value === '' || input.value === undefined) { return '' } return `${input.value}${input.unit || 'px'}` } /** * Apply corner settings from store data to component refs * @param cornerSetting - Corner data from store (can be string or object) * @param defaultValue - Fallback dimension string * @param linkedState - Ref for isLinked boolean * @param linkedValue - Ref for linked numeric value * @param linkedUnit - Ref for linked unit * @param topLeftValue - Ref for top-left numeric value * @param topLeftUnit - Ref for top-left unit * @param topRightValue - Ref for top-right numeric value * @param topRightUnit - Ref for top-right unit * @param bottomRightValue - Ref for bottom-right numeric value * @param bottomRightUnit - Ref for bottom-right unit * @param bottomLeftValue - Ref for bottom-left numeric value * @param bottomLeftUnit - Ref for bottom-left unit */ export function applyCornerSettings( cornerSetting: unknown, defaultValue: string, linkedState: Ref, linkedValue: Ref, linkedUnit: Ref, topLeftValue: Ref, topLeftUnit: Ref, topRightValue: Ref, topRightUnit: Ref, bottomRightValue: Ref, bottomRightUnit: Ref, bottomLeftValue: Ref, bottomLeftUnit: Ref, ): void { // Handle string or missing corner setting - set all to linked if (!cornerSetting || typeof cornerSetting === 'string') { const normalizedValue = parseDimension((cornerSetting as string) || defaultValue) const defaultParsed = parseDimension(defaultValue) linkedState.value = true linkedValue.value = normalizedValue.value !== '' ? Number(normalizedValue.value) : Number(defaultParsed.value) linkedUnit.value = normalizedValue.unit topLeftValue.value = linkedValue.value topLeftUnit.value = linkedUnit.value topRightValue.value = linkedValue.value topRightUnit.value = linkedUnit.value bottomRightValue.value = linkedValue.value bottomRightUnit.value = linkedUnit.value bottomLeftValue.value = linkedValue.value bottomLeftUnit.value = linkedUnit.value return } // Handle object corner setting with individual corner values const cornerObject = cornerSetting as Record const isLinkedValue = cornerObject.isLinked !== false const linkedValueRaw = (cornerObject.value as string) || defaultValue const linkedValueData = parseDimension(linkedValueRaw) const defaultParsed = parseDimension(defaultValue) const topLeftValueData = parseDimension((cornerObject.topLeft as string) || linkedValueRaw) const topRightValueData = parseDimension((cornerObject.topRight as string) || linkedValueRaw) const bottomRightValueData = parseDimension( (cornerObject.bottomRight as string) || linkedValueRaw, ) const bottomLeftValueData = parseDimension((cornerObject.bottomLeft as string) || linkedValueRaw) linkedState.value = isLinkedValue linkedValue.value = linkedValueData.value !== '' ? Number(linkedValueData.value) : Number(defaultParsed.value) linkedUnit.value = linkedValueData.unit const linkedFallback = linkedValue.value topLeftValue.value = topLeftValueData.value !== '' ? Number(topLeftValueData.value) : linkedFallback topLeftUnit.value = topLeftValueData.unit topRightValue.value = topRightValueData.value !== '' ? Number(topRightValueData.value) : linkedFallback topRightUnit.value = topRightValueData.unit bottomRightValue.value = bottomRightValueData.value !== '' ? Number(bottomRightValueData.value) : linkedFallback bottomRightUnit.value = bottomRightValueData.unit bottomLeftValue.value = bottomLeftValueData.value !== '' ? Number(bottomLeftValueData.value) : linkedFallback bottomLeftUnit.value = bottomLeftValueData.unit } /** * Build corner settings object from component refs for storing * @param isLinked - Whether corners are linked * @param linkedValue - Linked corner value * @param linkedUnit - Linked corner unit * @param topLeftValue - Top-left corner value * @param topLeftUnit - Top-left corner unit * @param topRightValue - Top-right corner value * @param topRightUnit - Top-right corner unit * @param bottomRightValue - Bottom-right corner value * @param bottomRightUnit - Bottom-right corner unit * @param bottomLeftValue - Bottom-left corner value * @param bottomLeftUnit - Bottom-left corner unit * @returns CornerSettings object ready for store */ export function buildCornerSettings( isLinked: boolean, linkedValue: number, linkedUnit: string, topLeftValue: number, topLeftUnit: string, topRightValue: number, topRightUnit: string, bottomRightValue: number, bottomRightUnit: string, bottomLeftValue: number, bottomLeftUnit: string, ): CornerSettings { return { isLinked, value: buildDimensionString(linkedValue, linkedUnit), topLeft: buildDimensionString(topLeftValue, topLeftUnit), topRight: buildDimensionString(topRightValue, topRightUnit), bottomRight: buildDimensionString(bottomRightValue, bottomRightUnit), bottomLeft: buildDimensionString(bottomLeftValue, bottomLeftUnit), } } /** * Toggle corner linked state and synchronize values * @param isLinked - Current linked state ref * @param linkedValue - Linked value ref * @param linkedUnit - Linked unit ref * @param topLeftValue - Top-left value ref * @param topLeftUnit - Top-left unit ref * @param topRightValue - Top-right value ref * @param topRightUnit - Top-right unit ref * @param bottomRightValue - Bottom-right value ref * @param bottomRightUnit - Bottom-right unit ref * @param bottomLeftValue - Bottom-left value ref * @param bottomLeftUnit - Bottom-left unit ref */ export function toggleCornerLinked( isLinked: Ref, linkedValue: Ref, linkedUnit: Ref, topLeftValue: Ref, topLeftUnit: Ref, topRightValue: Ref, topRightUnit: Ref, bottomRightValue: Ref, bottomRightUnit: Ref, bottomLeftValue: Ref, bottomLeftUnit: Ref, ): void { if (isLinked.value) { // Unlinking: copy linked values to individual corners isLinked.value = false topLeftValue.value = linkedValue.value topRightValue.value = linkedValue.value bottomRightValue.value = linkedValue.value bottomLeftValue.value = linkedValue.value topLeftUnit.value = linkedUnit.value topRightUnit.value = linkedUnit.value bottomRightUnit.value = linkedUnit.value bottomLeftUnit.value = linkedUnit.value } else { // Linking: copy top-left to linked value isLinked.value = true linkedValue.value = topLeftValue.value ?? linkedValue.value linkedUnit.value = topLeftUnit.value ?? linkedUnit.value } } /** * Safely extract a style value from store with fallback chain * @param storeValue - Value from store (may be undefined) * @param defaultValue - Default theme value (may be undefined) * @param fallback - Final fallback value * @returns The first non-undefined value in the chain, or fallback */ export function syncStyleValue( storeValue: T | undefined, defaultValue: T | undefined, fallback: T, ): T { return storeValue ?? defaultValue ?? fallback } /** * Get nested property value from object using dot notation path * @param obj - Source object * @param path - Dot-separated path (e.g., 'advancedMode.general.formBackground') * @returns Value at path or undefined */ export function getNestedValue( obj: FormStyleSettings | Record, path: string, ): unknown { return path.split('.').reduce((current: unknown, key: string) => { return current && typeof current === 'object' && key in current ? (current as Record)[key] : undefined }, obj) } /** * Update multiple store paths with the same value * Useful for syncing Quick mode and Advanced mode simultaneously * @param formBuilderStore - The form builder store instance * @param paths - Array of dot-notation paths to update * @param value - Value to set for all paths */ export function updateStylePaths( formBuilderStore: FormBuilderStore, paths: string[], value: unknown, ): void { paths.forEach((path) => { formBuilderStore.updateStyleSettings(path, value) }) } /** * Create a watcher for a style property that automatically syncs with store * Handles the common pattern of checking sync state, comparing values, and updating store * @param config - Watcher configuration object * @returns Cleanup function to stop the watcher */ export function createStyleWatcher(config: StyleWatcherConfig): () => void { const { ref: watchRef, storePath, syncPaths, formBuilderStore, isSyncingFromStore } = config let timeoutId: ReturnType | undefined const stop = watch(watchRef, (newValue) => { if (isSyncingFromStore.value) return if (timeoutId) { clearTimeout(timeoutId) } timeoutId = setTimeout(() => { const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath) if (newValue !== currentValue) { // If syncPaths provided, update multiple paths; otherwise just the storePath if (syncPaths && syncPaths.length > 0) { updateStylePaths(formBuilderStore, syncPaths, newValue) } else { formBuilderStore.updateStyleSettings(storePath, newValue) } } timeoutId = undefined }, 0) }) return () => { if (timeoutId) { clearTimeout(timeoutId) timeoutId = undefined } stop() } } /** * Create watchers for dimension value+unit pairs (e.g., borderWidth) * Handles the pattern of watching both value and unit refs together * @param valueRef - Ref containing numeric value * @param unitRef - Ref containing unit string * @param storePath - Primary store path to compare against * @param syncPaths - Array of paths to update (for dual-mode sync) * @param formBuilderStore - Form builder store instance * @param isSyncingFromStore - Flag to prevent circular updates * @returns Cleanup function to stop the watcher */ export function createDimensionWatcher( valueRef: Ref, unitRef: Ref, storePath: string, syncPaths: string[], formBuilderStore: FormBuilderStore, isSyncingFromStore: Ref, ): () => void { let timeoutId: ReturnType | undefined const stop = watch([valueRef, unitRef], () => { if (isSyncingFromStore.value) return if (timeoutId) { clearTimeout(timeoutId) } timeoutId = setTimeout(() => { const currentValue = getNestedValue(formBuilderStore.styleSettings, storePath) const newValue = buildDimensionString(valueRef.value, unitRef.value) if (newValue !== '' && newValue !== currentValue) { updateStylePaths(formBuilderStore, syncPaths, newValue) } timeoutId = undefined }, 0) }) return () => { if (timeoutId) { clearTimeout(timeoutId) timeoutId = undefined } stop() } }