Enhance ComponentPreview and FormBuilder Components with Drag-and-Drop Functionality
- Updated ComponentPreview.vue to implement a draggable container for nested components, allowing users to reorder fields within repeating groups. - Enhanced the user interface with visual feedback during drag-and-drop actions, improving usability and interaction. - Modified FormBuilderComponents.vue to include default properties for nested components, facilitating better organization and management of form fields. - Improved FormBuilderFieldSettingsModal.vue to provide clearer information about repeating group containers and their functionalities. - Refactored event handling for component updates and deletions to support nested components within sections and repeating groups, ensuring consistent data management. - Updated styles across components to enhance the visual experience during drag-and-drop operations and improve overall aesthetics.
This commit is contained in:
parent
f86fe87fc5
commit
415ac5a0d1
@ -144,24 +144,81 @@
|
|||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Default group preview (in edit mode) -->
|
<!-- Builder mode - show draggable container -->
|
||||||
<div v-if="!isPreview" class="repeating-groups space-y-4">
|
<div v-if="!isPreview" class="repeating-groups space-y-4">
|
||||||
<div class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
<!-- Container for draggable fields -->
|
||||||
<div class="flex justify-between items-center mb-3">
|
<div
|
||||||
<h4 class="text-sm font-medium text-gray-700">Item 1</h4>
|
class="repeating-group-container border-2 border-dashed border-gray-300 rounded-md p-4 min-h-[100px] bg-gray-50"
|
||||||
<button type="button" class="text-red-500 hover:text-red-700 text-sm">
|
:class="{
|
||||||
{{ component.props.removeText || 'Remove' }}
|
'border-blue-400 bg-blue-50': sectionDropStates[component.id]?.isDraggingOver
|
||||||
</button>
|
}"
|
||||||
|
@dragover.prevent="handleSectionDragOver($event, component.id)"
|
||||||
|
@dragleave="handleSectionDragLeave($event, component.id)"
|
||||||
|
@drop="handleSectionDrop($event, component.id)"
|
||||||
|
@dragenter.prevent="handleSectionDragEnter($event, component.id)"
|
||||||
|
>
|
||||||
|
<div v-if="component.props.children && component.props.children.length > 0" class="nested-components">
|
||||||
|
<draggable
|
||||||
|
v-model="component.props.children"
|
||||||
|
group="form-components"
|
||||||
|
item-key="id"
|
||||||
|
handle=".drag-handle"
|
||||||
|
ghost-class="ghost"
|
||||||
|
animation="300"
|
||||||
|
class="grid grid-cols-12 gap-2"
|
||||||
|
@end="onNestedDragEnd"
|
||||||
|
@add="onNestedComponentAdd"
|
||||||
|
>
|
||||||
|
<template #item="{ element: childElement, index: childIndex }">
|
||||||
|
<div
|
||||||
|
class="form-component relative border rounded-md overflow-hidden transition-all duration-200 bg-white"
|
||||||
|
:class="'border-gray-200 hover:border-blue-300 hover:shadow-md'"
|
||||||
|
:style="{
|
||||||
|
gridColumn: childElement.props.gridColumn || 'span 6'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Component actions -->
|
||||||
|
<div class="component-actions absolute right-1 top-1 flex space-x-1 z-10">
|
||||||
|
<button
|
||||||
|
class="p-1 text-gray-400 hover:text-blue-600 rounded"
|
||||||
|
title="Component settings"
|
||||||
|
@click.stop="openNestedComponentSettings(childElement)"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:cog-6-tooth" class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<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-3 h-3" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-gray-400 hover:text-red-500 rounded"
|
||||||
|
title="Remove from group"
|
||||||
|
@click.stop="removeFromSection(component.id, childIndex)"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:close" class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<component-preview :component="childElement" :is-preview="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<!-- Empty state -->
|
||||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
<div v-else class="text-center py-8">
|
||||||
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
|
<Icon name="material-symbols:add-circle-outline" class="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
||||||
:name="`${field.name}_1`" :options="field.options" disabled />
|
<p class="text-sm text-gray-500">Drag fields here to add them to this repeating group</p>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Item button for preview -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
@ -169,7 +226,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Functional groups (in preview mode) -->
|
<!-- Preview mode - show functional repeating groups -->
|
||||||
<div v-else class="repeating-groups space-y-4">
|
<div v-else class="repeating-groups space-y-4">
|
||||||
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData) || [])" :key="groupIndex"
|
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData) || [])" :key="groupIndex"
|
||||||
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
||||||
@ -181,12 +238,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="grid grid-cols-12 gap-2">
|
||||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
<!-- Render children components for each group item -->
|
||||||
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
|
<template v-for="(child, childIndex) in component.props.children" :key="childIndex">
|
||||||
:name="`${component.props.name}.${groupIndex}.${field.name}`" :options="field.options"
|
<div
|
||||||
:value="group[field.name]"
|
class="form-component"
|
||||||
@input="updateGroupField(component.props.name, groupIndex, field.name, $event)" />
|
:style="{
|
||||||
|
gridColumn: child.props.gridColumn || 'span 6'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component-preview :component="child" :is-preview="true" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -462,9 +524,16 @@
|
|||||||
<!-- In preview mode, show only the nested components (no placeholder) -->
|
<!-- In preview mode, show only the nested components (no placeholder) -->
|
||||||
<div v-if="isPreview" class="section-fields">
|
<div v-if="isPreview" class="section-fields">
|
||||||
<!-- Render nested components if they exist -->
|
<!-- Render nested components if they exist -->
|
||||||
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
<div v-if="component.props.children && component.props.children.length > 0" class="grid grid-cols-12 gap-2">
|
||||||
<template v-for="(childComponent, childIndex) in component.props.children" :key="childIndex">
|
<template v-for="(childComponent, childIndex) in component.props.children" :key="childIndex">
|
||||||
<component-preview :component="childComponent" :is-preview="true" />
|
<div
|
||||||
|
class="form-component"
|
||||||
|
:style="{
|
||||||
|
gridColumn: childComponent.props.gridColumn || 'span 6'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component-preview :component="childComponent" :is-preview="true" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Show subtle indication for empty sections in preview mode (optional) -->
|
<!-- Show subtle indication for empty sections in preview mode (optional) -->
|
||||||
@ -579,6 +648,7 @@ import { useFormBuilderStore } from '~/stores/formBuilder';
|
|||||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||||
import { safeGetField } from '~/composables/safeGetField';
|
import { safeGetField } from '~/composables/safeGetField';
|
||||||
import { onMounted, onUnmounted, watch, computed } from 'vue';
|
import { onMounted, onUnmounted, watch, computed } from 'vue';
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
@ -613,6 +683,9 @@ const sectionDropStates = ref({});
|
|||||||
const showNestedSettingsModal = ref(false);
|
const showNestedSettingsModal = ref(false);
|
||||||
const selectedNestedComponent = ref(null);
|
const selectedNestedComponent = ref(null);
|
||||||
|
|
||||||
|
// Track selected component ID for highlighting
|
||||||
|
const selectedComponentId = ref(null);
|
||||||
|
|
||||||
// Track timers and DOM elements for cleanup
|
// Track timers and DOM elements for cleanup
|
||||||
let lightbox = null;
|
let lightbox = null;
|
||||||
let importInput = null;
|
let importInput = null;
|
||||||
@ -1323,18 +1396,18 @@ const removeFromSection = (sectionId, childIndex) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSectionDragOver = (event, sectionId) => {
|
const handleSectionDragOver = (event, containerId) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Initialize section drop state if it doesn't exist
|
// Initialize container drop state if it doesn't exist
|
||||||
if (!sectionDropStates.value[sectionId]) {
|
if (!sectionDropStates.value[containerId]) {
|
||||||
sectionDropStates.value[sectionId] = { isDraggingOver: false };
|
sectionDropStates.value[containerId] = { isDraggingOver: false };
|
||||||
}
|
}
|
||||||
sectionDropStates.value[sectionId].isDraggingOver = true;
|
sectionDropStates.value[containerId].isDraggingOver = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSectionDragLeave = (event, sectionId) => {
|
const handleSectionDragLeave = (event, containerId) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
@ -1347,18 +1420,70 @@ const handleSectionDragLeave = (event, sectionId) => {
|
|||||||
event.clientY > rect.bottom
|
event.clientY > rect.bottom
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isOutside && sectionDropStates.value[sectionId]) {
|
if (isOutside && sectionDropStates.value[containerId]) {
|
||||||
sectionDropStates.value[sectionId].isDraggingOver = false;
|
sectionDropStates.value[containerId].isDraggingOver = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSectionDrop = (event, sectionId) => {
|
// Container drag handling functions
|
||||||
|
const handleContainerDragStart = (event) => {
|
||||||
|
console.log('Container drag started:', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContainerDragEnd = (event) => {
|
||||||
|
console.log('Container drag ended:', event);
|
||||||
|
// The draggable component automatically updates the array order
|
||||||
|
// We just need to update the parent container to trigger reactivity
|
||||||
|
if (props.component) {
|
||||||
|
formStore.updateComponent(props.component);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nested component selection
|
||||||
|
const selectNestedComponent = (nestedComponent) => {
|
||||||
|
if (!nestedComponent || !nestedComponent.id) return;
|
||||||
|
|
||||||
|
selectedComponentId.value = nestedComponent.id;
|
||||||
|
selectedNestedComponent.value = nestedComponent;
|
||||||
|
|
||||||
|
// Emit the selection event to parent
|
||||||
|
emit('select-nested-component', nestedComponent);
|
||||||
|
|
||||||
|
console.log('Selected nested component:', nestedComponent);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete nested component
|
||||||
|
const deleteNestedComponent = (componentId) => {
|
||||||
|
if (!componentId || !props.component) return;
|
||||||
|
|
||||||
|
// Find the component in the children array
|
||||||
|
const childIndex = props.component.props.children.findIndex(child => child.id === componentId);
|
||||||
|
if (childIndex !== -1) {
|
||||||
|
// Remove the component
|
||||||
|
const deletedComponent = props.component.props.children.splice(childIndex, 1)[0];
|
||||||
|
|
||||||
|
// Update the container to trigger reactivity
|
||||||
|
formStore.updateComponent(props.component);
|
||||||
|
|
||||||
|
// Clear selection if the deleted component was selected
|
||||||
|
if (selectedComponentId.value === componentId) {
|
||||||
|
selectedComponentId.value = null;
|
||||||
|
selectedNestedComponent.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Deleted nested component:', deletedComponent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleSectionDrop = (event, containerId) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Reset drag state
|
// Reset drag state
|
||||||
if (sectionDropStates.value[sectionId]) {
|
if (sectionDropStates.value[containerId]) {
|
||||||
sectionDropStates.value[sectionId].isDraggingOver = false;
|
sectionDropStates.value[containerId].isDraggingOver = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1384,7 +1509,7 @@ const handleSectionDrop = (event, sectionId) => {
|
|||||||
name: componentData.name,
|
name: componentData.name,
|
||||||
props: {
|
props: {
|
||||||
...componentData.defaultProps,
|
...componentData.defaultProps,
|
||||||
gridColumn: 'span 6', // Default to half width in sections
|
gridColumn: 'span 6', // Default to half width in containers
|
||||||
width: '50%',
|
width: '50%',
|
||||||
// Ensure the component has a proper label
|
// Ensure the component has a proper label
|
||||||
label: componentData.defaultProps.label || componentData.name || `${componentData.type.charAt(0).toUpperCase() + componentData.type.slice(1)} Field`,
|
label: componentData.defaultProps.label || componentData.name || `${componentData.type.charAt(0).toUpperCase() + componentData.type.slice(1)} Field`,
|
||||||
@ -1392,41 +1517,42 @@ const handleSectionDrop = (event, sectionId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the target section
|
// Find the target container (section or repeating-group)
|
||||||
const section = formStore.formComponents.find(comp => comp.id === sectionId);
|
const container = formStore.formComponents.find(comp => comp.id === containerId);
|
||||||
if (section) {
|
if (container && (container.type === 'form-section' || container.type === 'repeating-group')) {
|
||||||
// Initialize children array if it doesn't exist
|
// Initialize children array if it doesn't exist
|
||||||
if (!section.props.children) {
|
if (!container.props.children) {
|
||||||
section.props.children = [];
|
container.props.children = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the component to the section
|
// Add the component to the container
|
||||||
section.props.children.push(newComponent);
|
container.props.children.push(newComponent);
|
||||||
|
|
||||||
// Update the section in the form store
|
// Update the container in the form store
|
||||||
formStore.updateComponent(section);
|
formStore.updateComponent(container);
|
||||||
|
|
||||||
// Record the action in history
|
// Record the action in history
|
||||||
formStore.recordHistory('add_component_to_section', {
|
formStore.recordHistory('add_component_to_container', {
|
||||||
componentType: newComponent.type,
|
componentType: newComponent.type,
|
||||||
componentName: newComponent.name,
|
componentName: newComponent.name,
|
||||||
sectionId: sectionId
|
containerId: containerId,
|
||||||
|
containerType: container.type
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Component added to section:', newComponent);
|
console.log('Component added to container:', newComponent);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error dropping component into section:', error);
|
console.error('Error dropping component into container:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSectionDragEnter = (event, sectionId) => {
|
const handleSectionDragEnter = (event, containerId) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Initialize section drop state if it doesn't exist
|
// Initialize container drop state if it doesn't exist
|
||||||
if (!sectionDropStates.value[sectionId]) {
|
if (!sectionDropStates.value[containerId]) {
|
||||||
sectionDropStates.value[sectionId] = { isDraggingOver: false };
|
sectionDropStates.value[containerId] = { isDraggingOver: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1445,20 +1571,20 @@ const closeNestedSettingsModal = () => {
|
|||||||
const saveNestedComponentSettings = (updatedComponent) => {
|
const saveNestedComponentSettings = (updatedComponent) => {
|
||||||
if (!updatedComponent || !selectedNestedComponent.value) return;
|
if (!updatedComponent || !selectedNestedComponent.value) return;
|
||||||
|
|
||||||
// Find the parent section and update the nested component
|
// Find the parent container (section or repeating-group) and update the nested component
|
||||||
const parentSection = formStore.formComponents.find(comp =>
|
const parentContainer = formStore.formComponents.find(comp =>
|
||||||
comp.type === 'form-section' &&
|
(comp.type === 'form-section' || comp.type === 'repeating-group') &&
|
||||||
comp.props.children &&
|
comp.props.children &&
|
||||||
comp.props.children.some(child => child.id === updatedComponent.id)
|
comp.props.children.some(child => child.id === updatedComponent.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (parentSection) {
|
if (parentContainer) {
|
||||||
const childIndex = parentSection.props.children.findIndex(child => child.id === updatedComponent.id);
|
const childIndex = parentContainer.props.children.findIndex(child => child.id === updatedComponent.id);
|
||||||
if (childIndex !== -1) {
|
if (childIndex !== -1) {
|
||||||
// Update the nested component
|
// Update the nested component
|
||||||
parentSection.props.children[childIndex] = { ...updatedComponent };
|
parentContainer.props.children[childIndex] = { ...updatedComponent };
|
||||||
// Update the section to trigger reactivity
|
// Update the container to trigger reactivity
|
||||||
formStore.updateComponent(parentSection);
|
formStore.updateComponent(parentContainer);
|
||||||
|
|
||||||
console.log('Updated nested component:', updatedComponent);
|
console.log('Updated nested component:', updatedComponent);
|
||||||
}
|
}
|
||||||
@ -1658,6 +1784,65 @@ const getButtonSizeClass = (size) => {
|
|||||||
border-color: #93c5fd;
|
border-color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repeating-group-container {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeating-group-container:hover {
|
||||||
|
border-color: #93c5fd;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested Component Styles */
|
||||||
|
.nested-component {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-component:hover {
|
||||||
|
border-color: #93c5fd;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-component-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-component:hover .nested-component-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-children-container {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ghost class for drag preview */
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #c1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form component styles for nested components */
|
||||||
|
.form-component {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-component:hover {
|
||||||
|
border-color: #93c5fd;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-component:hover .component-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dynamic List Component */
|
/* Dynamic List Component */
|
||||||
.dynamic-list-container {
|
.dynamic-list-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -1773,6 +1958,19 @@ const getButtonSizeClass = (size) => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Preview mode grid styles */
|
||||||
|
.repeating-groups .grid,
|
||||||
|
.section-fields .grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeating-groups .form-component,
|
||||||
|
.section-fields .form-component {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.component-actions:hover {
|
.component-actions:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
@ -508,11 +508,17 @@ const availableComponents = [
|
|||||||
maxItems: 10,
|
maxItems: 10,
|
||||||
buttonText: 'Add Person',
|
buttonText: 'Add Person',
|
||||||
removeText: 'Remove',
|
removeText: 'Remove',
|
||||||
fields: [
|
width: '100%',
|
||||||
{ type: 'text', name: 'name', label: 'Name', placeholder: 'Enter name' },
|
gridColumn: 'span 12',
|
||||||
{ type: 'number', name: 'age', label: 'Age', placeholder: 'Enter age' },
|
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
||||||
{ type: 'email', name: 'email', label: 'Email', placeholder: 'Enter email' }
|
children: [], // Array to hold nested components (draggable fields)
|
||||||
]
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1337,117 +1337,32 @@ if (name && email) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group Fields Management -->
|
<!-- Container Information -->
|
||||||
<div class="space-y-4">
|
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||||
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Group Fields</h5>
|
<div class="flex items-start">
|
||||||
|
<Icon name="material-symbols:info" class="w-5 h-5 text-blue-600 mr-2 mt-0.5" />
|
||||||
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
<div>
|
||||||
<div v-for="(field, index) in (configModel.fields || [])" :key="index" class="border p-3 rounded bg-white">
|
<h4 class="font-medium text-sm text-blue-800 mb-1">Repeating Group Container</h4>
|
||||||
<div class="flex justify-between items-center mb-3">
|
<p class="text-xs text-blue-700">
|
||||||
<h4 class="font-medium text-sm text-gray-800">Field {{ index + 1 }}</h4>
|
This is a container component. Drag and drop fields into it in the form builder to create the repeating group structure.
|
||||||
<button
|
Each field you add will be repeated for every group item.
|
||||||
@click="removeGroupField(index)"
|
</p>
|
||||||
class="text-red-500 hover:text-red-700 p-1"
|
</div>
|
||||||
type="button"
|
</div>
|
||||||
title="Remove field"
|
</div>
|
||||||
>
|
|
||||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
<!-- Children Count -->
|
||||||
</button>
|
<div class="border rounded-md p-3 bg-gray-50">
|
||||||
</div>
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
<h5 class="text-sm font-medium text-gray-700">Group Fields</h5>
|
||||||
<FormKit
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
type="select"
|
{{ configModel.children?.length || 0 }} field(s) in this repeating group
|
||||||
label="Field Type"
|
</p>
|
||||||
:options="[
|
</div>
|
||||||
{ label: 'Text', value: 'text' },
|
<div class="text-sm text-gray-500">
|
||||||
{ label: 'Number', value: 'number' },
|
<Icon name="material-symbols:drag-indicator" class="w-4 h-4" />
|
||||||
{ label: 'Email', value: 'email' },
|
|
||||||
{ label: 'Textarea', value: 'textarea' },
|
|
||||||
{ label: 'Select', value: 'select' },
|
|
||||||
{ label: 'Date', value: 'date' },
|
|
||||||
{ label: 'Time', value: 'time' },
|
|
||||||
{ label: 'Checkbox', value: 'checkbox' },
|
|
||||||
{ label: 'Radio', value: 'radio' }
|
|
||||||
]"
|
|
||||||
v-model="field.type"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormKit
|
|
||||||
type="text"
|
|
||||||
label="Field Name"
|
|
||||||
v-model="field.name"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
|
||||||
placeholder="e.g., name, age, email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<FormKit
|
|
||||||
type="text"
|
|
||||||
label="Field Label"
|
|
||||||
v-model="field.label"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
|
||||||
placeholder="e.g., Full Name, Age, Email"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormKit
|
|
||||||
type="text"
|
|
||||||
label="Placeholder"
|
|
||||||
v-model="field.placeholder"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
|
||||||
placeholder="e.g., Enter your name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Options for select/radio/checkbox fields -->
|
|
||||||
<div v-if="['select', 'radio', 'checkbox'].includes(field.type)" class="mt-3">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Options</label>
|
|
||||||
<div class="border rounded-md p-2 bg-gray-50 space-y-2">
|
|
||||||
<div v-for="(option, optionIndex) in (field.options || [])" :key="optionIndex" class="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="field.options[optionIndex].label"
|
|
||||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
|
||||||
placeholder="Option label"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="field.options[optionIndex].value"
|
|
||||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
|
||||||
placeholder="Option value"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="removeFieldOption(field, optionIndex)"
|
|
||||||
class="text-red-500 hover:text-red-700 p-1"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:delete-outline" class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
@click="addFieldOption(field)"
|
|
||||||
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-3 h-3 mr-1" />
|
|
||||||
Add Option
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
@click="addGroupField"
|
|
||||||
class="w-full text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center py-2 border border-dashed border-blue-300 rounded-md hover:bg-blue-50"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
|
||||||
Add Field to Group
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,11 +59,8 @@ function handleChange(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.switch-input:disabled + .switch-label .switch-track {
|
.switch-input:disabled + .switch-label .switch-track {
|
||||||
@apply opacity-50 cursor-not-allowed;
|
@apply opacity-50;
|
||||||
}
|
cursor: not-allowed;
|
||||||
|
|
||||||
.switch-input:disabled + .switch-label {
|
|
||||||
@apply cursor-not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-thumb {
|
.switch-thumb {
|
||||||
@ -98,20 +95,9 @@ function handleChange(event) {
|
|||||||
|
|
||||||
/* Readonly styles for Switch */
|
/* Readonly styles for Switch */
|
||||||
.switch-input:disabled + .switch-label .switch-track {
|
.switch-input:disabled + .switch-label .switch-track {
|
||||||
@apply opacity-50 cursor-not-allowed;
|
@apply opacity-50;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-input:disabled + .switch-label {
|
|
||||||
@apply cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional readonly styling */
|
|
||||||
.switch-label.cursor-not-allowed {
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-label.cursor-not-allowed .switch-track {
|
|
||||||
opacity: 0.8;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@ -2334,17 +2334,17 @@ const handleUpdateComponent = (updatedComponent) => {
|
|||||||
// Check if this is a nested component inside a section
|
// Check if this is a nested component inside a section
|
||||||
let foundInSection = false;
|
let foundInSection = false;
|
||||||
|
|
||||||
// Look for the component in section children
|
// Look for the component in section or repeating-group children
|
||||||
formStore.formComponents.forEach(component => {
|
formStore.formComponents.forEach(component => {
|
||||||
if (component.type === 'form-section' && component.props.children) {
|
if ((component.type === 'form-section' || component.type === 'repeating-group') && component.props.children) {
|
||||||
const nestedIndex = component.props.children.findIndex(child => child.id === updatedComponent.id);
|
const nestedIndex = component.props.children.findIndex(child => child.id === updatedComponent.id);
|
||||||
if (nestedIndex !== -1) {
|
if (nestedIndex !== -1) {
|
||||||
// Update the nested component
|
// Update the nested component
|
||||||
component.props.children[nestedIndex] = updatedComponent;
|
component.props.children[nestedIndex] = updatedComponent;
|
||||||
// Update the entire section to trigger reactivity
|
// Update the entire container to trigger reactivity
|
||||||
formStore.updateComponent(component);
|
formStore.updateComponent(component);
|
||||||
foundInSection = true;
|
foundInSection = true;
|
||||||
console.log('Updated nested component in section:', updatedComponent);
|
console.log('Updated nested component in container:', updatedComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2366,14 +2366,14 @@ const handleDeleteComponent = (id) => {
|
|||||||
// Check if this is a nested component inside a section
|
// Check if this is a nested component inside a section
|
||||||
let foundInSection = false;
|
let foundInSection = false;
|
||||||
|
|
||||||
// Look for the component in section children
|
// Look for the component in section or repeating-group children
|
||||||
formStore.formComponents.forEach(component => {
|
formStore.formComponents.forEach(component => {
|
||||||
if (component.type === 'form-section' && component.props.children) {
|
if ((component.type === 'form-section' || component.type === 'repeating-group') && component.props.children) {
|
||||||
const nestedIndex = component.props.children.findIndex(child => child.id === id);
|
const nestedIndex = component.props.children.findIndex(child => child.id === id);
|
||||||
if (nestedIndex !== -1) {
|
if (nestedIndex !== -1) {
|
||||||
// Remove the nested component
|
// Remove the nested component
|
||||||
const deletedComponent = component.props.children.splice(nestedIndex, 1)[0];
|
const deletedComponent = component.props.children.splice(nestedIndex, 1)[0];
|
||||||
// Update the entire section to trigger reactivity
|
// Update the entire container to trigger reactivity
|
||||||
formStore.updateComponent(component);
|
formStore.updateComponent(component);
|
||||||
foundInSection = true;
|
foundInSection = true;
|
||||||
|
|
||||||
@ -2383,7 +2383,7 @@ const handleDeleteComponent = (id) => {
|
|||||||
formStore.selectedComponentId = null;
|
formStore.selectedComponentId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Deleted nested component from section:', deletedComponent);
|
console.log('Deleted nested component from container:', deletedComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user