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>
<div class="form-components">
<div v-for="group in groupedComponents" :key="group.category" class="mb-2">
<RsCollapse>
<RsCollapseItem
:title="group.category"
:open="group.category === 'Basic Inputs'"
class="text-sm font-medium"
<!-- Search Bar -->
<div class="search-container p-3 mb-2">
<div class="relative">
<input
type="text"
v-model="searchQuery"
placeholder="Search..."
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
v-for="component in getComponentsByCategory('Basic 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)"
>
<div class="grid grid-cols-1 gap-1.5 mt-2">
<div
v-for="component in group.components"
: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="{ 'hidden': !matchesSearch(component) }"
draggable="true"
@dragstart="onDragStart($event, 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="w-4 h-4 text-gray-600" />
</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>
</RsCollapseItem>
</RsCollapse>
<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>
<!-- 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>
</template>
@ -262,23 +343,10 @@ const availableComponents = [
}
];
// Group components by category
const groupedComponents = computed(() => {
const grouped = {};
availableComponents.forEach(component => {
if (!grouped[component.category]) {
grouped[component.category] = {
category: component.category,
components: []
};
}
grouped[component.category].components.push(component);
});
return Object.values(grouped);
});
// Get components by category for rendering in sections
const getComponentsByCategory = (category) => {
return availableComponents.filter(component => component.category === category);
};
// Check if component matches search query
const matchesSearch = (component) => {
@ -298,32 +366,29 @@ const onDragStart = (event, component) => {
// Set the drag data
event.dataTransfer.effectAllowed = 'copy';
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) => {
emit('add-component', component);
};
</script>
<style scoped>
.form-components {
@apply bg-white h-full;
}
.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 {
transform: scale(0.97);
}
:deep(.collapse-title) {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
:deep(.collapse-content) {
padding: 0.5rem;
}
</style>

View File

@ -58,22 +58,14 @@
<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>
<h2 class="text-sm font-medium text-gray-700">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"
/>
<button class="px-2 py-1 text-xs text-gray-500 hover:text-gray-700">
<Icon name="material-symbols:refresh" class="w-3.5 h-3.5" />
</button>
</div>
</div>
<div class="overflow-y-auto flex-1 p-2">
<div class="overflow-y-auto flex-1">
<FormBuilderComponents @add-component="handleAddComponent" />
</div>
</div>
@ -81,16 +73,18 @@
<!-- 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"
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>
<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 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-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 }"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@ -111,7 +105,7 @@
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"
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>
</div>