- Introduced `FormBuilderComponents.vue`, `FormBuilderCanvas.vue`, and `FormBuilderConfiguration.vue` for managing form elements. - Added `ComponentPreview.vue` for rendering previews of form components. - Implemented state management using Pinia in `stores/formBuilder.js` to handle form components and saved forms. - Created pages for form builder interface (`index.vue`) and form management (`manage.vue`). - Integrated toast notifications with `useToast.js` for user feedback. - Documented the form builder structure and features in `FORM_BUILDER_DOCUMENTATION.md` and `FORM_BUILDER_TECHNICAL_APPENDIX.md`. - Established a responsive layout and drag-and-drop functionality for a seamless user experience.
273 lines
7.9 KiB
Vue
273 lines
7.9 KiB
Vue
<template>
|
|
<div class="flex flex-col h-screen bg-gray-50">
|
|
<!-- Header Bar -->
|
|
<header
|
|
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<Icon
|
|
@click="navigateTo('/', { external: true })"
|
|
name="ph:arrow-circle-left-duotone"
|
|
class="cursor-pointer w-6 h-6"
|
|
/>
|
|
<img
|
|
src="@/assets/img/logo/logo-word-white.svg"
|
|
alt="Corrad Logo"
|
|
class="h-7"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<FormKit
|
|
type="text"
|
|
name="formName"
|
|
placeholder="Form Name"
|
|
v-model="formStore.formName"
|
|
validation="required"
|
|
validation-visibility="live"
|
|
:validation-messages="{ required: 'Please enter a form name' }"
|
|
class="form-name-input"
|
|
:classes="{
|
|
outer: 'mb-0',
|
|
}"
|
|
/>
|
|
|
|
<RsButton @click="handleSave" variant="primary" size="sm">
|
|
<Icon name="material-symbols:save" class="mr-1" />
|
|
Save Form
|
|
</RsButton>
|
|
|
|
<RsButton @click="handlePreview" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:preview" class="mr-1" />
|
|
Preview
|
|
</RsButton>
|
|
|
|
<RsButton @click="navigateToManage" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:settings" class="mr-1" />
|
|
Manage Forms
|
|
</RsButton>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="flex-1 flex overflow-hidden">
|
|
<!-- Left Panel - Components Sidebar -->
|
|
<div
|
|
class="w-64 bg-white border-r border-gray-200 flex flex-col overflow-hidden"
|
|
>
|
|
<div
|
|
class="p-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center"
|
|
>
|
|
<h2 class="text-sm font-medium text-gray-700">
|
|
Available Components
|
|
</h2>
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
class="px-2 py-1 pr-7 border border-gray-300 rounded text-xs w-24"
|
|
/>
|
|
<Icon
|
|
name="material-symbols:search"
|
|
class="absolute right-2 top-1/2 transform -translate-y-1/2 w-3.5 h-3.5 text-gray-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-y-auto flex-1 p-2">
|
|
<FormBuilderComponents @add-component="handleAddComponent" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Middle Panel - Form Canvas -->
|
|
<div class="flex-1 flex flex-col overflow-hidden">
|
|
<div
|
|
class="py-2 px-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center h-16"
|
|
>
|
|
<h2 class="text-sm font-medium text-gray-700">Form Canvas</h2>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-gray-500">Drag components to build your form</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 overflow-auto p-6">
|
|
<div
|
|
class="border-2 border-dashed border-gray-300 bg-white rounded-md min-h-[calc(100vh-200px)] p-4"
|
|
:class="{ 'border-blue-400 bg-blue-50': formStore.isDraggingOver }"
|
|
@dragover.prevent="handleDragOver"
|
|
@dragleave.prevent="handleDragLeave"
|
|
@drop.prevent="handleDrop"
|
|
>
|
|
<FormBuilderCanvas
|
|
:form-components="formStore.formComponents"
|
|
@select-component="handleSelectComponent"
|
|
@move-component="handleMoveComponent"
|
|
@delete-component="handleDeleteComponent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel - Configuration Sidebar -->
|
|
<div
|
|
class="w-72 bg-white border-l border-gray-200 flex flex-col overflow-hidden"
|
|
>
|
|
<div
|
|
class="p-3 border-b border-gray-200 bg-gray-50 h-16 flex items-center"
|
|
>
|
|
<h2 class="text-sm font-medium text-gray-700">Configuration</h2>
|
|
</div>
|
|
<div class="overflow-y-auto flex-1 p-3">
|
|
<FormBuilderConfiguration
|
|
v-if="formStore.selectedComponent"
|
|
:component="formStore.selectedComponent"
|
|
@update-component="handleUpdateComponent"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="flex items-center justify-center h-full text-gray-500"
|
|
>
|
|
<div class="text-center p-4">
|
|
<Icon
|
|
name="material-symbols:settings-suggest"
|
|
class="w-12 h-12 mx-auto mb-3 text-gray-300"
|
|
/>
|
|
<p class="text-sm font-medium">No component selected</p>
|
|
<p class="text-xs mt-1">Select a component to configure</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Modal -->
|
|
<RsModal v-model="showPreview" title="Form Preview" size="xl">
|
|
<div class="max-h-[70vh] overflow-y-auto p-4">
|
|
<FormKit type="form" @submit="handlePreviewSubmit">
|
|
<template
|
|
v-for="(component, index) in formStore.formComponents"
|
|
:key="index"
|
|
>
|
|
<component-preview :component="component" :is-preview="false" />
|
|
</template>
|
|
<FormKit type="submit" label="Submit" />
|
|
</FormKit>
|
|
</div>
|
|
</RsModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useFormBuilderStore } from "~/stores/formBuilder";
|
|
|
|
definePageMeta({
|
|
title: "Form Builder",
|
|
description: "Create dynamic forms with drag and drop",
|
|
layout: "empty",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
const router = useRouter();
|
|
const formStore = useFormBuilderStore();
|
|
const toast = useToast();
|
|
|
|
// Initialize the form builder
|
|
onMounted(() => {
|
|
formStore.loadSavedForms();
|
|
});
|
|
|
|
const showPreview = ref(false);
|
|
|
|
// Handler methods
|
|
const handleAddComponent = (component) => {
|
|
formStore.addComponent(component);
|
|
};
|
|
|
|
const handleSelectComponent = (component) => {
|
|
formStore.selectComponent(component.id);
|
|
};
|
|
|
|
const handleUpdateComponent = (updatedComponent) => {
|
|
formStore.updateComponent(updatedComponent);
|
|
};
|
|
|
|
const handleMoveComponent = ({ oldIndex, newIndex }) => {
|
|
formStore.moveComponent({ oldIndex, newIndex });
|
|
};
|
|
|
|
const handleDeleteComponent = (id) => {
|
|
formStore.deleteComponent(id);
|
|
};
|
|
|
|
const handleDragOver = (event) => {
|
|
formStore.setDraggingOver(true);
|
|
};
|
|
|
|
const handleDragLeave = (event) => {
|
|
formStore.setDraggingOver(false);
|
|
};
|
|
|
|
const handleDrop = (event) => {
|
|
formStore.setDraggingOver(false);
|
|
const componentData = JSON.parse(event.dataTransfer.getData("component"));
|
|
formStore.addComponent(componentData);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!formStore.formName.trim()) {
|
|
toast.error("Please enter a form name");
|
|
return;
|
|
}
|
|
|
|
if (formStore.formComponents.length === 0) {
|
|
toast.error("Please add at least one component to the form");
|
|
return;
|
|
}
|
|
|
|
formStore.saveForm();
|
|
toast.success("Form saved successfully");
|
|
};
|
|
|
|
const handlePreview = () => {
|
|
if (formStore.formComponents.length === 0) {
|
|
toast.error("Please add at least one component to the form");
|
|
return;
|
|
}
|
|
|
|
showPreview.value = true;
|
|
};
|
|
|
|
const handlePreviewSubmit = (formData) => {
|
|
console.log("Form submitted:", formData);
|
|
showPreview.value = false;
|
|
toast.success("Form submitted successfully");
|
|
};
|
|
|
|
const navigateToManage = () => {
|
|
router.push("/form-builder/manage");
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-name-input :deep(.formkit-inner) {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
color: white;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.form-name-input :deep(.formkit-inner:focus-within) {
|
|
border-color: rgba(255, 255, 255, 0.5);
|
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.form-name-input :deep(input::placeholder) {
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.form-name-input :deep(.formkit-message) {
|
|
color: rgba(255, 200, 200, 0.9);
|
|
font-size: 0.7rem;
|
|
position: absolute;
|
|
}
|
|
</style>
|