Enhance Form Builder UI with Search Functionality and Component Organization

- Added a search bar to filter available components in `FormBuilderComponents.vue`.
- Organized components into categories (Basic Inputs, Selection Inputs, Date and Time, Advanced, Layout) for better accessibility.
- Updated styles for improved user experience and interaction feedback.
- Refactored component retrieval logic to streamline rendering based on categories.
This commit is contained in:
Md Afiq Iskandar 2025-04-10 10:04:54 +08:00
parent 1f22375c95
commit e5c5d46dae
2 changed files with 134 additions and 75 deletions

View File

@ -1,33 +1,114 @@
<template> <template>
<div class="form-components"> <div class="form-components">
<div v-for="group in groupedComponents" :key="group.category" class="mb-2"> <!-- Search Bar -->
<RsCollapse> <div class="search-container p-3 mb-2">
<RsCollapseItem <div class="relative">
:title="group.category" <input
:open="group.category === 'Basic Inputs'" type="text"
class="text-sm font-medium" v-model="searchQuery"
> placeholder="Search..."
<div class="grid grid-cols-1 gap-1.5 mt-2"> class="w-full px-3 py-2 pl-9 bg-white border border-gray-300 rounded text-gray-700 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<Icon
name="material-symbols:search"
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
/>
</div>
</div>
<!-- Basic Inputs Category -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Basic Inputs</h3>
<div class="grid grid-cols-2 gap-1 px-2">
<div <div
v-for="component in group.components" v-for="component in getComponentsByCategory('Basic Inputs')"
:key="component.type" :key="component.type"
class="component-item border border-gray-200 rounded p-2 flex items-center cursor-grab hover:bg-gray-50 transition-colors" class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
:class="{ 'hidden': !matchesSearch(component) }" :class="{ 'hidden': !matchesSearch(component) }"
draggable="true" draggable="true"
@dragstart="onDragStart($event, component)" @dragstart="onDragStart($event, component)"
@click="addComponent(component)" @click="addComponent(component)"
> >
<div class="bg-gray-100 p-1.5 rounded mr-2 flex items-center justify-center w-8 h-8 flex-shrink-0"> <Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
<Icon :name="component.icon" class="w-4 h-4 text-gray-600" /> <span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
</div>
<div class="min-w-0 flex-1">
<div class="font-medium text-sm truncate">{{ component.name }}</div>
<div class="text-xs text-gray-500 truncate">{{ component.description }}</div>
</div> </div>
</div> </div>
</div> </div>
</RsCollapseItem>
</RsCollapse> <!-- Selection Inputs Category -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Selection Inputs</h3>
<div class="grid grid-cols-2 gap-1 px-2">
<div
v-for="component in getComponentsByCategory('Selection Inputs')"
:key="component.type"
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
:class="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, component)"
@click="addComponent(component)"
>
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
</div>
</div>
</div>
<!-- Date and Time Category -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Date and Time</h3>
<div class="grid grid-cols-2 gap-1 px-2">
<div
v-for="component in getComponentsByCategory('Date and Time')"
:key="component.type"
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
:class="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, component)"
@click="addComponent(component)"
>
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
</div>
</div>
</div>
<!-- Advanced Category -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Advanced</h3>
<div class="grid grid-cols-2 gap-1 px-2">
<div
v-for="component in getComponentsByCategory('Advanced')"
:key="component.type"
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
:class="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, component)"
@click="addComponent(component)"
>
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
</div>
</div>
</div>
<!-- Layout Category -->
<div class="component-category mb-6">
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Layout</h3>
<div class="grid grid-cols-2 gap-1 px-2">
<div
v-for="component in getComponentsByCategory('Layout')"
:key="component.type"
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
:class="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, component)"
@click="addComponent(component)"
>
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -262,23 +343,10 @@ const availableComponents = [
} }
]; ];
// Group components by category // Get components by category for rendering in sections
const groupedComponents = computed(() => { const getComponentsByCategory = (category) => {
const grouped = {}; return availableComponents.filter(component => component.category === category);
};
availableComponents.forEach(component => {
if (!grouped[component.category]) {
grouped[component.category] = {
category: component.category,
components: []
};
}
grouped[component.category].components.push(component);
});
return Object.values(grouped);
});
// Check if component matches search query // Check if component matches search query
const matchesSearch = (component) => { const matchesSearch = (component) => {
@ -298,32 +366,29 @@ const onDragStart = (event, component) => {
// Set the drag data // Set the drag data
event.dataTransfer.effectAllowed = 'copy'; event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('component', JSON.stringify(component)); event.dataTransfer.setData('component', JSON.stringify(component));
// Let browser handle the drag image naturally
// Don't call emit here to avoid double component creation
}; };
// Add a click handler for adding components directly // Add a component directly via click
const addComponent = (component) => { const addComponent = (component) => {
emit('add-component', component); emit('add-component', component);
}; };
</script> </script>
<style scoped> <style scoped>
.form-components {
@apply bg-white h-full;
}
.component-item { .component-item {
transition: transform 0.1s ease-in-out; @apply h-20;
transition: all 0.15s ease-in-out;
}
.component-item:hover {
transform: translateY(-2px);
} }
.component-item:active { .component-item:active {
transform: scale(0.97); transform: scale(0.97);
} }
:deep(.collapse-title) {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
:deep(.collapse-content) {
padding: 0.5rem;
}
</style> </style>

View File

@ -58,22 +58,14 @@
<div <div
class="p-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center" 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"> <h2 class="text-sm font-medium text-gray-700">Components</h2>
Available Components
</h2>
<div class="relative"> <div class="relative">
<input <button class="px-2 py-1 text-xs text-gray-500 hover:text-gray-700">
type="text" <Icon name="material-symbols:refresh" class="w-3.5 h-3.5" />
placeholder="Search..." </button>
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> </div>
<div class="overflow-y-auto flex-1 p-2"> <div class="overflow-y-auto flex-1">
<FormBuilderComponents @add-component="handleAddComponent" /> <FormBuilderComponents @add-component="handleAddComponent" />
</div> </div>
</div> </div>
@ -81,16 +73,18 @@
<!-- Middle Panel - Form Canvas --> <!-- Middle Panel - Form Canvas -->
<div class="flex-1 flex flex-col overflow-hidden"> <div class="flex-1 flex flex-col overflow-hidden">
<div <div
class="py-2 px-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center h-16" class="py-2 px-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center h-12"
> >
<h2 class="text-sm font-medium text-gray-700">Form Canvas</h2> <h2 class="text-sm font-medium text-gray-700">Form Canvas</h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-gray-500">Drag components to build your form</span> <span class="text-xs text-gray-500"
>Drag components to build your form</span
>
</div> </div>
</div> </div>
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto p-6">
<div <div
class="border-2 border-dashed border-gray-300 bg-white rounded-md min-h-[calc(100vh-200px)] p-4" class="border-2 border-dashed border-gray-300 bg-white rounded-md min-h-[calc(100vh-170px)] p-4"
:class="{ 'border-blue-400 bg-blue-50': formStore.isDraggingOver }" :class="{ 'border-blue-400 bg-blue-50': formStore.isDraggingOver }"
@dragover.prevent="handleDragOver" @dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave" @dragleave.prevent="handleDragLeave"
@ -111,7 +105,7 @@
class="w-72 bg-white border-l border-gray-200 flex flex-col overflow-hidden" class="w-72 bg-white border-l border-gray-200 flex flex-col overflow-hidden"
> >
<div <div
class="p-3 border-b border-gray-200 bg-gray-50 h-16 flex items-center" class="p-3 border-b border-gray-200 bg-gray-50 h-12 flex items-center"
> >
<h2 class="text-sm font-medium text-gray-700">Configuration</h2> <h2 class="text-sm font-medium text-gray-700">Configuration</h2>
</div> </div>