) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(props.fields || [])];
updated[index] = { ...updated[index], ...patch };
props.fields = updated;
});
};
const moveField = (index: number, direction: -1 | 1) => {
actions.setProp(selectedId, (props: any) => {
const updated = [...(props.fields || [])];
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= updated.length) return;
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
props.fields = updated;
});
};
return (
<>
{/* ContactForm field editor: add/remove/reorder fields, set each
field's label, name, type (full allowlist + textarea/select),
options (select only), and required flag. */}
{nodeProps.fields !== undefined && Array.isArray(nodeProps.fields) && (
(
updateField(index, { label: e.target.value })}
placeholder="Label"
style={smallInputStyle}
/>
updateField(index, { name: e.target.value })}
placeholder="Field name (e.g. email)"
style={smallInputStyle}
/>
updateField(index, { placeholder: e.target.value })}
placeholder="Placeholder"
style={smallInputStyle}
/>
updateField(index, { type: e.target.value })}
style={{ ...smallInputStyle, cursor: 'pointer' }}
>
{CONTACT_FIELD_TYPES.map((t) => {t} )}
{item.type === 'select' && (
updateField(index, {
options: e.target.value.split(',').map((s: string) => s.trim()).filter(Boolean),
})}
placeholder="Options (comma-separated)"
style={smallInputStyle}
/>
)}
updateField(index, { required: e.target.checked })}
/>
Required
moveField(index, -1)} style={{ ...moveBtnStyle, opacity: index === 0 ? 0.4 : 1 }} title="Move up">
moveField(index, 1)} style={{ ...moveBtnStyle, opacity: index === nodeProps.fields.length - 1 ? 0.4 : 1 }} title="Move down">
)}
emptyItem={{ type: 'text', label: 'New Field', name: 'field', placeholder: '', required: false }}
/>
)}
{/* Contact-form relay: where submissions are emailed. Present on ContactForm
and FormContainer (both have recipientEmail/thankYouUrl props). */}
{nodeProps.recipientEmail !== undefined && (
)}
{nodeProps.thankYouUrl !== undefined && (
Thank-you page URL (optional)
setProp('thankYouUrl', e.target.value)} placeholder="/thank-you (blank = hosted page)" style={inputStyle} />
)}
{/* Form action/method (FormContainer). SearchBar also has an `action`
prop but is distinguished via its unique `showButton` prop -- see
the dedicated Search block below -- so it doesn't get this label. */}
{nodeProps.action !== undefined && nodeProps.showButton === undefined && (
Form Action URL
setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
)}
{/* SearchBar: where the GET search request is submitted. */}
{nodeProps.showButton !== undefined && nodeProps.action !== undefined && (
)}
{nodeProps.showButton !== undefined && (
setProp('showButton', e.target.checked)} />
Show Search Button
)}
{nodeProps.method !== undefined && (
Method
{['GET', 'POST'].map((m) => (
setProp('method', m)} style={btnActiveStyle(nodeProps.method === m)}>{m}
))}
)}
{/* Input field props */}
{nodeProps.label !== undefined && (
Label
setProp('label', e.target.value)} style={inputStyle} />
)}
{nodeProps.placeholder !== undefined && (
Placeholder
setProp('placeholder', e.target.value)} style={inputStyle} />
)}
{nodeProps.name !== undefined && (
Field Name
setProp('name', e.target.value)} style={inputStyle} />
)}
{nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
Input Type
setProp('type', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
{['text', 'email', 'password', 'number', 'tel', 'url'].map((t) => (
{t}
))}
)}
{nodeProps.required !== undefined && (
setProp('required', e.target.checked)} />
Required
)}
{/* Button text */}
{nodeProps.text !== undefined && nodeProps.label === undefined && (
Button Text
setProp('text', e.target.value)} style={inputStyle} />
)}
{/* Subscribe form props */}
{nodeProps.buttonText !== undefined && (
Button Text
setProp('buttonText', e.target.value)} style={inputStyle} />
)}
{nodeProps.placeholderText !== undefined && (
Placeholder Text
setProp('placeholderText', e.target.value)} style={inputStyle} />
)}
{nodeProps.successMessage !== undefined && (
Success Message
setProp('successMessage', e.target.value)} style={inputStyle} />
)}
{/* Style */}
Background
setPropStyle('backgroundColor', v)} />
Padding
setPropStyle('padding', v)} />
Border Radius
setPropStyle('borderRadius', v)} />
{/* Box model: margin/padding (per-side), border, shadow, opacity --
common enh-batch rollout, applies to the whole form family since
they all spread `style` onto their root element. */}
setPropStyle(`margin${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)}
/>
setPropStyle(`padding${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)}
/>
setPropStyle('border', buildBorderShorthand(v))}
/>
Box Shadow
setPropStyle('boxShadow', v)} />
Opacity
setPropStyle('opacity', String(Number(e.target.value) / 100))}
style={{ width: '100%' }}
/>
{/* Animation & Visibility -- gated on the blank/false defaults added to
each owned component's craft.props (ContactForm, FormContainer,
InputField, TextareaField, FormButton, SubscribeForm, SearchBar).
No toHtml change needed: the export's buildDataAttrs() already
emits data-animation/data-hide-* from these exact prop names for
every node. */}
{nodeProps.animation !== undefined && (
{ setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }}
/>
{
setProp('hideOnDesktop', v.hideOnDesktop);
setProp('hideOnTablet', v.hideOnTablet);
setProp('hideOnMobile', v.hideOnMobile);
}}
/>
)}
>
);
};