import React, { useState, useRef, useEffect } from 'react' import { createPortal } from 'react-dom' import { X, Check, Minus, Plus, AlignLeft, AlignCenter, AlignRight, Maximize2, Palette, } from 'lucide-react' import { HexColorPicker } from 'react-colorful' import { useFormStore } from '../store/useFormStore' // Opened from a submit field (`field` + `onUpdateField`): edits that field with the the reference builder keys the storefront reads // (name/label, backgroundColor, textColor, fontSize, labelFontWeight, labelFontStyle, submitButtonAlignment, // submitButtonWidth). Without a field it edits the form-level settings.submit* keys. export default function InlineSubmitButtonEditor({ isOpen, onClose, anchorRef, field, onUpdateField }) { const settings = useFormStore((state) => state.settings) const setSettings = useFormStore((state) => state.setSettings) const setIsDirty = useFormStore((state) => state.setIsDirty) const [showBgPicker, setShowBgPicker] = useState(false) const [showTextPicker, setShowTextPicker] = useState(false) const [position, setPosition] = useState({ top: 200, left: 200 }) const modalRef = useRef(null) const textRef = useRef(null) // The button's own box: the text is edited in place, inside the button (the reference builder). const [btnBox, setBtnBox] = useState(null) useEffect(() => { if (!isOpen || !anchorRef?.current) { setBtnBox(null); return undefined } const measure = () => { const el = anchorRef.current if (!el) return const r = el.getBoundingClientRect() const cs = window.getComputedStyle(el) setBtnBox({ top: r.top, left: r.left, width: r.width, height: r.height, font: { fontFamily: cs.fontFamily, fontSize: cs.fontSize, fontWeight: cs.fontWeight, fontStyle: cs.fontStyle, lineHeight: cs.lineHeight, letterSpacing: cs.letterSpacing }, color: cs.color, backgroundColor: cs.backgroundColor, backgroundImage: cs.backgroundImage, borderRadius: cs.borderRadius, paddingLeft: cs.paddingLeft, paddingRight: cs.paddingRight, }) } measure() const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null if (ro) ro.observe(anchorRef.current) window.addEventListener('scroll', measure, true) window.addEventListener('resize', measure) return () => { if (ro) ro.disconnect() window.removeEventListener('scroll', measure, true) window.removeEventListener('resize', measure) } }, [isOpen, anchorRef, field]) // Escape closes the editor (and its colour pickers) from anywhere. useEffect(() => { if (!isOpen) return undefined const onKey = (e) => { if (e.key !== 'Escape') return e.stopPropagation() setShowBgPicker(false) setShowTextPicker(false) onClose() } document.addEventListener('keydown', onKey, true) return () => document.removeEventListener('keydown', onKey, true) }, [isOpen, onClose]) useEffect(() => { if (isOpen) { const updatePos = () => { if (!anchorRef?.current) { setPosition({ top: Math.max(20, Math.round(window.innerHeight / 2 - 180)), left: Math.max(20, Math.round(window.innerWidth / 2 - 170)) }) return } const rect = anchorRef.current.getBoundingClientRect() const modalWidth = 340 const modalHeight = 360 let left = Math.round(rect.left + rect.width / 2 - modalWidth / 2) let top = Math.round(rect.top - modalHeight - 12) if (top < 10) top = Math.round(rect.bottom + 12) if (left < 10) left = 10 if (left + modalWidth > window.innerWidth - 10) left = window.innerWidth - modalWidth - 10 setPosition({ top, left }) } updatePos() window.addEventListener('resize', updatePos) window.addEventListener('scroll', updatePos, true) const handleOutside = (e) => { if (modalRef.current && modalRef.current.contains(e.target)) return if (textRef.current && textRef.current.contains(e.target)) return if (anchorRef?.current && anchorRef.current.contains(e.target)) return onClose() } // Add listener with a slight timeout to ensure opening click has finished const timer = setTimeout(() => { document.addEventListener('mousedown', handleOutside) }, 50) return () => { clearTimeout(timer) window.removeEventListener('resize', updatePos) window.removeEventListener('scroll', updatePos, true) document.removeEventListener('mousedown', handleOutside) } } }, [isOpen, anchorRef, onClose]) if (!isOpen) return null const isField = Boolean(field && onUpdateField) const firstSet = (...vals) => vals.find((v) => v !== undefined && v !== null && v !== '') const fieldPad = (side) => firstSet(field?.buttonPadding?.[side], Number(field?.padding?.[side]) > 0 ? field.padding[side] : undefined) const fieldBg = isField && typeof field.backgroundColor === 'string' && !field.backgroundColor.includes('gradient') ? field.backgroundColor : null const btnText = isField ? firstSet(field.buttonText, field.label, field.name, settings.submitButtonText, 'Submit') : (settings.submitButtonText || 'Submit') const btnBg = isField ? firstSet(fieldBg, settings.submitButtonBgColor, '#4f46e5') : (settings.submitButtonBgColor || '#10B981') const btnColor = isField ? firstSet(field.textColor, settings.submitButtonTextColor, '#ffffff') : (settings.submitButtonTextColor || '#ffffff') const btnRadius = isField ? Number(firstSet(field.borderRadius, field.fieldBorderRadius, settings.submitButtonBorderRadius, 8)) : (settings.submitButtonBorderRadius ?? 8) const btnAlign = isField ? firstSet(field.submitButtonAlignment, field.align, settings.submitButtonAlignment, 'full') : (settings.submitButtonAlignment || 'full') const btnPaddingY = isField ? Number(firstSet(fieldPad('top'), settings.submitButtonPaddingY, 12)) : (settings.submitButtonPaddingY ?? 10) const btnPaddingX = isField ? Number(firstSet(fieldPad('left'), settings.submitButtonPaddingX, 24)) : (settings.submitButtonPaddingX ?? 20) const btnFontSize = isField ? Number(firstSet(field.fontSize, settings.submitButtonFontSize, 15)) : null // Same default as the canvas/site button (labelFontWeight unset = normal weight). const btnBold = isField ? !['normal', '400', 400, 'lighter', '300', 300].includes(field.labelFontWeight ?? 'normal') : false const btnItalic = isField ? field.labelFontStyle === 'italic' : false const btnWidth = isField ? (Number(field.submitButtonWidth) > 0 ? Number(field.submitButtonWidth) : '') : '' // Settings-path keys → field keys. Only the keys the user touched are written. const toFieldUpdates = (updates) => { const out = {} Object.entries(updates).forEach(([k, v]) => { if (k === 'submitButtonText') { out.label = v out.name = v if (field.buttonText !== undefined) out.buttonText = v } else if (k === 'submitButtonBgColor') { out.backgroundColor = v if (field.buttonGradient) out.buttonGradient = undefined } else if (k === 'submitButtonTextColor') { out.textColor = v } else if (k === 'submitButtonBorderRadius') { out.borderRadius = v out.fieldBorderRadius = v } else if (k === 'submitButtonAlignment') { out.submitButtonAlignment = v if (field.align !== undefined) out.align = v } else if (k === 'submitButtonPaddingY' || k === 'submitButtonPaddingX') { const cur = { top: btnPaddingY, bottom: btnPaddingY, left: btnPaddingX, right: btnPaddingX, ...(out.padding || {}) } if (k === 'submitButtonPaddingY') { cur.top = v; cur.bottom = v } else { cur.left = v; cur.right = v } out.padding = cur if (field.buttonPadding) out.buttonPadding = { ...field.buttonPadding, ...cur } } else { out[k] = v } }) return out } const handleUpdate = (updates) => { if (isField) onUpdateField(toFieldUpdates(updates)) else setSettings(updates) if (setIsDirty) setIsDirty(true) } return createPortal(
{/* Button text edited in place, inside the button */} {isField && btnBox && ( handleUpdate({ submitButtonText: e.target.value })} onKeyDown={(e) => { e.stopPropagation() if (e.key === 'Enter') { e.preventDefault(); onClose() } }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} autoFocus className="fixed z-[9998] outline-none text-center" style={{ top: btnBox.top, left: btnBox.left, width: btnBox.width, height: btnBox.height, ...btnBox.font, color: btnBox.color, backgroundColor: btnBox.backgroundColor, backgroundImage: btnBox.backgroundImage, borderRadius: btnBox.borderRadius, paddingLeft: btnBox.paddingLeft, paddingRight: btnBox.paddingRight, border: 'none', boxShadow: '0 0 0 2px #3b82f6', margin: 0, }} /> )}
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} >
Submit Button Style
{/* Button Label */}
handleUpdate({ submitButtonText: e.target.value })} className="w-full px-2.5 py-1.5 border border-gray-200 rounded-lg text-xs font-semibold focus:ring-1 focus:ring-gray-900 focus:border-gray-900 focus:outline-none text-gray-900" placeholder="Submit" />
{/* Colors */}
{showBgPicker && (
handleUpdate({ submitButtonBgColor: c })} />
)}
{showTextPicker && (
handleUpdate({ submitButtonTextColor: c })} />
)}
{/* Alignment */}
{[ { val: 'left', icon: AlignLeft, label: 'Left' }, { val: 'center', icon: AlignCenter, label: 'Center' }, { val: 'right', icon: AlignRight, label: 'Right' }, { val: 'full', icon: Maximize2, label: 'Full' }, ].map((item) => { const Icon = item.icon const isSelected = btnAlign === item.val return ( ) })}
{isField && (
handleUpdate({ fontSize: parseInt(e.target.value) || 15 })} className="w-full min-w-0 px-1 py-1 border border-gray-200 rounded-lg font-mono text-[11px] text-center focus:border-gray-900 focus:outline-none" />
{ const w = parseInt(e.target.value) || 0 // A fixed width needs a non-full alignment (full width wins on the site). handleUpdate({ submitButtonWidth: w, ...(w > 0 && btnAlign === 'full' ? { submitButtonAlignment: 'left' } : {}) }) }} className="w-full px-2 py-1 border border-gray-200 rounded-lg font-mono text-[11px] focus:border-gray-900 focus:outline-none disabled:bg-gray-50" />
)} {/* Corner Radius */}
Corner Radius {btnRadius}px
handleUpdate({ submitButtonBorderRadius: parseInt(e.target.value) || 0 }) } className="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-gray-900" />
{/* Padding */}
Padding Y (px) handleUpdate({ submitButtonPaddingY: parseInt(e.target.value) || 0 }) } className="w-full px-2 py-1 border border-gray-200 rounded-lg font-mono focus:border-gray-900 focus:outline-none" />
Padding X (px) handleUpdate({ submitButtonPaddingX: parseInt(e.target.value) || 0 }) } className="w-full px-2 py-1 border border-gray-200 rounded-lg font-mono focus:border-gray-900 focus:outline-none" />
Live preview updated
, document.body ) }