Enhance Component Preview and Form Builder Functionality
- Updated ComponentPreview.vue to improve handling of readonly states for select, checkbox, and radio components, ensuring proper styling and interaction. - Modified button component in ComponentPreview.vue to conditionally display button text and icon based on new props for better customization. - Enhanced FormBuilderComponents.vue by adding new properties for button configuration, including showLabel, showButtonText, buttonText, and icon. - Introduced new form field settings in FormBuilderFieldSettingsModal.vue to allow users to customize button size, icon, and visibility options for labels and text. - Improved overall user experience by refining placeholder visibility in builder mode and enhancing the button action script template functionality.
This commit is contained in:
parent
bae98c2b17
commit
707d8fe3b0
@ -27,14 +27,20 @@
|
||||
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||
:readonly="component.props.readonly || isPreview"
|
||||
:disabled="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
|
||||
:disabled="isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
|
||||
:options="component.props.options || undefined" :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" :classes="component.type === 'checkbox' ? {
|
||||
wrapper: 'mb-1',
|
||||
options: 'space-y-0.5'
|
||||
} : {}" :class="{ 'canvas-component': isPreview }" />
|
||||
} : {}" :class="{
|
||||
'canvas-component': isPreview,
|
||||
'readonly-select': component.props.readonly && component.type === 'select',
|
||||
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
|
||||
'readonly-radio': component.props.readonly && component.type === 'radio'
|
||||
}" />
|
||||
|
||||
<!-- Heading -->
|
||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||
@ -325,14 +331,15 @@
|
||||
|
||||
<!-- 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">
|
||||
<label v-if="component.props.label && component.props.showLabel !== false" 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' }}
|
||||
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
|
||||
</RsButton>
|
||||
|
||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||
@ -394,7 +401,7 @@
|
||||
'no-header': !component.props.showHeader
|
||||
}"
|
||||
>
|
||||
<!-- In preview mode, show the nested components or placeholder -->
|
||||
<!-- In preview mode, show only the nested components (no placeholder) -->
|
||||
<div v-if="isPreview" class="section-fields">
|
||||
<!-- Render nested components if they exist -->
|
||||
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
||||
@ -402,17 +409,7 @@
|
||||
<component-preview :component="childComponent" :is-preview="true" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Show placeholder if no nested components -->
|
||||
<div v-else class="section-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<Icon name="material-symbols:dashboard-customize-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
||||
<p class="text-sm text-gray-500 text-center mb-2">Form Section Container</p>
|
||||
<p class="text-xs text-gray-400 text-center">
|
||||
This section will contain the form fields grouped here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- No placeholder in preview mode when empty -->
|
||||
</div>
|
||||
|
||||
<!-- In form builder mode, show drop zone -->
|
||||
@ -477,14 +474,15 @@
|
||||
class="section-drop-placeholder"
|
||||
:class="{
|
||||
'drop-active': sectionDropStates[component.id]?.isDraggingOver,
|
||||
'empty': !component.props.children || component.props.children.length === 0
|
||||
'empty': !component.props.children || component.props.children.length === 0,
|
||||
'hidden-placeholder': component.props.showPlaceholder === false
|
||||
}"
|
||||
@dragover.prevent="handleSectionDragOver($event, component.id)"
|
||||
@dragleave="handleSectionDragLeave($event, component.id)"
|
||||
@drop="handleSectionDrop($event, component.id)"
|
||||
@dragenter.prevent="handleSectionDragEnter($event, component.id)"
|
||||
>
|
||||
<div class="placeholder-content">
|
||||
<div v-if="component.props.showPlaceholder !== false" class="placeholder-content">
|
||||
<Icon name="material-symbols:add-box-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
||||
<p class="text-sm text-gray-500 text-center mb-1">Drop Components Here</p>
|
||||
<p class="text-xs text-gray-400 text-center">
|
||||
@ -1271,6 +1269,32 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Readonly styles for select, checkbox, and radio components */
|
||||
:deep(.readonly-select),
|
||||
:deep(.readonly-checkbox),
|
||||
:deep(.readonly-radio) {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
:deep(.readonly-select select),
|
||||
:deep(.readonly-select .formkit-inner),
|
||||
:deep(.readonly-checkbox input[type="checkbox"]),
|
||||
:deep(.readonly-radio input[type="radio"]) {
|
||||
pointer-events: none !important;
|
||||
opacity: 0.8 !important;
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
:deep(.readonly-checkbox) .formkit-options,
|
||||
:deep(.readonly-radio) .formkit-options {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
:deep(.readonly-checkbox) .formkit-wrapper,
|
||||
:deep(.readonly-radio) .formkit-wrapper {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* Image Preview Component */
|
||||
.image-preview-container {
|
||||
display: flex;
|
||||
@ -1485,6 +1509,13 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.section-drop-placeholder.hidden-placeholder {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
/* Ghost styles for dragging */
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
|
@ -598,6 +598,10 @@ const availableComponents = [
|
||||
variant: 'primary', // primary, secondary, success, warning, danger
|
||||
size: 'md', // sm, md, lg
|
||||
disabled: false,
|
||||
showLabel: true, // Whether to show the label above the button
|
||||
showButtonText: true, // Whether to show text on the button
|
||||
buttonText: '', // Text to display on button (falls back to label if empty)
|
||||
icon: '', // Optional icon to display on button
|
||||
onClick: '' // Custom JavaScript code to execute
|
||||
}
|
||||
},
|
||||
@ -623,6 +627,7 @@ const availableComponents = [
|
||||
spacing: 'normal', // compact, normal, relaxed
|
||||
width: '100%',
|
||||
gridColumn: 'span 12',
|
||||
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
||||
children: [], // Array to hold nested components
|
||||
// Conditional Logic Properties
|
||||
conditionalLogic: {
|
||||
|
@ -497,19 +497,113 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Button Size"
|
||||
name="size"
|
||||
v-model="configModel.size"
|
||||
:options="[
|
||||
{ label: 'Small', value: 'sm' },
|
||||
{ label: 'Medium', value: 'md' },
|
||||
{ label: 'Large', value: 'lg' }
|
||||
]"
|
||||
help="Size of the button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Icon (Optional)"
|
||||
name="icon"
|
||||
v-model="configModel.icon"
|
||||
help="Icon name (e.g., 'material-symbols:add')"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="material-symbols:add"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Show Label Above Button"
|
||||
name="showLabel"
|
||||
v-model="configModel.showLabel"
|
||||
help="Display the label text above the button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Show Text on Button"
|
||||
name="showButtonText"
|
||||
v-model="configModel.showButtonText"
|
||||
help="Display text on the button (icon-only if disabled)"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Button Size"
|
||||
name="size"
|
||||
v-model="configModel.size"
|
||||
:options="[
|
||||
{ label: 'Small', value: 'sm' },
|
||||
{ label: 'Medium', value: 'md' },
|
||||
{ label: 'Large', value: 'lg' }
|
||||
]"
|
||||
help="Size of the button"
|
||||
v-if="configModel.showButtonText !== false"
|
||||
type="text"
|
||||
label="Button Text"
|
||||
name="buttonText"
|
||||
v-model="configModel.buttonText"
|
||||
help="Text displayed on the button (uses label if empty)"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="e.g., Submit, Save, Continue"
|
||||
/>
|
||||
|
||||
<!-- Button Action Script -->
|
||||
<div class="mt-6 space-y-3">
|
||||
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Button Click Action</h5>
|
||||
<p class="text-xs text-gray-600">
|
||||
Write JavaScript code to execute when this button is clicked. You can access form data and perform actions.
|
||||
</p>
|
||||
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<div class="mb-2 text-xs text-gray-600">
|
||||
<strong>Available functions:</strong> getField(), setField(), showField(), hideField(), enableField(), disableField(), showSuccess(), showError(), showInfo()
|
||||
</div>
|
||||
<textarea
|
||||
v-model="configModel.onClick"
|
||||
class="w-full h-32 font-mono text-sm p-2 border border-gray-300 rounded"
|
||||
placeholder="// Example:
|
||||
// Get values from other fields
|
||||
const name = getField('name');
|
||||
const email = getField('email');
|
||||
|
||||
// Show a message
|
||||
if (name && email) {
|
||||
showSuccess('Form data is valid!');
|
||||
} else {
|
||||
showError('Please fill in all required fields');
|
||||
}"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Quick Templates -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="insertButtonScriptTemplate('validation')"
|
||||
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||
>
|
||||
Insert Validation Example
|
||||
</button>
|
||||
<button
|
||||
@click="insertButtonScriptTemplate('calculation')"
|
||||
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||
>
|
||||
Insert Calculation Example
|
||||
</button>
|
||||
<button
|
||||
@click="insertButtonScriptTemplate('toggle')"
|
||||
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||
>
|
||||
Insert Toggle Fields Example
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Dynamic List Configuration -->
|
||||
@ -902,6 +996,7 @@
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="configModel.showHeader"
|
||||
type="select"
|
||||
label="Header Size"
|
||||
name="headerSize"
|
||||
@ -916,6 +1011,7 @@
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="configModel.showHeader"
|
||||
type="switch"
|
||||
label="Collapsible"
|
||||
name="collapsible"
|
||||
@ -925,13 +1021,22 @@
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="configModel.showHeader && configModel.collapsible"
|
||||
type="switch"
|
||||
label="Start Collapsed"
|
||||
name="collapsed"
|
||||
v-model="configModel.collapsed"
|
||||
help="Start with this section collapsed"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
:disabled="!configModel.collapsible"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Show Placeholder"
|
||||
name="showPlaceholder"
|
||||
v-model="configModel.showPlaceholder"
|
||||
help="Show placeholder when empty in builder mode"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -2162,6 +2267,80 @@ const handleReset = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Button script template helper
|
||||
const insertButtonScriptTemplate = (templateType) => {
|
||||
let template = '';
|
||||
|
||||
switch (templateType) {
|
||||
case 'validation':
|
||||
template = `// Form validation example
|
||||
const name = getField('name');
|
||||
const email = getField('email');
|
||||
const phone = getField('phone');
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !email) {
|
||||
showError('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if (email && !email.includes('@')) {
|
||||
showError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
// Success message
|
||||
showSuccess('Form validation passed!');
|
||||
`;
|
||||
break;
|
||||
case 'calculation':
|
||||
template = `// Calculation example
|
||||
const quantity = Number(getField('quantity') || 0);
|
||||
const price = Number(getField('price') || 0);
|
||||
const taxRate = 0.08; // 8% tax
|
||||
|
||||
// Calculate subtotal
|
||||
const subtotal = quantity * price;
|
||||
setField('subtotal', subtotal.toFixed(2));
|
||||
|
||||
// Calculate tax
|
||||
const tax = subtotal * taxRate;
|
||||
setField('tax', tax.toFixed(2));
|
||||
|
||||
// Calculate total
|
||||
const total = subtotal + tax;
|
||||
setField('total', total.toFixed(2));
|
||||
|
||||
showInfo('Calculation completed');
|
||||
`;
|
||||
break;
|
||||
case 'toggle':
|
||||
template = `// Toggle fields visibility example
|
||||
const selectedOption = getField('options');
|
||||
|
||||
// Hide all detail fields first
|
||||
hideField('details_option_1');
|
||||
hideField('details_option_2');
|
||||
hideField('details_option_3');
|
||||
|
||||
// Show only the relevant detail field
|
||||
if (selectedOption === 'option_1') {
|
||||
showField('details_option_1');
|
||||
} else if (selectedOption === 'option_2') {
|
||||
showField('details_option_2');
|
||||
} else if (selectedOption === 'option_3') {
|
||||
showField('details_option_3');
|
||||
}
|
||||
`;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
configModel.value.onClick = template;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
isOpen.value = false
|
||||
emit('close')
|
||||
|
Loading…
x
Reference in New Issue
Block a user