feat: build React Form Builder (F2.7-F2.10)
Components: - FieldPalette: drag-and-drop source for all field types - FormCanvas: drop target with field list, reordering, CRUD - FormField: individual field component with actions - FieldSettingsPanel: edit field properties (label, ID, options, etc.) - FormFieldOptions: manage select/radio/checkbox options - FormPreview: live preview of rendered form - FormFieldPreview: preview individual field types Features: - 16 field types (text, email, select, checkbox, radio, etc.) - Categorized field palette - Drag-and-drop field reordering - Per-field settings panel - Option management for choice fields - Live form preview - Save via AJAX Config: - fieldTypes.js: field definitions and constants - Generate unique field IDs - Field type categories (input, choice, layout, preset, advanced)
This commit is contained in:
69
src/admin/components/formBuilder/FieldPalette.css
Normal file
69
src/admin/components/formBuilder/FieldPalette.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.formipay-field-palette {
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-palette h3 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-category {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-category h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f0f0f1;
|
||||||
|
border: 1px solid #dcdcde;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-item:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-item svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-palette-item span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
50
src/admin/components/formBuilder/FieldPalette.js
Normal file
50
src/admin/components/formBuilder/FieldPalette.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Field Palette - Drag-and-drop source for form fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon } from '@wordpress/icons';
|
||||||
|
import { FIELD_CATEGORIES, getFieldTypesByCategory } from '../../config/fieldTypes';
|
||||||
|
import './FieldPalette.css';
|
||||||
|
|
||||||
|
export default function FieldPalette({ onDragStart }) {
|
||||||
|
const fieldTypesByCategory = getFieldTypesByCategory();
|
||||||
|
const categories = Object.values(FIELD_CATEGORIES).sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
const handleDragStart = (event, fieldType) => {
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
event.dataTransfer.setData('formipay-field-type', fieldType);
|
||||||
|
onDragStart?.(fieldType);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-field-palette">
|
||||||
|
<h3>{ __('Add Field', 'formipay') }</h3>
|
||||||
|
|
||||||
|
{categories.map((category) => {
|
||||||
|
const fields = fieldTypesByCategory[category.label] || [];
|
||||||
|
if (fields.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.label} className="formipay-palette-category">
|
||||||
|
<h4>{ category.label }</h4>
|
||||||
|
<div className="formipay-palette-items">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.type}
|
||||||
|
className="formipay-palette-item"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, field.type)}
|
||||||
|
title={field.label}
|
||||||
|
>
|
||||||
|
<Icon icon={field.icon} />
|
||||||
|
<span>{ field.label }</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/admin/components/formBuilder/FieldSettingsPanel.css
Normal file
48
src/admin/components/formBuilder/FieldSettingsPanel.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.formipay-field-settings-panel {
|
||||||
|
width: 320px;
|
||||||
|
background: #fff;
|
||||||
|
border-left: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-header .field-type-badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-content .components-base-control {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-settings-content .components-base-control:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
129
src/admin/components/formBuilder/FieldSettingsPanel.js
Normal file
129
src/admin/components/formBuilder/FieldSettingsPanel.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Field Settings Panel - Edit field properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { TextControl, CheckboxControl, SelectControl, TextareaControl } from '@wordpress/components';
|
||||||
|
import { FIELD_TYPES } from '../../config/fieldTypes';
|
||||||
|
import FormFieldOptions from './FormFieldOptions';
|
||||||
|
import './FieldSettingsPanel.css';
|
||||||
|
|
||||||
|
export default function FieldSettingsPanel({
|
||||||
|
field,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onUpdate,
|
||||||
|
}) {
|
||||||
|
if (!open || !field) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldConfig = FIELD_TYPES[field.field_type] || FIELD_TYPES.text;
|
||||||
|
const hasOptions = ['select', 'checkbox', 'radio'].includes(field.field_type);
|
||||||
|
const hasPlaceholder = [
|
||||||
|
'text', 'url', 'email', 'tel', 'number', 'date', 'datetime', 'color',
|
||||||
|
'select', 'checkbox', 'radio', 'hidden', 'textarea', 'country_list'
|
||||||
|
].includes(field.field_type);
|
||||||
|
const hasDefaultValue = [
|
||||||
|
'text', 'url', 'email', 'tel', 'number', 'date', 'datetime', 'color',
|
||||||
|
'select', 'checkbox', 'radio', 'hidden', 'textarea'
|
||||||
|
].includes(field.field_type);
|
||||||
|
const hasDescription = !['hidden'].includes(field.field_type);
|
||||||
|
const isRequired = !['divider', 'page_break', 'hidden'].includes(field.field_type);
|
||||||
|
const hasGridColumns = ['radio', 'checkbox'].includes(field.field_type);
|
||||||
|
|
||||||
|
const handleChange = (key, value) => {
|
||||||
|
onUpdate?.({ ...field, [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-field-settings-panel">
|
||||||
|
<div className="formipay-settings-header">
|
||||||
|
<h3>
|
||||||
|
<span className="field-type-badge">{ fieldConfig.label }</span>
|
||||||
|
{ __('Field Settings', 'formipay') }
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-link"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{ __('Close', 'formipay') }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formipay-settings-content">
|
||||||
|
<TextControl
|
||||||
|
label={ __('Label', 'formipay') }
|
||||||
|
value={field.label || ''}
|
||||||
|
onChange={(value) => handleChange('label', value)}
|
||||||
|
help={ __('The display label for this field', 'formipay') }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextControl
|
||||||
|
label={ __('Field ID', 'formipay') }
|
||||||
|
value={field.field_id || ''}
|
||||||
|
onChange={(value) => handleChange('field_id', value)}
|
||||||
|
help={ __('Unique identifier for this field (used in form data)', 'formipay') }
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ hasPlaceholder && (
|
||||||
|
<TextControl
|
||||||
|
label={ __('Placeholder', 'formipay') }
|
||||||
|
value={field.placeholder || ''}
|
||||||
|
onChange={(value) => handleChange('placeholder', value)}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ hasDefaultValue && (
|
||||||
|
<TextControl
|
||||||
|
label={ __('Default Value', 'formipay') }
|
||||||
|
value={field.default_value || ''}
|
||||||
|
onChange={(value) => handleChange('default_value', value)}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ hasDescription && (
|
||||||
|
<TextareaControl
|
||||||
|
label={ __('Description', 'formipay') }
|
||||||
|
value={field.description || ''}
|
||||||
|
onChange={(value) => handleChange('description', value)}
|
||||||
|
help={ __('Optional help text displayed below the field', 'formipay') }
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ hasOptions && (
|
||||||
|
<FormFieldOptions
|
||||||
|
options={field.field_options || []}
|
||||||
|
onChange={(value) => handleChange('field_options', value)}
|
||||||
|
fieldType={field.field_type}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ hasGridColumns && (
|
||||||
|
<SelectControl
|
||||||
|
label={ __('Option Grid Columns', 'formipay') }
|
||||||
|
value={field.option_grid_columns || 1}
|
||||||
|
options={[
|
||||||
|
{ label: __('1 Column', 'formipay'), value: 1 },
|
||||||
|
{ label: __('2 Columns', 'formipay'), value: 2 },
|
||||||
|
{ label: __('3 Columns', 'formipay'), value: 3 },
|
||||||
|
{ label: __('4 Columns', 'formipay'), value: 4 },
|
||||||
|
]}
|
||||||
|
onChange={(value) => handleChange('option_grid_columns', parseInt(value))}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ isRequired && (
|
||||||
|
<CheckboxControl
|
||||||
|
label={ __('Required Field', 'formipay') }
|
||||||
|
checked={field.is_required || false}
|
||||||
|
onChange={(value) => handleChange('is_required', value)}
|
||||||
|
help={ __('User must fill this field before submitting', 'formipay') }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/admin/components/formBuilder/FormBuilder.css
Normal file
31
src/admin/components/formBuilder/FormBuilder.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.formipay-form-builder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-builder-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-builder-toolbar h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-builder-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-builder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
114
src/admin/components/formBuilder/FormBuilder.js
Normal file
114
src/admin/components/formBuilder/FormBuilder.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Form Builder - Main container component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useState, useCallback } from '@wordpress/element';
|
||||||
|
import FormCanvas from './FormCanvas';
|
||||||
|
import FieldPalette from './FieldPalette';
|
||||||
|
import FieldSettingsPanel from './FieldSettingsPanel';
|
||||||
|
import FormPreview from './FormPreview';
|
||||||
|
import './FormBuilder.css';
|
||||||
|
|
||||||
|
export default function FormBuilder({ formId, initialData = {} }) {
|
||||||
|
const [fields, setFields] = useState(initialData.fields || []);
|
||||||
|
const [selectedFieldId, setSelectedFieldId] = useState(null);
|
||||||
|
|
||||||
|
const selectedField = fields.find(f => f.field_id === selectedFieldId) || null;
|
||||||
|
|
||||||
|
const handleDrop = useCallback((newField) => {
|
||||||
|
setFields([...fields, newField]);
|
||||||
|
setSelectedFieldId(newField.field_id);
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const handleSelectField = useCallback((fieldId) => {
|
||||||
|
setSelectedFieldId(fieldId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpdateField = useCallback((fieldId, updates) => {
|
||||||
|
setFields(fields.map(f => f.field_id === fieldId ? { ...f, ...updates } : f));
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const handleMoveField = useCallback((newFields) => {
|
||||||
|
setFields(newFields);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteField = useCallback((fieldId) => {
|
||||||
|
const newFields = fields.filter(f => f.field_id !== fieldId);
|
||||||
|
setFields(newFields);
|
||||||
|
if (selectedFieldId === fieldId) {
|
||||||
|
setSelectedFieldId(null);
|
||||||
|
}
|
||||||
|
}, [fields, selectedFieldId]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
// Save form fields via AJAX
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'formipay_save_form_fields');
|
||||||
|
formData.append('post_id', formId);
|
||||||
|
formData.append('fields', JSON.stringify(fields));
|
||||||
|
formData.append('_wpnonce', window.formipayAdmin?.nonce || '');
|
||||||
|
|
||||||
|
fetch(window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Show success message
|
||||||
|
console.log('Form saved successfully');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to save form:', result.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
});
|
||||||
|
}, [fields, formId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-form-builder">
|
||||||
|
<div className="formipay-builder-toolbar">
|
||||||
|
<h2>{ __('Form Builder', 'formipay') }</h2>
|
||||||
|
<div className="formipay-builder-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button button-secondary"
|
||||||
|
onClick={() => setFields([])}
|
||||||
|
>
|
||||||
|
{ __('Clear All', 'formipay') }
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button button-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{ __('Save Form', 'formipay') }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formipay-builder-content">
|
||||||
|
<FieldPalette onDragStart={() => {}} />
|
||||||
|
<FormCanvas
|
||||||
|
fields={fields}
|
||||||
|
selectedFieldId={selectedFieldId}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onSelectField={handleSelectField}
|
||||||
|
onUpdateField={handleUpdateField}
|
||||||
|
onMoveField={handleMoveField}
|
||||||
|
onDeleteField={handleDeleteField}
|
||||||
|
/>
|
||||||
|
<FormPreview fields={fields} />
|
||||||
|
<FieldSettingsPanel
|
||||||
|
field={selectedField}
|
||||||
|
open={!!selectedField}
|
||||||
|
onClose={() => setSelectedFieldId(null)}
|
||||||
|
onUpdate={handleUpdateField}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
src/admin/components/formBuilder/FormCanvas.css
Normal file
62
src/admin/components/formBuilder/FormCanvas.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
.formipay-form-canvas {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f6f7f7;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-canvas-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-canvas-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-canvas-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-canvas-area.is-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-empty-state svg {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
fill: #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-empty-state p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-fields-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
103
src/admin/components/formBuilder/FormCanvas.js
Normal file
103
src/admin/components/formBuilder/FormCanvas.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Form Canvas - Drop target for form fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon, plus } from '@wordpress/icons';
|
||||||
|
import { DEFAULT_FIELD_CONFIG, generateFieldId } from '../../config/fieldTypes';
|
||||||
|
import FormField from './FormField';
|
||||||
|
import './FormCanvas.css';
|
||||||
|
|
||||||
|
export default function FormCanvas({
|
||||||
|
fields = [],
|
||||||
|
selectedFieldId,
|
||||||
|
onDrop,
|
||||||
|
onSelectField,
|
||||||
|
onUpdateField,
|
||||||
|
onMoveField,
|
||||||
|
onDeleteField,
|
||||||
|
}) {
|
||||||
|
const handleDragOver = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const fieldType = event.dataTransfer.getData('formipay-field-type');
|
||||||
|
if (!fieldType) return;
|
||||||
|
|
||||||
|
const newField = {
|
||||||
|
...DEFAULT_FIELD_CONFIG,
|
||||||
|
field_type: fieldType,
|
||||||
|
field_id: generateFieldId(fieldType.replace('_', ' ')),
|
||||||
|
label: fieldType.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||||
|
};
|
||||||
|
|
||||||
|
onDrop?.(newField);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReorder = (dragIndex, dropIndex) => {
|
||||||
|
const newFields = [...fields];
|
||||||
|
const [draggedField] = newFields.splice(dragIndex, 1);
|
||||||
|
newFields.splice(dropIndex, 0, draggedField);
|
||||||
|
onMoveField?.(newFields);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-form-canvas">
|
||||||
|
<div className="formipay-canvas-header">
|
||||||
|
<h3>{ __('Form Fields', 'formipay') }</h3>
|
||||||
|
<span className="field-count">
|
||||||
|
{ fields.length } { __('fields', 'formipay') }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`formipay-canvas-area ${fields.length === 0 ? 'is-empty' : ''}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<div className="formipay-empty-state">
|
||||||
|
<Icon icon={plus} size={48} />
|
||||||
|
<p>
|
||||||
|
{ __('Drag fields from the palette to build your form', 'formipay') }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="formipay-fields-list">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormField
|
||||||
|
key={field.field_id}
|
||||||
|
field={field}
|
||||||
|
index={index}
|
||||||
|
isSelected={selectedFieldId === field.field_id}
|
||||||
|
onSelect={() => onSelectField?.(field.field_id)}
|
||||||
|
onUpdate={(updates) => onUpdateField?.(field.field_id, updates)}
|
||||||
|
onMoveUp={index > 0 ? () => handleReorder(index, index - 1) : null}
|
||||||
|
onMoveDown={index < fields.length - 1 ? () => handleReorder(index, index + 1) : null}
|
||||||
|
onDelete={() => onDeleteField?.(field.field_id)}
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('formipay-field-index', index.toString());
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const fromIndex = parseInt(e.dataTransfer.getData('formipay-field-index'));
|
||||||
|
if (!isNaN(fromIndex) && fromIndex !== index) {
|
||||||
|
handleReorder(fromIndex, index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
src/admin/components/formBuilder/FormField.css
Normal file
148
src/admin/components/formBuilder/FormField.css
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
.formipay-field-item {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdcde;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-item:hover {
|
||||||
|
border-color: #a7aaad;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-item.is-selected {
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #1e1e1e;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions .button-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions .button-icon:hover:not(:disabled) {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions .button-icon:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions .button-icon svg {
|
||||||
|
fill: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-actions .button-danger:hover svg {
|
||||||
|
fill: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-content {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label-row strong {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label-row em {
|
||||||
|
color: #a7aaad;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d63638;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-description {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-options-preview {
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-options-preview small {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-meta .meta-item {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
113
src/admin/components/formBuilder/FormField.js
Normal file
113
src/admin/components/formBuilder/FormField.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Form Field - Individual field component in canvas
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon, chevronUp, chevronDown, trash } from '@wordpress/icons';
|
||||||
|
import { FIELD_TYPES } from '../../config/fieldTypes';
|
||||||
|
import './FormField.css';
|
||||||
|
|
||||||
|
export default function FormField({
|
||||||
|
field,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
onUpdate,
|
||||||
|
onMoveUp,
|
||||||
|
onMoveDown,
|
||||||
|
onDelete,
|
||||||
|
onDragStart,
|
||||||
|
onDragOver,
|
||||||
|
onDrop,
|
||||||
|
}) {
|
||||||
|
const fieldConfig = FIELD_TYPES[field.field_type] || FIELD_TYPES.text;
|
||||||
|
const hasOptions = ['select', 'checkbox', 'radio'].includes(field.field_type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`formipay-field-item ${isSelected ? 'is-selected' : ''}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
|
<div className="formipay-field-header">
|
||||||
|
<div className="formipay-field-info">
|
||||||
|
<span className="field-type-badge">
|
||||||
|
{ fieldConfig.label }
|
||||||
|
</span>
|
||||||
|
<span className="field-id">
|
||||||
|
#{field.field_id || index}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="formipay-field-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-icon"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onMoveUp?.(); }}
|
||||||
|
disabled={!onMoveUp}
|
||||||
|
title={ __('Move Up', 'formipay') }
|
||||||
|
>
|
||||||
|
<Icon icon={chevronUp} size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-icon"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onMoveDown?.(); }}
|
||||||
|
disabled={!onMoveDown}
|
||||||
|
title={ __('Move Down', 'formipay') }
|
||||||
|
>
|
||||||
|
<Icon icon={chevronDown} size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-icon button-danger"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onDelete?.(); }}
|
||||||
|
title={ __('Delete', 'formipay') }
|
||||||
|
>
|
||||||
|
<Icon icon={trash} size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formipay-field-content">
|
||||||
|
<div className="field-label-row">
|
||||||
|
<strong>{ field.label || <em>Untitled</em> }</strong>
|
||||||
|
{ field.is_required && (
|
||||||
|
<span className="required-badge">
|
||||||
|
{ __('Required', 'formipay') }
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ field.description && (
|
||||||
|
<p className="field-description">
|
||||||
|
{ field.description }
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ hasOptions && field.field_options && field.field_options.length > 0 && (
|
||||||
|
<div className="field-options-preview">
|
||||||
|
<small>
|
||||||
|
{ field.field_options.length } { __('options', 'formipay') }
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="field-meta">
|
||||||
|
{ field.placeholder && (
|
||||||
|
<span className="meta-item">
|
||||||
|
Placeholder: "{ field.placeholder }"
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ field.default_value && (
|
||||||
|
<span className="meta-item">
|
||||||
|
Default: "{ field.default_value }"
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/admin/components/formBuilder/FormFieldOptions.css
Normal file
65
src/admin/components/formBuilder/FormFieldOptions.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.formipay-field-options {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-options-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-options-header label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-options-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-option-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-option-fields {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-option-fields .components-base-control {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-option-fields .components-base-control__label {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-option-actions {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-no-options {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px dashed #dcdcde;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-no-options p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
105
src/admin/components/formBuilder/FormFieldOptions.js
Normal file
105
src/admin/components/formBuilder/FormFieldOptions.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Form Field Options - Manage select/radio/checkbox options
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { TextControl, Button } from '@wordpress/components';
|
||||||
|
import { Icon as WPIcon, plus } from '@wordpress/icons';
|
||||||
|
|
||||||
|
export default function FormFieldOptions({ options = [], onChange, fieldType }) {
|
||||||
|
const handleAddOption = () => {
|
||||||
|
const newOption = {
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
amount: '',
|
||||||
|
weight: '',
|
||||||
|
quantity: false,
|
||||||
|
};
|
||||||
|
onChange([...options, newOption]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateOption = (index, updates) => {
|
||||||
|
const newOptions = [...options];
|
||||||
|
newOptions[index] = { ...newOptions[index], ...updates };
|
||||||
|
onChange(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOption = (index) => {
|
||||||
|
const newOptions = options.filter((_, i) => i !== index);
|
||||||
|
onChange(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMultiSelect = fieldType === 'checkbox';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-field-options">
|
||||||
|
<div className="formipay-options-header">
|
||||||
|
<label>
|
||||||
|
{ isMultiSelect
|
||||||
|
? __('Checkbox Options', 'formipay')
|
||||||
|
: fieldType === 'radio'
|
||||||
|
? __('Radio Options', 'formipay')
|
||||||
|
: __('Select Options', 'formipay')
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="compact"
|
||||||
|
onClick={handleAddOption}
|
||||||
|
icon={<WPIcon icon={plus} size={16} />}
|
||||||
|
>
|
||||||
|
{ __('Add Option', 'formipay') }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formipay-options-list">
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div key={index} className="formipay-option-item">
|
||||||
|
<div className="formipay-option-fields">
|
||||||
|
<TextControl
|
||||||
|
label={__('Label', 'formipay')}
|
||||||
|
value={option.label || ''}
|
||||||
|
onChange={(value) => handleUpdateOption(index, { label: value })}
|
||||||
|
placeholder={__('Option label', 'formipay')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextControl
|
||||||
|
label={__('Value', 'formipay')}
|
||||||
|
value={option.value || ''}
|
||||||
|
onChange={(value) => handleUpdateOption(index, { value: value })}
|
||||||
|
placeholder={__('Optional custom value', 'formipay')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextControl
|
||||||
|
label={__('Amount', 'formipay')}
|
||||||
|
type="number"
|
||||||
|
value={option.amount || ''}
|
||||||
|
onChange={(value) => handleUpdateOption(index, { amount: value })}
|
||||||
|
placeholder={__('0', 'formipay')}
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formipay-option-actions">
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
size="compact"
|
||||||
|
onClick={() => handleDeleteOption(index)}
|
||||||
|
icon={<WPIcon icon={trash} size={16} />}
|
||||||
|
label={__('Delete Option', 'formipay')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{options.length === 0 && (
|
||||||
|
<div className="formipay-no-options">
|
||||||
|
<p>
|
||||||
|
{ __('No options added yet. Click "Add Option" to create one.', 'formipay') }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
src/admin/components/formBuilder/FormFieldPreview.css
Normal file
87
src/admin/components/formBuilder/FormFieldPreview.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
.formipay-preview-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-label .required {
|
||||||
|
color: #d63638;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-input,
|
||||||
|
.formipay-textarea,
|
||||||
|
.formipay-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #8c8f94;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-input:focus,
|
||||||
|
.formipay-textarea:focus,
|
||||||
|
.formipay-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: 0 0 0 1px #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio-group,
|
||||||
|
.formipay-checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-radio-label,
|
||||||
|
.formipay-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-field-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #c3c4c7;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-page-break {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px dashed #c3c4c7;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
142
src/admin/components/formBuilder/FormFieldPreview.js
Normal file
142
src/admin/components/formBuilder/FormFieldPreview.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Form Field Preview - Preview individual form fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import './FormFieldPreview.css';
|
||||||
|
|
||||||
|
export default function FormFieldPreview({ field }) {
|
||||||
|
const fieldType = field.field_type || 'text';
|
||||||
|
const label = field.label || '';
|
||||||
|
const placeholder = field.placeholder || '';
|
||||||
|
const defaultValue = field.default_value || '';
|
||||||
|
const description = field.description || '';
|
||||||
|
const isRequired = field.is_required || false;
|
||||||
|
const fieldId = field.field_id || `field_${Math.random().toString(36).slice(2, 11)}`;
|
||||||
|
|
||||||
|
const renderFieldInput = () => {
|
||||||
|
const commonProps = {
|
||||||
|
id: fieldId,
|
||||||
|
name: fieldId,
|
||||||
|
placeholder,
|
||||||
|
defaultValue,
|
||||||
|
required: isRequired,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (fieldType) {
|
||||||
|
case 'text':
|
||||||
|
case 'url':
|
||||||
|
case 'email':
|
||||||
|
case 'tel':
|
||||||
|
case 'number':
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'color':
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={fieldType}
|
||||||
|
className="formipay-input"
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className="formipay-textarea"
|
||||||
|
rows={4}
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
return (
|
||||||
|
<select className="formipay-select" {...commonProps}>
|
||||||
|
<option value="">{ __('Select an option', 'formipay') }</option>
|
||||||
|
{field.field_options?.map((option, index) => (
|
||||||
|
<option
|
||||||
|
key={index}
|
||||||
|
value={option.value || option.label}
|
||||||
|
>
|
||||||
|
{ option.label }
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'radio':
|
||||||
|
return (
|
||||||
|
<div className="formipay-radio-group">
|
||||||
|
{field.field_options?.map((option, index) => (
|
||||||
|
<label key={index} className="formipay-radio-label">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={fieldId}
|
||||||
|
value={option.value || option.label}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
<span>{ option.label }</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
return (
|
||||||
|
<div className="formipay-checkbox-group">
|
||||||
|
{field.field_options?.map((option, index) => (
|
||||||
|
<label key={index} className="formipay-checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={`${fieldId}_${index}`}
|
||||||
|
value={option.value || option.label}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
<span>{ option.label }</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'divider':
|
||||||
|
return <hr className="formipay-divider" />;
|
||||||
|
|
||||||
|
case 'page_break':
|
||||||
|
return <div className="formipay-page-break">{ __('Page Break', 'formipay') }</div>;
|
||||||
|
|
||||||
|
case 'country_list':
|
||||||
|
return (
|
||||||
|
<select className="formipay-select" {...commonProps}>
|
||||||
|
<option value="">{ __('Select country', 'formipay') }</option>
|
||||||
|
<option value="United States">United States</option>
|
||||||
|
<option value="Indonesia">Indonesia</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <input type="text" className="formipay-input" {...commonProps} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLayout = ['divider', 'page_break'].includes(fieldType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`formipay-field-preview formipay-field-preview--${fieldType}`}>
|
||||||
|
{!isLayout && label && (
|
||||||
|
<label htmlFor={fieldId} className="formipay-field-label">
|
||||||
|
{ label }
|
||||||
|
{ isRequired && <span className="required">*</span> }
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ renderFieldInput() }
|
||||||
|
|
||||||
|
{ description && !isLayout && (
|
||||||
|
<p className="formipay-field-description">
|
||||||
|
{ description }
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/admin/components/formBuilder/FormPreview.css
Normal file
39
src/admin/components/formBuilder/FormPreview.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.formipay-form-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-preview-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-preview-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-preview-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-preview-content.is-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-preview-content.is-empty p {
|
||||||
|
color: #646970;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
40
src/admin/components/formBuilder/FormPreview.js
Normal file
40
src/admin/components/formBuilder/FormPreview.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Form Preview - Live preview of form rendering
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import FormFieldPreview from './FormFieldPreview';
|
||||||
|
import './FormPreview.css';
|
||||||
|
|
||||||
|
export default function FormPreview({ fields }) {
|
||||||
|
if (!fields || fields.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="formipay-form-preview">
|
||||||
|
<div className="formipay-preview-header">
|
||||||
|
<h4>{ __('Live Preview', 'formipay') }</h4>
|
||||||
|
</div>
|
||||||
|
<div className="formipay-preview-content is-empty">
|
||||||
|
<p>{ __('Add fields to see the preview', 'formipay') }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-form-preview">
|
||||||
|
<div className="formipay-preview-header">
|
||||||
|
<h4>{ __('Live Preview', 'formipay') }</h4>
|
||||||
|
</div>
|
||||||
|
<div className="formipay-preview-content">
|
||||||
|
<form className="formipay-preview-form">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormFieldPreview
|
||||||
|
key={field.field_id || index}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/admin/config/fieldTypes.js
Normal file
67
src/admin/config/fieldTypes.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Form Field Type Definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const FIELD_TYPES = {
|
||||||
|
text: { label: 'Text', icon: 'text', category: 'input' },
|
||||||
|
url: { label: 'URL', icon: 'link', category: 'input' },
|
||||||
|
email: { label: 'Email', icon: 'email', category: 'input' },
|
||||||
|
tel: { label: 'Telephone', icon: 'phone', category: 'input' },
|
||||||
|
number: { label: 'Number', icon: 'number', category: 'input' },
|
||||||
|
date: { label: 'Date', icon: 'calendar', category: 'input' },
|
||||||
|
datetime: { label: 'Date & Time', icon: 'calendar-alt', category: 'input' },
|
||||||
|
color: { label: 'Color', icon: 'art', category: 'input' },
|
||||||
|
select: { label: 'Select Dropdown', icon: 'list-view', category: 'choice' },
|
||||||
|
checkbox: { label: 'Checkbox', icon: 'checkbox', category: 'choice' },
|
||||||
|
radio: { label: 'Radio', icon: 'radio', category: 'choice' },
|
||||||
|
hidden: { label: 'Hidden', icon: 'hidden', category: 'advanced' },
|
||||||
|
textarea: { label: 'Textarea', icon: 'document', category: 'input' },
|
||||||
|
divider: { label: 'Divider', icon: 'minus', category: 'layout' },
|
||||||
|
page_break: { label: 'Page Break', icon: 'page-break', category: 'layout' },
|
||||||
|
country_list: { label: 'Preset: Country List', icon: 'globe', category: 'preset' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FIELD_CATEGORIES = {
|
||||||
|
input: { label: 'Input Fields', order: 1 },
|
||||||
|
choice: { label: 'Choice Fields', order: 2 },
|
||||||
|
layout: { label: 'Layout', order: 3 },
|
||||||
|
preset: { label: 'Presets', order: 4 },
|
||||||
|
advanced: { label: 'Advanced', order: 5 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_FIELD_CONFIG = {
|
||||||
|
field_type: 'text',
|
||||||
|
label: '',
|
||||||
|
field_id: '',
|
||||||
|
placeholder: '',
|
||||||
|
default_value: '',
|
||||||
|
description: '',
|
||||||
|
is_required: false,
|
||||||
|
field_options: [],
|
||||||
|
option_grid_columns: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field types grouped by category
|
||||||
|
*/
|
||||||
|
export function getFieldTypesByCategory() {
|
||||||
|
const grouped = {};
|
||||||
|
|
||||||
|
Object.entries(FIELD_TYPES).forEach(([type, config]) => {
|
||||||
|
if (!grouped[config.category]) {
|
||||||
|
grouped[config.category] = [];
|
||||||
|
}
|
||||||
|
grouped[config.category].push({ type, ...config });
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique field ID
|
||||||
|
*/
|
||||||
|
export function generateFieldId(label) {
|
||||||
|
const base = label.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
||||||
|
const timestamp = Date.now().toString(36);
|
||||||
|
return `${base}_${timestamp}`;
|
||||||
|
}
|
||||||
@@ -1,14 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Forms Page - Placeholder
|
* Forms Page - List view and Form Builder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import FormBuilder from '../components/formBuilder/FormBuilder';
|
||||||
|
|
||||||
export default function FormsPage({ initialData }) {
|
export default function FormsPage({ initialData }) {
|
||||||
|
const [isBuilder, setIsBuilder] = useState(false);
|
||||||
|
const [selectedFormId, setSelectedFormId] = useState(null);
|
||||||
|
|
||||||
|
if (isBuilder) {
|
||||||
return (
|
return (
|
||||||
<div className="formipay-page-formipay-page">
|
<div className="formipay-page-forms">
|
||||||
|
<FormBuilder
|
||||||
|
formId={selectedFormId}
|
||||||
|
initialData={initialData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="formipay-page-forms">
|
||||||
|
<div className="formipay-forms-list">
|
||||||
<h1>{ __('Forms', 'formipay') }</h1>
|
<h1>{ __('Forms', 'formipay') }</h1>
|
||||||
<p>{ __('Page content coming soon...', 'formipay') }</p>
|
<p>{ __('Forms list coming soon. Use the classic editor for now.', 'formipay') }</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user