628 lines
31 KiB
HTML
628 lines
31 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Edit Form - {{ form.title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="min-h-screen bg-gray-50">
|
|
<!-- Edit Mode Header -->
|
|
<div class="bg-gradient-to-r from-temple-red/10 to-temple-red/5 border-b border-temple-red/20">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<i data-lucide="edit-3" class="w-6 h-6 text-temple-red"></i>
|
|
<div>
|
|
<span class="text-sm font-semibold text-temple-red block">Edit Mode</span>
|
|
<strong class="text-gray-900">{{ form.title }}</strong>
|
|
<br>
|
|
<span class="text-sm text-gray-500">Last updated: {{ form.updated_at|date:"M d, Y g:i A" }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="{% url 'form_preview' form.id %}" class="inline-flex items-center gap-2 border border-temple-red text-temple-red hover:bg-temple-red hover:text-white px-4 py-2 rounded-xl text-sm transition font-medium" target="_blank">
|
|
<i data-lucide="eye" class="w-4 h-4"></i> Preview
|
|
</a>
|
|
<a href="{% url 'form_list' %}" class="inline-flex items-center gap-2 border border-gray-300 text-gray-600 hover:bg-gray-100 px-4 py-2 rounded-xl text-sm transition">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> Back to Forms
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Builder Container -->
|
|
<div id="form-builder-app"></div>
|
|
</div>
|
|
{% endblock %}
|
|
<!-- Form Data Script -->
|
|
<script type="application/json" id="form-data">
|
|
{
|
|
"id": {{ form.id|safe }},
|
|
"title": {{ form.title|safe }},
|
|
"description": {{ form.description|safe }},
|
|
"structure": {{ form.structure|safe }},
|
|
"created_at": "{{ form.created_at|date:'c' }}",
|
|
"updated_at": "{{ form.updated_at|date:'c' }}"
|
|
}
|
|
</script>
|
|
|
|
<!-- Enhanced Form Builder for Edit Mode -->
|
|
<script type="module" src="https://unpkg.com/preact@10.19.6/dist/preact.module.js"></script>
|
|
<script type="module" src="https://unpkg.com/htm@3.1.1/dist/htm.module.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
|
|
<!-- FilePond for file uploads -->
|
|
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
|
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
|
|
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
|
|
<script src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
|
|
|
|
<script type="module">
|
|
const { h, Component, render, useState, useEffect, useRef } = preact;
|
|
const html = htm.bind(h);
|
|
|
|
// Field Types Configuration
|
|
const FIELD_TYPES = [
|
|
{ type: 'text', name: 'Text Input', icon: 'T' },
|
|
{ type: 'email', name: 'Email Input', icon: '@' },
|
|
{ type: 'phone', name: 'Phone Input', icon: '📞' },
|
|
{ type: 'date', name: 'Date Picker', icon: '📅' },
|
|
{ type: 'file', name: 'File Upload', icon: '📎' },
|
|
{ type: 'dropdown', name: 'Dropdown', icon: '▼' },
|
|
{ type: 'radio', name: 'Radio Buttons', icon: '○' },
|
|
{ type: 'checkbox', name: 'Checkboxes', icon: '☐' },
|
|
{ type: 'rating', name: 'Rating', icon: '⭐' }
|
|
];
|
|
|
|
// Field Components
|
|
class BaseField extends Component {
|
|
render() {
|
|
const { field, onSelect, onRemove, onMoveUp, onMoveDown, isSelected, index, totalFields } = this.props;
|
|
|
|
return html`
|
|
<div class="bg-white border ${isSelected ? 'border-temple-red ring-2 ring-temple-red/20' : 'border-gray-200 hover:border-temple-red'} rounded-xl p-5 mb-5 transition-all" onClick=${() => onSelect(field.id)}>
|
|
<div class="flex justify-between items-center mb-4">
|
|
<span class="font-bold text-gray-900 flex items-center gap-2">
|
|
${field.label || 'Untitled Field'}
|
|
${field.required ? html`<span class="text-red-500">*</span>` : ''}
|
|
</span>
|
|
<div class="flex gap-1">
|
|
<button class="p-1 text-gray-400 hover:text-temple-red transition" onClick=${(e) => { e.stopPropagation(); onMoveUp(index); }} disabled=${index === 0}>⬆️</button>
|
|
<button class="p-1 text-gray-400 hover:text-temple-red transition" onClick=${(e) => { e.stopPropagation(); onMoveDown(index); }} disabled=${index === totalFields - 1}>⬇️</button>
|
|
<button class="p-1 text-gray-400 hover:text-red-500 transition" onClick=${(e) => { e.stopPropagation(); onRemove(index); }}>🗑️</button>
|
|
</div>
|
|
</div>
|
|
${this.renderFieldContent()}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class TextField extends BaseField {
|
|
renderFieldContent() {
|
|
return html`<input type="text" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" placeholder=${this.props.field.placeholder || ''} />`;
|
|
}
|
|
}
|
|
|
|
class EmailField extends BaseField {
|
|
renderFieldContent() {
|
|
return html`<input type="email" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" placeholder=${this.props.field.placeholder || ''} />`;
|
|
}
|
|
}
|
|
|
|
class PhoneField extends BaseField {
|
|
renderFieldContent() {
|
|
return html`<input type="tel" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" placeholder=${this.props.field.placeholder || ''} />`;
|
|
}
|
|
}
|
|
|
|
class DateField extends BaseField {
|
|
renderFieldContent() {
|
|
return html`<input type="date" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" />`;
|
|
}
|
|
}
|
|
|
|
class FileField extends BaseField {
|
|
constructor(props) {
|
|
super(props);
|
|
this.filePondRef = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initFilePond();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.filePondRef) {
|
|
this.filePondRef.destroy();
|
|
}
|
|
}
|
|
|
|
initFilePond() {
|
|
const { field } = this.props;
|
|
const inputElement = document.getElementById(`file-upload-${field.id}`);
|
|
|
|
if (!inputElement || typeof FilePond === 'undefined') return;
|
|
|
|
const acceptedFileTypes = field.fileTypesString
|
|
? field.fileTypesString.split(',').map(type => type.trim())
|
|
: ['*'];
|
|
|
|
this.filePondRef = FilePond.create(inputElement, {
|
|
allowMultiple: field.multiple || false,
|
|
maxFiles: field.maxFiles || 1,
|
|
maxFileSize: field.maxFileSize ? `${field.maxFileSize}MB` : '5MB',
|
|
acceptedFileTypes: acceptedFileTypes,
|
|
labelIdle: 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
|
|
credits: false
|
|
});
|
|
}
|
|
|
|
renderFieldContent() {
|
|
const { field } = this.props;
|
|
return html`
|
|
<div>
|
|
<input id="file-upload-${field.id}" type="file" />
|
|
${field.fileTypes ? html`<div class="text-sm text-gray-500 mt-2">Accepted: ${field.fileTypes.join(', ')}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class DropdownField extends BaseField {
|
|
renderFieldContent() {
|
|
const { field } = this.props;
|
|
return html`
|
|
<select class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm bg-white focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition">
|
|
${field.options.map(option => html`<option>${option.value}</option>`)}
|
|
</select>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class RadioField extends BaseField {
|
|
renderFieldContent() {
|
|
const { field } = this.props;
|
|
return html`
|
|
<div class="space-y-2 mt-3">
|
|
${field.options.map(option => html`
|
|
<div class="flex items-center gap-2">
|
|
<input type="radio" name=${field.id} id=${option.id} class="w-4 h-4 text-temple-red border-gray-300 focus:ring-2 focus:ring-temple-red/20" />
|
|
<label for=${option.id} class="text-sm text-gray-700 cursor-pointer">${option.value}</label>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class CheckboxField extends BaseField {
|
|
renderFieldContent() {
|
|
const { field } = this.props;
|
|
return html`
|
|
<div class="space-y-2 mt-3">
|
|
${field.options.map(option => html`
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" id=${option.id} class="w-4 h-4 text-temple-red border-gray-300 rounded focus:ring-2 focus:ring-temple-red/20" />
|
|
<label for=${option.id} class="text-sm text-gray-700 cursor-pointer">${option.value}</label>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class RatingField extends BaseField {
|
|
renderFieldContent() {
|
|
return html`
|
|
<div class="flex gap-1 mt-3">
|
|
${[1, 2, 3, 4, 5].map(n => html`
|
|
<span style="font-size: 24px; color: #fbbf24; cursor: pointer;">★</span>
|
|
`)}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Field Factory
|
|
function createFieldComponent(field, props) {
|
|
const fieldProps = { ...props, field };
|
|
|
|
switch (field.type) {
|
|
case 'text': return html`<${TextField} ...${fieldProps} />`;
|
|
case 'email': return html`<${EmailField} ...${fieldProps} />`;
|
|
case 'phone': return html`<${PhoneField} ...${fieldProps} />`;
|
|
case 'date': return html`<${DateField} ...${fieldProps} />`;
|
|
case 'file': return html`<${FileField} ...${fieldProps} />`;
|
|
case 'dropdown': return html`<${DropdownField} ...${fieldProps} />`;
|
|
case 'radio': return html`<${RadioField} ...${fieldProps} />`;
|
|
case 'checkbox': return html`<${CheckboxField} ...${fieldProps} />`;
|
|
case 'rating': return html`<${RatingField} ...${fieldProps} />`;
|
|
default: return html`<${TextField} ...${fieldProps} />`;
|
|
}
|
|
}
|
|
|
|
// Edit Form Builder Component
|
|
class EditFormBuilder extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const formDataElement = document.getElementById('form-data');
|
|
let formData;
|
|
|
|
try {
|
|
formData = JSON.parse(formDataElement.textContent);
|
|
if (!formData.structure || !formData.structure.wizards) {
|
|
throw new Error('Invalid form data structure');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading form data:', error);
|
|
alert('Error loading form data. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
this.state = {
|
|
form: formData.structure,
|
|
originalForm: JSON.parse(JSON.stringify(formData.structure)),
|
|
formId: formData.id,
|
|
isDirty: false,
|
|
isSaving: false,
|
|
isDragOver: false,
|
|
selectedFieldId: null,
|
|
currentWizardId: formData.structure.wizards?.[0]?.id || 'wizard-1'
|
|
};
|
|
|
|
this.canvasRef = null;
|
|
this.sortableInstance = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initializeDragAndDrop();
|
|
|
|
if (typeof FilePond !== 'undefined') {
|
|
FilePond.registerPlugin(
|
|
FilePondPluginFileValidateType,
|
|
FilePondPluginFileValidateSize
|
|
);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
if (this.canvasRef && !this.sortableInstance) {
|
|
this.initializeDragAndDrop();
|
|
}
|
|
}
|
|
|
|
initializeDragAndDrop() {
|
|
if (!this.canvasRef || typeof Sortable === 'undefined') return;
|
|
|
|
this.sortableInstance = Sortable.create(this.canvasRef, {
|
|
animation: 150,
|
|
handle: '.bg-white',
|
|
onEnd: (evt) => {
|
|
const { oldIndex, newIndex } = evt;
|
|
const currentWizard = this.getCurrentWizard();
|
|
const field = currentWizard.fields[oldIndex];
|
|
currentWizard.fields.splice(oldIndex, 1);
|
|
currentWizard.fields.splice(newIndex, 0, field);
|
|
this.setState({ isDirty: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
getCurrentWizard() {
|
|
return this.state.form.wizards.find(wizard => wizard.id === this.state.currentWizardId) || null;
|
|
}
|
|
|
|
getCurrentWizardFields() {
|
|
const wizard = this.getCurrentWizard();
|
|
return wizard ? wizard.fields : [];
|
|
}
|
|
|
|
getSelectedField() {
|
|
for (const wizard of this.state.form.wizards) {
|
|
const field = wizard.fields.find(field => field.id === this.state.selectedFieldId);
|
|
if (field) return field;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
onDragStart = (e, fieldType) => {
|
|
e.dataTransfer.setData('text/plain', fieldType);
|
|
}
|
|
|
|
onDragOver = (e) => {
|
|
e.preventDefault();
|
|
this.setState({ isDragOver: true });
|
|
}
|
|
|
|
onDragLeave = () => {
|
|
this.setState({ isDragOver: false });
|
|
}
|
|
|
|
onDrop = (e) => {
|
|
e.preventDefault();
|
|
this.setState({ isDragOver: false });
|
|
const fieldType = e.dataTransfer.getData('text/plain');
|
|
this.addField(fieldType);
|
|
}
|
|
|
|
addField = (type) => {
|
|
const field = {
|
|
id: 'field-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
|
|
type: type,
|
|
label: this.getDefaultLabel(type),
|
|
placeholder: this.getDefaultPlaceholder(type),
|
|
required: false,
|
|
wizardId: this.state.currentWizardId,
|
|
options: type === 'dropdown' || type === 'radio' || type === 'checkbox'
|
|
? [{ id: 'opt-1', value: 'Option 1' }, { id: 'opt-2', value: 'Option 2' }]
|
|
: []
|
|
};
|
|
|
|
if (type === 'file') {
|
|
field.fileTypes = ['image/*', '.pdf', '.doc', '.docx'];
|
|
field.fileTypesString = 'image/*, .pdf, .doc, .docx';
|
|
field.maxFileSize = 5;
|
|
field.maxFiles = 1;
|
|
field.multiple = false;
|
|
}
|
|
|
|
const currentWizard = this.getCurrentWizard();
|
|
currentWizard.fields.push(field);
|
|
this.setState({ selectedFieldId: field.id, isDirty: true });
|
|
}
|
|
|
|
getDefaultLabel(type) {
|
|
const labels = {
|
|
text: 'Text Input',
|
|
email: 'Email Address',
|
|
phone: 'Phone Number',
|
|
date: 'Select Date',
|
|
file: 'Upload Files',
|
|
dropdown: 'Select Option',
|
|
radio: 'Choose One',
|
|
checkbox: 'Select Options',
|
|
rating: 'Rate Your Experience'
|
|
};
|
|
return labels[type] || 'Field Label';
|
|
}
|
|
|
|
getDefaultPlaceholder(type) {
|
|
const placeholders = {
|
|
text: 'Enter text here...',
|
|
email: 'your.email@example.com',
|
|
phone: '(123) 456-7890'
|
|
};
|
|
return placeholders[type] || '';
|
|
}
|
|
|
|
selectField = (fieldId) => {
|
|
this.setState({ selectedFieldId: fieldId });
|
|
}
|
|
|
|
removeField = (index) => {
|
|
const currentWizard = this.getCurrentWizard();
|
|
currentWizard.fields.splice(index, 1);
|
|
if (currentWizard.fields[index - 1]) {
|
|
this.setState({ selectedFieldId: currentWizard.fields[index - 1].id, isDirty: true });
|
|
} else {
|
|
this.setState({ selectedFieldId: null, isDirty: true });
|
|
}
|
|
}
|
|
|
|
moveFieldUp = (index) => {
|
|
if (index > 0) {
|
|
const currentWizard = this.getCurrentWizard();
|
|
const field = currentWizard.fields[index];
|
|
currentWizard.fields.splice(index, 1);
|
|
currentWizard.fields.splice(index - 1, 0, field);
|
|
this.setState({ isDirty: true });
|
|
}
|
|
}
|
|
|
|
moveFieldDown = (index) => {
|
|
const currentWizard = this.getCurrentWizard();
|
|
if (index < currentWizard.fields.length - 1) {
|
|
const field = currentWizard.fields[index];
|
|
currentWizard.fields.splice(index, 1);
|
|
currentWizard.fields.splice(index + 1, 0, field);
|
|
this.setState({ isDirty: true });
|
|
}
|
|
}
|
|
|
|
selectWizard = (wizardId) => {
|
|
this.setState({ currentWizardId: wizardId, selectedFieldId: null });
|
|
}
|
|
|
|
updateForm = async () => {
|
|
try {
|
|
this.setState({ isSaving: true });
|
|
const response = await fetch(`/api/forms/${this.state.formId}/update/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': (() => {
|
|
const cookieName = 'csrftoken';
|
|
const name = cookieName + '=';
|
|
const ca = document.cookie.split(';');
|
|
for (const c of ca) {
|
|
if (c.trim().startsWith(name)) {
|
|
return c.substring(name.length, c.length);
|
|
}
|
|
}
|
|
return '';
|
|
})()
|
|
},
|
|
body: JSON.stringify({ form: this.state.form })
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
this.setState({ isSaving: false, isDirty: false, originalForm: JSON.parse(JSON.stringify(this.state.form)) });
|
|
alert('Form updated successfully!');
|
|
} else {
|
|
throw new Error(result.error || 'Failed to update form');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating form:', error);
|
|
this.setState({ isSaving: false });
|
|
alert('Error updating form: ' + error.message);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const currentWizard = this.getCurrentWizard();
|
|
const currentWizardFields = this.getCurrentWizardFields();
|
|
const selectedField = this.getSelectedField();
|
|
|
|
return html`
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
<!-- Left Sidebar -->
|
|
<div class="lg:col-span-1">
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5 mb-6">
|
|
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
<i data-lucide="layout" class="w-5 h-5 text-temple-red"></i> Field Types
|
|
</h3>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
${FIELD_TYPES.map(fieldType => html`
|
|
<div class="bg-gray-50 border border-gray-200 rounded-xl p-3 cursor-grab hover:bg-white hover:border-temple-red hover:shadow-md transition-all"
|
|
draggable="true"
|
|
onDragStart=${(e) => this.onDragStart(e, fieldType.type)}>
|
|
<div class="w-8 h-8 bg-temple-red/10 rounded-lg flex items-center justify-center mb-2 text-sm">${fieldType.icon}</div>
|
|
<span class="text-xs font-semibold text-gray-700 block">${fieldType.name}</span>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
|
|
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
<i data-lucide="layers" class="w-5 h-5 text-temple-red"></i> Form Steps
|
|
</h3>
|
|
<div class="space-y-2 mb-4">
|
|
${this.state.form.wizards.map((wizard, index) => html`
|
|
<div class="flex items-center justify-between p-2 rounded-lg ${wizard.id === this.state.currentWizardId ? 'bg-temple-red text-white' : 'bg-gray-50 hover:bg-gray-100'} cursor-pointer transition"
|
|
onClick=${() => this.selectWizard(wizard.id)}>
|
|
<span class="text-sm font-medium">${wizard.title || 'Untitled Step'}</span>
|
|
${this.state.form.wizards.length > 1 ? html`
|
|
<button class="text-xs hover:opacity-80 transition"
|
|
onClick=${(e) => { e.stopPropagation(); this.state.form.wizards.splice(index, 1); this.setState({ currentWizardId: this.state.form.wizards[0].id, isDirty: true }); }}>
|
|
🗑️
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
`)}
|
|
</div>
|
|
<button class="w-full border border-dashed border-gray-300 text-gray-600 hover:border-temple-red hover:text-temple-red px-3 py-2 rounded-xl text-sm font-medium transition">
|
|
Add Step
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center Canvas -->
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 min-h-[600px]">
|
|
${currentWizard ? html`
|
|
<div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
|
|
<div class="text-xl font-bold text-temple-red">${currentWizard.title || 'Untitled Step'}</div>
|
|
<input type="text" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" placeholder="Step Title" value=${currentWizard.title || ''} onInput=${(e) => { currentWizard.title = e.target.value; this.setState({ isDirty: true }); }} />
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="border-2 border-dashed border-gray-200 rounded-xl p-8 text-center mb-6 ${this.state.isDragOver ? 'border-temple-red bg-temple-red/5' : ''}"
|
|
onDragOver=${this.onDragOver}
|
|
onDragLeave=${this.onDragLeave}
|
|
onDrop=${this.onDrop}>
|
|
<p class="text-gray-500 text-sm">Drag and drop fields here to build your form</p>
|
|
</div>
|
|
|
|
<div ref=${el => this.canvasRef = el}>
|
|
${currentWizardFields.map((field, index) =>
|
|
createFieldComponent(field, {
|
|
key: field.id,
|
|
onSelect: this.selectField,
|
|
onRemove: this.removeField,
|
|
onMoveUp: this.moveFieldUp,
|
|
onMoveDown: this.moveFieldDown,
|
|
isSelected: this.state.selectedFieldId === field.id,
|
|
index: index,
|
|
totalFields: currentWizardFields.length
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Sidebar -->
|
|
<div class="lg:col-span-1">
|
|
${selectedField ? html`
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5 sticky top-4">
|
|
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
<i data-lucide="settings" class="w-5 h-5 text-temple-red"></i> Field Properties
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Label</label>
|
|
<input type="text" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" value=${selectedField?.label || ''} onInput=${(e) => { if (selectedField) { selectedField.label = e.target.value; this.setState({ isDirty: true }); } }} />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Placeholder</label>
|
|
<input type="text" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition" value=${selectedField?.placeholder || ''} onInput=${(e) => { if (selectedField) { selectedField.placeholder = e.target.value; this.setState({ isDirty: true }); } }} />
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<input type="checkbox" id="required" checked=${selectedField?.required || false} class="w-4 h-4 text-temple-red border-gray-300 rounded focus:ring-2 focus:ring-temple-red/20" onChange=${(e) => { if (selectedField) { selectedField.required = e.target.checked; this.setState({ isDirty: true }); } }} />
|
|
<label for="required" class="text-sm font-medium text-gray-700 cursor-pointer">Required Field</label>
|
|
</div>
|
|
|
|
<button class="w-full border border-red-300 text-red-600 hover:bg-red-50 px-4 py-2.5 rounded-xl text-sm font-semibold transition"
|
|
onClick=${() => {
|
|
const currentWizard = this.getCurrentWizard();
|
|
const index = currentWizard.fields.findIndex(f => f.id === this.state.selectedFieldId);
|
|
if (index !== -1) {
|
|
this.removeField(index);
|
|
}
|
|
}}>
|
|
Delete Field
|
|
</button>
|
|
</div>
|
|
</div>
|
|
` : html`
|
|
<div class="bg-gray-50 rounded-2xl border border-gray-200 p-8 text-center">
|
|
<i data-lucide="mouse-pointer-2" class="w-12 h-12 text-gray-400 mx-auto mb-3"></i>
|
|
<p class="text-sm text-gray-500">Select a field to edit its properties</p>
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer Actions -->
|
|
<div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div class="flex items-center gap-4 text-sm text-gray-600">
|
|
<span><strong class="text-gray-900">${this.state.form.wizards.length}</strong> Steps</span>
|
|
<span><strong class="text-gray-900">${this.state.form.wizards.reduce((total, wizard) => total + wizard.fields.length, 0)}</strong> Fields</span>
|
|
${this.state.isDirty ? html`<span class="inline-flex items-center gap-1 px-2 py-1 bg-amber-100 text-amber-800 rounded-lg text-xs font-bold uppercase"><i data-lucide="alert-circle" class="w-3 h-3"></i> Unsaved</span>` : ''}
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button class="inline-flex items-center gap-2 border border-temple-red text-temple-red hover:bg-temple-red hover:text-white px-6 py-2.5 rounded-xl text-sm font-semibold transition ${this.state.isSaving || !this.state.isDirty ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
onClick=${this.updateForm} disabled=${this.state.isSaving || !this.state.isDirty}>
|
|
${this.state.isSaving ? html`<span class="inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></span> Saving...` : html`<i data-lucide="save" class="w-4 h-4"></i> Save Changes`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Render edit form builder
|
|
render(html`<${EditFormBuilder} />`, document.getElementById('form-builder-app'));
|
|
lucide.createIcons();
|
|
</script>
|
|
{% endblock %} |