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"
|
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||||
:readonly="component.props.readonly || isPreview"
|
: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"
|
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
||||||
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||||
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
||||||
:maxFiles="component.props.maxFiles || undefined" :classes="component.type === 'checkbox' ? {
|
:maxFiles="component.props.maxFiles || undefined" :classes="component.type === 'checkbox' ? {
|
||||||
wrapper: 'mb-1',
|
wrapper: 'mb-1',
|
||||||
options: 'space-y-0.5'
|
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 -->
|
<!-- Heading -->
|
||||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||||
@ -325,14 +331,15 @@
|
|||||||
|
|
||||||
<!-- Button Component -->
|
<!-- Button Component -->
|
||||||
<div v-else-if="component.type === 'button'" class="py-2">
|
<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 }}
|
{{ component.props.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
||||||
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
||||||
class="button-component">
|
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>
|
</RsButton>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||||
@ -394,7 +401,7 @@
|
|||||||
'no-header': !component.props.showHeader
|
'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">
|
<div v-if="isPreview" class="section-fields">
|
||||||
<!-- Render nested components if they exist -->
|
<!-- Render nested components if they exist -->
|
||||||
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
||||||
@ -402,17 +409,7 @@
|
|||||||
<component-preview :component="childComponent" :is-preview="true" />
|
<component-preview :component="childComponent" :is-preview="true" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- No placeholder in preview mode when empty -->
|
||||||
<!-- 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- In form builder mode, show drop zone -->
|
<!-- In form builder mode, show drop zone -->
|
||||||
@ -477,14 +474,15 @@
|
|||||||
class="section-drop-placeholder"
|
class="section-drop-placeholder"
|
||||||
:class="{
|
:class="{
|
||||||
'drop-active': sectionDropStates[component.id]?.isDraggingOver,
|
'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)"
|
@dragover.prevent="handleSectionDragOver($event, component.id)"
|
||||||
@dragleave="handleSectionDragLeave($event, component.id)"
|
@dragleave="handleSectionDragLeave($event, component.id)"
|
||||||
@drop="handleSectionDrop($event, component.id)"
|
@drop="handleSectionDrop($event, component.id)"
|
||||||
@dragenter.prevent="handleSectionDragEnter($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" />
|
<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-sm text-gray-500 text-center mb-1">Drop Components Here</p>
|
||||||
<p class="text-xs text-gray-400 text-center">
|
<p class="text-xs text-gray-400 text-center">
|
||||||
@ -1271,6 +1269,32 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
|||||||
pointer-events: none;
|
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 Component */
|
||||||
.image-preview-container {
|
.image-preview-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1485,6 +1509,13 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
|||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-drop-placeholder.hidden-placeholder {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ghost styles for dragging */
|
/* Ghost styles for dragging */
|
||||||
.ghost {
|
.ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -598,6 +598,10 @@ const availableComponents = [
|
|||||||
variant: 'primary', // primary, secondary, success, warning, danger
|
variant: 'primary', // primary, secondary, success, warning, danger
|
||||||
size: 'md', // sm, md, lg
|
size: 'md', // sm, md, lg
|
||||||
disabled: false,
|
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
|
onClick: '' // Custom JavaScript code to execute
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -623,6 +627,7 @@ const availableComponents = [
|
|||||||
spacing: 'normal', // compact, normal, relaxed
|
spacing: 'normal', // compact, normal, relaxed
|
||||||
width: '100%',
|
width: '100%',
|
||||||
gridColumn: 'span 12',
|
gridColumn: 'span 12',
|
||||||
|
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
||||||
children: [], // Array to hold nested components
|
children: [], // Array to hold nested components
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
|
@ -497,19 +497,113 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<FormKit
|
||||||
type="select"
|
v-if="configModel.showButtonText !== false"
|
||||||
label="Button Size"
|
type="text"
|
||||||
name="size"
|
label="Button Text"
|
||||||
v-model="configModel.size"
|
name="buttonText"
|
||||||
:options="[
|
v-model="configModel.buttonText"
|
||||||
{ label: 'Small', value: 'sm' },
|
help="Text displayed on the button (uses label if empty)"
|
||||||
{ label: 'Medium', value: 'md' },
|
|
||||||
{ label: 'Large', value: 'lg' }
|
|
||||||
]"
|
|
||||||
help="Size of the button"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
: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>
|
</template>
|
||||||
|
|
||||||
<!-- Dynamic List Configuration -->
|
<!-- Dynamic List Configuration -->
|
||||||
@ -902,6 +996,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader"
|
||||||
type="select"
|
type="select"
|
||||||
label="Header Size"
|
label="Header Size"
|
||||||
name="headerSize"
|
name="headerSize"
|
||||||
@ -916,6 +1011,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader"
|
||||||
type="switch"
|
type="switch"
|
||||||
label="Collapsible"
|
label="Collapsible"
|
||||||
name="collapsible"
|
name="collapsible"
|
||||||
@ -925,13 +1021,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader && configModel.collapsible"
|
||||||
type="switch"
|
type="switch"
|
||||||
label="Start Collapsed"
|
label="Start Collapsed"
|
||||||
name="collapsed"
|
name="collapsed"
|
||||||
v-model="configModel.collapsed"
|
v-model="configModel.collapsed"
|
||||||
help="Start with this section collapsed"
|
help="Start with this section collapsed"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
: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>
|
||||||
</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 = () => {
|
const handleClose = () => {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
emit('close')
|
emit('close')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user