corrad-bp/components/FormBuilderCanvas.vue
Md Afiq Iskandar 316420282b Add form builder components and functionality
- 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.
2025-04-09 12:18:50 +08:00

141 lines
3.9 KiB
Vue

<template>
<div class="form-builder-canvas">
<div v-if="formComponents.length === 0" class="flex flex-col items-center justify-center h-full py-16 text-gray-400">
<Icon name="material-symbols:layers-outline" class="w-16 h-16 mb-4 text-gray-300" />
<p class="text-base font-medium">Drag components here</p>
<p class="text-xs mt-1">Or click a component from the sidebar</p>
</div>
<draggable
v-else
v-model="componentList"
group="form-components"
item-key="id"
handle=".drag-handle"
ghost-class="ghost"
animation="300"
@end="onDragEnd"
>
<template #item="{ element, index }">
<div
class="form-component relative mb-3 border rounded-md overflow-hidden transition-all"
:class="{
'ring-2 ring-blue-400 bg-blue-50 border-transparent': selectedComponentId === element.id,
'bg-white border-gray-200 hover:border-gray-300': selectedComponentId !== element.id
}"
@click.capture="selectComponent(element)"
>
<div class="component-actions absolute right-1.5 top-1.5 flex space-x-1 z-10">
<button
class="p-1 text-gray-400 hover:text-gray-600 rounded"
title="Drag to reorder"
>
<span class="drag-handle cursor-move">
<Icon name="material-symbols:drag-indicator" class="w-4 h-4" />
</span>
</button>
<button
class="p-1 text-gray-400 hover:text-red-500 rounded"
title="Delete component"
@click.stop="deleteComponent(element.id)"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<div class="p-3">
<component-preview
:component="element"
:is-preview="true"
/>
</div>
</div>
</template>
</draggable>
</div>
</template>
<script setup>
import draggable from 'vuedraggable';
import { onMounted, watch } from 'vue';
const props = defineProps({
formComponents: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['select-component', 'move-component', 'delete-component']);
const selectedComponentId = ref(null);
// Watch for changes in formComponents
watch(() => props.formComponents, (newComponents) => {
// If the currently selected component is no longer in the list, deselect it
if (selectedComponentId.value &&
!newComponents.some(comp => comp.id === selectedComponentId.value)) {
selectedComponentId.value = null;
}
}, { deep: true });
// Create a reactive list with the form components
const componentList = computed({
get: () => props.formComponents,
set: (value) => {
// Do nothing here to avoid reactivity loops - let onDragEnd handle the changes
}
});
// Handle component selection
const selectComponent = (component) => {
selectedComponentId.value = component.id;
// Use a copy of the component to prevent reactivity issues
const componentCopy = JSON.parse(JSON.stringify(component));
emit('select-component', componentCopy);
};
// Handle component deletion
const deleteComponent = (id) => {
if (selectedComponentId.value === id) {
selectedComponentId.value = null;
}
emit('delete-component', id);
};
// Handle drag end event for reordering
const onDragEnd = (event) => {
if (event.oldIndex !== event.newIndex) {
emit('move-component', {
oldIndex: event.oldIndex,
newIndex: event.newIndex
});
}
};
</script>
<style scoped>
.ghost {
opacity: 0.5;
background: #e0f2fe;
border: 1px dashed #60a5fa;
}
.form-component {
transition: all 0.2s ease;
}
.form-component:hover .component-actions {
opacity: 1;
}
.component-actions {
opacity: 0;
transition: opacity 0.15s ease;
}
.form-component:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
</style>