Add Switch Component and Enhance Form Builder Functionality
- Introduced a new Switch component for toggling options within forms, enhancing user interactivity. - Updated formkit-custom.js to include the Switch component with appropriate props. - Enhanced formkit-theme.js to define styles for the Switch component, ensuring consistent theming. - Added CSS styles for the Switch component to improve visual presentation and user experience. - Updated FormBuilderCanvas and FormBuilderComponents to support the new Switch component in the form builder interface. - Enhanced documentation to include details about the new Switch component and its usage within forms.
This commit is contained in:
parent
373d4fbeda
commit
96fafd33d7
@ -2,6 +2,7 @@ import { createInput } from "@formkit/vue";
|
||||
import OneTimePassword from "~/components/formkit/OneTimePassword.vue";
|
||||
import MaskText from "~/components/formkit/TextMask.vue";
|
||||
import FileDropzone from "~/components/formkit/FileDropzone.vue";
|
||||
import Switch from "~/components/formkit/Switch.vue";
|
||||
|
||||
export default {
|
||||
otp: createInput(OneTimePassword, {
|
||||
@ -13,4 +14,7 @@ export default {
|
||||
dropzone: createInput(FileDropzone, {
|
||||
props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"],
|
||||
}),
|
||||
switch: createInput(Switch, {
|
||||
props: ["value"],
|
||||
}),
|
||||
};
|
||||
|
@ -29,6 +29,13 @@ const OtpClassification = {
|
||||
message: "formkit-message-otp",
|
||||
};
|
||||
|
||||
const switchClassification = {
|
||||
label: "formkit-label-switch",
|
||||
inner: "formkit-inner-switch",
|
||||
input: "formkit-input-switch",
|
||||
message: "formkit-message-switch",
|
||||
};
|
||||
|
||||
const colorClassification = {
|
||||
label: "formkit-label-color",
|
||||
input: "formkit-input-color",
|
||||
@ -86,6 +93,7 @@ export default {
|
||||
week: textClassification,
|
||||
otp: OtpClassification,
|
||||
mask: textClassification,
|
||||
switch: switchClassification,
|
||||
dropzone: {
|
||||
...textClassification,
|
||||
inner: "formkit-inner-dropzone",
|
||||
|
15
assets/style/css/form/switch.css
Normal file
15
assets/style/css/form/switch.css
Normal file
@ -0,0 +1,15 @@
|
||||
.formkit-label-switch {
|
||||
@apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||
}
|
||||
|
||||
.formkit-inner-switch {
|
||||
@apply flex items-center justify-between w-full;
|
||||
}
|
||||
|
||||
.formkit-input-switch {
|
||||
@apply sr-only;
|
||||
}
|
||||
|
||||
.formkit-message-switch {
|
||||
@apply formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||
}
|
@ -5,9 +5,30 @@
|
||||
:data-name="component.props.name"
|
||||
:data-type="component.type"
|
||||
>
|
||||
<!-- Hidden Field Special Display -->
|
||||
<div v-if="component.type === 'hidden' && isPreview" class="py-2">
|
||||
<div class="hidden-field-preview border-2 border-dashed border-gray-300 bg-gray-50 rounded-lg p-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Icon name="heroicons:eye-slash" class="w-4 h-4 text-gray-400" />
|
||||
<span class="text-sm font-medium text-gray-600">Hidden Field</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="text-xs text-gray-500">
|
||||
<strong>Name:</strong> {{ component.props.name || 'unnamed' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<strong>Value:</strong> {{ component.props.value || '(empty)' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-400">
|
||||
{{ component.props.help }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Input Types (including radio and checkbox) -->
|
||||
<FormKit
|
||||
v-if="isInputType"
|
||||
v-else-if="isInputType"
|
||||
:id="`preview-${component.id}`"
|
||||
:type="component.type"
|
||||
:name="component.props.name"
|
||||
@ -21,6 +42,11 @@
|
||||
:value="component.props.value || undefined"
|
||||
:accept="component.props.accept || undefined"
|
||||
:max="component.props.max || undefined"
|
||||
:mask="component.props.mask || undefined"
|
||||
:digits="component.props.digits || undefined"
|
||||
:multiple="component.props.multiple || undefined"
|
||||
:maxSize="component.props.maxSize || undefined"
|
||||
:maxFiles="component.props.maxFiles || undefined"
|
||||
:preserve-events="isPreview"
|
||||
@input.capture.stop="isPreview ? $event.stopPropagation() : null"
|
||||
@click.capture.stop="isPreview ? $event.stopPropagation() : null"
|
||||
@ -104,6 +130,226 @@
|
||||
<hr class="border-gray-200" />
|
||||
</div>
|
||||
|
||||
<!-- Image Preview Component -->
|
||||
<div v-else-if="component.type === 'image-preview'" class="py-2">
|
||||
<div class="image-preview-container" :class="{ 'has-caption': component.props.showCaption && component.props.caption }">
|
||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ component.props.label }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="image-container relative"
|
||||
:class="{ 'cursor-zoom-in': component.props.showZoom }"
|
||||
@click="handleImageClick"
|
||||
>
|
||||
<img
|
||||
:src="component.props.imageUrl"
|
||||
:alt="component.props.altText || 'Image preview'"
|
||||
class="max-w-full rounded"
|
||||
:style="{
|
||||
maxWidth: component.props.maxWidth || '100%',
|
||||
height: component.props.height || 'auto',
|
||||
}"
|
||||
/>
|
||||
<div v-if="component.props.showZoom" class="absolute bottom-2 right-2 bg-black bg-opacity-60 text-white p-1 rounded">
|
||||
<Icon name="material-symbols:zoom-in" class="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="component.props.showCaption && component.props.caption" class="mt-1 text-sm text-gray-500 text-center">
|
||||
{{ component.props.caption }}
|
||||
</div>
|
||||
|
||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||
{{ component.props.help }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeating Group Component -->
|
||||
<div v-else-if="component.type === 'repeating-group'" class="py-2">
|
||||
<fieldset class="border rounded-md p-4">
|
||||
<legend class="text-sm font-medium px-2">{{ component.props.label || 'Group' }}</legend>
|
||||
|
||||
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
||||
{{ component.props.help }}
|
||||
</div>
|
||||
|
||||
<!-- Default group preview (in edit mode) -->
|
||||
<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">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-sm font-medium text-gray-700">Item 1</h4>
|
||||
<button type="button" class="text-red-500 hover:text-red-700 text-sm">
|
||||
{{ component.props.removeText || 'Remove' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
||||
<FormKit
|
||||
:type="field.type"
|
||||
:label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:name="`${field.name}_1`"
|
||||
:options="field.options"
|
||||
disabled
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
{{ component.props.buttonText || 'Add Item' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Functional groups (in form view) -->
|
||||
<div v-else class="repeating-groups space-y-4">
|
||||
<div
|
||||
v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])"
|
||||
:key="groupIndex"
|
||||
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
|
||||
<button
|
||||
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
|
||||
type="button"
|
||||
class="text-red-500 hover:text-red-700 text-sm"
|
||||
@click="removeGroupItem(groupIndex)"
|
||||
>
|
||||
{{ component.props.removeText || 'Remove' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
||||
<FormKit
|
||||
:type="field.type"
|
||||
:label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:name="`${component.props.name}.${groupIndex}.${field.name}`"
|
||||
:options="field.options"
|
||||
v-model="group[field.name]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
|
||||
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"
|
||||
@click="addGroupItem"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
{{ component.props.buttonText || 'Add Item' }}
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic List Component -->
|
||||
<div v-else-if="component.type === 'dynamic-list'" class="py-2">
|
||||
<div class="dynamic-list-container">
|
||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ component.props.label }}
|
||||
</label>
|
||||
|
||||
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
||||
{{ component.props.help }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div v-if="isPreview" class="list-items space-y-2">
|
||||
<div v-for="(item, index) in component.props.defaultItems" :key="index" class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
:value="item"
|
||||
:placeholder="component.props.placeholder"
|
||||
disabled
|
||||
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700 disabled:bg-gray-50"
|
||||
/>
|
||||
<button type="button" class="ml-2 text-red-500 hover:text-red-700">
|
||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add button for preview -->
|
||||
<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"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
{{ component.props.buttonText || 'Add Item' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="list-items space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in (previewFormData[component.props.name] || [])"
|
||||
:key="index"
|
||||
class="flex items-center"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="previewFormData[component.props.name][index]"
|
||||
:placeholder="component.props.placeholder"
|
||||
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700"
|
||||
/>
|
||||
<button
|
||||
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
|
||||
type="button"
|
||||
class="ml-2 text-red-500 hover:text-red-700"
|
||||
@click="removeListItem(index)"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
|
||||
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"
|
||||
@click="addListItem"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
{{ component.props.buttonText || 'Add Item' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Component -->
|
||||
<div v-else-if="component.type === 'button'" class="py-2">
|
||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ component.props.label }}
|
||||
</label>
|
||||
|
||||
<RsButton
|
||||
:type="component.props.buttonType || 'button'"
|
||||
:variant="component.props.variant || 'primary'"
|
||||
:size="component.props.size || 'md'"
|
||||
:disabled="component.props.disabled || false"
|
||||
@click="handleButtonClick"
|
||||
class="button-component"
|
||||
>
|
||||
{{ component.props.label || 'Button' }}
|
||||
</RsButton>
|
||||
|
||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||
{{ component.props.help }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Unknown Component Type Fallback -->
|
||||
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
||||
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
||||
@ -112,6 +358,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useNuxtApp } from '#app';
|
||||
import { useFormBuilderStore } from '~/stores/formBuilder';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
@ -123,6 +372,117 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// Get access to the form builder store
|
||||
const formStore = useFormBuilderStore();
|
||||
const previewFormData = computed(() => formStore.previewFormData || {});
|
||||
|
||||
// Repeating group and dynamic list functionality
|
||||
const addGroupItem = () => {
|
||||
if (props.isPreview) return;
|
||||
|
||||
const groupName = props.component.props.name;
|
||||
if (!groupName) return;
|
||||
|
||||
// Get current groups or initialize empty array
|
||||
const currentGroups = formStore.previewFormData[groupName] || [];
|
||||
|
||||
// Create a new empty group
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (props.component.props.fields) {
|
||||
props.component.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Add the new group to the list
|
||||
currentGroups.push(newGroup);
|
||||
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
};
|
||||
|
||||
const removeGroupItem = (index) => {
|
||||
if (props.isPreview) return;
|
||||
|
||||
const groupName = props.component.props.name;
|
||||
if (!groupName) return;
|
||||
|
||||
// Get current groups
|
||||
const currentGroups = [...(formStore.previewFormData[groupName] || [])];
|
||||
|
||||
// Remove the group at the specified index
|
||||
currentGroups.splice(index, 1);
|
||||
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
};
|
||||
|
||||
const addListItem = () => {
|
||||
if (props.isPreview) return;
|
||||
|
||||
const listName = props.component.props.name;
|
||||
if (!listName) return;
|
||||
|
||||
// Get current items or initialize empty array
|
||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||
|
||||
// Add an empty item
|
||||
currentItems.push('');
|
||||
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
};
|
||||
|
||||
const removeListItem = (index) => {
|
||||
if (props.isPreview) return;
|
||||
|
||||
const listName = props.component.props.name;
|
||||
if (!listName) return;
|
||||
|
||||
// Get current items
|
||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||
|
||||
// Remove the item at the specified index
|
||||
currentItems.splice(index, 1);
|
||||
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
};
|
||||
|
||||
// Handle button click
|
||||
const handleButtonClick = () => {
|
||||
if (props.isPreview) return;
|
||||
|
||||
// Execute custom onClick code if provided
|
||||
if (props.component.props.onClick) {
|
||||
try {
|
||||
// Create a safe execution context
|
||||
const context = {
|
||||
formData: formStore.previewFormData,
|
||||
componentName: props.component.props.name,
|
||||
buttonLabel: props.component.props.label
|
||||
};
|
||||
|
||||
// Execute the custom code
|
||||
const func = new Function('context', props.component.props.onClick);
|
||||
func(context);
|
||||
} catch (error) {
|
||||
console.error('Error executing button onClick:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Default behavior - log the button click
|
||||
console.log(`Button clicked: ${props.component.props.label || 'Unnamed Button'}`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Check if the component is a standard FormKit input type (excluding specialized components)
|
||||
const isStandardInputType = computed(() => {
|
||||
const standardInputTypes = [
|
||||
@ -134,18 +494,63 @@ const isStandardInputType = computed(() => {
|
||||
return standardInputTypes.includes(props.component.type);
|
||||
});
|
||||
|
||||
// Check if the component is any FormKit input type (including checkbox)
|
||||
// Check if the component is any FormKit input type (including checkbox and custom types)
|
||||
const isInputType = computed(() => {
|
||||
const inputTypes = [
|
||||
'text', 'textarea', 'number', 'email', 'password',
|
||||
'date', 'time', 'datetime-local', 'url', 'tel',
|
||||
'select', 'checkbox', 'radio', 'file', 'range',
|
||||
'color', 'hidden'
|
||||
'color', 'hidden', 'mask', 'otp', 'dropzone', 'switch'
|
||||
];
|
||||
|
||||
return inputTypes.includes(props.component.type);
|
||||
});
|
||||
|
||||
// Handle image click for image preview component
|
||||
const handleImageClick = (event) => {
|
||||
// Only do something if zoom is enabled
|
||||
if (!props.component.props.showZoom) return;
|
||||
|
||||
// Get the image URL
|
||||
const imageUrl = props.component.props.imageUrl;
|
||||
if (!imageUrl) return;
|
||||
|
||||
// Create a lightbox effect to view the image
|
||||
const lightbox = document.createElement('div');
|
||||
lightbox.style.position = 'fixed';
|
||||
lightbox.style.top = '0';
|
||||
lightbox.style.left = '0';
|
||||
lightbox.style.width = '100%';
|
||||
lightbox.style.height = '100%';
|
||||
lightbox.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
|
||||
lightbox.style.display = 'flex';
|
||||
lightbox.style.alignItems = 'center';
|
||||
lightbox.style.justifyContent = 'center';
|
||||
lightbox.style.zIndex = '1000';
|
||||
lightbox.style.cursor = 'zoom-out';
|
||||
|
||||
// Create the image element
|
||||
const img = document.createElement('img');
|
||||
img.src = imageUrl;
|
||||
img.style.maxWidth = '90%';
|
||||
img.style.maxHeight = '90%';
|
||||
img.style.objectFit = 'contain';
|
||||
|
||||
// Add close functionality
|
||||
lightbox.onclick = () => {
|
||||
document.body.removeChild(lightbox);
|
||||
};
|
||||
|
||||
// Prevent image click from closing the lightbox
|
||||
img.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// Add to DOM
|
||||
lightbox.appendChild(img);
|
||||
document.body.appendChild(lightbox);
|
||||
};
|
||||
|
||||
// Compute style based on grid properties
|
||||
const componentStyle = computed(() => {
|
||||
// Only apply grid styles in the non-preview mode (actual form)
|
||||
@ -179,45 +584,56 @@ const componentStyle = computed(() => {
|
||||
|
||||
/* Improve visibility of disabled inputs in the canvas view */
|
||||
:deep(.canvas-component.formkit-disabled),
|
||||
:deep(.canvas-component .formkit-disabled) {
|
||||
opacity: 1 !important;
|
||||
:deep(.canvas-component.formkit-disabled input),
|
||||
:deep(.canvas-component.formkit-disabled select),
|
||||
:deep(.canvas-component.formkit-disabled textarea) {
|
||||
opacity: 0.8 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.canvas-component .formkit-input) {
|
||||
opacity: 1 !important;
|
||||
background-color: white !important;
|
||||
border-color: #d1d5db !important;
|
||||
color: #111827 !important;
|
||||
cursor: default !important;
|
||||
/* Image Preview Component */
|
||||
.image-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.canvas-component .formkit-inner) {
|
||||
background-color: #ffffff !important;
|
||||
border-color: #d1d5db !important;
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.canvas-component .formkit-label) {
|
||||
opacity: 1 !important;
|
||||
color: #374151 !important;
|
||||
font-weight: 500 !important;
|
||||
.image-container img {
|
||||
display: block;
|
||||
border-radius: 0.375rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.canvas-component .formkit-help) {
|
||||
opacity: 1 !important;
|
||||
color: #6b7280 !important;
|
||||
.has-caption {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.canvas-component[data-type="checkbox"] .formkit-input),
|
||||
:deep(.canvas-component[data-type="radio"] .formkit-input) {
|
||||
opacity: 1 !important;
|
||||
border-color: #9ca3af !important;
|
||||
/* Repeating Group Component */
|
||||
.repeating-groups {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.canvas-component[data-type="select"] .formkit-input) {
|
||||
appearance: none !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
|
||||
background-position: right 0.5rem center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: 1.5em 1.5em !important;
|
||||
.group-item {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.group-item:hover {
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
/* Dynamic List Component */
|
||||
.dynamic-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-items {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -2,8 +2,24 @@
|
||||
<div class="form-builder-canvas">
|
||||
<div v-if="formComponents.length === 0" class="flex flex-col items-center justify-center h-full py-16 text-gray-400">
|
||||
<Icon name="material-symbols:layers-outline" class="w-16 h-16 mb-4 text-gray-300" />
|
||||
<p class="text-base font-medium">Drag components here</p>
|
||||
<p class="text-xs mt-1">Or click a component from the sidebar</p>
|
||||
<div class="text-center max-w-md">
|
||||
<p class="text-base font-medium text-gray-600 mb-2">Start Building Your Form</p>
|
||||
<p class="text-sm text-gray-500 mb-4">Drag components from the left sidebar or click on them to add to your form</p>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-left">
|
||||
<div class="flex items-start">
|
||||
<Icon name="heroicons:light-bulb" class="w-5 h-5 text-blue-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-blue-800 mb-1">Quick Start Tips:</p>
|
||||
<ul class="text-xs text-blue-700 space-y-1">
|
||||
<li>• Add form fields from the sidebar</li>
|
||||
<li>• Click any field to configure its settings</li>
|
||||
<li>• Use the settings button to customize fields</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid-container">
|
||||
@ -19,16 +35,24 @@
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="form-component relative border rounded-md overflow-hidden transition-all"
|
||||
class="form-component relative border rounded-md overflow-hidden transition-all duration-200 cursor-pointer"
|
||||
: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
|
||||
'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
|
||||
}"
|
||||
:style="{
|
||||
gridColumn: element.props.gridColumn || 'span 12'
|
||||
}"
|
||||
@click.capture="selectComponent(element)"
|
||||
>
|
||||
<!-- Selection Indicator -->
|
||||
<div
|
||||
v-if="selectedComponentId === element.id"
|
||||
class="absolute top-2 left-2 flex items-center space-x-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-20"
|
||||
>
|
||||
<Icon name="heroicons:check-circle" class="w-3 h-3" />
|
||||
<span>Selected</span>
|
||||
</div>
|
||||
<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"
|
||||
|
@ -16,9 +16,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Inputs Category -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Basic Inputs</h3>
|
||||
<!-- 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>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div
|
||||
v-for="component in getComponentsByCategory('Basic Inputs')"
|
||||
@ -27,6 +27,7 @@
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
@ -35,9 +36,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selection Inputs Category -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Selection Inputs</h3>
|
||||
<!-- 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
|
||||
v-for="component in getComponentsByCategory('Selection Inputs')"
|
||||
@ -46,6 +47,7 @@
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
@ -54,9 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date and Time Category -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Date and Time</h3>
|
||||
<!-- 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
|
||||
v-for="component in getComponentsByCategory('Date and Time')"
|
||||
@ -65,6 +67,7 @@
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
@ -73,9 +76,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Category -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Advanced</h3>
|
||||
<!-- 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
|
||||
v-for="component in getComponentsByCategory('Advanced')"
|
||||
@ -84,6 +87,7 @@
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
@ -92,9 +96,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout Category -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Layout</h3>
|
||||
<!-- 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
|
||||
v-for="component in getComponentsByCategory('Layout')"
|
||||
@ -103,6 +107,7 @@
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@dragend="onDragEnd($event)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
@ -185,6 +190,58 @@ const availableComponents = [
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'url',
|
||||
name: 'URL Field',
|
||||
category: 'Basic Inputs',
|
||||
icon: 'material-symbols:link',
|
||||
description: 'URL/website address input',
|
||||
defaultProps: {
|
||||
type: 'url',
|
||||
placeholder: 'https://example.com',
|
||||
help: '',
|
||||
validation: 'url'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tel',
|
||||
name: 'Phone Field',
|
||||
category: 'Basic Inputs',
|
||||
icon: 'heroicons:device-phone-mobile',
|
||||
description: 'Telephone number input',
|
||||
defaultProps: {
|
||||
type: 'tel',
|
||||
placeholder: '+1 (555) 123-4567',
|
||||
help: '',
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mask',
|
||||
name: 'Masked Input',
|
||||
category: 'Basic Inputs',
|
||||
icon: 'heroicons:pencil-square',
|
||||
description: 'Input field with custom masking',
|
||||
defaultProps: {
|
||||
type: 'mask',
|
||||
placeholder: 'Enter value...',
|
||||
help: 'Input will be formatted according to the mask',
|
||||
mask: '###-###-####',
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'hidden',
|
||||
name: 'Hidden Field',
|
||||
category: 'Basic Inputs',
|
||||
icon: 'heroicons:eye-slash',
|
||||
description: 'Hidden field for storing values',
|
||||
defaultProps: {
|
||||
type: 'hidden',
|
||||
value: '',
|
||||
help: 'This field is hidden from users'
|
||||
}
|
||||
},
|
||||
|
||||
// Selection inputs
|
||||
{
|
||||
@ -241,6 +298,21 @@ const availableComponents = [
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
name: 'Switch Toggle',
|
||||
category: 'Selection Inputs',
|
||||
icon: 'material-symbols:toggle-on',
|
||||
description: 'Modern toggle switch',
|
||||
defaultProps: {
|
||||
type: 'switch',
|
||||
label: 'Enable Option',
|
||||
name: 'switch_field',
|
||||
help: 'Toggle this option on or off',
|
||||
value: false,
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
|
||||
// Date and Time
|
||||
{
|
||||
@ -279,6 +351,35 @@ const availableComponents = [
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'range',
|
||||
name: 'Range Slider',
|
||||
category: 'Date and Time',
|
||||
icon: 'material-symbols:linear-scale',
|
||||
description: 'Numeric range slider',
|
||||
defaultProps: {
|
||||
type: 'range',
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
value: 50,
|
||||
help: 'Drag to select a value',
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'color',
|
||||
name: 'Color Picker',
|
||||
category: 'Date and Time',
|
||||
icon: 'material-symbols:palette',
|
||||
description: 'Color selection input',
|
||||
defaultProps: {
|
||||
type: 'color',
|
||||
value: '#3b82f6',
|
||||
help: 'Click to select a color',
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
|
||||
// Advanced
|
||||
{
|
||||
@ -294,6 +395,111 @@ const availableComponents = [
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'image-preview',
|
||||
name: 'Image Preview',
|
||||
category: 'Advanced',
|
||||
icon: 'material-symbols:image-outline',
|
||||
description: 'Display an image with preview capabilities',
|
||||
defaultProps: {
|
||||
label: 'Image Preview',
|
||||
name: 'image_preview',
|
||||
help: 'Image preview with zoom capability',
|
||||
imageUrl: 'https://placehold.co/600x400',
|
||||
altText: 'Preview image',
|
||||
caption: 'Image caption',
|
||||
showZoom: true,
|
||||
showCaption: true,
|
||||
maxWidth: '100%',
|
||||
height: 'auto'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'repeating-group',
|
||||
name: 'Repeating Group',
|
||||
category: 'Advanced',
|
||||
icon: 'material-symbols:repeat',
|
||||
description: 'Collect multiple entries of the same data structure',
|
||||
defaultProps: {
|
||||
label: 'Person Information',
|
||||
name: 'person_group',
|
||||
help: 'Add multiple people with their details',
|
||||
minItems: 1,
|
||||
maxItems: 10,
|
||||
buttonText: 'Add Person',
|
||||
removeText: 'Remove',
|
||||
fields: [
|
||||
{ type: 'text', name: 'name', label: 'Name', placeholder: 'Enter name' },
|
||||
{ type: 'number', name: 'age', label: 'Age', placeholder: 'Enter age' },
|
||||
{ type: 'email', name: 'email', label: 'Email', placeholder: 'Enter email' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dynamic-list',
|
||||
name: 'Dynamic List',
|
||||
category: 'Advanced',
|
||||
icon: 'material-symbols:format-list-bulleted-add',
|
||||
description: 'Add multiple items to a dynamic list',
|
||||
defaultProps: {
|
||||
label: 'Items List',
|
||||
name: 'items_list',
|
||||
help: 'Add or remove items from the list',
|
||||
placeholder: 'Enter item',
|
||||
buttonText: 'Add Item',
|
||||
minItems: 0,
|
||||
maxItems: 20,
|
||||
defaultItems: ['Item 1', 'Item 2']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'otp',
|
||||
name: 'OTP Input',
|
||||
category: 'Advanced',
|
||||
icon: 'heroicons:key',
|
||||
description: 'One-time password verification field',
|
||||
defaultProps: {
|
||||
label: 'Verification Code',
|
||||
name: 'otp_code',
|
||||
help: 'Enter the verification code sent to you',
|
||||
digits: 6,
|
||||
validation: 'required'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dropzone',
|
||||
name: 'File Dropzone',
|
||||
category: 'Advanced',
|
||||
icon: 'heroicons:cloud-arrow-up',
|
||||
description: 'Drag & drop file upload area',
|
||||
defaultProps: {
|
||||
label: 'Upload Files',
|
||||
name: 'file_upload',
|
||||
help: 'Drag files here or click to browse',
|
||||
accept: 'image/*,.pdf,.doc,.docx',
|
||||
multiple: true,
|
||||
maxSize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
validation: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Button',
|
||||
category: 'Advanced',
|
||||
icon: 'material-symbols:smart-button',
|
||||
description: 'Action button for forms',
|
||||
defaultProps: {
|
||||
label: 'Click Me',
|
||||
name: 'action_button',
|
||||
help: 'Button for triggering actions',
|
||||
buttonType: 'button', // button, submit, reset
|
||||
variant: 'primary', // primary, secondary, success, warning, danger
|
||||
size: 'md', // sm, md, lg
|
||||
disabled: false,
|
||||
onClick: '' // Custom JavaScript code to execute
|
||||
}
|
||||
},
|
||||
|
||||
// Layout
|
||||
{
|
||||
@ -376,25 +582,97 @@ const onDragStart = (event, component) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Set the drag data
|
||||
// Prepare the JSON data
|
||||
const jsonData = JSON.stringify(componentWithGrid);
|
||||
|
||||
try {
|
||||
// For Mac/Safari compatibility, use multiple data formats
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('component', JSON.stringify(componentWithGrid));
|
||||
|
||||
// Add data in multiple formats for maximum compatibility
|
||||
// text/plain is the most universally supported
|
||||
event.dataTransfer.setData('text/plain', jsonData);
|
||||
|
||||
// Custom format - might not work in all browsers but provides context
|
||||
try {
|
||||
event.dataTransfer.setData('component', jsonData);
|
||||
} catch (err) {
|
||||
console.log('Custom format not supported in this browser');
|
||||
}
|
||||
|
||||
// Store component data in a global variable as fallback for Safari
|
||||
window.__draggedComponentData = componentWithGrid;
|
||||
|
||||
// Add visual feedback
|
||||
event.target.classList.add('dragging');
|
||||
|
||||
// Set a drag image
|
||||
try {
|
||||
const dragIcon = document.createElement('div');
|
||||
dragIcon.className = 'drag-preview';
|
||||
dragIcon.innerHTML = `<div class="p-2 bg-blue-100 border border-blue-300 rounded text-xs">${component.name}</div>`;
|
||||
dragIcon.style.position = 'absolute';
|
||||
dragIcon.style.top = '-1000px';
|
||||
document.body.appendChild(dragIcon);
|
||||
|
||||
event.dataTransfer.setDragImage(dragIcon, 0, 0);
|
||||
|
||||
// Clean up the drag image element after a short delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(dragIcon);
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.log('Custom drag image not supported in this browser');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in drag start:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a component directly via click
|
||||
const addComponent = (component) => {
|
||||
emit('add-component', component);
|
||||
};
|
||||
|
||||
// Handle drag end event
|
||||
const onDragEnd = (event) => {
|
||||
event.target.classList.remove('dragging');
|
||||
|
||||
// Clear the fallback data after a short delay
|
||||
setTimeout(() => {
|
||||
window.__draggedComponentData = null;
|
||||
}, 300);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-components {
|
||||
@apply bg-white h-full;
|
||||
|
||||
/* More compact layout */
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.component-category {
|
||||
@apply mb-3;
|
||||
}
|
||||
|
||||
.component-category h3 {
|
||||
@apply text-xs font-semibold px-3 mb-2 uppercase tracking-wider text-gray-700;
|
||||
}
|
||||
|
||||
.component-item {
|
||||
@apply h-20;
|
||||
transition: all 0.15s ease-in-out;
|
||||
min-height: 4.5rem;
|
||||
}
|
||||
|
||||
.component-item .iconify {
|
||||
@apply w-5 h-5 mb-1;
|
||||
}
|
||||
|
||||
.component-item span {
|
||||
@apply leading-tight;
|
||||
}
|
||||
|
||||
.component-item:hover {
|
||||
@ -404,4 +682,16 @@ const addComponent = (component) => {
|
||||
.component-item:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.component-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: #e0f2fe;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.drag-preview {
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
@ -116,6 +116,237 @@
|
||||
help="Size of the heading"
|
||||
/>
|
||||
|
||||
<!-- Image Preview Settings -->
|
||||
<template v-if="component.type === 'image-preview'">
|
||||
<FormKit
|
||||
v-if="showField('imageUrl')"
|
||||
type="text"
|
||||
label="Image URL"
|
||||
name="imageUrl"
|
||||
v-model="configModel.imageUrl"
|
||||
help="URL of the image to display"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('altText')"
|
||||
type="text"
|
||||
label="Alt Text"
|
||||
name="altText"
|
||||
v-model="configModel.altText"
|
||||
help="Alternative text for accessibility"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('caption')"
|
||||
type="text"
|
||||
label="Caption"
|
||||
name="caption"
|
||||
v-model="configModel.caption"
|
||||
help="Caption displayed below the image"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('maxWidth')"
|
||||
type="text"
|
||||
label="Max Width"
|
||||
name="maxWidth"
|
||||
v-model="configModel.maxWidth"
|
||||
help="Maximum width of the image (px or %)"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('height')"
|
||||
type="text"
|
||||
label="Height"
|
||||
name="height"
|
||||
v-model="configModel.height"
|
||||
help="Height of the image (px or auto)"
|
||||
/>
|
||||
|
||||
<div v-if="showField('showZoom')" class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">Display Options</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="configModel.showZoom" class="mr-2 h-4 w-4 rounded border-gray-300">
|
||||
<span class="text-sm text-gray-700">Enable zoom on click</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="configModel.showCaption" class="mr-2 h-4 w-4 rounded border-gray-300">
|
||||
<span class="text-sm text-gray-700">Show caption</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Dynamic List Settings -->
|
||||
<template v-if="component.type === 'dynamic-list'">
|
||||
<FormKit
|
||||
v-if="showField('buttonText')"
|
||||
type="text"
|
||||
label="Add Button Text"
|
||||
name="buttonText"
|
||||
v-model="configModel.buttonText"
|
||||
help="Text for the add item button"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
v-if="showField('minItems')"
|
||||
type="number"
|
||||
label="Min Items"
|
||||
name="minItems"
|
||||
v-model="configModel.minItems"
|
||||
help="Minimum number of items"
|
||||
min="0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('maxItems')"
|
||||
type="number"
|
||||
label="Max Items"
|
||||
name="maxItems"
|
||||
v-model="configModel.maxItems"
|
||||
help="Maximum number of items"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showField('defaultItems')">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Default Items</label>
|
||||
<div class="border rounded-md p-3 bg-gray-50 space-y-2">
|
||||
<div v-for="(item, index) in configModel.defaultItems" :key="index" class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
v-model="configModel.defaultItems[index]"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
@click="configModel.defaultItems.splice(index, 1)"
|
||||
class="ml-2 text-red-500 hover:text-red-700"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="configModel.defaultItems.push('')"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
Add Default Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Repeating Group Settings -->
|
||||
<template v-if="component.type === 'repeating-group'">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
v-if="showField('minItems')"
|
||||
type="number"
|
||||
label="Min Items"
|
||||
name="minItems"
|
||||
v-model="configModel.minItems"
|
||||
help="Minimum number of groups"
|
||||
min="0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('maxItems')"
|
||||
type="number"
|
||||
label="Max Items"
|
||||
name="maxItems"
|
||||
v-model="configModel.maxItems"
|
||||
help="Maximum number of groups"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
v-if="showField('buttonText')"
|
||||
type="text"
|
||||
label="Add Button Text"
|
||||
name="buttonText"
|
||||
v-model="configModel.buttonText"
|
||||
help="Text for the add button"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('removeText')"
|
||||
type="text"
|
||||
label="Remove Button Text"
|
||||
name="removeText"
|
||||
v-model="configModel.removeText"
|
||||
help="Text for the remove button"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showField('fields')">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Group Fields</label>
|
||||
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
||||
<div v-for="(field, index) in configModel.fields" :key="index" class="border p-3 rounded bg-white">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h4 class="font-medium text-sm">Field {{ index + 1 }}</h4>
|
||||
<button
|
||||
@click="configModel.fields.splice(index, 1)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mb-2">
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Field Type"
|
||||
:options="[
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'Date', value: 'date' }
|
||||
]"
|
||||
v-model="field.type"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Field Name"
|
||||
v-model="field.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Field Label"
|
||||
v-model="field.label"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Placeholder"
|
||||
v-model="field.placeholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="addGroupField"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
Add Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Options For Select/Radio/Checkbox -->
|
||||
<div v-if="showField('options')">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
@ -174,6 +405,61 @@
|
||||
help="File types, e.g. '.jpg,.png,image/*'"
|
||||
/>
|
||||
|
||||
<!-- Mask Pattern -->
|
||||
<FormKit
|
||||
v-if="showField('mask')"
|
||||
type="text"
|
||||
label="Input Mask"
|
||||
name="mask"
|
||||
v-model="configModel.mask"
|
||||
help="Pattern for input formatting, e.g. '###-###-####' for phone numbers"
|
||||
/>
|
||||
|
||||
<!-- OTP Digits -->
|
||||
<FormKit
|
||||
v-if="showField('digits')"
|
||||
type="number"
|
||||
label="Number of Digits"
|
||||
name="digits"
|
||||
v-model="configModel.digits"
|
||||
help="Number of OTP digits (typically 4-8)"
|
||||
min="4"
|
||||
max="8"
|
||||
/>
|
||||
|
||||
<!-- Dropzone Settings -->
|
||||
<template v-if="component.type === 'dropzone'">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
v-if="showField('maxSize')"
|
||||
type="number"
|
||||
label="Max File Size (bytes)"
|
||||
name="maxSize"
|
||||
v-model="configModel.maxSize"
|
||||
help="Maximum file size in bytes (5MB = 5242880)"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="showField('maxFiles')"
|
||||
type="number"
|
||||
label="Max Files"
|
||||
name="maxFiles"
|
||||
v-model="configModel.maxFiles"
|
||||
help="Maximum number of files"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="configModel.multiple"
|
||||
class="mr-2 h-4 w-4 rounded border-gray-300"
|
||||
>
|
||||
<span class="text-sm text-gray-700">Allow multiple files</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<!-- Information Display Configuration -->
|
||||
@ -592,7 +878,7 @@ const showField = (fieldName) => {
|
||||
case 'name':
|
||||
return !['heading', 'paragraph', 'divider'].includes(componentType);
|
||||
case 'placeholder':
|
||||
return ['text', 'textarea', 'email', 'password', 'number', 'select'].includes(componentType);
|
||||
return ['text', 'textarea', 'email', 'password', 'number', 'select', 'dynamic-list'].includes(componentType);
|
||||
case 'help':
|
||||
return !['heading', 'paragraph', 'divider'].includes(componentType);
|
||||
case 'value':
|
||||
@ -602,17 +888,54 @@ const showField = (fieldName) => {
|
||||
case 'options':
|
||||
return ['select', 'radio', 'checkbox'].includes(componentType);
|
||||
case 'accept':
|
||||
return componentType === 'file';
|
||||
return componentType === 'file' || componentType === 'dropzone';
|
||||
case 'mask':
|
||||
return componentType === 'mask';
|
||||
case 'digits':
|
||||
return componentType === 'otp';
|
||||
case 'maxSize':
|
||||
case 'maxFiles':
|
||||
case 'multiple':
|
||||
return componentType === 'dropzone';
|
||||
case 'id':
|
||||
return true; // Always show component ID in advanced tab
|
||||
case 'width':
|
||||
return true; // Always show width in basic tab
|
||||
case 'title':
|
||||
return componentType === 'info-display';
|
||||
case 'imageUrl':
|
||||
case 'altText':
|
||||
case 'caption':
|
||||
case 'showZoom':
|
||||
case 'showCaption':
|
||||
case 'maxWidth':
|
||||
case 'height':
|
||||
return componentType === 'image-preview';
|
||||
case 'minItems':
|
||||
case 'maxItems':
|
||||
return ['repeating-group', 'dynamic-list'].includes(componentType);
|
||||
case 'buttonText':
|
||||
case 'removeText':
|
||||
return componentType === 'repeating-group' || componentType === 'dynamic-list';
|
||||
case 'defaultItems':
|
||||
return componentType === 'dynamic-list';
|
||||
case 'fields':
|
||||
return componentType === 'info-display' || componentType === 'repeating-group';
|
||||
case 'layout':
|
||||
case 'showBorder':
|
||||
case 'fields':
|
||||
return componentType === 'info-display';
|
||||
case 'min':
|
||||
case 'max':
|
||||
case 'step':
|
||||
return componentType === 'range' || componentType === 'number';
|
||||
case 'buttonType':
|
||||
case 'variant':
|
||||
case 'size':
|
||||
case 'disabled':
|
||||
case 'onClick':
|
||||
return componentType === 'button';
|
||||
case 'value':
|
||||
return ['color', 'range', 'switch', 'hidden'].includes(componentType);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -653,28 +976,55 @@ const removeInfoField = (index) => {
|
||||
configModel.value.fields.splice(index, 1);
|
||||
};
|
||||
|
||||
// Add a new field to repeating group component
|
||||
const addGroupField = () => {
|
||||
if (!configModel.value.fields) {
|
||||
configModel.value.fields = [];
|
||||
}
|
||||
|
||||
configModel.value.fields.push({
|
||||
type: 'text',
|
||||
name: `field_${configModel.value.fields.length + 1}`,
|
||||
label: `Field ${configModel.value.fields.length + 1}`,
|
||||
placeholder: 'Enter value'
|
||||
});
|
||||
};
|
||||
|
||||
// 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'],
|
||||
number: ['label', 'name', 'placeholder', 'help', 'min', 'max', 'step', 'width'],
|
||||
email: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
password: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
url: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
tel: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
hidden: ['name', 'value', 'help', 'width'],
|
||||
|
||||
// Selection types
|
||||
select: ['label', 'name', 'placeholder', 'help', 'options', 'width'],
|
||||
checkbox: ['label', 'name', 'help', 'options', 'width'],
|
||||
radio: ['label', 'name', 'help', 'options', 'width'],
|
||||
switch: ['label', 'name', 'help', 'value', 'width'],
|
||||
|
||||
// Date and time
|
||||
date: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
time: ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
'datetime-local': ['label', 'name', 'placeholder', 'help', 'width'],
|
||||
range: ['label', 'name', 'help', 'min', 'max', 'step', 'value', 'width'],
|
||||
color: ['label', 'name', 'help', 'value', 'width'],
|
||||
|
||||
// Advanced
|
||||
file: ['label', 'name', 'help', 'accept', 'width'],
|
||||
otp: ['label', 'name', 'help', 'digits', 'width'],
|
||||
mask: ['label', 'name', 'placeholder', 'help', 'mask', 'width'],
|
||||
dropzone: ['label', 'name', 'help', 'accept', 'multiple', 'maxSize', 'maxFiles', 'width'],
|
||||
button: ['label', 'name', 'help', 'buttonType', 'variant', 'size', 'disabled', 'onClick', 'width'],
|
||||
'image-preview': ['label', 'name', 'help', 'imageUrl', 'altText', 'caption', 'showZoom', 'showCaption', 'maxWidth', 'height', 'width'],
|
||||
'repeating-group': ['label', 'name', 'help', 'minItems', 'maxItems', 'buttonText', 'removeText', 'fields', 'width'],
|
||||
'dynamic-list': ['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'width'],
|
||||
|
||||
// Layout elements
|
||||
heading: ['value', 'level', 'width'],
|
||||
@ -695,7 +1045,7 @@ const getComponentWidthPercent = () => {
|
||||
|
||||
// Computed property to determine if validation tab should be shown
|
||||
const showValidationTab = computed(() => {
|
||||
const nonValidationComponents = ['heading', 'paragraph', 'divider', 'info-display'];
|
||||
const nonValidationComponents = ['heading', 'paragraph', 'divider', 'info-display', 'button'];
|
||||
return !nonValidationComponents.includes(props.component.type);
|
||||
});
|
||||
|
||||
|
1319
components/FormBuilderFieldSettingsModal.vue
Normal file
1319
components/FormBuilderFieldSettingsModal.vue
Normal file
File diff suppressed because it is too large
Load Diff
2326
components/FormTemplatesModal.vue
Normal file
2326
components/FormTemplatesModal.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -117,14 +117,8 @@ watch(
|
||||
></Icon>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<NuxtScrollbar
|
||||
:style="{
|
||||
'max-height': height,
|
||||
}"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
<slot v-if="!$slots.body"></slot>
|
||||
</NuxtScrollbar>
|
||||
</div>
|
||||
<div v-if="!hideFooter" class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
@ -147,3 +141,93 @@ watch(
|
||||
</transition-group>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
padding: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto; /* Allow the overlay to scroll */
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin: 1.5rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 3rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-top {
|
||||
align-items: flex-start;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.modal-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-end {
|
||||
align-items: flex-end;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.modal-hide-overlay {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
41
components/formkit/Switch.vue
Normal file
41
components/formkit/Switch.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
function handleChange(event) {
|
||||
props.context.node.input(event.target.checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-start">
|
||||
<div class="relative inline-block w-11 h-6">
|
||||
<input
|
||||
:id="context.id"
|
||||
type="checkbox"
|
||||
:checked="context._value"
|
||||
:disabled="context.disabled"
|
||||
@change="handleChange"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styles for better visual feedback */
|
||||
.peer:checked + div {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
.peer:disabled + div {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.peer:focus + div {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
@ -1,239 +1,397 @@
|
||||
# Form Builder Documentation
|
||||
# Form Builder User Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The Form Builder is a powerful drag-and-drop interface for creating dynamic forms. It provides an intuitive, visual way to build forms by selecting components, configuring their properties, and arranging them in your desired layout. Perfect for creating everything from simple contact forms to complex multi-step surveys. Forms can be used independently or integrated with the Process Builder for workflow automation.
|
||||
The Form Builder is a modern, intuitive drag-and-drop interface for creating dynamic forms. Featuring a smart collapsible settings panel, visual grid system, and intelligent component recommendations, it makes form creation accessible to both developers and non-technical users.
|
||||
|
||||
> For technical implementation details, please refer to [Form Builder Technical Appendix](FORM_BUILDER_TECHNICAL_APPENDIX.md)
|
||||
> For technical implementation details, see the [Technical Guide](TECHNICAL_GUIDE.md)
|
||||
|
||||
## Interface Overview
|
||||
|
||||
### Three-Panel Layout
|
||||
- **Left Panel**: Component Library with categorized components
|
||||
- **Center Panel**: Form Canvas with 12-column grid system
|
||||
- **Right Panel**: Smart Collapsible Settings Panel
|
||||
|
||||
### Key Features
|
||||
- **Smart Settings Panel**: Auto-opens when components are selected, collapses to save space
|
||||
- **Visual Grid System**: Intuitive width selection with grid previews
|
||||
- **Quick Settings**: Inline editing for common properties
|
||||
- **Smart Recommendations**: Automatic width suggestions based on field type
|
||||
- **Real-time Preview**: See changes instantly as you build
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Accessing the Form Builder
|
||||
1. Navigate to `/form-builder` in your browser
|
||||
2. You'll see a three-panel interface:
|
||||
- Left: Component Library
|
||||
- Middle: Form Canvas
|
||||
- Right: Configuration Panel
|
||||
### Creating Your First Form
|
||||
|
||||
### Quick Start Guide
|
||||
1. **Create a New Form**
|
||||
- Click "New Form" in the header
|
||||
- Enter a form name and description
|
||||
- Start adding components
|
||||
1. **Start Building**
|
||||
- Navigate to the Form Builder
|
||||
- Click "New Form" or start with a template
|
||||
- The interface loads with an empty canvas
|
||||
|
||||
2. **Add Components**
|
||||
- Drag components from the left panel
|
||||
- Or click components to add them to the end
|
||||
- Components are organized by category for easy finding
|
||||
- **Drag & Drop**: Drag any component from the left panel to the canvas
|
||||
- **Click to Add**: Click a component to add it at the end of the form
|
||||
- **Smart Placement**: Components automatically find optimal grid positions
|
||||
|
||||
3. **Configure Components**
|
||||
- Click any component in the canvas to select it
|
||||
- Use the right panel to configure its properties
|
||||
- Changes are previewed in real-time
|
||||
- **Auto-Selection**: Click any component to select it
|
||||
- **Quick Settings**: The right panel automatically opens with common settings
|
||||
- **Inline Editing**: Edit labels, names, widths, and validation directly
|
||||
- **Full Settings**: Click "Full Settings" for advanced configuration
|
||||
|
||||
4. **Save and Preview**
|
||||
- Click "Save" to store your form
|
||||
- Use "Preview" to test the form
|
||||
- Access saved forms via "Manage Forms"
|
||||
4. **Save & Preview**
|
||||
- **Auto-Save**: Changes are saved automatically
|
||||
- **Preview Mode**: Toggle to test your form as users will see it
|
||||
- **Form Management**: Access all saved forms from the header
|
||||
|
||||
## Available Components
|
||||
## Smart Settings Panel System
|
||||
|
||||
### Panel Behavior
|
||||
- **Auto-Open**: Automatically expands when you select a component
|
||||
- **Collapsible**: Toggle between expanded (320px) and collapsed (48px) states
|
||||
- **Quick Access**: Common settings available without opening full modal
|
||||
- **Context-Aware**: Shows only relevant settings for each component type
|
||||
|
||||
### Quick Settings Available
|
||||
- **Label**: Component display name
|
||||
- **Field Name**: Internal identifier
|
||||
- **Width**: S/M/L/XL quick sizing buttons
|
||||
- **Required**: Toggle validation
|
||||
- **Placeholder**: Hint text (where applicable)
|
||||
|
||||
### Collapsed State Features
|
||||
- **Component Icon**: Visual indicator of selected component type
|
||||
- **Type Badge**: Short identifier (TXT, BTN, SEL, etc.)
|
||||
- **Toggle Button**: One-click expansion
|
||||
- **Space-Efficient**: Only 48px wide when collapsed
|
||||
|
||||
## Enhanced Grid System
|
||||
|
||||
### Visual Width Selection
|
||||
The new grid system replaces technical percentages with intuitive options:
|
||||
|
||||
#### Width Options
|
||||
- **Narrow (25%)**: Small inputs like age, zip codes, short codes
|
||||
- **Small (33%)**: Short text fields, city names, grouped inputs
|
||||
- **Medium (50%)**: Names, phone numbers, paired inputs
|
||||
- **Wide (75%)**: Addresses, URLs, prominent fields
|
||||
- **Full (100%)**: Text areas, headings, single-column layouts
|
||||
|
||||
#### Visual Grid Preview
|
||||
- **12-Column Grid**: Mini preview shows exactly which columns your component will occupy
|
||||
- **Active Columns**: Highlighted squares show component width
|
||||
- **Current Selection**: Visual feedback shows your current choice
|
||||
- **Grid Math**: Displays "6 of 12 columns" instead of technical details
|
||||
|
||||
### Smart Recommendations
|
||||
The system automatically suggests optimal widths based on component type:
|
||||
|
||||
- **Narrow Recommended**: Number, Date, Time, Color, OTP fields
|
||||
- **Small Recommended**: Text inputs
|
||||
- **Medium Recommended**: Email, Phone, Password, Select, Masked inputs
|
||||
- **Wide Recommended**: URL, File uploads
|
||||
- **Full Recommended**: Text areas, Headings, Choice groups, Buttons
|
||||
|
||||
#### Visual Indicators
|
||||
- **Green Ring**: Recommended options have green highlighting
|
||||
- **Selection State**: Current choice highlighted in blue
|
||||
- **Combined State**: Selected + recommended shows green background
|
||||
|
||||
## Component Library
|
||||
|
||||
### Basic Inputs
|
||||
Perfect for collecting simple text and numeric data:
|
||||
- **Text Field**: Single line text input
|
||||
- Use for: Names, titles, short answers
|
||||
- Features: Placeholder text, help text, validation
|
||||
Perfect for collecting simple data:
|
||||
|
||||
- **Text Area**: Multi-line text input
|
||||
- Use for: Comments, descriptions, long answers
|
||||
- Features: Resizable, character count option
|
||||
**Text Field**
|
||||
- Single-line text input
|
||||
- Smart width: Small (33%)
|
||||
- Use for: Names, titles, short answers
|
||||
- Quick settings: Label, name, placeholder, required
|
||||
|
||||
- **Number**: Numeric input field
|
||||
- Use for: Age, quantity, numeric values
|
||||
- Features: Min/max limits, step values
|
||||
**Text Area**
|
||||
- Multi-line text input
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Comments, descriptions, long text
|
||||
- Features: Resizable, character limits
|
||||
|
||||
- **Email**: Email address input
|
||||
- Use for: Contact forms, user registration
|
||||
- Features: Built-in email validation
|
||||
**Number Field**
|
||||
- Numeric input with validation
|
||||
- Smart width: Narrow (25%)
|
||||
- Use for: Age, quantity, prices
|
||||
- Features: Min/max limits, step values
|
||||
|
||||
- **Password**: Secure password input
|
||||
- Use for: Login forms, security inputs
|
||||
- Features: Password masking, strength indicators
|
||||
**Email Field**
|
||||
- Email input with validation
|
||||
- Smart width: Medium (50%)
|
||||
- Use for: Contact forms, registration
|
||||
- Features: Built-in email format validation
|
||||
|
||||
### Selection Inputs
|
||||
For choosing from predefined options:
|
||||
- **Select Dropdown**: Single selection menu
|
||||
- Use for: Country selection, categories
|
||||
- Features: Search, option groups, custom values
|
||||
**Password Field**
|
||||
- Secure password input
|
||||
- Smart width: Medium (50%)
|
||||
- Use for: Authentication forms
|
||||
- Features: Password masking, strength indicators
|
||||
|
||||
- **Checkbox Group**: Multiple choice selection
|
||||
- Use for: Multiple selections, preferences
|
||||
- Features: Select all, option layout control
|
||||
### Selection Components
|
||||
For choosing from options:
|
||||
|
||||
- **Radio Group**: Single choice selection
|
||||
- Use for: Exclusive choices, yes/no questions
|
||||
- Features: Button or traditional style
|
||||
**Select Dropdown**
|
||||
- Single selection from options
|
||||
- Smart width: Medium (50%)
|
||||
- Use for: Countries, categories, status
|
||||
- Features: Search, custom values, option groups
|
||||
|
||||
### Date and Time
|
||||
Temporal input components:
|
||||
- **Date Picker**: Date selection
|
||||
- Use for: Birthdays, scheduling
|
||||
- Features: Date range limits, format options
|
||||
**Checkbox Group**
|
||||
- Multiple selection options
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Preferences, multiple choices
|
||||
- Features: Select all option, layout control
|
||||
|
||||
- **Time Picker**: Time selection
|
||||
- Use for: Scheduling, time slots
|
||||
- Features: 12/24 hour format, minute steps
|
||||
**Radio Group**
|
||||
- Single choice from group
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Exclusive choices, yes/no questions
|
||||
- Features: Button or traditional styles
|
||||
|
||||
- **Date & Time**: Combined selection
|
||||
- Use for: Event scheduling, appointments
|
||||
- Features: Single field for date and time
|
||||
**Switch Toggle**
|
||||
- Boolean on/off control
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Settings, feature toggles
|
||||
- Features: Custom labels, default states
|
||||
|
||||
### Date & Time Components
|
||||
For temporal inputs:
|
||||
|
||||
**Date Picker**
|
||||
- Date selection interface
|
||||
- Smart width: Narrow (25%)
|
||||
- Use for: Birthdays, deadlines, scheduling
|
||||
- Features: Date ranges, format options, validation
|
||||
|
||||
**Time Picker**
|
||||
- Time selection interface
|
||||
- Smart width: Narrow (25%)
|
||||
- Use for: Appointments, schedules
|
||||
- Features: 12/24 hour format, minute intervals
|
||||
|
||||
**Date & Time**
|
||||
- Combined date and time picker
|
||||
- Smart width: Medium (50%)
|
||||
- Use for: Event scheduling, timestamps
|
||||
- Features: Single field for both values
|
||||
|
||||
### Advanced Components
|
||||
Specialized input types:
|
||||
- **File Upload**: File input field
|
||||
- Use for: Document upload, image submission
|
||||
- Features: File type restrictions, size limits
|
||||
Specialized functionality:
|
||||
|
||||
- **Repeater**: Repeatable field groups
|
||||
- Use for: Multiple entries, dynamic lists
|
||||
- Features: Add/remove controls, ordering
|
||||
**File Upload**
|
||||
- Standard file input
|
||||
- Smart width: Wide (75%)
|
||||
- Use for: Document uploads, attachments
|
||||
- Features: File type restrictions, size limits
|
||||
|
||||
- **Group**: Field organization
|
||||
- Use for: Related fields, form sections
|
||||
- Features: Collapsible, conditional display
|
||||
**File Drop Zone**
|
||||
- Drag & drop upload area
|
||||
- Smart width: Wide (75%)
|
||||
- Use for: Multiple files, intuitive uploads
|
||||
- Features: Visual feedback, progress indicators
|
||||
|
||||
**OTP Input**
|
||||
- One-time password entry
|
||||
- Smart width: Narrow (25%)
|
||||
- Use for: Two-factor authentication
|
||||
- Features: Auto-focus, numeric only
|
||||
|
||||
**Masked Input**
|
||||
- Formatted text input
|
||||
- Smart width: Medium (50%)
|
||||
- Use for: Phone numbers, SSN, custom formats
|
||||
- Features: Real-time formatting, validation
|
||||
|
||||
### Layout Components
|
||||
Form structure and organization:
|
||||
- **Heading**: Section titles
|
||||
- Use for: Form sections, categories
|
||||
- Features: Multiple heading levels
|
||||
|
||||
- **Paragraph**: Descriptive text
|
||||
- Use for: Instructions, help text
|
||||
- Features: Rich text formatting
|
||||
**Heading**
|
||||
- Section titles and organization
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Form sections, categories
|
||||
- Features: Multiple heading levels (H1-H6)
|
||||
|
||||
- **Divider**: Visual separator
|
||||
- Use for: Section breaks
|
||||
- Features: Various styles
|
||||
**Paragraph**
|
||||
- Descriptive text content
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Instructions, explanations
|
||||
- Features: Rich text formatting options
|
||||
|
||||
## Form Configuration
|
||||
**Divider**
|
||||
- Visual section separator
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Content organization
|
||||
- Features: Various visual styles
|
||||
|
||||
### Basic Settings
|
||||
- Form name and description
|
||||
- Success/error messages
|
||||
- Submit button text
|
||||
- Form layout options
|
||||
**Button**
|
||||
- Action triggers
|
||||
- Smart width: Full (100%)
|
||||
- Use for: Form submission, navigation
|
||||
- Features: Custom styling, click handlers
|
||||
|
||||
### Validation Options
|
||||
- Required fields
|
||||
- Input patterns
|
||||
- Custom error messages
|
||||
- Cross-field validation
|
||||
## Form Configuration Workflows
|
||||
|
||||
### Advanced Settings
|
||||
- Form submission behavior
|
||||
- Success/failure redirects
|
||||
- Custom CSS classes
|
||||
- Event handlers
|
||||
### Quick Settings Workflow (80% of edits)
|
||||
1. Click any component to select it
|
||||
2. Settings panel auto-opens on the right
|
||||
3. Edit common properties inline:
|
||||
- Change label text
|
||||
- Adjust field width with S/M/L/XL buttons
|
||||
- Toggle required validation
|
||||
- Set placeholder text
|
||||
4. Changes apply immediately
|
||||
|
||||
### Full Settings Workflow (20% of edits)
|
||||
1. Select component in quick settings panel
|
||||
2. Click "Full Settings" button
|
||||
3. Access comprehensive modal with:
|
||||
- All component properties
|
||||
- Advanced validation rules
|
||||
- Visual width selector with grid preview
|
||||
- Component-specific options
|
||||
- Reset to defaults option
|
||||
|
||||
### Bulk Operations
|
||||
- **Duplicate**: Copy component with "Duplicate" button
|
||||
- **Delete**: Remove component with "Delete" button
|
||||
- **Reorder**: Drag components to reposition
|
||||
- **Multi-select**: Select multiple components for batch operations
|
||||
|
||||
## Form Templates & Management
|
||||
|
||||
### Template System
|
||||
- **Blank Form**: Start from scratch
|
||||
- **Contact Form**: Pre-built contact form
|
||||
- **Survey Form**: Multi-question survey template
|
||||
- **Registration Form**: User registration template
|
||||
- **Custom Templates**: Save your own templates
|
||||
|
||||
### Form Management
|
||||
- **Auto-Save**: Changes saved automatically every 30 seconds
|
||||
- **Version History**: Track form changes over time
|
||||
- **Duplicate Forms**: Copy existing forms as starting points
|
||||
- **Export/Import**: JSON format for form portability
|
||||
- **Form Library**: Organize forms by category or project
|
||||
|
||||
## Process Builder Integration
|
||||
|
||||
The Form Builder integrates with the Process Builder to create workflow-driven forms:
|
||||
### Workflow Integration
|
||||
Forms integrate seamlessly with the Process Builder for automated workflows:
|
||||
|
||||
### Connecting Forms to Processes
|
||||
1. **Create and Save Your Form**
|
||||
- Design your form in the Form Builder
|
||||
- Save the form with a clear name and description
|
||||
- Forms must be saved before they can be used in processes
|
||||
1. **Create Form**: Design your form in Form Builder
|
||||
2. **Save Form**: Forms must be saved before use in processes
|
||||
3. **Add to Process**: Select saved form in Process Builder Form Task
|
||||
4. **Data Flow**: Form submissions become process variables
|
||||
5. **Decision Logic**: Use form data in process gateways and conditions
|
||||
|
||||
2. **Using Forms in Process Builder**
|
||||
- In Process Builder, add a Form Task to your process
|
||||
- Select your form from the Form Selector dropdown
|
||||
- The form will be presented to users when they reach this task in the workflow
|
||||
|
||||
3. **Form Data in Processes**
|
||||
- Data submitted through forms becomes available as process variables
|
||||
- Use form data to drive process decisions in gateways
|
||||
- Reference form fields in scripts and conditions
|
||||
|
||||
### Form URL Parameters
|
||||
- Forms can accept URL parameters to pre-populate fields
|
||||
- Process variables can be passed to forms as parameters
|
||||
- Use the `formId` parameter to load a specific form
|
||||
### Advanced Integration Features
|
||||
- **URL Parameters**: Pre-populate form fields from process variables
|
||||
- **Conditional Logic**: Show/hide fields based on process state
|
||||
- **Multi-Step Forms**: Split complex forms across multiple process steps
|
||||
- **Data Validation**: Cross-reference form data with process requirements
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Form Design
|
||||
1. Group related fields together
|
||||
2. Use clear, concise labels
|
||||
3. Provide help text for complex fields
|
||||
4. Maintain consistent styling
|
||||
5. Consider mobile users
|
||||
1. **Start Simple**: Begin with essential fields only
|
||||
2. **Use Smart Widths**: Trust the width recommendations
|
||||
3. **Group Related Fields**: Use consistent widths for grouped fields
|
||||
4. **Logical Flow**: Order fields in a natural sequence
|
||||
5. **Mobile-First**: Test forms on mobile devices
|
||||
|
||||
### Validation
|
||||
1. Validate on the appropriate event
|
||||
2. Provide clear error messages
|
||||
3. Show validation status clearly
|
||||
4. Use appropriate validation rules
|
||||
### Grid Layout Optimization
|
||||
1. **Mix Widths**: Combine narrow and wide fields effectively
|
||||
2. **Visual Balance**: Avoid too many full-width fields in sequence
|
||||
3. **Logical Grouping**: Use similar widths for related fields
|
||||
4. **Space Efficiency**: Let the system auto-optimize grid placement
|
||||
|
||||
### User Experience
|
||||
1. Keep forms as short as possible
|
||||
2. Use appropriate field types
|
||||
3. Provide clear instructions
|
||||
4. Show progress in multi-step forms
|
||||
5. Ensure keyboard navigation
|
||||
1. **Clear Labels**: Use descriptive, action-oriented labels
|
||||
2. **Helpful Placeholders**: Provide examples in placeholder text
|
||||
3. **Progressive Disclosure**: Use sections for complex forms
|
||||
4. **Validation Feedback**: Set appropriate validation rules
|
||||
5. **Success States**: Configure clear success messages
|
||||
|
||||
## Managing Forms
|
||||
### Performance Optimization
|
||||
1. **Component Limits**: Keep forms under 50 components when possible
|
||||
2. **Conditional Logic**: Use sparingly to maintain performance
|
||||
3. **File Uploads**: Set reasonable file size limits
|
||||
4. **Auto-Save**: Leverage automatic saving features
|
||||
|
||||
### Saved Forms
|
||||
- View all forms in the management interface
|
||||
- Search and filter forms
|
||||
- Duplicate existing forms
|
||||
- Archive unused forms
|
||||
## Accessibility Features
|
||||
|
||||
### Form Actions
|
||||
- Preview: Test the form
|
||||
- Edit: Modify form structure
|
||||
- Delete: Remove unused forms
|
||||
- Export: Save form configuration
|
||||
### Built-in Accessibility
|
||||
- **Keyboard Navigation**: Full keyboard support
|
||||
- **Screen Reader Support**: Proper ARIA labels and descriptions
|
||||
- **Focus Management**: Clear focus indicators
|
||||
- **High Contrast**: Accessible color combinations
|
||||
- **Touch Targets**: Mobile-friendly touch targets
|
||||
|
||||
### Form Analytics
|
||||
- View submission statistics
|
||||
- Track completion rates
|
||||
- Identify problem fields
|
||||
- Monitor usage patterns
|
||||
### Accessibility Best Practices
|
||||
1. **Descriptive Labels**: Always provide clear field labels
|
||||
2. **Help Text**: Use help text for complex fields
|
||||
3. **Error Messages**: Provide clear, actionable error messages
|
||||
4. **Logical Order**: Ensure tab order follows visual layout
|
||||
5. **Alternative Text**: Provide alt text for any images
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Form Not Saving**
|
||||
- Check your connection
|
||||
- Ensure form has a name
|
||||
- Verify all components are valid
|
||||
|
||||
2. **Components Not Dragging**
|
||||
- Clear browser cache
|
||||
- Check for JavaScript errors
|
||||
- Ensure proper mouse/touch interaction
|
||||
**Panel Not Opening**
|
||||
- Ensure component is properly selected
|
||||
- Check browser console for JavaScript errors
|
||||
- Refresh page if panel state becomes stuck
|
||||
|
||||
3. **Validation Not Working**
|
||||
- Verify validation rules syntax
|
||||
- Check field names are unique
|
||||
- Ensure validation is enabled
|
||||
**Grid Layout Issues**
|
||||
- Use the optimize layout feature
|
||||
- Check for invalid width values
|
||||
- Reset component to default width
|
||||
|
||||
4. **Form Not Appearing in Process Builder**
|
||||
- Verify the form was saved successfully
|
||||
- Check user permissions
|
||||
- Refresh the Process Builder page
|
||||
**Components Not Saving**
|
||||
- Verify internet connection
|
||||
- Check browser local storage limits
|
||||
- Try manual save if auto-save fails
|
||||
|
||||
**Performance Issues**
|
||||
- Reduce number of components
|
||||
- Simplify conditional logic
|
||||
- Clear browser cache
|
||||
|
||||
### Getting Help
|
||||
- Check the technical documentation
|
||||
- Contact support team
|
||||
- Submit bug reports
|
||||
- Request features
|
||||
- **Documentation**: Complete technical guides available
|
||||
- **Examples**: Sample forms and templates provided
|
||||
- **Support**: Contact support team for assistance
|
||||
- **Community**: Join user community forums
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Components
|
||||
- **Component Extensions**: Add custom component types
|
||||
- **Styling Overrides**: Custom CSS for branded forms
|
||||
- **Validation Rules**: Create custom validation logic
|
||||
- **Integration Hooks**: Connect to external services
|
||||
|
||||
### API Integration
|
||||
- **Form Data API**: Programmatic access to form submissions
|
||||
- **Component API**: Dynamically modify form components
|
||||
- **Validation API**: Custom server-side validation
|
||||
- **Webhook Support**: Real-time form submission notifications
|
||||
|
||||
### Enterprise Features
|
||||
- **Team Collaboration**: Multi-user form editing
|
||||
- **Permission Management**: Role-based access control
|
||||
- **Audit Logging**: Track all form changes
|
||||
- **White Labeling**: Custom branding options
|
||||
|
||||
---
|
||||
|
||||
For technical details about implementation, component structure, and development guidelines, please refer to the [Technical Appendix](FORM_BUILDER_TECHNICAL_APPENDIX.md).
|
||||
*This documentation reflects all current features including the smart collapsible settings panel, enhanced grid system with visual previews, intelligent width recommendations, and comprehensive UX improvements implemented in the latest version.*
|
||||
|
||||
Last updated: June 10, 2024
|
||||
Last updated: December 2024
|
587
doc/form-builder/grid-system.md
Normal file
587
doc/form-builder/grid-system.md
Normal file
@ -0,0 +1,587 @@
|
||||
# Form Builder Grid System - Complete Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The form builder uses an intelligent 12-column CSS Grid system that automatically manages component placement and provides intuitive width selection through visual previews and smart recommendations. This system replaces technical percentages with user-friendly names and includes automatic gap-filling optimization.
|
||||
|
||||
## Grid Architecture
|
||||
|
||||
### CSS Grid Foundation
|
||||
```css
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row dense; /* Enables automatic gap filling */
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
### Component Grid Placement
|
||||
```css
|
||||
.form-component {
|
||||
grid-column: span 12; /* Default to full width */
|
||||
width: 100% !important; /* Force width within grid cell */
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
```
|
||||
|
||||
## Width Options System
|
||||
|
||||
### Available Width Options
|
||||
|
||||
| Name | Percentage | Grid Columns | Grid Span | Use Cases |
|
||||
|--------|------------|--------------|--------------|-----------|
|
||||
| Narrow | 25% | 3 of 12 | `span 3` | Small inputs like age, zip codes, short codes |
|
||||
| Small | 33% | 4 of 12 | `span 4` | Short text fields, city names, grouped inputs |
|
||||
| Medium | 50% | 6 of 12 | `span 6` | Names, phone numbers, paired inputs |
|
||||
| Wide | 75% | 9 of 12 | `span 9` | Addresses, URLs, prominent fields |
|
||||
| Full | 100% | 12 of 12 | `span 12` | Text areas, headings, single-column layouts |
|
||||
|
||||
### Width Selection Interface
|
||||
|
||||
#### Visual Grid Preview
|
||||
Each width option displays a mini 12-column grid preview:
|
||||
```vue
|
||||
<div class="grid-container-mini">
|
||||
<div
|
||||
v-for="i in 12"
|
||||
:key="i"
|
||||
class="grid-cell"
|
||||
:class="{
|
||||
'active': i <= option.gridColumns,
|
||||
'inactive': i > option.gridColumns
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Current Selection Feedback
|
||||
The interface shows:
|
||||
- Current width name (e.g., "Medium")
|
||||
- Grid columns used (e.g., "6 of 12 columns")
|
||||
- Visual representation of current selection
|
||||
- Percentage equivalent for technical reference
|
||||
|
||||
## Smart Recommendation System
|
||||
|
||||
### Field Type Recommendations
|
||||
|
||||
The system automatically suggests optimal widths based on component type:
|
||||
|
||||
#### Narrow Fields (25% - 3 columns)
|
||||
**Recommended for**: Small, precise inputs
|
||||
- `number`: Age, quantity, counts
|
||||
- `date`: Birth dates, deadlines
|
||||
- `time`: Appointment times
|
||||
- `color`: Color picker inputs
|
||||
- `otp`: Verification codes
|
||||
|
||||
```javascript
|
||||
const narrowRecommended = ['number', 'date', 'time', 'color', 'otp']
|
||||
```
|
||||
|
||||
#### Small Fields (33% - 4 columns)
|
||||
**Recommended for**: Short text inputs
|
||||
- `text`: Names, titles, short answers
|
||||
|
||||
```javascript
|
||||
const smallRecommended = ['text']
|
||||
```
|
||||
|
||||
#### Medium Fields (50% - 6 columns)
|
||||
**Recommended for**: Standard form inputs
|
||||
- `email`: Email addresses
|
||||
- `tel`: Phone numbers
|
||||
- `password`: Password fields
|
||||
- `mask`: Formatted inputs (SSN, phone)
|
||||
- `select`: Dropdown selections
|
||||
- `datetime-local`: Combined date/time
|
||||
|
||||
```javascript
|
||||
const mediumRecommended = ['email', 'tel', 'password', 'mask', 'select', 'datetime-local']
|
||||
```
|
||||
|
||||
#### Wide Fields (75% - 9 columns)
|
||||
**Recommended for**: Longer inputs requiring more space
|
||||
- `url`: Website addresses
|
||||
- `file`: File upload fields
|
||||
- `dropzone`: Drag & drop upload areas
|
||||
|
||||
```javascript
|
||||
const wideRecommended = ['url', 'file', 'dropzone']
|
||||
```
|
||||
|
||||
#### Full Width Fields (100% - 12 columns)
|
||||
**Recommended for**: Content and multi-option components
|
||||
- `textarea`: Multi-line text areas
|
||||
- `heading`: Section headings
|
||||
- `paragraph`: Descriptive text
|
||||
- `checkbox`: Checkbox groups
|
||||
- `radio`: Radio button groups
|
||||
- `range`: Range sliders
|
||||
- `switch`: Toggle switches
|
||||
- `button`: Action buttons
|
||||
- `info-display`: Information displays
|
||||
|
||||
```javascript
|
||||
const fullRecommended = [
|
||||
'textarea', 'heading', 'paragraph', 'checkbox',
|
||||
'radio', 'range', 'switch', 'button', 'info-display'
|
||||
]
|
||||
```
|
||||
|
||||
### Visual Recommendation Indicators
|
||||
|
||||
- **Green Ring**: Recommended options have green highlighting
|
||||
- **Selection State**: Current choice highlighted in blue
|
||||
- **Combined State**: Selected + recommended shows green background with checkmark
|
||||
- **Recommendation Badge**: "Recommended" text appears next to optimal choices
|
||||
|
||||
## Smart Grid Placement Algorithm
|
||||
|
||||
### Optimal Placement Logic
|
||||
|
||||
The system automatically finds the best position for new components:
|
||||
|
||||
```javascript
|
||||
findOptimalGridPlacement() {
|
||||
if (this.formComponents.length === 0) {
|
||||
// First component - full width
|
||||
return {
|
||||
gridColumn: 'span 12',
|
||||
rowIndex: 0,
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
|
||||
// Group components by their implicit rows
|
||||
const rows = this.analyzeGridRows()
|
||||
|
||||
// Find row with remaining space
|
||||
const rowWithSpace = rows.find(row => row.remainingSpace > 0)
|
||||
|
||||
if (rowWithSpace) {
|
||||
// Use remaining space in existing row
|
||||
const remainingColumns = rowWithSpace.remainingSpace
|
||||
const widthPercent = this.columnsToPercentage(remainingColumns)
|
||||
|
||||
return {
|
||||
gridColumn: `span ${remainingColumns}`,
|
||||
rowIndex: rows.indexOf(rowWithSpace),
|
||||
width: `${widthPercent}%`
|
||||
}
|
||||
} else {
|
||||
// Create new row
|
||||
return {
|
||||
gridColumn: 'span 12',
|
||||
rowIndex: rows.length,
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Row Analysis
|
||||
```javascript
|
||||
analyzeGridRows() {
|
||||
const rows = []
|
||||
let currentRowSpace = 12
|
||||
|
||||
this.formComponents.forEach(component => {
|
||||
const spanMatch = component.props.gridColumn?.match(/span\s+(\d+)/) || []
|
||||
const columnSpan = parseInt(spanMatch[1]) || 12
|
||||
|
||||
if (columnSpan <= currentRowSpace) {
|
||||
// Add to current row
|
||||
currentRowSpace -= columnSpan
|
||||
} else {
|
||||
// Start new row
|
||||
rows.push({ remainingSpace: currentRowSpace })
|
||||
currentRowSpace = 12 - columnSpan
|
||||
}
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
```
|
||||
|
||||
## Grid Layout Optimization
|
||||
|
||||
### Automatic Gap Filling
|
||||
|
||||
The `grid-auto-flow: row dense` CSS property enables automatic gap filling:
|
||||
- Components automatically fill available spaces
|
||||
- No manual positioning required
|
||||
- Optimal use of screen real estate
|
||||
|
||||
### Layout Optimization Algorithm
|
||||
|
||||
```javascript
|
||||
optimizeGridLayout() {
|
||||
const rows = this.groupComponentsByRows()
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.remainingSpace > 0) {
|
||||
this.distributeRemainingSpace(row)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
distributeRemainingSpace(row) {
|
||||
if (row.components.length === 1 && row.remainingSpace > 0) {
|
||||
// Expand single component to fill row
|
||||
const comp = row.components[0]
|
||||
this.updateComponentSize(comp.component, 12)
|
||||
} else if (row.components.length > 1) {
|
||||
// Distribute space proportionally
|
||||
const extraSpanPerComponent = Math.floor(row.remainingSpace / row.components.length)
|
||||
let remainingExtraSpan = row.remainingSpace % row.components.length
|
||||
|
||||
row.components.forEach(comp => {
|
||||
let extraSpan = extraSpanPerComponent
|
||||
if (remainingExtraSpan > 0) {
|
||||
extraSpan += 1
|
||||
remainingExtraSpan--
|
||||
}
|
||||
|
||||
const newSpan = comp.span + extraSpan
|
||||
this.updateComponentSize(comp.component, newSpan)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Collapsible Panel Integration
|
||||
|
||||
### Quick Width Selector
|
||||
|
||||
The collapsible panel includes a compact width selector:
|
||||
|
||||
```vue
|
||||
<div class="width-selector-compact">
|
||||
<button
|
||||
v-for="option in compactWidthOptions"
|
||||
:key="option.value"
|
||||
@click="updateQuickSetting('width', option.value, option.gridColumns)"
|
||||
class="width-btn"
|
||||
:class="{ 'active': getComponentWidthPercent() === option.value }"
|
||||
>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Compact Options
|
||||
```javascript
|
||||
const compactWidthOptions = [
|
||||
{ name: 'S', value: 25, gridColumns: 3, description: 'Small (25%)' },
|
||||
{ name: 'M', value: 50, gridColumns: 6, description: 'Medium (50%)' },
|
||||
{ name: 'L', value: 75, gridColumns: 9, description: 'Large (75%)' },
|
||||
{ name: 'XL', value: 100, gridColumns: 12, description: 'Extra Large (100%)' }
|
||||
]
|
||||
```
|
||||
|
||||
## Grid Component Implementation
|
||||
|
||||
### Component Props Structure
|
||||
```typescript
|
||||
interface ComponentProps {
|
||||
width: string // e.g., "50%"
|
||||
gridColumn: string // e.g., "span 6"
|
||||
label?: string
|
||||
name?: string
|
||||
// ... other props
|
||||
}
|
||||
```
|
||||
|
||||
### Width Calculation Functions
|
||||
|
||||
```javascript
|
||||
// Convert percentage to grid columns
|
||||
const percentageToGridColumns = (percentage) => {
|
||||
const mapping = {
|
||||
25: 3, // 3/12 = 25%
|
||||
33: 4, // 4/12 = 33.33%
|
||||
50: 6, // 6/12 = 50%
|
||||
66: 8, // 8/12 = 66.67%
|
||||
75: 9, // 9/12 = 75%
|
||||
100: 12 // 12/12 = 100%
|
||||
}
|
||||
return mapping[percentage] || Math.round((percentage / 100) * 12)
|
||||
}
|
||||
|
||||
// Convert grid columns to percentage
|
||||
const gridColumnsToPercentage = (columns) => {
|
||||
const mapping = {
|
||||
3: 25, // 3/12 = 25%
|
||||
4: 33, // 4/12 = 33.33%
|
||||
6: 50, // 6/12 = 50%
|
||||
8: 66, // 8/12 = 66.67%
|
||||
9: 75, // 9/12 = 75%
|
||||
12: 100 // 12/12 = 100%
|
||||
}
|
||||
return mapping[columns] || Math.round((columns / 12) * 100)
|
||||
}
|
||||
|
||||
// Set component width with grid sync
|
||||
const setComponentWidth = (percentage, gridColumns) => {
|
||||
component.props.width = `${percentage}%`
|
||||
component.props.gridColumn = `span ${gridColumns}`
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Mobile Optimization
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
.grid-container {
|
||||
grid-template-columns: 1fr; /* Single column on mobile */
|
||||
column-gap: 0;
|
||||
}
|
||||
|
||||
.form-component {
|
||||
grid-column: span 1 !important; /* Force single column */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tablet Optimization
|
||||
```css
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(8, 1fr); /* 8 columns on tablet */
|
||||
}
|
||||
|
||||
.form-component[style*="span 12"] {
|
||||
grid-column: span 8 !important; /* Adjust full-width components */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resize Handle System
|
||||
|
||||
### Interactive Resizing
|
||||
```vue
|
||||
<div v-if="selectedComponentId === component.id && resizeMode" class="resize-handles">
|
||||
<div
|
||||
class="resize-handle resize-handle-right"
|
||||
@mousedown="startResize($event, component)"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Resize Logic with Snap-to-Grid
|
||||
```javascript
|
||||
const handleResize = (event) => {
|
||||
const deltaX = event.clientX - initialX.value
|
||||
const container = document.querySelector('.grid-container')
|
||||
const containerWidth = container.offsetWidth
|
||||
const deltaPercentage = (deltaX / containerWidth) * 100
|
||||
|
||||
let newWidth = initialWidth.value + deltaPercentage
|
||||
newWidth = Math.max(25, Math.min(100, newWidth))
|
||||
|
||||
// Snap to standard widths
|
||||
const standardWidths = [25, 33, 50, 66, 75, 100]
|
||||
for (const std of standardWidths) {
|
||||
if (Math.abs(newWidth - std) < 5) {
|
||||
newWidth = std
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to grid columns and update
|
||||
const gridColumns = percentageToGridColumns(newWidth)
|
||||
updateComponentSize(component, newWidth, gridColumns)
|
||||
}
|
||||
```
|
||||
|
||||
## CSS Styling Reference
|
||||
|
||||
### Grid Container Styles
|
||||
```css
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
### Width Selector Modal Styles
|
||||
```css
|
||||
.width-option {
|
||||
@apply border border-gray-200 rounded-lg p-4 transition-all duration-200 hover:border-blue-300 hover:bg-blue-50 cursor-pointer flex items-center space-x-4;
|
||||
}
|
||||
|
||||
.width-option.selected {
|
||||
@apply border-blue-500 bg-blue-50 ring-2 ring-blue-200;
|
||||
}
|
||||
|
||||
.width-option.recommended {
|
||||
@apply ring-2 ring-green-200 border-green-300;
|
||||
}
|
||||
|
||||
.width-option.selected.recommended {
|
||||
@apply border-green-500 bg-green-50 ring-2 ring-green-200;
|
||||
}
|
||||
```
|
||||
|
||||
### Grid Preview Styles
|
||||
```css
|
||||
.grid-container-mini {
|
||||
@apply grid grid-cols-12 gap-1 w-32;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
@apply h-2 rounded-sm transition-colors duration-200;
|
||||
}
|
||||
|
||||
.grid-cell.active {
|
||||
@apply bg-blue-500;
|
||||
}
|
||||
|
||||
.grid-cell.inactive {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
```
|
||||
|
||||
### Quick Selector Styles
|
||||
```css
|
||||
.width-selector-compact {
|
||||
@apply flex space-x-1;
|
||||
}
|
||||
|
||||
.width-btn {
|
||||
@apply flex-1 px-2 py-1.5 text-xs font-medium text-center rounded-md border border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.width-btn.active {
|
||||
@apply border-blue-500 bg-blue-50 text-blue-700 ring-1 ring-blue-200;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Grid Layout Design
|
||||
1. **Start Simple**: Begin with recommended widths
|
||||
2. **Mix Widths**: Combine different widths for visual interest
|
||||
3. **Logical Grouping**: Use consistent widths for related fields
|
||||
4. **Visual Balance**: Avoid too many full-width fields in sequence
|
||||
5. **Mobile-First**: Consider how layouts adapt on smaller screens
|
||||
|
||||
### Performance Considerations
|
||||
1. **Grid Optimization**: Use the automatic optimization feature
|
||||
2. **Component Limits**: Keep forms under 50 components for best performance
|
||||
3. **Responsive Testing**: Test across different screen sizes
|
||||
4. **Browser Support**: Ensure CSS Grid polyfills for older browsers
|
||||
|
||||
### User Experience Guidelines
|
||||
1. **Intuitive Widths**: Trust the recommendation system
|
||||
2. **Visual Feedback**: Use the grid preview for precision
|
||||
3. **Consistent Patterns**: Establish width patterns across forms
|
||||
4. **Progressive Enhancement**: Start with basics, add complexity gradually
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Grid Layout Not Working**
|
||||
- Check CSS Grid browser support
|
||||
- Verify `grid-template-columns` is applied
|
||||
- Ensure components have valid `gridColumn` values
|
||||
|
||||
**Components Overlapping**
|
||||
- Check for invalid span values (must be 1-12)
|
||||
- Verify total spans in row don't exceed 12
|
||||
- Use grid optimization to fix layouts
|
||||
|
||||
**Responsive Issues**
|
||||
- Test media query breakpoints
|
||||
- Verify mobile-specific grid rules
|
||||
- Check component width calculations
|
||||
|
||||
**Performance Problems**
|
||||
- Reduce number of components
|
||||
- Use virtual scrolling for large forms
|
||||
- Optimize grid calculations
|
||||
|
||||
### Debugging Tools
|
||||
|
||||
```javascript
|
||||
// Debug grid layout
|
||||
const debugGridLayout = () => {
|
||||
const rows = groupComponentsByRows()
|
||||
console.table(rows.map(row => ({
|
||||
components: row.components.length,
|
||||
remainingSpace: row.remainingSpace,
|
||||
totalSpan: 12 - row.remainingSpace
|
||||
})))
|
||||
}
|
||||
|
||||
// Validate component grid properties
|
||||
const validateGridProps = (component) => {
|
||||
const span = component.props.gridColumn?.match(/span\s+(\d+)/)?.[1]
|
||||
const width = parseInt(component.props.width)
|
||||
|
||||
console.log({
|
||||
id: component.id,
|
||||
span: parseInt(span),
|
||||
width: width,
|
||||
valid: span >= 1 && span <= 12 && width > 0 && width <= 100
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Upgrading from Percentage-Only System
|
||||
|
||||
1. **Update Component Props**:
|
||||
```javascript
|
||||
// Old system
|
||||
component.props.width = "50%"
|
||||
|
||||
// New system
|
||||
component.props.width = "50%"
|
||||
component.props.gridColumn = "span 6"
|
||||
```
|
||||
|
||||
2. **Migrate Existing Forms**:
|
||||
```javascript
|
||||
const migrateFormToGrid = (form) => {
|
||||
return {
|
||||
...form,
|
||||
components: form.components.map(component => ({
|
||||
...component,
|
||||
props: {
|
||||
...component.props,
|
||||
gridColumn: component.props.gridColumn ||
|
||||
`span ${percentageToGridColumns(parseInt(component.props.width) || 100)}`
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update CSS**:
|
||||
```css
|
||||
/* Remove old percentage-based widths */
|
||||
.form-component {
|
||||
/* width: var(--component-width); // Remove this */
|
||||
grid-column: var(--grid-column); /* Add this */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This grid system documentation reflects the complete implementation including visual selection, smart recommendations, automatic optimization, and responsive behavior.*
|
||||
|
||||
**Last updated**: December 2024
|
File diff suppressed because it is too large
Load Diff
7
public/images/form-template-placeholder.svg
Normal file
7
public/images/form-template-placeholder.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="200" fill="#f1f5f9" />
|
||||
<rect x="40" y="40" width="320" height="30" rx="4" fill="#e2e8f0" />
|
||||
<rect x="40" y="85" width="320" height="30" rx="4" fill="#e2e8f0" />
|
||||
<rect x="40" y="130" width="150" height="30" rx="4" fill="#e2e8f0" />
|
||||
<text x="200" y="20" font-family="Arial, sans-serif" font-size="12" text-anchor="middle" fill="#64748b">Form Template</text>
|
||||
</svg>
|
After Width: | Height: | Size: 465 B |
@ -24,14 +24,16 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
onSubmit: false,
|
||||
onValidation: false
|
||||
},
|
||||
scriptMode: 'safe' // 'safe' or 'advanced'
|
||||
scriptMode: 'safe', // 'safe' or 'advanced'
|
||||
|
||||
// Form preview data
|
||||
previewFormData: {},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
selectedComponent: (state) => {
|
||||
return state.selectedComponentId
|
||||
? state.formComponents.find(c => c.id === state.selectedComponentId)
|
||||
: null;
|
||||
if (!state.selectedComponentId) return null;
|
||||
return state.formComponents.find(c => c.id === state.selectedComponentId);
|
||||
},
|
||||
|
||||
formConfig: (state) => {
|
||||
@ -56,6 +58,10 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
...entry,
|
||||
isCurrent: index === state.currentHistoryIndex
|
||||
}));
|
||||
},
|
||||
|
||||
getPreviewFormData: (state) => {
|
||||
return state.previewFormData;
|
||||
}
|
||||
},
|
||||
|
||||
@ -163,6 +169,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
},
|
||||
|
||||
addComponent(component) {
|
||||
// Add debugging
|
||||
console.log('FormStore: Adding component', component.type);
|
||||
|
||||
// Store the state before the change for history
|
||||
const beforeComponents = [...this.formComponents];
|
||||
|
||||
@ -170,16 +179,165 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
const { gridColumn, rowIndex, width } = this.findOptimalGridPlacement();
|
||||
|
||||
const newComponentId = uuidv4();
|
||||
|
||||
// Log the incoming component structure
|
||||
console.log('Component before processing:', JSON.stringify({
|
||||
type: component.type,
|
||||
name: component.name,
|
||||
hasDefaultProps: !!component.defaultProps,
|
||||
defaultPropsKeys: component.defaultProps ? Object.keys(component.defaultProps) : []
|
||||
}));
|
||||
|
||||
try {
|
||||
// Create a deep copy of the default props to avoid reference issues
|
||||
const defaultProps = component.defaultProps ? JSON.parse(JSON.stringify(component.defaultProps)) : {};
|
||||
|
||||
// Ensure the component has necessary grid properties
|
||||
defaultProps.width = defaultProps.width || width;
|
||||
defaultProps.gridColumn = defaultProps.gridColumn || gridColumn;
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// Handle special component types
|
||||
switch (component.type) {
|
||||
case 'image-preview':
|
||||
// Ensure all required image preview properties
|
||||
defaultProps.imageUrl = defaultProps.imageUrl || 'https://placehold.co/600x400';
|
||||
defaultProps.altText = defaultProps.altText || 'Preview image';
|
||||
defaultProps.caption = defaultProps.caption || '';
|
||||
defaultProps.showZoom = defaultProps.showZoom !== undefined ? defaultProps.showZoom : true;
|
||||
defaultProps.showCaption = defaultProps.showCaption !== undefined ? defaultProps.showCaption : true;
|
||||
defaultProps.maxWidth = defaultProps.maxWidth || '100%';
|
||||
defaultProps.height = defaultProps.height || 'auto';
|
||||
break;
|
||||
|
||||
case 'repeating-group':
|
||||
// Ensure all required repeating group properties
|
||||
defaultProps.minItems = defaultProps.minItems !== undefined ? defaultProps.minItems : 1;
|
||||
defaultProps.maxItems = defaultProps.maxItems !== undefined ? defaultProps.maxItems : 10;
|
||||
defaultProps.buttonText = defaultProps.buttonText || 'Add Item';
|
||||
defaultProps.removeText = defaultProps.removeText || 'Remove';
|
||||
defaultProps.fields = defaultProps.fields || [
|
||||
{ type: 'text', name: 'field_1', label: 'Field 1', placeholder: 'Enter value' }
|
||||
];
|
||||
break;
|
||||
|
||||
case 'dynamic-list':
|
||||
// Ensure all required dynamic list properties
|
||||
defaultProps.placeholder = defaultProps.placeholder || 'Enter item';
|
||||
defaultProps.buttonText = defaultProps.buttonText || 'Add Item';
|
||||
defaultProps.minItems = defaultProps.minItems !== undefined ? defaultProps.minItems : 0;
|
||||
defaultProps.maxItems = defaultProps.maxItems !== undefined ? defaultProps.maxItems : 20;
|
||||
defaultProps.defaultItems = Array.isArray(defaultProps.defaultItems) ? defaultProps.defaultItems : ['Item 1', 'Item 2'];
|
||||
break;
|
||||
|
||||
case 'info-display':
|
||||
// Ensure all required info display properties
|
||||
defaultProps.title = defaultProps.title || 'Information';
|
||||
defaultProps.layout = defaultProps.layout || 'vertical';
|
||||
defaultProps.showBorder = defaultProps.showBorder !== undefined ? defaultProps.showBorder : true;
|
||||
defaultProps.backgroundColor = defaultProps.backgroundColor || '#f8fafc';
|
||||
defaultProps.fields = Array.isArray(defaultProps.fields) ? defaultProps.fields : [
|
||||
{ label: 'Info Item', value: 'Value', key: 'item_1' }
|
||||
];
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
// Ensure all required file upload properties
|
||||
defaultProps.accept = defaultProps.accept || '.pdf,.doc,.docx,.jpg,.jpeg,.png';
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Ensure all required heading properties
|
||||
defaultProps.value = defaultProps.value || 'Heading';
|
||||
defaultProps.level = defaultProps.level || 2;
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
// Ensure all required paragraph properties
|
||||
defaultProps.value = defaultProps.value || 'Paragraph text';
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
// Ensure options array exists
|
||||
if (!Array.isArray(defaultProps.options) || defaultProps.options.length === 0) {
|
||||
defaultProps.options = [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' }
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
// Ensure all required range properties
|
||||
defaultProps.min = defaultProps.min !== undefined ? defaultProps.min : 0;
|
||||
defaultProps.max = defaultProps.max !== undefined ? defaultProps.max : 100;
|
||||
defaultProps.step = defaultProps.step !== undefined ? defaultProps.step : 1;
|
||||
defaultProps.value = defaultProps.value !== undefined ? defaultProps.value : 50;
|
||||
break;
|
||||
|
||||
case 'color':
|
||||
// Ensure color has a default value
|
||||
defaultProps.value = defaultProps.value || '#3b82f6';
|
||||
break;
|
||||
|
||||
case 'switch':
|
||||
// Ensure switch has a default value
|
||||
defaultProps.value = defaultProps.value !== undefined ? defaultProps.value : false;
|
||||
break;
|
||||
|
||||
case 'hidden':
|
||||
// Ensure hidden field has a value
|
||||
defaultProps.value = defaultProps.value || '';
|
||||
break;
|
||||
|
||||
case 'button':
|
||||
// Ensure all required button properties
|
||||
defaultProps.buttonType = defaultProps.buttonType || 'button';
|
||||
defaultProps.variant = defaultProps.variant || 'primary';
|
||||
defaultProps.size = defaultProps.size || 'md';
|
||||
defaultProps.disabled = defaultProps.disabled !== undefined ? defaultProps.disabled : false;
|
||||
defaultProps.onClick = defaultProps.onClick || '';
|
||||
break;
|
||||
|
||||
case 'mask':
|
||||
// Ensure all required mask properties
|
||||
defaultProps.mask = defaultProps.mask || '###-###-####';
|
||||
break;
|
||||
|
||||
case 'otp':
|
||||
// Ensure all required OTP properties
|
||||
defaultProps.digits = defaultProps.digits !== undefined ? defaultProps.digits : 6;
|
||||
break;
|
||||
|
||||
case 'dropzone':
|
||||
// Ensure all required dropzone properties
|
||||
defaultProps.accept = defaultProps.accept || 'image/*,.pdf,.doc,.docx';
|
||||
defaultProps.multiple = defaultProps.multiple !== undefined ? defaultProps.multiple : true;
|
||||
defaultProps.maxSize = defaultProps.maxSize !== undefined ? defaultProps.maxSize : 5242880; // 5MB
|
||||
defaultProps.maxFiles = defaultProps.maxFiles !== undefined ? defaultProps.maxFiles : 5;
|
||||
break;
|
||||
|
||||
case 'switch':
|
||||
// Ensure switch has a default value
|
||||
defaultProps.value = defaultProps.value !== undefined ? defaultProps.value : false;
|
||||
break;
|
||||
}
|
||||
|
||||
const newComponent = {
|
||||
...component,
|
||||
id: newComponentId,
|
||||
props: {
|
||||
...component.defaultProps,
|
||||
name: `${component.type}_${this.formComponents.length + 1}`,
|
||||
label: `${component.name} ${this.formComponents.length + 1}`,
|
||||
width: width,
|
||||
gridColumn: gridColumn
|
||||
}
|
||||
props: defaultProps
|
||||
};
|
||||
|
||||
this.formComponents.push(newComponent);
|
||||
@ -191,13 +349,19 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
this.recordHistory('add_component', {
|
||||
componentType: component.type,
|
||||
componentId: newComponentId,
|
||||
componentName: newComponent.props.label,
|
||||
componentName: newComponent.props.label || newComponent.type,
|
||||
beforeState: {
|
||||
components: beforeComponents,
|
||||
selectedComponentId: null // Was null before adding
|
||||
},
|
||||
newComponent: newComponent
|
||||
});
|
||||
|
||||
console.log('Component successfully added:', newComponent.type, newComponent.id);
|
||||
} catch (error) {
|
||||
console.error('Error adding component:', error);
|
||||
console.error('Problematic component:', component);
|
||||
}
|
||||
},
|
||||
|
||||
// Find optimal placement for a new component in the grid
|
||||
@ -426,6 +590,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
this.hasUnsavedChanges = false;
|
||||
},
|
||||
|
||||
// Update preview form data
|
||||
updatePreviewFormData(data) {
|
||||
this.previewFormData = { ...data };
|
||||
},
|
||||
|
||||
// Get forms from the backend
|
||||
async getForms() {
|
||||
try {
|
||||
|
Loading…
x
Reference in New Issue
Block a user