Implement component resizing and grid optimization in Form Builder
- Added functionality for resizing components within the Form Builder, allowing users to adjust widths dynamically. - Introduced a method to find optimal grid placement for new components, ensuring efficient use of available space. - Enhanced layout optimization after component addition, deletion, and reordering to maintain a clean and organized interface. - Updated the FormBuilderConfiguration to include width settings for components, improving customization options. - Refactored styles for grid layout and component previews to enhance visual consistency and user experience.
This commit is contained in:
parent
e5c5d46dae
commit
63a7d0f870
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="component-preview">
|
||||
<div class="component-preview" :style="componentStyle">
|
||||
<!-- Basic Input Types -->
|
||||
<FormKit
|
||||
v-if="isInputType"
|
||||
@ -76,6 +76,23 @@ const isInputType = computed(() => {
|
||||
|
||||
return inputTypes.includes(props.component.type);
|
||||
});
|
||||
|
||||
// Compute style based on grid properties
|
||||
const componentStyle = computed(() => {
|
||||
// Only apply grid styles in the non-preview mode (actual form)
|
||||
if (props.isPreview) {
|
||||
return {}; // Styling is handled by parent in canvas mode
|
||||
}
|
||||
|
||||
// Apply grid column in preview mode
|
||||
const gridColumn = props.component.props.gridColumn || 'span 12';
|
||||
|
||||
return {
|
||||
gridColumn: gridColumn,
|
||||
width: '100%', // Always use 100% within the grid cell
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -6,58 +6,81 @@
|
||||
<p class="text-xs mt-1">Or click a component from the sidebar</p>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-else
|
||||
v-model="componentList"
|
||||
group="form-components"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="ghost"
|
||||
animation="300"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="form-component relative mb-3 border rounded-md overflow-hidden transition-all"
|
||||
:class="{
|
||||
'ring-2 ring-blue-400 bg-blue-50 border-transparent': selectedComponentId === element.id,
|
||||
'bg-white border-gray-200 hover:border-gray-300': selectedComponentId !== element.id
|
||||
}"
|
||||
@click.capture="selectComponent(element)"
|
||||
>
|
||||
<div class="component-actions absolute right-1.5 top-1.5 flex space-x-1 z-10">
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
title="Drag to reorder"
|
||||
<div v-else class="grid-container">
|
||||
<draggable
|
||||
v-model="componentList"
|
||||
group="form-components"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="ghost"
|
||||
animation="300"
|
||||
class="draggable-container"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="form-component relative border rounded-md overflow-hidden transition-all"
|
||||
:class="{
|
||||
'ring-2 ring-blue-400 bg-blue-50 border-transparent': selectedComponentId === element.id,
|
||||
'bg-white border-gray-200 hover:border-gray-300': selectedComponentId !== element.id
|
||||
}"
|
||||
:style="{
|
||||
gridColumn: element.props.gridColumn || 'span 12'
|
||||
}"
|
||||
@click.capture="selectComponent(element)"
|
||||
>
|
||||
<div class="component-actions absolute right-1.5 top-1.5 flex space-x-1 z-10">
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<span class="drag-handle cursor-move">
|
||||
<Icon name="material-symbols:drag-indicator" class="w-4 h-4" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
title="Resize component"
|
||||
@click.stop="toggleResizeMode(element)"
|
||||
>
|
||||
<Icon name="material-symbols:resize" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-red-500 rounded"
|
||||
title="Delete component"
|
||||
@click.stop="deleteComponent(element.id)"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<component-preview
|
||||
:component="element"
|
||||
:is-preview="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Resize handle - only shown when in resize mode -->
|
||||
<div
|
||||
v-if="resizeMode && selectedComponentId === element.id"
|
||||
class="resize-handles"
|
||||
>
|
||||
<span class="drag-handle cursor-move">
|
||||
<Icon name="material-symbols:drag-indicator" class="w-4 h-4" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-red-500 rounded"
|
||||
title="Delete component"
|
||||
@click.stop="deleteComponent(element.id)"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
class="resize-handle resize-handle-right"
|
||||
@mousedown.stop.prevent="startResize($event, element)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<component-preview
|
||||
:component="element"
|
||||
:is-preview="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { onMounted, watch, onUnmounted, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
formComponents: {
|
||||
@ -66,9 +89,13 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select-component', 'move-component', 'delete-component']);
|
||||
const emit = defineEmits(['select-component', 'move-component', 'delete-component', 'update-component', 'optimize-layout']);
|
||||
|
||||
const selectedComponentId = ref(null);
|
||||
const resizeMode = ref(false);
|
||||
const resizing = ref(false);
|
||||
const initialWidth = ref(0);
|
||||
const initialX = ref(0);
|
||||
|
||||
// Watch for changes in formComponents
|
||||
watch(() => props.formComponents, (newComponents) => {
|
||||
@ -76,6 +103,7 @@ watch(() => props.formComponents, (newComponents) => {
|
||||
if (selectedComponentId.value &&
|
||||
!newComponents.some(comp => comp.id === selectedComponentId.value)) {
|
||||
selectedComponentId.value = null;
|
||||
resizeMode.value = false;
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
@ -100,10 +128,115 @@ const selectComponent = (component) => {
|
||||
const deleteComponent = (id) => {
|
||||
if (selectedComponentId.value === id) {
|
||||
selectedComponentId.value = null;
|
||||
resizeMode.value = false;
|
||||
}
|
||||
emit('delete-component', id);
|
||||
};
|
||||
|
||||
// Toggle resize mode
|
||||
const toggleResizeMode = (component) => {
|
||||
resizeMode.value = !resizeMode.value;
|
||||
selectedComponentId.value = component.id;
|
||||
};
|
||||
|
||||
// Start resizing
|
||||
const startResize = (event, component) => {
|
||||
resizing.value = true;
|
||||
selectedComponentId.value = component.id;
|
||||
|
||||
// Store initial values
|
||||
initialWidth.value = parseInt(component.props.width) || 100;
|
||||
initialX.value = event.clientX;
|
||||
|
||||
// Add event listeners
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', stopResize);
|
||||
};
|
||||
|
||||
// Handle resize event
|
||||
const handleResize = (event) => {
|
||||
if (!resizing.value || !selectedComponentId.value) return;
|
||||
|
||||
// Calculate new width
|
||||
const component = props.formComponents.find(c => c.id === selectedComponentId.value);
|
||||
if (!component) return;
|
||||
|
||||
// Calculate delta
|
||||
const deltaX = event.clientX - initialX.value;
|
||||
|
||||
// Convert to percentage of container width
|
||||
const container = document.querySelector('.grid-container');
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.offsetWidth;
|
||||
const deltaPercentage = (deltaX / containerWidth) * 100;
|
||||
|
||||
// Calculate new width (with constraints)
|
||||
let newWidth = initialWidth.value + deltaPercentage;
|
||||
|
||||
// Constrain to reasonable values
|
||||
newWidth = Math.max(25, Math.min(100, newWidth)); // Min 25%, max 100%
|
||||
|
||||
// Get the current column span
|
||||
const currentSpanMatch = component.props.gridColumn?.match(/span\s+(\d+)/) || [];
|
||||
const currentSpan = parseInt(currentSpanMatch[1]) || 12;
|
||||
|
||||
// Define standard widths for snap points (25%, 33%, 50%, 66%, 75%, 100%)
|
||||
const standardWidths = [25, 33, 50, 66, 75, 100];
|
||||
|
||||
// Snap to nearest standard width if within 5%
|
||||
for (const std of standardWidths) {
|
||||
if (Math.abs(newWidth - std) < 5) {
|
||||
newWidth = std;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert precise percentages to exact grid column spans
|
||||
// This ensures the visual appearance matches the percentage
|
||||
let gridColumns;
|
||||
switch (newWidth) {
|
||||
case 25: gridColumns = 3; break; // 3/12 = 25%
|
||||
case 33: gridColumns = 4; break; // 4/12 = 33.33%
|
||||
case 50: gridColumns = 6; break; // 6/12 = 50%
|
||||
case 66: gridColumns = 8; break; // 8/12 = 66.67%
|
||||
case 75: gridColumns = 9; break; // 9/12 = 75%
|
||||
case 100: gridColumns = 12; break; // 12/12 = 100%
|
||||
default: gridColumns = Math.round((newWidth / 100) * 12);
|
||||
}
|
||||
|
||||
// Only update if the span actually changed to avoid unnecessary rerenders
|
||||
if (gridColumns !== currentSpan) {
|
||||
// Update component's width and grid column span
|
||||
const updatedComponent = {
|
||||
...component,
|
||||
props: {
|
||||
...component.props,
|
||||
width: `${newWidth}%`,
|
||||
gridColumn: `span ${gridColumns}`
|
||||
}
|
||||
};
|
||||
|
||||
// Signal component update
|
||||
emit('update-component', updatedComponent);
|
||||
|
||||
// Signal that a resize has occurred that might require layout optimization
|
||||
// Using nextTick to ensure the update is processed first
|
||||
nextTick(() => {
|
||||
emit('optimize-layout');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Stop resizing
|
||||
const stopResize = () => {
|
||||
resizing.value = false;
|
||||
|
||||
// Remove event listeners
|
||||
document.removeEventListener('mousemove', handleResize);
|
||||
document.removeEventListener('mouseup', stopResize);
|
||||
};
|
||||
|
||||
// Handle drag end event for reordering
|
||||
const onDragEnd = (event) => {
|
||||
if (event.oldIndex !== event.newIndex) {
|
||||
@ -113,17 +246,43 @@ const onDragEnd = (event) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up event listeners when component is unmounted
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleResize);
|
||||
document.removeEventListener('mouseup', stopResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row dense; /* This enables automatic filling of gaps */
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.draggable-container {
|
||||
display: contents; /* This makes draggable container not interfere with the grid */
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #e0f2fe;
|
||||
border: 1px dashed #60a5fa;
|
||||
width: 100% !important;
|
||||
grid-column: span 12 !important;
|
||||
}
|
||||
|
||||
.form-component {
|
||||
transition: all 0.2s ease;
|
||||
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 {
|
||||
@ -138,4 +297,35 @@ const onDragEnd = (event) => {
|
||||
.form-component:hover {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.resize-handles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.resize-handle-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
background-color: rgba(37, 99, 235, 0.2);
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.resize-handle-right:hover,
|
||||
.resize-handle-right:active {
|
||||
background-color: rgba(37, 99, 235, 0.4);
|
||||
width: 8px;
|
||||
}
|
||||
</style>
|
@ -363,9 +363,20 @@ const matchesSearch = (component) => {
|
||||
|
||||
// Handle drag start event
|
||||
const onDragStart = (event, component) => {
|
||||
// Just set the basic component data, the optimal grid placement
|
||||
// will be calculated in the store when adding the component
|
||||
const componentWithGrid = {
|
||||
...component,
|
||||
defaultProps: {
|
||||
...component.defaultProps,
|
||||
width: '100%'
|
||||
// Note: gridColumn is now determined by the store's findOptimalGridPlacement method
|
||||
}
|
||||
};
|
||||
|
||||
// Set the drag data
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('component', JSON.stringify(component));
|
||||
event.dataTransfer.setData('component', JSON.stringify(componentWithGrid));
|
||||
};
|
||||
|
||||
// Add a component directly via click
|
||||
|
@ -24,6 +24,34 @@
|
||||
<div class="custom-tab-content p-4 border border-gray-200 rounded-b bg-white">
|
||||
<!-- Basic Tab -->
|
||||
<div v-if="activeTab === 'basic'" class="space-y-3">
|
||||
<!-- Width Configuration -->
|
||||
<div v-if="showField('width')">
|
||||
<label class="text-sm font-medium mb-1 block">Component Width</label>
|
||||
<div class="grid grid-cols-4 gap-2 mt-2">
|
||||
<button
|
||||
v-for="width in [25, 33, 50, 66, 75, 100]"
|
||||
:key="width"
|
||||
@click="setComponentWidth(width)"
|
||||
class="py-1 px-2 border rounded text-xs"
|
||||
:class="{
|
||||
'bg-blue-50 border-blue-200 text-blue-600': getComponentWidthPercent() === width,
|
||||
'bg-white border-gray-200 text-gray-700 hover:bg-gray-50': getComponentWidthPercent() !== width
|
||||
}"
|
||||
>
|
||||
{{ width }}%
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center mt-2">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
class="bg-blue-600 h-2.5 rounded-full"
|
||||
:style="{ width: configModel.width || '100%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="ml-2 text-xs text-gray-500">{{ configModel.width || '100%' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Standard Fields For All Components -->
|
||||
<FormKit
|
||||
v-if="showField('label')"
|
||||
@ -350,6 +378,8 @@ const showField = (fieldName) => {
|
||||
return componentType === 'repeater';
|
||||
case 'id':
|
||||
return true; // Always show component ID in advanced tab
|
||||
case 'width':
|
||||
return true; // Always show width in basic tab
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -371,6 +401,76 @@ const addOption = () => {
|
||||
const removeOption = (index) => {
|
||||
configModel.value.options.splice(index, 1);
|
||||
};
|
||||
|
||||
// Inside the <script setup> section
|
||||
// Add width to fieldsToShow
|
||||
const fieldsToShow = {
|
||||
// Basic input types
|
||||
text: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
textarea: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
number: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
email: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
password: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
|
||||
// Selection types
|
||||
select: ['label', 'name', 'placeholder', 'help', 'options', 'width'],
|
||||
checkbox: ['label', 'name', 'help', 'options', 'width'],
|
||||
radio: ['label', 'name', 'help', 'options', 'width'],
|
||||
|
||||
// Date and time
|
||||
date: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
time: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
'datetime-local': ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
|
||||
// Advanced
|
||||
file: ['label', 'name', 'help', 'accept', 'width'],
|
||||
repeater: ['label', 'name', 'help', 'max', 'width'],
|
||||
group: ['label', 'name', 'help', 'width'],
|
||||
|
||||
// Layout elements
|
||||
heading: ['value', 'level', 'width'],
|
||||
paragraph: ['value', 'width'],
|
||||
divider: ['width']
|
||||
};
|
||||
|
||||
// Add these methods
|
||||
const getComponentWidthPercent = () => {
|
||||
if (!configModel.value.width) return 100;
|
||||
|
||||
// Parse the width from percentage string
|
||||
const widthStr = configModel.value.width.toString();
|
||||
const match = widthStr.match(/(\d+)%/);
|
||||
return match ? parseInt(match[1]) : 100;
|
||||
};
|
||||
|
||||
const setComponentWidth = (widthPercent) => {
|
||||
// Convert precise percentages to exact grid column spans
|
||||
// This ensures the visual appearance matches the percentage
|
||||
let gridColumns;
|
||||
switch (widthPercent) {
|
||||
case 25: gridColumns = 3; break; // 3/12 = 25%
|
||||
case 33: gridColumns = 4; break; // 4/12 = 33.33%
|
||||
case 50: gridColumns = 6; break; // 6/12 = 50%
|
||||
case 66: gridColumns = 8; break; // 8/12 = 66.67%
|
||||
case 75: gridColumns = 9; break; // 9/12 = 75%
|
||||
case 100: gridColumns = 12; break; // 12/12 = 100%
|
||||
default: gridColumns = Math.round((widthPercent / 100) * 12);
|
||||
}
|
||||
|
||||
// Update the configModel
|
||||
configModel.value = {
|
||||
...configModel.value,
|
||||
width: `${widthPercent}%`,
|
||||
gridColumn: `span ${gridColumns}`
|
||||
};
|
||||
|
||||
// Emit update event
|
||||
emit('update-component', {
|
||||
id: props.component.id,
|
||||
...props.component,
|
||||
props: configModel.value
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -95,6 +95,8 @@
|
||||
@select-component="handleSelectComponent"
|
||||
@move-component="handleMoveComponent"
|
||||
@delete-component="handleDeleteComponent"
|
||||
@update-component="handleUpdateComponent"
|
||||
@optimize-layout="handleOptimizeLayout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -135,16 +137,19 @@
|
||||
<!-- Preview Modal -->
|
||||
<RsModal v-model="showPreview" title="Form Preview" size="xl">
|
||||
<div class="max-h-[70vh] overflow-y-auto p-4">
|
||||
<FormKit type="form" @submit="handlePreviewSubmit">
|
||||
<template
|
||||
v-for="(component, index) in formStore.formComponents"
|
||||
:key="index"
|
||||
>
|
||||
<component-preview :component="component" :is-preview="false" />
|
||||
</template>
|
||||
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false">
|
||||
<div class="grid-preview-container">
|
||||
<template
|
||||
v-for="(component, index) in formStore.formComponents"
|
||||
:key="index"
|
||||
>
|
||||
<component-preview :component="component" :is-preview="false" />
|
||||
</template>
|
||||
</div>
|
||||
<FormKit type="submit" label="Submit" />
|
||||
</FormKit>
|
||||
</div>
|
||||
<template #footer> </template>
|
||||
</RsModal>
|
||||
</div>
|
||||
</template>
|
||||
@ -239,6 +244,10 @@ const handlePreviewSubmit = (formData) => {
|
||||
const navigateToManage = () => {
|
||||
router.push("/form-builder/manage");
|
||||
};
|
||||
|
||||
const handleOptimizeLayout = () => {
|
||||
// Implementation of handleOptimizeLayout method
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -263,4 +272,14 @@ const navigateToManage = () => {
|
||||
font-size: 0.7rem;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.grid-preview-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
@ -34,13 +34,18 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
|
||||
actions: {
|
||||
addComponent(component) {
|
||||
// Find optimal grid placement for the new component
|
||||
const { gridColumn, rowIndex, width } = this.findOptimalGridPlacement();
|
||||
|
||||
const newComponent = {
|
||||
...component,
|
||||
id: uuidv4(),
|
||||
props: {
|
||||
...component.defaultProps,
|
||||
name: `${component.type}_${this.formComponents.length + 1}`,
|
||||
label: `${component.name} ${this.formComponents.length + 1}`
|
||||
label: `${component.name} ${this.formComponents.length + 1}`,
|
||||
width: width,
|
||||
gridColumn: gridColumn
|
||||
}
|
||||
};
|
||||
|
||||
@ -48,6 +53,87 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
this.selectComponent(newComponent.id);
|
||||
},
|
||||
|
||||
// Find optimal placement for a new component in the grid
|
||||
findOptimalGridPlacement() {
|
||||
if (this.formComponents.length === 0) {
|
||||
// First component - full width
|
||||
return {
|
||||
gridColumn: 'span 12',
|
||||
rowIndex: 0,
|
||||
width: '100%'
|
||||
};
|
||||
}
|
||||
|
||||
// Group components by their implicit row
|
||||
const rows = [];
|
||||
let currentRowY = 0;
|
||||
let currentRowIndex = 0;
|
||||
let currentRowSpace = 0;
|
||||
|
||||
// Sort components by their position in the form (to handle reordering)
|
||||
// This assumes components are ordered top to bottom
|
||||
const sortedComponents = [...this.formComponents];
|
||||
|
||||
// Track used columns in each row
|
||||
sortedComponents.forEach(component => {
|
||||
const spanMatch = component.props.gridColumn?.match(/span\s+(\d+)/) || [];
|
||||
const columnSpan = parseInt(spanMatch[1]) || 12;
|
||||
|
||||
// If this is the first component in a row or there's enough space
|
||||
if (currentRowSpace === 0) {
|
||||
// Start a new row
|
||||
currentRowSpace = 12 - columnSpan;
|
||||
rows[currentRowIndex] = { components: [component], remainingSpace: currentRowSpace };
|
||||
} else if (columnSpan <= currentRowSpace) {
|
||||
// Add to current row
|
||||
currentRowSpace -= columnSpan;
|
||||
rows[currentRowIndex].components.push(component);
|
||||
rows[currentRowIndex].remainingSpace = currentRowSpace;
|
||||
} else {
|
||||
// Start a new row
|
||||
currentRowIndex++;
|
||||
currentRowSpace = 12 - columnSpan;
|
||||
rows[currentRowIndex] = { components: [component], remainingSpace: currentRowSpace };
|
||||
}
|
||||
});
|
||||
|
||||
// Find the row with remaining space
|
||||
const rowWithSpace = rows.find(row => row.remainingSpace > 0);
|
||||
|
||||
if (rowWithSpace) {
|
||||
// Use remaining space in an existing row
|
||||
const remainingColumns = rowWithSpace.remainingSpace;
|
||||
|
||||
// Calculate width percentage based on columns
|
||||
// Convert columns to percentage (each column is 8.33% of the grid)
|
||||
let widthPercent;
|
||||
|
||||
// Map grid columns to standard width percentages
|
||||
switch (remainingColumns) {
|
||||
case 3: widthPercent = 25; break; // 3/12 = 25%
|
||||
case 4: widthPercent = 33; break; // 4/12 = 33.33%
|
||||
case 6: widthPercent = 50; break; // 6/12 = 50%
|
||||
case 8: widthPercent = 66; break; // 8/12 = 66.67%
|
||||
case 9: widthPercent = 75; break; // 9/12 = 75%
|
||||
case 12: widthPercent = 100; break; // 12/12 = 100%
|
||||
default: widthPercent = Math.round((remainingColumns / 12) * 100);
|
||||
}
|
||||
|
||||
return {
|
||||
gridColumn: `span ${remainingColumns}`,
|
||||
rowIndex: rows.indexOf(rowWithSpace),
|
||||
width: `${widthPercent}%`
|
||||
};
|
||||
} else {
|
||||
// No space in existing rows, create a new row
|
||||
return {
|
||||
gridColumn: 'span 12',
|
||||
rowIndex: rows.length,
|
||||
width: '100%'
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
selectComponent(id) {
|
||||
this.selectedComponentId = id;
|
||||
},
|
||||
@ -63,6 +149,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
if (oldIndex !== newIndex) {
|
||||
const component = this.formComponents.splice(oldIndex, 1)[0];
|
||||
this.formComponents.splice(newIndex, 0, component);
|
||||
|
||||
// Optimize layout after reordering
|
||||
this.optimizeGridLayout();
|
||||
}
|
||||
},
|
||||
|
||||
@ -80,6 +169,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
this.selectedComponentId = this.formComponents[newIndex].id;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize layout after deletion
|
||||
this.optimizeGridLayout();
|
||||
}
|
||||
},
|
||||
|
||||
@ -129,6 +221,111 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
loadSavedForms() {
|
||||
const savedForms = JSON.parse(localStorage.getItem('savedForms') || '[]');
|
||||
this.savedForms = savedForms;
|
||||
},
|
||||
|
||||
// Optimize the grid layout by analyzing the current components
|
||||
// and adjusting their sizes to fill available spaces
|
||||
optimizeGridLayout() {
|
||||
// Skip if no components
|
||||
if (this.formComponents.length === 0) return;
|
||||
|
||||
// Group components by their implicit row (similar to findOptimalGridPlacement)
|
||||
const rows = [];
|
||||
let currentRowIndex = 0;
|
||||
let currentRowSpace = 12; // Full width available initially
|
||||
|
||||
// Sort components by their position in the form
|
||||
const sortedComponents = [...this.formComponents];
|
||||
|
||||
// First pass: Group components into rows
|
||||
sortedComponents.forEach(component => {
|
||||
const spanMatch = component.props.gridColumn?.match(/span\s+(\d+)/) || [];
|
||||
const columnSpan = parseInt(spanMatch[1]) || 12;
|
||||
|
||||
// If this is the first component in a row or there's enough space
|
||||
if (currentRowSpace === 12) { // Start of a new row
|
||||
currentRowSpace = 12 - columnSpan;
|
||||
rows[currentRowIndex] = {
|
||||
components: [{ component, span: columnSpan }],
|
||||
remainingSpace: currentRowSpace
|
||||
};
|
||||
} else if (columnSpan <= currentRowSpace) {
|
||||
// Add to current row
|
||||
currentRowSpace -= columnSpan;
|
||||
rows[currentRowIndex].components.push({ component, span: columnSpan });
|
||||
rows[currentRowIndex].remainingSpace = currentRowSpace;
|
||||
} else {
|
||||
// Start a new row
|
||||
currentRowIndex++;
|
||||
currentRowSpace = 12 - columnSpan;
|
||||
rows[currentRowIndex] = {
|
||||
components: [{ component, span: columnSpan }],
|
||||
remainingSpace: currentRowSpace
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: Optimize each row
|
||||
rows.forEach(row => {
|
||||
// Skip full rows
|
||||
if (row.remainingSpace === 0) return;
|
||||
|
||||
// If there's only one component in a row with remaining space,
|
||||
// expand it to fill the row
|
||||
if (row.components.length === 1 && row.remainingSpace > 0) {
|
||||
const comp = row.components[0];
|
||||
const totalSpan = 12; // Full row
|
||||
this.updateComponentSize(comp.component, totalSpan);
|
||||
}
|
||||
// If there are multiple components in a row with remaining space,
|
||||
// distribute the space proportionally
|
||||
else if (row.components.length > 1 && row.remainingSpace > 0) {
|
||||
// Calculate how much extra space each component gets
|
||||
const extraSpanPerComponent = Math.floor(row.remainingSpace / row.components.length);
|
||||
let remainingExtraSpan = row.remainingSpace % row.components.length;
|
||||
|
||||
// Distribute the remaining columns among components
|
||||
row.components.forEach(comp => {
|
||||
// Calculate new span, adding extra space plus one more if there's remainder
|
||||
let extraSpan = extraSpanPerComponent;
|
||||
if (remainingExtraSpan > 0) {
|
||||
extraSpan += 1;
|
||||
remainingExtraSpan--;
|
||||
}
|
||||
|
||||
const newSpan = comp.span + extraSpan;
|
||||
this.updateComponentSize(comp.component, newSpan);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update a component's size based on a new column span
|
||||
updateComponentSize(component, newSpan) {
|
||||
// Convert the span to a standard width percentage
|
||||
let widthPercent;
|
||||
switch (newSpan) {
|
||||
case 3: widthPercent = 25; break;
|
||||
case 4: widthPercent = 33; break;
|
||||
case 6: widthPercent = 50; break;
|
||||
case 8: widthPercent = 66; break;
|
||||
case 9: widthPercent = 75; break;
|
||||
case 12: widthPercent = 100; break;
|
||||
default: widthPercent = Math.round((newSpan / 12) * 100);
|
||||
}
|
||||
|
||||
// Update the component
|
||||
const index = this.formComponents.findIndex(c => c.id === component.id);
|
||||
if (index !== -1) {
|
||||
this.formComponents[index] = {
|
||||
...component,
|
||||
props: {
|
||||
...component.props,
|
||||
width: `${widthPercent}%`,
|
||||
gridColumn: `span ${newSpan}`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user