Enhance Form Builder with New Component Insertion and Styling Updates

- Added functionality to insert components at a specific index in the form builder, allowing for more flexible component arrangement.
- Updated the form store to support the new insertion method, ensuring components can be added dynamically at designated positions.
- Modified CSS styles for form components, including adjustments to disabled states and ghost component appearance for improved user experience.
- Refined drag-and-drop behavior by removing animation for immediate feedback during reordering, enhancing usability.
- Introduced a new background color for disabled states to maintain visual consistency across the form builder.
This commit is contained in:
Afiq 2025-08-06 19:24:03 +08:00
parent 577128a799
commit b29c035370
6 changed files with 95 additions and 13 deletions

View File

@ -10,6 +10,7 @@ html[data-theme="default"] {
--border-color: 228, 228, 231; --border-color: 228, 228, 231;
--bg-1: 243, 244, 246; --bg-1: 243, 244, 246;
--bg-2: 255, 255, 255; --bg-2: 255, 255, 255;
--bg-disabled: 250, 250, 250;
--scroll-color: 170, 170, 170; --scroll-color: 170, 170, 170;
--scroll-hover-color: 155, 155, 155; --scroll-hover-color: 155, 155, 155;
--fk-border-color: 228, 228, 231; --fk-border-color: 228, 228, 231;

View File

@ -3,7 +3,7 @@
} }
.formkit-outer-global { .formkit-outer-global {
@apply mb-4 text-[rgb(var(--text-color))] formkit-disabled:opacity-50; @apply mb-4 text-[rgb(var(--text-color))];
} }
.formkit-help-global { .formkit-help-global {

View File

@ -26,9 +26,9 @@
bg-[rgb(var(--bg-2))] bg-[rgb(var(--bg-2))]
placeholder-[rgb(var(--fk-placeholder-color))] placeholder-[rgb(var(--fk-placeholder-color))]
focus:outline-none focus:outline-none
disabled:bg-[rgb(var(--bg-1))] disabled:bg-[rgb(var(--bg-disabled))]
disabled:border-[rgb(var(--bg-1))] disabled:border-[rgb(var(--bg-disabled))]
disabled:placeholder-[rgb(var(--bg-1))]; disabled:placeholder-[rgb(var(--bg-disabled))];
} }
.formkit-prefix-text { .formkit-prefix-text {

View File

@ -29,13 +29,14 @@
item-key="id" item-key="id"
handle=".drag-handle" handle=".drag-handle"
ghost-class="ghost" ghost-class="ghost"
animation="300" animation="0"
class="draggable-container" class="draggable-container"
@end="onDragEnd" @end="onDragEnd"
@start="onDragStart"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<div <div
class="form-component relative border rounded-md overflow-hidden transition-all duration-200 cursor-pointer" class="form-component relative border rounded-md overflow-hidden cursor-pointer"
:class="{ :class="{
'ring-2 ring-blue-500 bg-blue-50 border-blue-300 shadow-lg': selectedComponentId === element.id, 'ring-2 ring-blue-500 bg-blue-50 border-blue-300 shadow-lg': selectedComponentId === element.id,
'bg-white border-gray-200 hover:border-blue-300 hover:shadow-md hover:bg-blue-25': selectedComponentId !== element.id 'bg-white border-gray-200 hover:border-blue-300 hover:shadow-md hover:bg-blue-25': selectedComponentId !== element.id
@ -114,7 +115,7 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['select-component', 'move-component', 'delete-component', 'update-component', 'optimize-layout', 'select-nested-component']); const emit = defineEmits(['select-component', 'move-component', 'delete-component', 'update-component', 'optimize-layout', 'select-nested-component', 'insert-component-at-index']);
const selectedComponentId = ref(null); const selectedComponentId = ref(null);
const resizeMode = ref(false); const resizeMode = ref(false);
@ -273,6 +274,11 @@ const stopResize = () => {
document.removeEventListener('mouseup', stopResize); document.removeEventListener('mouseup', stopResize);
}; };
// Handle drag start from internal reordering
const onDragStart = (event) => {
// Internal reordering
};
// Handle drag end event for reordering // Handle drag end event for reordering
const onDragEnd = (event) => { const onDragEnd = (event) => {
if (event.oldIndex !== event.newIndex) { if (event.oldIndex !== event.newIndex) {
@ -294,7 +300,7 @@ onUnmounted(() => {
.grid-container { .grid-container {
display: grid; display: grid;
grid-template-columns: repeat(12, 1fr); grid-template-columns: repeat(12, 1fr);
grid-auto-flow: row dense; /* This enables automatic filling of gaps */ grid-auto-flow: row; /* Changed: Remove 'dense' to preserve intentional spacing */
column-gap: 16px; column-gap: 16px;
row-gap: 16px; row-gap: 16px;
width: 100%; width: 100%;
@ -307,20 +313,23 @@ onUnmounted(() => {
} }
.ghost { .ghost {
opacity: 0.5; opacity: 0.3;
background: #e0f2fe; background: #dbeafe;
border: 1px dashed #60a5fa; border: 2px dashed #3b82f6;
width: 100% !important; width: 100% !important;
grid-column: span 12 !important; grid-column: span 12 !important;
transform: scale(0.95);
} }
.form-component { .form-component {
transition: all 0.2s ease; /* Removed transition for immediate snapping */
grid-column: span 12; /* Default to full width */ grid-column: span 12; /* Default to full width */
width: 100% !important; /* Force the width within the grid cell */ width: 100% !important; /* Force the width within the grid cell */
margin-bottom: 16px; margin-bottom: 16px;
} }
.form-component:hover .component-actions { .form-component:hover .component-actions {
opacity: 1; opacity: 1;
} }

View File

@ -330,6 +330,7 @@
@delete-component="handleDeleteComponent" @delete-component="handleDeleteComponent"
@update-component="handleUpdateComponent" @update-component="handleUpdateComponent"
@optimize-layout="handleOptimizeLayout" @optimize-layout="handleOptimizeLayout"
@insert-component-at-index="handleInsertComponentAtIndex"
/> />
<!-- Instruction Overlay when no component is selected --> <!-- Instruction Overlay when no component is selected -->
@ -2727,6 +2728,11 @@ const handleOptimizeLayout = () => {
formStore.optimizeGridLayout(); formStore.optimizeGridLayout();
}; };
// Handle inserting component at specific index (for drop zones)
const handleInsertComponentAtIndex = ({ component, index }) => {
formStore.insertComponentAtIndex(component, index);
};
// Add the new handler function // Add the new handler function
const handleDragEnter = (event) => { const handleDragEnter = (event) => {
// Prevent default to allow drop // Prevent default to allow drop

View File

@ -425,7 +425,7 @@ export const useFormBuilderStore = defineStore('formBuilder', {
}, },
// Find optimal placement for a new component in the grid // Find optimal placement for a new component in the grid
findOptimalGridPlacement() { findOptimalGridPlacement(respectDesignSpacing = true) {
if (this.formComponents.length === 0) { if (this.formComponents.length === 0) {
// First component - full width // First component - full width
return { return {
@ -435,6 +435,16 @@ export const useFormBuilderStore = defineStore('formBuilder', {
}; };
} }
// If respecting design spacing, always create a new row (don't auto-fill gaps)
if (respectDesignSpacing) {
return {
gridColumn: 'span 12',
rowIndex: this.formComponents.length,
width: '100%'
};
}
// Legacy auto-fill behavior (kept for compatibility)
// Group components by their implicit row // Group components by their implicit row
const rows = []; const rows = [];
let currentRowY = 0; let currentRowY = 0;
@ -505,6 +515,62 @@ export const useFormBuilderStore = defineStore('formBuilder', {
} }
}, },
// Insert component at a specific index (for drop zones)
insertComponentAtIndex(component, index) {
console.log('FormStore: Inserting component at index', index, component.type);
// Store the state before the change for history
const beforeComponents = [...this.formComponents];
const newComponentId = uuidv4();
try {
// Create a deep copy of the default props to avoid reference issues
const defaultProps = component.defaultProps ? JSON.parse(JSON.stringify(component.defaultProps)) : {};
// Set default grid properties - respect design spacing by using full width
defaultProps.width = defaultProps.width || '100%';
defaultProps.gridColumn = defaultProps.gridColumn || 'span 12';
// Generate a default name based on component type if not provided
if (!defaultProps.name) {
defaultProps.name = `${component.type}_${this.formComponents.length + 1}`;
}
// Generate a default label based on component name if not provided
if (!defaultProps.label && !['heading', 'paragraph', 'divider'].includes(component.type)) {
defaultProps.label = `${component.name} ${this.formComponents.length + 1}`;
}
// Create the new component object
const newComponent = {
id: newComponentId,
type: component.type,
name: component.name,
icon: component.icon,
category: component.category,
props: defaultProps
};
// Insert the component at the specified index
this.formComponents.splice(index, 0, newComponent);
// Select the new component
this.selectedComponentId = newComponentId;
// Mark as having unsaved changes
this.hasUnsavedChanges = true;
// Record history
this.recordAction('Add component', beforeComponents, [...this.formComponents]);
console.log('FormStore: Component inserted at index', index, 'Total components:', this.formComponents.length);
} catch (error) {
console.error('Error inserting component at index:', error);
console.error('Problematic component:', component);
}
},
selectComponent(id) { selectComponent(id) {
// Don't record history for selection changes // Don't record history for selection changes
this.selectedComponentId = id; this.selectedComponentId = id;