= ({
fields = defaultFields,
submitText = 'Send Message',
submitColor = '#3b82f6',
formAction = '#',
successMessage = 'Thank you! We\'ll get back to you soon.',
style = {},
labelColor = '#374151',
inputBg = '#ffffff',
inputBorder = '#d1d5db',
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const inputBaseStyle: CSSProperties = {
width: '100%',
padding: '10px 14px',
fontSize: '14px',
fontFamily: 'Inter, sans-serif',
border: `1px solid ${inputBorder}`,
borderRadius: '6px',
backgroundColor: inputBg,
color: '#1f2937',
boxSizing: 'border-box',
outline: 'none',
};
return (
);
};
/* ---------- Craft config ---------- */
ContactForm.craft = {
displayName: 'Contact Form',
props: {
fields: defaultFields,
submitText: 'Send Message',
submitColor: '#3b82f6',
formAction: '#',
successMessage: 'Thank you! We\'ll get back to you soon.',
style: {
backgroundColor: '#ffffff',
borderRadius: '12px',
border: '1px solid #e5e7eb',
},
labelColor: '#374151',
inputBg: '#ffffff',
inputBorder: '#d1d5db',
recipientEmail: '',
thankYouUrl: '',
animation: '',
animationDelay: '',
hideOnDesktop: false,
hideOnTablet: false,
hideOnMobile: false,
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
};
/* ---------- HTML export ---------- */
(ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string, nodeId?: string) => {
const formStyle = cssPropsToString({
padding: '32px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
...props.style,
});
// Sanitized -- raw string-interpolation sinks in inputStyleStr/labelHtml
// below.
const labelColor = cssValue(props.labelColor) || '#374151';
const inputBg = cssValue(props.inputBg) || '#ffffff';
const inputBorder = cssValue(props.inputBorder) || '#d1d5db';
const inputStyleStr = `width:100%;padding:10px 14px;font-size:14px;font-family:Inter,sans-serif;border:1px solid ${inputBorder};border-radius:6px;background-color:${inputBg};color:#1f2937;box-sizing:border-box;outline:none`;
const fieldsHtml = (props.fields || defaultFields).map((field, i) => {
const reqStar = field.required ? '*' : '';
// Deterministic id: index + slugified name, so repeated fields with the
// same name (or no name) still get unique, stable ids -- no Math.random.
const fieldId = `field-${i}-${slugId(field.name)}`;
const labelHtml = ``;
const reqAttr = field.required ? ' required' : '';
let inputHtml = '';
if (field.type === 'textarea') {
inputHtml = ``;
} else if (field.type === 'select') {
const opts = (field.options || []).map((o) => ``).join('');
inputHtml = ``;
} else {
inputHtml = ``;
}
return `${labelHtml}${inputHtml}
`;
}).join('\n ');
const btnStyle = cssPropsToString({
padding: '12px 32px',
fontSize: '16px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
backgroundColor: props.submitColor || '#3b82f6',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
alignSelf: 'flex-start',
});
const { marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, props.formAction, nodeId);
// The form-sender relay delivers success via a full-page 303 redirect
// (to thankYouUrl or a hosted thanks.php page) -- there is no in-page JS
// that reveals an inline success element today. Emit successMessage as a
// forward-compatible data attribute so a future AJAX/JS submission mode
// can read it, without implying a live mechanism that doesn't exist yet.
const successAttr = props.successMessage ? ` data-whp-success-message="${escapeAttr(props.successMessage)}"` : '';
return {
html: `${marker}`,
};
};