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;
--bg-1: 243, 244, 246;
--bg-2: 255, 255, 255;
--bg-disabled: 250, 250, 250;
--scroll-color: 170, 170, 170;
--scroll-hover-color: 155, 155, 155;
--fk-border-color: 228, 228, 231;

View File

@ -3,7 +3,7 @@
}
.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 {

View File

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

View File

@ -29,13 +29,14 @@
item-key="id"
handle=".drag-handle"
ghost-class="ghost"
animation="300"
animation="0"
class="draggable-container"
@end="onDragEnd"
@start="onDragStart"
>
<template #item="{ element, index }">
<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="{
'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
@ -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 resizeMode = ref(false);
@ -273,6 +274,11 @@ const stopResize = () => {
document.removeEventListener('mouseup', stopResize);
};
// Handle drag start from internal reordering
const onDragStart = (event) => {
// Internal reordering
};
// Handle drag end event for reordering
const onDragEnd = (event) => {
if (event.oldIndex !== event.newIndex) {
@ -294,7 +300,7 @@ onUnmounted(() => {
.grid-container {
display: grid;
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;
row-gap: 16px;
width: 100%;
@ -307,20 +313,23 @@ onUnmounted(() => {
}
.ghost {
opacity: 0.5;
background: #e0f2fe;
border: 1px dashed #60a5fa;
opacity: 0.3;
background: #dbeafe;
border: 2px dashed #3b82f6;
width: 100% !important;
grid-column: span 12 !important;
transform: scale(0.95);
}
.form-component {
transition: all 0.2s ease;
/* Removed transition for immediate snapping */
grid-column: span 12; /* Default to full width */
width: 100% !important; /* Force the width within the grid cell */
margin-bottom: 16px;
}
.form-component:hover .component-actions {
opacity: 1;
}

View File

@ -330,6 +330,7 @@
@delete-component="handleDeleteComponent"
@update-component="handleUpdateComponent"
@optimize-layout="handleOptimizeLayout"
@insert-component-at-index="handleInsertComponentAtIndex"
/>
<!-- Instruction Overlay when no component is selected -->
@ -2727,6 +2728,11 @@ const handleOptimizeLayout = () => {
formStore.optimizeGridLayout();
};
// Handle inserting component at specific index (for drop zones)
const handleInsertComponentAtIndex = ({ component, index }) => {
formStore.insertComponentAtIndex(component, index);
};
// Add the new handler function
const handleDragEnter = (event) => {
// 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
findOptimalGridPlacement() {
findOptimalGridPlacement(respectDesignSpacing = true) {
if (this.formComponents.length === 0) {
// First component - full width
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
const rows = [];
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) {
// Don't record history for selection changes
this.selectedComponentId = id;