1020 lines
26 KiB
HTML
1020 lines
26 KiB
HTML
{% extends 'base.html' %}
|
||
{% load static i18n %}
|
||
|
||
{% block title %}
|
||
Edit Application Form - {{ applicant_form.name }}
|
||
{% endblock %}
|
||
|
||
{% block customCSS %}
|
||
<style>
|
||
:root {
|
||
--primary: #00636e;
|
||
--primary-light: #89bcc2ff;
|
||
--primary-dark: #00636e;
|
||
--secondary: #7209b7;
|
||
--success: #4cc9f0;
|
||
--warning: #f72585;
|
||
--light: #f8f9fa;
|
||
--dark: #212529;
|
||
--gray-100: #f8f9fa;
|
||
--gray-200: #e9ecef;
|
||
--gray-300: #dee2e6;
|
||
--gray-400: #ced4da;
|
||
--gray-500: #adb5bd;
|
||
--gray-600: #6c757d;
|
||
--gray-700: #495057;
|
||
--gray-800: #343a40;
|
||
--gray-900: #212529;
|
||
--border-radius: 12px;
|
||
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
{% comment %} body {
|
||
font-family: var(--font-family);
|
||
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
|
||
color: var(--gray-800);
|
||
line-height: 1.6;
|
||
padding: 24px;
|
||
min-height: 100vh;
|
||
} {% endcomment %}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 32px;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid var(--gray-200);
|
||
}
|
||
|
||
.header h1 {
|
||
font-weight: 700;
|
||
font-size: 28px;
|
||
color: var(--gray-900);
|
||
margin: 0;
|
||
}
|
||
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--gray-600);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.breadcrumb a {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.breadcrumb a:hover {
|
||
color: var(--primary-dark);
|
||
}
|
||
|
||
.breadcrumb i {
|
||
font-size: 12px;
|
||
color: var(--gray-400);
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
padding: 28px;
|
||
margin-bottom: 28px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.card:hover {
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--gray-900);
|
||
}
|
||
|
||
.form-details-edit {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.form-details-edit label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.form-details-edit input[type="text"],
|
||
.form-details-edit textarea {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 1px solid var(--gray-300);
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.form-details-edit input[type="text"]:focus,
|
||
.form-details-edit textarea:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
||
}
|
||
|
||
.form-details-edit textarea {
|
||
min-height: 100px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
text-decoration: none;
|
||
border: none;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--primary-dark);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--gray-100);
|
||
color: var(--gray-800);
|
||
border: 1px solid var(--gray-300);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--gray-200);
|
||
}
|
||
|
||
.form-builder-container {
|
||
display: flex;
|
||
gap: 28px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.palette {
|
||
width: 300px;
|
||
background: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
padding: 24px;
|
||
height: fit-content;
|
||
position: sticky;
|
||
top: 24px;
|
||
}
|
||
|
||
.palette h2 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 20px;
|
||
color: var(--gray-900);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.palette h2 i {
|
||
color: var(--primary);
|
||
}
|
||
|
||
.field-type {
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border: 2px solid var(--gray-200);
|
||
border-radius: 10px;
|
||
cursor: grab;
|
||
background: var(--gray-50);
|
||
transition: var(--transition);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.field-type:hover {
|
||
border-color: var(--primary);
|
||
background: rgba(67, 97, 238, 0.05);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.field-type i {
|
||
font-size: 20px;
|
||
color: var(--primary);
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(67, 97, 238, 0.1);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.canvas-container {
|
||
flex-grow: 1;
|
||
background: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
padding: 28px;
|
||
}
|
||
|
||
.canvas-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.canvas-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--gray-900);
|
||
}
|
||
|
||
.form-canvas {
|
||
min-height: 400px;
|
||
border: 2px dashed var(--gray-300);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 28px;
|
||
background: var(--gray-50);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.form-canvas.drag-over {
|
||
border-color: var(--primary);
|
||
background: rgba(67, 97, 238, 0.05);
|
||
}
|
||
|
||
.drop-hint {
|
||
text-align: center;
|
||
color: var(--gray-500);
|
||
padding: 60px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.drop-hint i {
|
||
font-size: 48px;
|
||
color: var(--gray-400);
|
||
}
|
||
|
||
.drop-hint p {
|
||
font-size: 18px;
|
||
max-width: 400px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.form-field-item {
|
||
padding: 20px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid var(--gray-200);
|
||
border-left: 4px solid var(--primary);
|
||
border-radius: 10px;
|
||
background: white;
|
||
cursor: move;
|
||
transition: var(--transition);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.form-field-item:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.form-field-item.dragging {
|
||
opacity: 0.8;
|
||
background: var(--gray-50);
|
||
border: 2px dashed var(--primary);
|
||
}
|
||
|
||
.form-field-item.drag-over-top {
|
||
border-top: 3px solid var(--success);
|
||
}
|
||
|
||
.form-field-item.drag-over-bottom {
|
||
border-bottom: 3px solid var(--success);
|
||
}
|
||
|
||
.field-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.field-title {
|
||
font-weight: 600;
|
||
font-size: 18px;
|
||
margin: 0;
|
||
color: var(--gray-900);
|
||
}
|
||
|
||
.field-type-label {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 4px 10px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.field-meta {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 16px;
|
||
color: var(--gray-600);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.field-meta div {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.field-meta i {
|
||
color: var(--primary);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.field-controls {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--gray-200);
|
||
}
|
||
|
||
.field-config {
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid var(--gray-200);
|
||
display: none;
|
||
}
|
||
|
||
.field-config label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.field-config input[type="text"],
|
||
.field-config textarea {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 1px solid var(--gray-300);
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
transition: var(--transition);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.field-config input[type="text"]:focus,
|
||
.field-config textarea:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
||
}
|
||
|
||
.field-config textarea {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.field-config .field-name {
|
||
background: var(--gray-100);
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.output-data {
|
||
background: var(--gray-100);
|
||
padding: 20px;
|
||
margin-top: 24px;
|
||
border-radius: 10px;
|
||
white-space: pre-wrap;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
max-height: 200px;
|
||
overflow: auto;
|
||
border: 1px solid var(--gray-300);
|
||
}
|
||
|
||
.messages {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.messages li {
|
||
padding: 16px 20px;
|
||
border-radius: 10px;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.messages .success {
|
||
background: rgba(76, 201, 240, 0.15);
|
||
color: #0a7ea4;
|
||
border: 1px solid rgba(76, 201, 240, 0.3);
|
||
}
|
||
|
||
.messages .error {
|
||
background: rgba(247, 37, 133, 0.15);
|
||
color: #a11a5d;
|
||
border: 1px solid rgba(247, 37, 133, 0.3);
|
||
}
|
||
|
||
.messages i {
|
||
font-size: 20px;
|
||
}
|
||
|
||
/* Responsive adjustments */
|
||
@media (max-width: 992px) {
|
||
.form-builder-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.palette {
|
||
position: static;
|
||
width: 100%;
|
||
}
|
||
|
||
.form-details-edit {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Animation for field appearance */
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.form-field-item {
|
||
animation: fadeInUp 0.4s ease-out;
|
||
}
|
||
|
||
/* Loading state */
|
||
.btn.loading {
|
||
position: relative;
|
||
color: transparent;
|
||
}
|
||
|
||
.btn.loading::after {
|
||
content: "";
|
||
position: absolute;
|
||
width: 20px;
|
||
height: 20px;
|
||
top: 50%;
|
||
left: 50%;
|
||
margin: -10px 0 0 -10px;
|
||
border: 2px solid transparent;
|
||
border-top-color: white;
|
||
border-radius: 50%;
|
||
animation: button-loading 1s ease infinite;
|
||
}
|
||
|
||
@keyframes button-loading {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container">
|
||
{% if messages %}
|
||
<ul class="messages">
|
||
{% for message in messages %}
|
||
<li class="{{ message.tags }}">
|
||
{% if message.tags == "success" %}
|
||
<i>✓</i>
|
||
{% else %}
|
||
<i>!</i>
|
||
{% endif %}
|
||
{{ message }}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
<div class="header">
|
||
<div>
|
||
<h1>Edit Application Form</h1>
|
||
<div class="breadcrumb">
|
||
<a href="{% url 'applicant:job_forms_list' job_id=job.internal_job_id %}">Forms</a>
|
||
<i>›</i>
|
||
<span>{{ applicant_form.name }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<a href="{% url 'applicant:job_forms_list' job_id=job.internal_job_id %}" class="btn btn-secondary">
|
||
<i>←</i> Back to Forms
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">Form Details</h2>
|
||
</div>
|
||
<form method="post" class="form-details-edit">
|
||
{% csrf_token %}
|
||
<div>
|
||
<label for="{{ form_details.name.id_for_label }}">Form Name</label>
|
||
{{ form_details.name }}
|
||
</div>
|
||
<div>
|
||
<label for="{{ form_details.description.id_for_label }}">Description</label>
|
||
{{ form_details.description }}
|
||
</div>
|
||
<div class="action-buttons" style="margin-top: 20px;">
|
||
<button type="submit" name="save_form_details" class="btn univ-color">
|
||
Save Form Details
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="form-builder-container">
|
||
<div class="palette">
|
||
<h2><i>+</i> Add Field</h2>
|
||
<div class="field-type" data-type="text" draggable="true">
|
||
<i>T</i> Text Input
|
||
</div>
|
||
<div class="field-type" data-type="email" draggable="true">
|
||
<i>@</i> Email
|
||
</div>
|
||
<div class="field-type" data-type="select" draggable="true">
|
||
<i>▼</i> Dropdown
|
||
</div>
|
||
<div class="field-type" data-type="checkbox" draggable="true">
|
||
<i>✓</i> Checkbox
|
||
</div>
|
||
<div class="field-type" data-type="textarea" draggable="true">
|
||
<i>≡</i> Paragraph
|
||
</div>
|
||
<div class="field-type" data-type="date" draggable="true">
|
||
<i>📅</i> Date
|
||
</div>
|
||
<div class="field-type" data-type="radio" draggable="true">
|
||
<i>●</i> Radio Buttons
|
||
</div>
|
||
</div>
|
||
|
||
<div class="canvas-container">
|
||
<div class="canvas-header">
|
||
<h2 class="canvas-title">Form Structure</h2>
|
||
<button id="save-form-btn" class="btn univ-color">
|
||
Save Form Structure
|
||
</button>
|
||
</div>
|
||
<div id="form-canvas" class="form-canvas">
|
||
<div class="drop-hint">
|
||
<i>+</i>
|
||
<p>Drag fields from the left panel to build your form</p>
|
||
</div>
|
||
</div>
|
||
<div class="output-data" id="output-data" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block customJS %}
|
||
<script>
|
||
// --- DATA INITIALIZATION ---
|
||
let formFields = JSON.parse('{{ initial_fields_json|safe }}');
|
||
const formCanvas = document.getElementById('form-canvas');
|
||
const dropHint = formCanvas.querySelector('.drop-hint');
|
||
const saveBtn = document.getElementById('save-form-btn');
|
||
|
||
// --- Utility Functions ---
|
||
function generateFieldName(label, existingFieldNames) {
|
||
let baseName = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').trim('_');
|
||
if (!baseName) baseName = 'field';
|
||
|
||
let fieldName = baseName;
|
||
let counter = 1;
|
||
while (existingFieldNames.includes(fieldName)) {
|
||
fieldName = `${baseName}_${counter}`;
|
||
counter++;
|
||
}
|
||
return fieldName;
|
||
}
|
||
|
||
function renderForm() {
|
||
formCanvas.innerHTML = '';
|
||
if (formFields.length === 0) {
|
||
formCanvas.appendChild(dropHint);
|
||
}
|
||
|
||
const existingNames = [];
|
||
formFields.forEach(field => {
|
||
field.field_name = generateFieldName(field.label, existingNames);
|
||
existingNames.push(field.field_name);
|
||
});
|
||
|
||
formFields.forEach((field, index) => {
|
||
const fieldElement = createFieldElement(field, index);
|
||
formCanvas.appendChild(fieldElement);
|
||
});
|
||
}
|
||
|
||
function createFieldElement(field, index) {
|
||
const item = document.createElement('div');
|
||
item.className = 'form-field-item';
|
||
item.dataset.index = index;
|
||
item.dataset.id = field.id || index;
|
||
item.draggable = true;
|
||
|
||
const typeIcons = {
|
||
'text': 'T',
|
||
'email': '@',
|
||
'select': '▼',
|
||
'checkbox': '✓',
|
||
'textarea': '≡',
|
||
'date': '📅',
|
||
'radio': '●'
|
||
};
|
||
|
||
const fieldDisplay = `
|
||
<div class="field-header">
|
||
<h3 class="field-title">${field.label}</h3>
|
||
<span class="field-type-label">${field.field_type}</span>
|
||
</div>
|
||
<div class="field-meta">
|
||
<div><i>🏷️</i> ${field.field_name}</div>
|
||
<div><i>${field.required ? '✅' : '❌'}</i> ${field.required ? 'Required' : 'Optional'}</div>
|
||
${field.field_type === 'select' || field.field_type === 'radio' ?
|
||
`<div><i>📋</i> ${field.choices ? field.choices.split(',').filter(c => c.trim()).length + ' options' : 'No options'}</div>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
const configPanel = `
|
||
<div class="field-config">
|
||
<label>Label</label>
|
||
<input type="text" value="${field.label}" data-prop="label" class="config-input">
|
||
|
||
<label>
|
||
<input type="checkbox" ${field.required ? 'checked' : ''} data-prop="required" class="config-input">
|
||
Required field
|
||
</label>
|
||
|
||
${field.field_type === 'select' || field.field_type === 'radio' ?
|
||
`<label>Options (comma separated)</label>
|
||
<textarea data-prop="choices" class="config-input">${field.choices}</textarea>` : ''}
|
||
|
||
<label>Help Text (optional)</label>
|
||
<textarea data-prop="help_text" class="config-input">${field.help_text || ''}</textarea>
|
||
|
||
<label>Field Name (auto-generated)</label>
|
||
<div class="field-name">${field.field_name}</div>
|
||
</div>
|
||
`;
|
||
|
||
const controls = `
|
||
<div class="field-controls">
|
||
<button type="button" class="btn btn-secondary toggle-config">Configure</button>
|
||
<button type="button" class="btn btn-secondary remove-field">Remove</button>
|
||
</div>
|
||
`;
|
||
|
||
item.innerHTML = fieldDisplay + configPanel + controls;
|
||
return item;
|
||
}
|
||
|
||
// ✅ NEW: Update field display without re-rendering entire form
|
||
function updateFieldDisplay(fieldElement, fieldData) {
|
||
// Update label
|
||
const titleEl = fieldElement.querySelector('.field-title');
|
||
if (titleEl) titleEl.textContent = fieldData.label;
|
||
|
||
// Update field name display
|
||
const fieldNameDisplay = fieldElement.querySelector('.field-name');
|
||
if (fieldNameDisplay) fieldNameDisplay.textContent = fieldData.field_name;
|
||
|
||
// Update required status
|
||
const requiredEl = fieldElement.querySelector('.field-meta div:nth-child(2)');
|
||
if (requiredEl) {
|
||
requiredEl.innerHTML = `<i>${fieldData.required ? '✅' : '❌'}</i> ${fieldData.required ? 'Required' : 'Optional'}`;
|
||
}
|
||
|
||
// Update choices count (if applicable)
|
||
if (fieldData.field_type === 'select' || fieldData.field_type === 'radio') {
|
||
const choicesEl = fieldElement.querySelector('.field-meta div:nth-child(3)');
|
||
if (choicesEl) {
|
||
const count = fieldData.choices ? fieldData.choices.split(',').filter(c => c.trim()).length : 0;
|
||
choicesEl.innerHTML = `<i>📋</i> ${count > 0 ? count + ' options' : 'No options'}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleConfigChange(event) {
|
||
const input = event.target;
|
||
if (!input.closest('.field-config')) return;
|
||
|
||
const prop = input.dataset.prop;
|
||
const fieldItem = input.closest('.form-field-item');
|
||
const index = parseInt(fieldItem.dataset.index);
|
||
let value;
|
||
|
||
if (prop === 'required') {
|
||
value = input.checked;
|
||
} else if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
|
||
value = input.value;
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
// Update data model
|
||
formFields[index][prop] = value;
|
||
|
||
// Regenerate field_name for this field only (ensure uniqueness)
|
||
const otherNames = formFields
|
||
.filter((f, i) => i !== index)
|
||
.map(f => f.field_name);
|
||
formFields[index].field_name = generateFieldName(formFields[index].label, otherNames);
|
||
|
||
// ✅ Update DOM directly instead of re-rendering
|
||
updateFieldDisplay(fieldItem, formFields[index]);
|
||
}
|
||
|
||
// Prevent config panel from closing when clicking inside inputs
|
||
formCanvas.addEventListener('mousedown', (e) => {
|
||
if (e.target.closest('.field-config')) {
|
||
e.stopPropagation();
|
||
}
|
||
});
|
||
|
||
function toggleConfig(event) {
|
||
if (event.target.classList.contains('toggle-config')) {
|
||
const configPanel = event.target.closest('.form-field-item').querySelector('.field-config');
|
||
const isHidden = configPanel.style.display === 'none';
|
||
|
||
configPanel.style.display = isHidden ? 'block' : 'none';
|
||
event.target.textContent = isHidden ? 'Hide Configuration' : 'Configure';
|
||
event.stopPropagation();
|
||
}
|
||
}
|
||
|
||
function removeField(event) {
|
||
if (event.target.classList.contains('remove-field')) {
|
||
if (confirm("Are you sure you want to remove this field? This action cannot be undone.")) {
|
||
const index = parseInt(event.target.closest('.form-field-item').dataset.index);
|
||
formFields.splice(index, 1);
|
||
renderForm(); // Only re-render on remove
|
||
}
|
||
event.stopPropagation();
|
||
}
|
||
}
|
||
|
||
// --- Drag and Drop Logic ---
|
||
let draggedItem = null;
|
||
let draggedFieldElement = null;
|
||
|
||
document.querySelectorAll('.field-type').forEach(typeEl => {
|
||
typeEl.addEventListener('dragstart', (e) => {
|
||
draggedItem = { type: typeEl.dataset.type, label: typeEl.textContent.trim().replace(/^[^a-zA-Z]+/, '') };
|
||
e.dataTransfer.setData('text/plain', typeEl.dataset.type);
|
||
typeEl.classList.add('dragging');
|
||
});
|
||
typeEl.addEventListener('dragend', () => {
|
||
draggedItem = null;
|
||
typeEl.classList.remove('dragging');
|
||
});
|
||
});
|
||
|
||
formCanvas.addEventListener('dragstart', (e) => {
|
||
if (e.target.classList.contains('form-field-item')) {
|
||
draggedFieldElement = e.target;
|
||
e.dataTransfer.setData('text/plain', 'reorder');
|
||
setTimeout(() => e.target.classList.add('dragging'), 0);
|
||
}
|
||
});
|
||
|
||
formCanvas.addEventListener('dragend', (e) => {
|
||
if (e.target.classList.contains('form-field-item')) {
|
||
e.target.classList.remove('dragging');
|
||
draggedFieldElement = null;
|
||
}
|
||
});
|
||
|
||
formCanvas.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
formCanvas.classList.add('drag-over');
|
||
|
||
const isDraggingField = !!draggedFieldElement;
|
||
|
||
if (draggedItem && !isDraggingField) {
|
||
return;
|
||
}
|
||
|
||
if (isDraggingField) {
|
||
const target = e.target.closest('.form-field-item');
|
||
if (target && target !== draggedFieldElement) {
|
||
const targetRect = target.getBoundingClientRect();
|
||
const isTopHalf = e.clientY < targetRect.top + targetRect.height / 2;
|
||
|
||
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
|
||
el.classList.remove('drag-over-top', 'drag-over-bottom');
|
||
});
|
||
|
||
if (isTopHalf) {
|
||
target.classList.add('drag-over-top');
|
||
} else {
|
||
target.classList.add('drag-over-bottom');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
formCanvas.addEventListener('dragleave', (e) => {
|
||
if (e.relatedTarget && !formCanvas.contains(e.relatedTarget)) {
|
||
formCanvas.classList.remove('drag-over');
|
||
}
|
||
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
|
||
el.classList.remove('drag-over-top', 'drag-over-bottom');
|
||
});
|
||
});
|
||
|
||
formCanvas.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
formCanvas.classList.remove('drag-over');
|
||
|
||
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
|
||
el.classList.remove('drag-over-top', 'drag-over-bottom');
|
||
});
|
||
|
||
if (draggedItem) {
|
||
const newField = {
|
||
id: Date.now(),
|
||
label: draggedItem.label,
|
||
field_type: draggedItem.type,
|
||
required: true,
|
||
help_text: '',
|
||
choices: '',
|
||
field_name: '',
|
||
order: formFields.length,
|
||
};
|
||
formFields.push(newField);
|
||
renderForm(); // Re-render on add
|
||
draggedItem = null;
|
||
return;
|
||
}
|
||
|
||
if (draggedFieldElement) {
|
||
const dropTarget = e.target.closest('.form-field-item');
|
||
if (dropTarget && dropTarget !== draggedFieldElement) {
|
||
const fromIndex = parseInt(draggedFieldElement.dataset.index);
|
||
let toIndex = parseInt(dropTarget.dataset.index);
|
||
|
||
const targetRect = dropTarget.getBoundingClientRect();
|
||
const isTopHalf = e.clientY < targetRect.top + targetRect.height / 2;
|
||
|
||
const [itemToMove] = formFields.splice(fromIndex, 1);
|
||
|
||
if (!isTopHalf) {
|
||
toIndex += 1;
|
||
}
|
||
|
||
formFields.splice(toIndex, 0, itemToMove);
|
||
renderForm(); // Re-render on reorder
|
||
}
|
||
draggedFieldElement = null;
|
||
}
|
||
});
|
||
|
||
// --- Event Listeners ---
|
||
// Only re-render on structural changes (add/remove/reorder)
|
||
// Config changes update DOM directly
|
||
formCanvas.addEventListener('change', handleConfigChange);
|
||
formCanvas.addEventListener('input', handleConfigChange);
|
||
formCanvas.addEventListener('click', toggleConfig);
|
||
formCanvas.addEventListener('click', removeField);
|
||
|
||
// Save button handler
|
||
saveBtn.addEventListener('click', async () => {
|
||
// Update order
|
||
formFields.forEach((field, index) => {
|
||
field.order = index;
|
||
});
|
||
|
||
// Show loading state
|
||
saveBtn.classList.add('loading');
|
||
saveBtn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(window.location.href, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': '{{ csrf_token }}'
|
||
},
|
||
body: JSON.stringify(formFields)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
// Show success message
|
||
const message = document.createElement('li');
|
||
message.className = 'success';
|
||
message.innerHTML = '<i>✓</i> Form structure saved successfully!';
|
||
|
||
const messagesContainer = document.querySelector('.messages') ||
|
||
document.querySelector('.container').insertAdjacentHTML('afterbegin', '<ul class="messages"></ul>') ||
|
||
document.querySelector('.messages');
|
||
|
||
if (messagesContainer) {
|
||
messagesContainer.appendChild(message);
|
||
setTimeout(() => message.remove(), 5000);
|
||
}
|
||
|
||
// Update output for debugging
|
||
document.getElementById('output-data').textContent = JSON.stringify(formFields, null, 2);
|
||
document.getElementById('output-data').style.display = 'block';
|
||
} else {
|
||
alert(`Error saving form: ${result.message}`);
|
||
}
|
||
} catch (e) {
|
||
console.error("Fetch Error:", e);
|
||
alert("A network error occurred while saving the form.");
|
||
} finally {
|
||
saveBtn.classList.remove('loading');
|
||
saveBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Initial render
|
||
renderForm();
|
||
</script>
|
||
{% endblock %} |