140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
import React, { useCallback } from 'react';
|
|||
|
|
import { useEditor } from '@craftjs/core';
|
||
|
|
import {
|
||
|
|
BG_COLORS,
|
||
|
|
SPACING_PRESETS,
|
||
|
|
RADIUS_PRESETS,
|
||
|
|
} from '../../../constants/presets';
|
||
|
|
import {
|
||
|
|
StylePanelProps,
|
||
|
|
SectionLabel,
|
||
|
|
ColorSwatchGrid,
|
||
|
|
PresetButtonGrid,
|
||
|
|
CollapsibleSection,
|
||
|
|
labelStyle,
|
||
|
|
inputStyle,
|
||
|
|
btnActiveStyle,
|
||
|
|
sectionGap,
|
||
|
|
} from './shared';
|
||
|
|
|
||
|
|
/* ---------- FORM ---------- */
|
||
|
|
export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||
|
|
const { actions } = useEditor();
|
||
|
|
|
||
|
|
const setProp = useCallback((key: string, value: any) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => { props[key] = value; });
|
||
|
|
}, [actions, selectedId]);
|
||
|
|
|
||
|
|
const setPropStyle = useCallback((key: string, value: string) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
props.style = { ...props.style, [key]: value };
|
||
|
|
});
|
||
|
|
}, [actions, selectedId]);
|
||
|
|
|
||
|
|
const style = nodeProps.style || {};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* Form action/method */}
|
||
|
|
{nodeProps.action !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Form Action URL</label>
|
||
|
|
<input type="text" value={nodeProps.action || ''} onChange={(e) => setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.method !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Method</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
||
|
|
{['GET', 'POST'].map((m) => (
|
||
|
|
<button key={m} onClick={() => setProp('method', m)} style={btnActiveStyle(nodeProps.method === m)}>{m}</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Input field props */}
|
||
|
|
{nodeProps.label !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Label</label>
|
||
|
|
<input type="text" value={nodeProps.label || ''} onChange={(e) => setProp('label', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.placeholder !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Placeholder</label>
|
||
|
|
<input type="text" value={nodeProps.placeholder || ''} onChange={(e) => setProp('placeholder', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.name !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Field Name</label>
|
||
|
|
<input type="text" value={nodeProps.name || ''} onChange={(e) => setProp('name', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Input Type</label>
|
||
|
|
<select value={nodeProps.type} onChange={(e) => setProp('type', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
|
||
|
|
{['text', 'email', 'password', 'number', 'tel', 'url'].map((t) => (
|
||
|
|
<option key={t} value={t}>{t}</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.required !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||
|
|
<input type="checkbox" checked={nodeProps.required || false} onChange={(e) => setProp('required', e.target.checked)} />
|
||
|
|
Required
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Button text */}
|
||
|
|
{nodeProps.text !== undefined && nodeProps.label === undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Button Text</label>
|
||
|
|
<input type="text" value={nodeProps.text || ''} onChange={(e) => setProp('text', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Subscribe form props */}
|
||
|
|
{nodeProps.buttonText !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Button Text</label>
|
||
|
|
<input type="text" value={nodeProps.buttonText || ''} onChange={(e) => setProp('buttonText', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.placeholderText !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Placeholder Text</label>
|
||
|
|
<input type="text" value={nodeProps.placeholderText || ''} onChange={(e) => setProp('placeholderText', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.successMessage !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Success Message</label>
|
||
|
|
<input type="text" value={nodeProps.successMessage || ''} onChange={(e) => setProp('successMessage', e.target.value)} style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Style */}
|
||
|
|
<CollapsibleSection title="Style" defaultOpen={false}>
|
||
|
|
<div className="guided-section">
|
||
|
|
<SectionLabel>Background</SectionLabel>
|
||
|
|
<ColorSwatchGrid colors={BG_COLORS} activeValue={style.backgroundColor} onSelect={(v: string) => setPropStyle('backgroundColor', v)} />
|
||
|
|
</div>
|
||
|
|
<div className="guided-section">
|
||
|
|
<SectionLabel>Padding</SectionLabel>
|
||
|
|
<PresetButtonGrid presets={SPACING_PRESETS} activeValue={style.padding as string} onSelect={(v) => setPropStyle('padding', v)} />
|
||
|
|
</div>
|
||
|
|
<div className="guided-section">
|
||
|
|
<SectionLabel>Border Radius</SectionLabel>
|
||
|
|
<PresetButtonGrid presets={RADIUS_PRESETS} activeValue={style.borderRadius as string} onSelect={(v) => setPropStyle('borderRadius', v)} />
|
||
|
|
</div>
|
||
|
|
</CollapsibleSection>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
};
|