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:
parent
1f22375c95
commit
e5c5d46dae
@ -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>
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user