Enhance FormBuilderComponents with Search and Filter Functionality
- Updated FormBuilderComponents.vue to introduce an enhanced search bar with filtering options for components, improving user experience in locating specific items. - Added a filter dropdown to allow users to filter components by category, with clear options for selection and removal of filters. - Implemented recently used components section to provide quick access to frequently utilized items, enhancing workflow efficiency. - Enhanced search functionality to highlight matching components and provide feedback when no results are found, ensuring better usability. - Updated styles for improved visual consistency and responsiveness across different screen sizes. - Modified settings.local.json to include ESLint command in the allowed commands list for better development practices.
This commit is contained in:
parent
3abc215b42
commit
07539e2344
@ -4,7 +4,8 @@
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(yarn dev:*)",
|
||||
"Bash(yarn lint:*)",
|
||||
"Bash(yarn build:*)"
|
||||
"Bash(yarn build:*)",
|
||||
"Bash(npx eslint:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
@ -1,30 +1,112 @@
|
||||
<template>
|
||||
<div class="form-components">
|
||||
<!-- Search Bar -->
|
||||
<!-- Enhanced 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"
|
||||
placeholder="Search components..."
|
||||
class="w-full px-3 py-2 pl-9 pr-10 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"
|
||||
@focus="showSearchOptions = true"
|
||||
/>
|
||||
<Icon
|
||||
name="material-symbols:search"
|
||||
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
/>
|
||||
<button
|
||||
@click="toggleFilterDropdown"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 hover:bg-gray-100 rounded transition-colors"
|
||||
:class="{ 'text-blue-500': hasActiveFilters }"
|
||||
>
|
||||
<Icon name="material-symbols:filter-list" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Dropdown -->
|
||||
<div
|
||||
v-if="showFilterDropdown"
|
||||
class="absolute z-50 mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg"
|
||||
>
|
||||
<div class="p-3">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Filter by Category</h4>
|
||||
<div class="space-y-2 max-h-48 overflow-y-auto">
|
||||
<label
|
||||
v-for="category in availableCategories"
|
||||
:key="category"
|
||||
class="flex items-center space-x-2 cursor-pointer hover:bg-gray-50 p-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="category"
|
||||
v-model="selectedCategories"
|
||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-600">{{ category }}</span>
|
||||
<span class="text-xs text-gray-400 ml-auto">
|
||||
({{ getComponentsByCategory(category).length }})
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3 pt-2 border-t border-gray-100">
|
||||
<button
|
||||
@click="clearFilters"
|
||||
class="text-xs text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<button
|
||||
@click="showFilterDropdown = false"
|
||||
class="text-xs text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Inputs Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Text Inputs</h3>
|
||||
<!-- Category Filter Chips -->
|
||||
<div v-if="selectedCategories.length > 0" class="px-3 mb-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="category in selectedCategories"
|
||||
:key="category"
|
||||
class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
|
||||
>
|
||||
{{ category }}
|
||||
<button
|
||||
@click="removeCategory(category)"
|
||||
class="ml-1 hover:bg-blue-200 rounded-full p-0.5"
|
||||
>
|
||||
<Icon name="material-symbols:close" class="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
@click="clearFilters"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 ml-1"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recently Used Section -->
|
||||
<div v-if="recentlyUsed.length > 0 && !searchQuery && selectedCategories.length === 0" class="mb-4">
|
||||
<div class="flex items-center justify-between px-3 mb-2">
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Recently Used</h3>
|
||||
<button
|
||||
@click="clearRecentlyUsed"
|
||||
class="text-xs text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<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) }"
|
||||
v-for="component in recentlyUsed.slice(0, 4)"
|
||||
:key="'recent-' + component.type"
|
||||
class="component-item recent-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200 relative"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@ -32,106 +114,292 @@
|
||||
>
|
||||
<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 class="absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Results State -->
|
||||
<div v-if="isSearching && filteredComponentsCount === 0" class="p-6 text-center text-gray-500">
|
||||
<Icon name="material-symbols:search-off" class="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">No components found</h3>
|
||||
<p class="text-xs text-gray-400 mb-3">Try adjusting your search or filter criteria</p>
|
||||
<button
|
||||
@click="clearSearch"
|
||||
class="text-xs text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Clear search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Inputs Category -->
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Basic Inputs')">
|
||||
<div
|
||||
@click="toggleCategory('Basic Inputs')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Text Inputs</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Basic Inputs').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Basic Inputs'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Basic Inputs']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
v-for="component in getComponentsByCategory('Basic Inputs')"
|
||||
:key="component.type"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selection & Toggle Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Selection & Toggle</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Selection Inputs')">
|
||||
<div
|
||||
@click="toggleCategory('Selection Inputs')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Selection & Toggle</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Selection Inputs').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Selection Inputs'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Selection Inputs']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<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) }"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date & Pickers Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Date & Pickers</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Date and Time')">
|
||||
<div
|
||||
@click="toggleCategory('Date and Time')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Date & Pickers</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Date and Time').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Date and Time'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Date and Time']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<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) }"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced & Upload Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Advanced & Upload</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Advanced')">
|
||||
<div
|
||||
@click="toggleCategory('Advanced')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Advanced & Upload</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Advanced').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Advanced'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Advanced']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<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) }"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout & Content Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Layout & Content</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Layout')">
|
||||
<div
|
||||
@click="toggleCategory('Layout')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Layout & Content</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Layout').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Layout'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Layout']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<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) }"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom HTML Category -->
|
||||
<div class="component-category mb-4">
|
||||
<h3 class="text-gray-700 text-xs font-semibold px-3 mb-2 uppercase tracking-wider">Custom HTML</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="component-category mb-4" v-if="shouldShowCategory('Custom HTML')">
|
||||
<div
|
||||
@click="toggleCategory('Custom HTML')"
|
||||
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 rounded transition-colors"
|
||||
>
|
||||
<h3 class="text-gray-700 text-xs font-semibold uppercase tracking-wider">Custom HTML</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
({{ getVisibleComponentsInCategory('Custom HTML').length }})
|
||||
</span>
|
||||
<Icon
|
||||
:name="collapsedCategories['Custom HTML'] ? 'material-symbols:expand-more' : 'material-symbols:expand-less'"
|
||||
class="w-4 h-4 text-gray-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!collapsedCategories['Custom HTML']"
|
||||
class="grid grid-cols-2 gap-1 px-2 transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
v-for="component in getComponentsByCategory('Custom HTML')"
|
||||
: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) }"
|
||||
class="component-item enhanced-card rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-all duration-200 border border-gray-200"
|
||||
:class="{ 'hidden': !matchesSearch(component), 'highlight-match': highlightComponent(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
@mouseenter="showTooltip($event, component)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="text-xs text-gray-600 text-center leading-tight component-name"
|
||||
v-html="getHighlightedName(component)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
v-if="showTooltipData.show"
|
||||
ref="tooltip"
|
||||
class="tooltip-popup"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<div class="tooltip-content">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<Icon :name="showTooltipData.component?.icon" class="w-4 h-4 text-gray-600" />
|
||||
<span class="font-medium text-sm text-gray-800">{{ showTooltipData.component?.name }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600">{{ showTooltipData.component?.description }}</p>
|
||||
<div class="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
|
||||
<span class="text-xs text-gray-400">{{ showTooltipData.component?.category }}</span>
|
||||
<span class="text-xs text-blue-600">Click to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -140,7 +408,31 @@
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['add-component']);
|
||||
|
||||
// Search and filtering state
|
||||
const searchQuery = ref('');
|
||||
const selectedCategories = ref([]);
|
||||
const showFilterDropdown = ref(false);
|
||||
const showSearchOptions = ref(false);
|
||||
const recentlyUsed = ref([]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const closeDropdown = (event) => {
|
||||
if (!event.target.closest('.search-container')) {
|
||||
showFilterDropdown.value = false;
|
||||
showSearchOptions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdown);
|
||||
loadRecentlyUsed();
|
||||
loadCategoryStates();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeDropdown);
|
||||
});
|
||||
|
||||
// Define available form components
|
||||
const availableComponents = [
|
||||
@ -1431,22 +1723,227 @@ fileInput.addEventListener('change', (e) => {
|
||||
}
|
||||
];
|
||||
|
||||
// Get all available categories
|
||||
const availableCategories = computed(() => {
|
||||
return [...new Set(availableComponents.map(component => component.category))];
|
||||
});
|
||||
|
||||
// Get components by category for rendering in sections
|
||||
const getComponentsByCategory = (category) => {
|
||||
return availableComponents.filter(component => component.category === category);
|
||||
};
|
||||
|
||||
// Check if component matches search query
|
||||
// Get filtered components count for empty state
|
||||
const filteredComponentsCount = computed(() => {
|
||||
let components = availableComponents;
|
||||
|
||||
// Apply category filter
|
||||
if (selectedCategories.value.length > 0) {
|
||||
components = components.filter(component =>
|
||||
selectedCategories.value.includes(component.category)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery.value) {
|
||||
components = components.filter(component => matchesSearch(component));
|
||||
}
|
||||
|
||||
return components.length;
|
||||
});
|
||||
|
||||
// Check if we are in search mode
|
||||
const isSearching = computed(() => {
|
||||
return searchQuery.value.length > 0 || selectedCategories.value.length > 0;
|
||||
});
|
||||
|
||||
// Check if there are active filters
|
||||
const hasActiveFilters = computed(() => {
|
||||
return selectedCategories.value.length > 0;
|
||||
});
|
||||
|
||||
// Enhanced search matching with highlighting support
|
||||
const matchesSearch = (component) => {
|
||||
if (!searchQuery.value) return true;
|
||||
// Always show if no search query
|
||||
if (!searchQuery.value) {
|
||||
// But apply category filter if active
|
||||
if (selectedCategories.value.length > 0) {
|
||||
return selectedCategories.value.includes(component.category);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return (
|
||||
const matchesText = (
|
||||
component.name.toLowerCase().includes(query) ||
|
||||
component.description.toLowerCase().includes(query) ||
|
||||
component.type.toLowerCase().includes(query) ||
|
||||
component.category.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
// Apply both search and category filter
|
||||
if (selectedCategories.value.length > 0) {
|
||||
return matchesText && selectedCategories.value.includes(component.category);
|
||||
}
|
||||
|
||||
return matchesText;
|
||||
};
|
||||
|
||||
// Filter functions
|
||||
const toggleFilterDropdown = () => {
|
||||
showFilterDropdown.value = !showFilterDropdown.value;
|
||||
showSearchOptions.value = false;
|
||||
};
|
||||
|
||||
const removeCategory = (category) => {
|
||||
selectedCategories.value = selectedCategories.value.filter(c => c !== category);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
selectedCategories.value = [];
|
||||
showFilterDropdown.value = false;
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = '';
|
||||
selectedCategories.value = [];
|
||||
};
|
||||
|
||||
// Recently used functions
|
||||
const addToRecentlyUsed = (component) => {
|
||||
// Remove if already exists
|
||||
recentlyUsed.value = recentlyUsed.value.filter(c => c.type !== component.type);
|
||||
// Add to beginning
|
||||
recentlyUsed.value.unshift(component);
|
||||
// Keep only last 8
|
||||
recentlyUsed.value = recentlyUsed.value.slice(0, 8);
|
||||
// Save to localStorage
|
||||
saveRecentlyUsed();
|
||||
};
|
||||
|
||||
const loadRecentlyUsed = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('formBuilder_recentComponents');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
// Validate that components still exist
|
||||
recentlyUsed.value = parsed.filter(component =>
|
||||
availableComponents.some(c => c.type === component.type)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load recently used components:', error);
|
||||
recentlyUsed.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const saveRecentlyUsed = () => {
|
||||
try {
|
||||
localStorage.setItem('formBuilder_recentComponents', JSON.stringify(recentlyUsed.value));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save recently used components:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const clearRecentlyUsed = () => {
|
||||
recentlyUsed.value = [];
|
||||
localStorage.removeItem('formBuilder_recentComponents');
|
||||
};
|
||||
|
||||
// Category collapse/expand functionality
|
||||
const collapsedCategories = ref({});
|
||||
|
||||
const toggleCategory = (category) => {
|
||||
collapsedCategories.value[category] = !collapsedCategories.value[category];
|
||||
// Save state to localStorage
|
||||
localStorage.setItem('formBuilder_collapsedCategories', JSON.stringify(collapsedCategories.value));
|
||||
};
|
||||
|
||||
const loadCategoryStates = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('formBuilder_collapsedCategories');
|
||||
if (saved) {
|
||||
collapsedCategories.value = JSON.parse(saved);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load category states:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Category visibility functions
|
||||
const shouldShowCategory = (category) => {
|
||||
const componentsInCategory = getComponentsByCategory(category);
|
||||
const visibleComponents = componentsInCategory.filter(component => matchesSearch(component));
|
||||
return visibleComponents.length > 0;
|
||||
};
|
||||
|
||||
const getVisibleComponentsInCategory = (category) => {
|
||||
const componentsInCategory = getComponentsByCategory(category);
|
||||
return componentsInCategory.filter(component => matchesSearch(component));
|
||||
};
|
||||
|
||||
// Component highlighting
|
||||
const highlightComponent = (component) => {
|
||||
if (!searchQuery.value) return false;
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return component.name.toLowerCase().includes(query);
|
||||
};
|
||||
|
||||
// Tooltip functionality
|
||||
const showTooltipData = ref({ show: false, component: null });
|
||||
const tooltip = ref(null);
|
||||
const tooltipStyle = ref({});
|
||||
|
||||
const showTooltip = (event, component) => {
|
||||
showTooltipData.value = { show: true, component };
|
||||
|
||||
nextTick(() => {
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const tooltipEl = tooltip.value;
|
||||
|
||||
if (tooltipEl) {
|
||||
const tooltipRect = tooltipEl.getBoundingClientRect();
|
||||
|
||||
// Position tooltip to the right of the component
|
||||
let left = rect.right + 8;
|
||||
let top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
|
||||
|
||||
// Adjust if tooltip would go off screen
|
||||
if (left + tooltipRect.width > window.innerWidth) {
|
||||
left = rect.left - tooltipRect.width - 8;
|
||||
}
|
||||
|
||||
if (top < 0) {
|
||||
top = 8;
|
||||
} else if (top + tooltipRect.height > window.innerHeight) {
|
||||
top = window.innerHeight - tooltipRect.height - 8;
|
||||
}
|
||||
|
||||
tooltipStyle.value = {
|
||||
position: 'fixed',
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
zIndex: 1000
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
showTooltipData.value = { show: false, component: null };
|
||||
};
|
||||
|
||||
// Search highlighting functionality
|
||||
const highlightSearchTerm = (text, searchTerm) => {
|
||||
if (!searchTerm || !text) return text;
|
||||
|
||||
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
|
||||
};
|
||||
|
||||
const getHighlightedName = (component) => {
|
||||
if (!searchQuery.value) return component.name;
|
||||
return highlightSearchTerm(component.name, searchQuery.value);
|
||||
};
|
||||
|
||||
// Handle drag start event
|
||||
@ -1511,6 +2008,7 @@ const onDragStart = (event, component) => {
|
||||
|
||||
// Add a component directly via click
|
||||
const addComponent = (component) => {
|
||||
addToRecentlyUsed(component);
|
||||
emit('add-component', component);
|
||||
};
|
||||
|
||||
@ -1527,9 +2025,7 @@ const onDragEnd = (event) => {
|
||||
|
||||
<style scoped>
|
||||
.form-components {
|
||||
@apply bg-white h-full;
|
||||
|
||||
/* More compact layout */
|
||||
@apply bg-white h-full relative;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@ -1538,13 +2034,33 @@ const onDragEnd = (event) => {
|
||||
}
|
||||
|
||||
.component-category h3 {
|
||||
@apply text-xs font-semibold px-3 mb-2 uppercase tracking-wider text-gray-700;
|
||||
@apply text-xs font-semibold uppercase tracking-wider text-gray-700;
|
||||
}
|
||||
|
||||
/* Enhanced component items */
|
||||
.component-item {
|
||||
@apply h-20;
|
||||
transition: all 0.15s ease-in-out;
|
||||
min-height: 4.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.component-item.enhanced-card {
|
||||
@apply shadow-sm;
|
||||
}
|
||||
|
||||
.component-item.enhanced-card:hover {
|
||||
@apply shadow-md border-gray-300;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.component-item.highlight-match {
|
||||
@apply ring-2 ring-yellow-200 bg-yellow-50 border-yellow-300;
|
||||
}
|
||||
|
||||
.component-item.recent-item::after {
|
||||
content: '';
|
||||
@apply absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1;
|
||||
}
|
||||
|
||||
.component-item .iconify {
|
||||
@ -1555,10 +2071,6 @@ const onDragEnd = (event) => {
|
||||
@apply leading-tight;
|
||||
}
|
||||
|
||||
.component-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.component-item:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
@ -1570,8 +2082,120 @@ const onDragEnd = (event) => {
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
/* Tooltip styles */
|
||||
.tooltip-popup {
|
||||
@apply bg-white border border-gray-200 rounded-lg shadow-lg;
|
||||
max-width: 250px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
@apply p-3;
|
||||
}
|
||||
|
||||
/* Filter dropdown styles */
|
||||
.search-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Category header hover effects */
|
||||
.component-category > div:first-child:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
/* Smooth transitions for category expansion */
|
||||
.component-category .grid {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Search highlight styles */
|
||||
.highlight-text {
|
||||
@apply bg-yellow-200;
|
||||
}
|
||||
|
||||
.search-highlight,
|
||||
:deep(.search-highlight) {
|
||||
@apply bg-yellow-200 px-1 py-0.5 rounded text-gray-800 font-medium;
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.component-name {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.component-name :deep(mark) {
|
||||
@apply bg-yellow-200 px-1 py-0.5 rounded text-gray-800 font-medium;
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Enhanced search input */
|
||||
.search-container input:focus {
|
||||
@apply ring-2 ring-blue-500 border-blue-500;
|
||||
}
|
||||
|
||||
/* Filter chip styles */
|
||||
.filter-chip {
|
||||
@apply inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full;
|
||||
}
|
||||
|
||||
.filter-chip button {
|
||||
@apply ml-1 hover:bg-blue-200 rounded-full p-0.5 transition-colors;
|
||||
}
|
||||
|
||||
/* Recently used indicator */
|
||||
.recent-indicator {
|
||||
@apply absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full transform translate-x-1 -translate-y-1;
|
||||
}
|
||||
|
||||
/* Empty state styles */
|
||||
.empty-state {
|
||||
@apply text-center text-gray-500 py-8;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
@apply text-sm font-medium text-gray-600 mb-1;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
@apply text-xs text-gray-400 mb-3;
|
||||
}
|
||||
|
||||
.drag-preview {
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.component-item {
|
||||
@apply h-16;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.component-item .iconify {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.component-item span {
|
||||
@apply text-xs;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for category expand/collapse */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.component-category .grid {
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user