- Updated ComponentPreview.vue to initialize default values for standard input fields based on the component's defaultValue property, improving user experience during form previews. - Enhanced FormBuilderFieldSettingsModal.vue by introducing a Default Value field, allowing users to set initial values for various input types, with appropriate help texts and placeholders for better guidance. - Implemented helper functions to determine the visibility and type of the Default Value field based on the component type, ensuring consistent behavior across supported input types.
5094 lines
192 KiB
Vue
5094 lines
192 KiB
Vue
<template>
|
|
<RsModal
|
|
v-model="isOpen"
|
|
:title="modalTitle"
|
|
size="xl"
|
|
position="center"
|
|
@close="handleClose"
|
|
>
|
|
<template #body>
|
|
<div v-if="component" class="field-settings-modal">
|
|
<!-- Component Info Header -->
|
|
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex-shrink-0">
|
|
<Icon :name="getComponentIcon(component.type)" class="w-8 h-8 text-blue-600" />
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="text-lg font-medium text-gray-900">
|
|
{{ getComponentTypeName(component.type) }}
|
|
</h3>
|
|
<p class="text-sm text-gray-600">
|
|
{{ getComponentDescription(component.type) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Tabs -->
|
|
<div class="border-b border-gray-200 mb-6">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<button
|
|
v-for="tab in availableTabs"
|
|
:key="tab.id"
|
|
@click="activeTab = tab.id"
|
|
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap"
|
|
:class="activeTab === tab.id
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
>
|
|
<Icon :name="tab.icon" class="w-4 h-4 inline mr-2" />
|
|
{{ tab.label }}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content">
|
|
<!-- Basic Settings Tab -->
|
|
<div v-if="activeTab === 'basic'" class="space-y-6">
|
|
<!-- Essential Settings Section -->
|
|
<div class="settings-section">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:identification" class="w-5 h-5 mr-2" />
|
|
Essential Settings
|
|
</h4>
|
|
<p class="section-description">Basic information and identification for this field</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<!-- Field Type Changer -->
|
|
<div v-if="availableTypeChanges.length > 0" class="type-changer-section">
|
|
<FormKit
|
|
type="select"
|
|
label="Field Type"
|
|
name="fieldType"
|
|
:modelValue="component.type"
|
|
@input="handleTypeChange"
|
|
:options="[
|
|
{ label: getComponentTypeName(component.type), value: component.type, disabled: true },
|
|
...availableTypeChanges.map(type => ({
|
|
label: `Change to ${getComponentTypeName(type)}`,
|
|
value: type
|
|
}))
|
|
]"
|
|
help="Change this field to a similar compatible type"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Change Preview -->
|
|
<div v-if="showTypeChangePreview" class="type-change-preview">
|
|
<div class="preview-header">
|
|
<Icon name="heroicons:arrow-right" class="w-4 h-4 text-blue-600" />
|
|
<span class="text-sm font-medium text-gray-700">Preview Change</span>
|
|
</div>
|
|
<div class="preview-content">
|
|
<div class="preview-item">
|
|
<span class="preview-label">From:</span>
|
|
<span class="preview-value">{{ getComponentTypeName(component.type) }}</span>
|
|
</div>
|
|
<div class="preview-item">
|
|
<span class="preview-label">To:</span>
|
|
<span class="preview-value">{{ getComponentTypeName(pendingTypeChange) }}</span>
|
|
</div>
|
|
<div class="preview-item">
|
|
<span class="preview-label">Preserved:</span>
|
|
<span class="preview-value">{{ getPreservedProperties(component.type, pendingTypeChange).join(', ') }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="preview-actions">
|
|
<RsButton @click="confirmTypeChange" variant="primary" size="sm">
|
|
<Icon name="heroicons:check" class="w-4 h-4 mr-1" />
|
|
Confirm Change
|
|
</RsButton>
|
|
<RsButton @click="cancelTypeChange" variant="secondary" size="sm">
|
|
<Icon name="heroicons:x-mark" class="w-4 h-4 mr-1" />
|
|
Cancel
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Heading-specific Essential Settings -->
|
|
<template v-if="component.type === 'heading'">
|
|
<FormKit
|
|
type="text"
|
|
label="Heading Text"
|
|
name="value"
|
|
v-model="configModel.value"
|
|
help="The heading text that will be displayed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Personal Information, Contact Details"
|
|
validation="required"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Heading Level"
|
|
name="level"
|
|
v-model="configModel.level"
|
|
:options="[
|
|
{ label: 'Heading 1 (Largest)', value: 1 },
|
|
{ label: 'Heading 2 (Large)', value: 2 },
|
|
{ label: 'Heading 3 (Medium)', value: 3 },
|
|
{ label: 'Heading 4 (Small)', value: 4 },
|
|
{ label: 'Heading 5 (Smaller)', value: 5 },
|
|
{ label: 'Heading 6 (Smallest)', value: 6 }
|
|
]"
|
|
help="Choose the size and importance of this heading"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Paragraph-specific Essential Settings -->
|
|
<template v-else-if="component.type === 'paragraph'">
|
|
<FormKit
|
|
type="textarea"
|
|
label="Paragraph Text"
|
|
name="value"
|
|
v-model="configModel.value"
|
|
help="The text content that will be displayed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Enter your paragraph text here..."
|
|
validation="required"
|
|
rows="4"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Info Display-specific Essential Settings -->
|
|
<template v-else-if="component.type === 'info-display'">
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Component Title"
|
|
name="title"
|
|
v-model="configModel.title"
|
|
help="Title displayed at the top of the information display"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., User Information, Contact Details"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Component Name (Internal)"
|
|
name="name"
|
|
v-model="configModel.name"
|
|
help="Used internally to identify this component"
|
|
validation="required|alpha_numeric"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., user_info, contact_details"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Component Label"
|
|
name="label"
|
|
v-model="configModel.label"
|
|
help="Label displayed above the component (optional)"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., User Information Section"
|
|
/>
|
|
|
|
<FormKit
|
|
type="textarea"
|
|
label="Help Text"
|
|
name="help"
|
|
v-model="configModel.help"
|
|
help="Additional information or instructions for users"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., This section displays your profile information"
|
|
rows="2"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Layout Style"
|
|
name="layout"
|
|
v-model="configModel.layout"
|
|
:options="[
|
|
{ label: 'Vertical (Label above value)', value: 'vertical' },
|
|
{ label: 'Horizontal (Label: Value)', value: 'horizontal' },
|
|
{ label: 'Grid (2 columns)', value: 'grid' },
|
|
{ label: 'Side by Side (2 columns)', value: 'side-by-side' }
|
|
]"
|
|
help="How to display the information fields"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Standard form field Essential Settings -->
|
|
<template v-else>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-if="showField('label') && component.type !== 'button'"
|
|
type="text"
|
|
label="Field Label"
|
|
name="label"
|
|
v-model="configModel.label"
|
|
help="What users will see above this field"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Full Name, Email Address"
|
|
/>
|
|
|
|
<!-- Button Text (instead of label for buttons) -->
|
|
<FormKit
|
|
v-if="component.type === 'button'"
|
|
type="text"
|
|
label="Button Text"
|
|
name="label"
|
|
v-model="configModel.label"
|
|
help="Text displayed on the button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Submit, Save, Continue"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="showField('name')"
|
|
type="text"
|
|
label="Field Name (Internal)"
|
|
name="name"
|
|
v-model="configModel.name"
|
|
help="Used internally to identify this field"
|
|
validation="required|alpha_numeric"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., full_name, email_address"
|
|
/>
|
|
</div>
|
|
|
|
<FormKit
|
|
v-if="showField('placeholder')"
|
|
type="text"
|
|
label="Placeholder Text"
|
|
name="placeholder"
|
|
v-model="configModel.placeholder"
|
|
help="Hint text shown inside the empty field"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., Enter your email address..."
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="showField('help')"
|
|
type="textarea"
|
|
label="Help Text"
|
|
name="help"
|
|
v-model="configModel.help"
|
|
help="Additional instructions or guidance for users"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., We'll use this to send you updates about your order"
|
|
rows="2"
|
|
/>
|
|
|
|
<!-- Default Value Field -->
|
|
<FormKit
|
|
v-if="showDefaultValueField()"
|
|
:type="getDefaultValueFieldType()"
|
|
label="Default Value"
|
|
name="defaultValue"
|
|
v-model="configModel.defaultValue"
|
|
:help="getDefaultValueHelp()"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:placeholder="getDefaultValuePlaceholder()"
|
|
:options="component.type === 'select' || component.type === 'radio' || component.type === 'checkbox' ? configModel.options : undefined"
|
|
:rows="component.type === 'textarea' ? 2 : undefined"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="showField('rows')"
|
|
type="number"
|
|
label="Number of Rows"
|
|
name="rows"
|
|
v-model="configModel.rows"
|
|
help="Number of visible text lines in the textarea"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="3"
|
|
min="1"
|
|
max="20"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="showField('readonly')"
|
|
type="switch"
|
|
label="Read Only"
|
|
name="readonly"
|
|
v-model="configModel.readonly"
|
|
help="Users cannot edit this field value"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Component-Specific Settings -->
|
|
<div v-if="hasSpecificSettings" class="settings-section">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:cog-6-tooth" class="w-5 h-5 mr-2" />
|
|
{{ getComponentTypeName(component.type) }} Settings
|
|
</h4>
|
|
<p class="section-description">Settings specific to this type of field</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<!-- Text Input Specific -->
|
|
<template v-if="component.type === 'mask'">
|
|
<FormKit
|
|
type="text"
|
|
label="Input Mask Pattern"
|
|
name="mask"
|
|
v-model="configModel.mask"
|
|
help="Pattern for formatting input (e.g., ###-###-#### for phone)"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="###-###-####"
|
|
/>
|
|
<div class="mask-examples">
|
|
<p class="text-sm font-medium text-gray-700 mb-2">Common patterns:</p>
|
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
<RsButton @click="configModel.mask = '###-###-####'" variant="secondary-outline" size="sm">
|
|
Phone: ###-###-####
|
|
</RsButton>
|
|
<RsButton @click="configModel.mask = '##/##/####'" variant="secondary-outline" size="sm">
|
|
Date: ##/##/####
|
|
</RsButton>
|
|
<RsButton @click="configModel.mask = 'AA-####'" variant="secondary-outline" size="sm">
|
|
Code: AA-####
|
|
</RsButton>
|
|
<RsButton @click="configModel.mask = '#### #### #### ####'" variant="secondary-outline" size="sm">
|
|
Credit Card: #### #### #### ####
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- OTP Specific -->
|
|
<template v-if="component.type === 'otp'">
|
|
<FormKit
|
|
type="range"
|
|
label="Number of Digits"
|
|
name="digits"
|
|
v-model="configModel.digits"
|
|
help="How many digits for the verification code"
|
|
:min="4"
|
|
:max="8"
|
|
:step="1"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
<div class="text-center mt-2">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
{{ configModel.digits || 6 }} digits
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- File Upload Specific -->
|
|
<template v-if="component.type === 'dropzone'">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Accepted File Types"
|
|
name="accept"
|
|
v-model="configModel.accept"
|
|
help="File types allowed (e.g., image/*, .pdf)"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="image/*,.pdf,.doc"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Max File Size (MB)"
|
|
name="maxSize"
|
|
v-model="configModel.maxSizeMB"
|
|
help="Maximum size per file in megabytes"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="5"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Max Number of Files"
|
|
name="maxFiles"
|
|
v-model="configModel.maxFiles"
|
|
help="Maximum number of files allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="1"
|
|
placeholder="5"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Allow Multiple Files"
|
|
name="multiple"
|
|
v-model="configModel.multiple"
|
|
help="Enable uploading multiple files at once"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Range Slider Specific -->
|
|
<template v-if="component.type === 'range'">
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Value"
|
|
name="min"
|
|
v-model="configModel.min"
|
|
help="Lowest value allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Value"
|
|
name="max"
|
|
v-model="configModel.max"
|
|
help="Highest value allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Step Size"
|
|
name="step"
|
|
v-model="configModel.step"
|
|
help="Increment between values"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0.01"
|
|
:step="0.01"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
|
|
|
|
<!-- Button Configuration -->
|
|
<template v-if="component.type === 'button'">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="select"
|
|
label="Button Type"
|
|
name="buttonType"
|
|
v-model="configModel.buttonType"
|
|
:options="[
|
|
{ label: '🔵 Button (Regular)', value: 'button' },
|
|
{ label: '✅ Submit (Form Submit)', value: 'submit' },
|
|
{ label: '🔄 Reset (Clear Form)', value: 'reset' }
|
|
]"
|
|
help="Choose the button behavior"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Button Style"
|
|
name="variant"
|
|
v-model="configModel.variant"
|
|
:options="[
|
|
{ label: 'Primary (Blue)', value: 'primary' },
|
|
{ label: 'Secondary (Gray)', value: 'secondary' },
|
|
{ label: 'Success (Green)', value: 'success' },
|
|
{ label: 'Danger (Red)', value: 'danger' },
|
|
{ label: 'Warning (Orange)', value: 'warning' },
|
|
{ label: 'Custom Color', value: 'custom' }
|
|
]"
|
|
help="Visual appearance of the button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Custom Color Picker -->
|
|
<div v-if="configModel.variant === 'custom'" class="space-y-3">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Custom Color Settings</h5>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="color"
|
|
label="Background Color"
|
|
name="customBackgroundColor"
|
|
v-model="configModel.customBackgroundColor"
|
|
help="Choose the button background color"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="color"
|
|
label="Text Color"
|
|
name="customTextColor"
|
|
v-model="configModel.customTextColor"
|
|
help="Choose the button text color"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="color"
|
|
label="Border Color"
|
|
name="customBorderColor"
|
|
v-model="configModel.customBorderColor"
|
|
help="Choose the button border color"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Border Width (px)"
|
|
name="customBorderWidth"
|
|
v-model="configModel.customBorderWidth"
|
|
help="Set the border width in pixels"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0"
|
|
:max="10"
|
|
placeholder="2"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Border Radius (px)"
|
|
name="customBorderRadius"
|
|
v-model="configModel.customBorderRadius"
|
|
help="Set the corner roundness in pixels"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0"
|
|
:max="50"
|
|
placeholder="6"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Hover Effect"
|
|
name="customHoverEffect"
|
|
v-model="configModel.customHoverEffect"
|
|
:options="[
|
|
{ label: 'None', value: 'none' },
|
|
{ label: 'Darken', value: 'darken' },
|
|
{ label: 'Lighten', value: 'lighten' },
|
|
{ label: 'Scale', value: 'scale' },
|
|
{ label: 'Glow', value: 'glow' }
|
|
]"
|
|
help="Choose hover animation effect"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Color Preview -->
|
|
<div class="mt-4 p-3 bg-gray-50 border border-gray-200 rounded-lg">
|
|
<h6 class="text-sm font-medium text-gray-700 mb-2">Color Preview</h6>
|
|
<div class="flex items-center space-x-3">
|
|
<button
|
|
class="px-4 py-2 rounded font-medium transition-all duration-200"
|
|
:style="getCustomButtonStyles()"
|
|
disabled
|
|
>
|
|
{{ configModel.buttonText || configModel.label || 'Button Preview' }}
|
|
</button>
|
|
<div class="text-xs text-gray-600">
|
|
<div>Background: {{ configModel.customBackgroundColor || '#3b82f6' }}</div>
|
|
<div>Text: {{ configModel.customTextColor || '#ffffff' }}</div>
|
|
<div>Border: {{ configModel.customBorderColor || 'transparent' }}</div>
|
|
</div>
|
|
</div>
|
|
</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' }"
|
|
/>
|
|
|
|
<div class="field-wrapper">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Icon (Optional)</label>
|
|
<IconBrowser
|
|
v-model="configModel.icon"
|
|
size="compact"
|
|
:show-preview="false"
|
|
placeholder="Search Material Design icons..."
|
|
/>
|
|
<div class="text-xs text-gray-600 mt-1">
|
|
Icon displayed on the button
|
|
</div>
|
|
</div>
|
|
</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
|
|
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 Link Settings -->
|
|
<div class="mt-6 space-y-3">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Button Link Settings</h5>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Link Type"
|
|
name="linkType"
|
|
v-model="configModel.linkType"
|
|
:options="[
|
|
{ label: 'No Link', value: 'none' },
|
|
{ label: 'Custom URL', value: 'url' },
|
|
{ label: 'Process Link', value: 'process' }
|
|
]"
|
|
help="Choose how the button should behave when clicked"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Custom URL Link -->
|
|
<div v-if="configModel.linkType === 'url'" class="space-y-3">
|
|
<FormKit
|
|
type="text"
|
|
label="URL"
|
|
name="linkUrl"
|
|
v-model="configModel.linkUrl"
|
|
help="Enter the URL to navigate to when button is clicked"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="https://example.com"
|
|
validation="url"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Open In"
|
|
name="linkTarget"
|
|
v-model="configModel.linkTarget"
|
|
:options="[
|
|
{ label: 'Same Window', value: '_self' },
|
|
{ label: 'New Window/Tab', value: '_blank' }
|
|
]"
|
|
help="Choose how the link should open"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Process Link -->
|
|
<div v-if="configModel.linkType === 'process'" class="space-y-3">
|
|
<FormKit
|
|
type="select"
|
|
label="Select Process"
|
|
name="linkProcessId"
|
|
v-model="configModel.linkProcessId"
|
|
:options="publishedProcesses"
|
|
help="Choose a published process to link to"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Select a process..."
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Open In"
|
|
name="linkTarget"
|
|
v-model="configModel.linkTarget"
|
|
:options="[
|
|
{ label: 'Same Window', value: '_self' },
|
|
{ label: 'New Window/Tab', value: '_blank' }
|
|
]"
|
|
help="Choose how the link should open"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Iframe Parameters -->
|
|
<div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<h6 class="text-sm font-medium text-blue-800 mb-3">Iframe Integration Parameters</h6>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<FormKit
|
|
type="switch"
|
|
label="Debug Mode"
|
|
name="iframeDebug"
|
|
v-model="configModel.iframeDebug"
|
|
help="Enable debug mode (show UI chrome) - OFF for iframe mode"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Hide Completion"
|
|
name="iframeHideComplete"
|
|
v-model="configModel.iframeHideComplete"
|
|
help="Hide completion message (auto-advance)"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
|
<FormKit
|
|
type="select"
|
|
label="Theme"
|
|
name="iframeTheme"
|
|
v-model="configModel.iframeTheme"
|
|
:options="[
|
|
{ label: 'Default', value: '' },
|
|
{ label: 'Dark', value: 'dark' },
|
|
{ label: 'Light', value: 'light' }
|
|
]"
|
|
help="Apply custom theme to the workflow"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Custom Parameters"
|
|
name="iframeCustomParams"
|
|
v-model="configModel.iframeCustomParams"
|
|
help="Additional URL parameters (e.g., 'param1=value1¶m2=value2')"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="param1=value1¶m2=value2"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-3 p-2 bg-gray-100 rounded text-xs text-gray-600">
|
|
<strong>Generated URL Preview:</strong><br>
|
|
<code class="text-blue-600">{{ getIframeUrlPreview() }}</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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(), setFieldByLabel(), getFieldOptions(), showField(), hideField(), enableField(), disableField(), showSuccess(), showError(), showInfo(), showWarning(), showConfirm(), showToast()
|
|
</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 -->
|
|
<template v-if="component.type === 'dynamic-list'">
|
|
<div class="space-y-6">
|
|
<!-- Basic List Settings -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Add Button Text"
|
|
name="buttonText"
|
|
v-model="configModel.buttonText"
|
|
help="Text for the add item button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Add Item"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Item Type"
|
|
name="itemType"
|
|
v-model="configModel.itemType"
|
|
:options="[
|
|
{ label: 'Text', value: 'text' },
|
|
{ label: 'Number', value: 'number' },
|
|
{ label: 'Email', value: 'email' },
|
|
{ label: 'URL', value: 'url' }
|
|
]"
|
|
help="Type of data for list items"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Item Limits -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Items"
|
|
name="minItems"
|
|
v-model="configModel.minItems"
|
|
help="Minimum number of items required"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0"
|
|
placeholder="0"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Items"
|
|
name="maxItems"
|
|
v-model="configModel.maxItems"
|
|
help="Maximum number of items allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="1"
|
|
placeholder="10"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Item Validation -->
|
|
<FormKit
|
|
type="text"
|
|
label="Item Validation Rules"
|
|
name="itemValidation"
|
|
v-model="configModel.itemValidation"
|
|
help="Validation rules for individual items (e.g., 'required|min:3|max:50')"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="required|min:3"
|
|
/>
|
|
|
|
<!-- Behavior Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Behavior Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="switch"
|
|
label="Allow Duplicates"
|
|
name="allowDuplicates"
|
|
v-model="configModel.allowDuplicates"
|
|
help="Allow duplicate items in the list"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Sorting"
|
|
name="enableSorting"
|
|
v-model="configModel.enableSorting"
|
|
help="Allow drag & drop reordering"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Search"
|
|
name="enableSearch"
|
|
v-model="configModel.enableSearch"
|
|
help="Add search/filter functionality"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Show Item Counter"
|
|
name="showItemCounter"
|
|
v-model="configModel.showItemCounter"
|
|
help="Display current item count"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Confirm Delete"
|
|
name="confirmDelete"
|
|
v-model="configModel.confirmDelete"
|
|
help="Require confirmation before deleting"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Bulk Operations"
|
|
name="bulkOperations"
|
|
v-model="configModel.bulkOperations"
|
|
help="Enable bulk select/delete operations"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import/Export Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Import/Export Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Import"
|
|
name="importEnabled"
|
|
v-model="configModel.importEnabled"
|
|
help="Allow importing items from files"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Export Format"
|
|
name="exportFormat"
|
|
v-model="configModel.exportFormat"
|
|
:options="[
|
|
{ label: 'JSON', value: 'json' },
|
|
{ label: 'CSV', value: 'csv' },
|
|
{ label: 'Text', value: 'txt' }
|
|
]"
|
|
help="Default format for exporting items"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Default Items -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Default Items</h5>
|
|
|
|
<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"
|
|
placeholder="Enter default item"
|
|
/>
|
|
<button
|
|
@click="removeDefaultItem(index)"
|
|
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="addDefaultItem"
|
|
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>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Data Table Configuration -->
|
|
<template v-if="component.type === 'repeating-table'">
|
|
<div class="space-y-6">
|
|
<!-- Table Limits -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Records"
|
|
name="minRecords"
|
|
v-model="configModel.minRecords"
|
|
help="Minimum number of records required"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0"
|
|
placeholder="0"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Records"
|
|
name="maxRecords"
|
|
v-model="configModel.maxRecords"
|
|
help="Maximum number of records allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="1"
|
|
placeholder="50"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Button Text Settings -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Add Button Text"
|
|
name="buttonText"
|
|
v-model="configModel.buttonText"
|
|
help="Text for the add record button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Add Record"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Edit Button Text"
|
|
name="editText"
|
|
v-model="configModel.editText"
|
|
help="Text for the edit button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Edit"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Delete Button Text"
|
|
name="deleteText"
|
|
v-model="configModel.deleteText"
|
|
help="Text for the delete button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Delete"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Table Behavior Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Table Behavior</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="switch"
|
|
label="Show Row Numbers"
|
|
name="showRowNumbers"
|
|
v-model="configModel.showRowNumbers"
|
|
help="Display row numbers in the table"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Allow Edit"
|
|
name="allowEdit"
|
|
v-model="configModel.allowEdit"
|
|
help="Allow editing existing records"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Allow Delete"
|
|
name="allowDelete"
|
|
v-model="configModel.allowDelete"
|
|
help="Allow deleting records"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Confirm Delete"
|
|
name="confirmDelete"
|
|
v-model="configModel.confirmDelete"
|
|
help="Require confirmation before deleting"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Search"
|
|
name="enableSearch"
|
|
v-model="configModel.enableSearch"
|
|
help="Add search functionality to the table"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Export"
|
|
name="enableExport"
|
|
v-model="configModel.enableExport"
|
|
help="Allow exporting table data"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table Columns Management -->
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h5 class="text-sm font-medium text-gray-700">Table Columns</h5>
|
|
<RsButton @click="addTableColumn" variant="primary" size="sm">
|
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
|
Add Column
|
|
</RsButton>
|
|
</div>
|
|
|
|
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
|
<div v-for="(column, index) in (configModel.columns || [])" :key="index" class="border p-3 rounded bg-white">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<h4 class="font-medium text-sm text-gray-800">Column {{ index + 1 }}</h4>
|
|
<button
|
|
@click="removeTableColumn(index)"
|
|
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-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Column Name"
|
|
v-model="column.name"
|
|
placeholder="e.g., name, email, phone"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Column Label"
|
|
v-model="column.label"
|
|
placeholder="e.g., Name, Email, Phone"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Column Type"
|
|
v-model="column.type"
|
|
:options="[
|
|
{ label: 'Text', value: 'text' },
|
|
{ label: 'Number', value: 'number' },
|
|
{ label: 'Email', value: 'email' },
|
|
{ label: 'Phone', value: 'tel' },
|
|
{ label: 'Date', value: 'date' },
|
|
{ label: 'Time', value: 'time' },
|
|
{ label: 'URL', value: 'url' },
|
|
{ label: 'Select', value: 'select' },
|
|
{ label: 'Checkbox', value: 'checkbox' },
|
|
{ label: 'Text Area', value: 'textarea' }
|
|
]"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Placeholder"
|
|
v-model="column.placeholder"
|
|
placeholder="Enter placeholder text"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Validation Rules"
|
|
v-model="column.validation"
|
|
placeholder="e.g., required|email"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<div class="flex items-center">
|
|
<label class="flex items-center">
|
|
<input type="checkbox" v-model="column.required" class="mr-2" />
|
|
<span class="text-sm text-gray-700">Required Field</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options for select type -->
|
|
<div v-if="column.type === 'select'" class="mt-4">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<h6 class="text-sm font-medium text-gray-700">Select Options</h6>
|
|
<button
|
|
@click="addColumnOption(index)"
|
|
class="text-blue-600 hover:text-blue-800 text-sm"
|
|
type="button"
|
|
>
|
|
<Icon name="heroicons:plus" class="w-3 h-3 mr-1" />
|
|
Add Option
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div v-for="(option, optionIndex) in (column.options || [])" :key="optionIndex" class="flex items-center space-x-2">
|
|
<input
|
|
type="text"
|
|
v-model="option.label"
|
|
placeholder="Option label"
|
|
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
|
/>
|
|
<input
|
|
type="text"
|
|
v-model="option.value"
|
|
placeholder="Option value"
|
|
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
|
/>
|
|
<button
|
|
@click="removeColumnOption(index, optionIndex)"
|
|
class="text-red-500 hover:text-red-700"
|
|
type="button"
|
|
>
|
|
<Icon name="material-symbols:delete-outline" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Actions Management -->
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h5 class="text-sm font-medium text-gray-700">Custom Actions</h5>
|
|
<RsButton @click="addCustomAction" variant="primary" size="sm">
|
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
|
Add Action
|
|
</RsButton>
|
|
</div>
|
|
<p class="text-xs text-gray-500">Add custom action buttons that appear in each table row</p>
|
|
|
|
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
|
<div v-for="(action, index) in (configModel.customActions || [])" :key="index" class="border p-3 rounded bg-white">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<h4 class="font-medium text-sm text-gray-800">Action {{ index + 1 }}</h4>
|
|
<button
|
|
@click="removeCustomAction(index)"
|
|
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-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Action Label"
|
|
v-model="action.label"
|
|
placeholder="e.g., View Details, Edit Item"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Action URL"
|
|
v-model="action.url"
|
|
placeholder="e.g., /details/{id} or https://example.com/edit/{id}"
|
|
help="Use {fieldName} for dynamic values from row data"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="URL Type"
|
|
v-model="action.urlType"
|
|
:options="[
|
|
{ label: 'Static URL', value: 'static' },
|
|
{ label: 'Dynamic URL (with placeholders)', value: 'dynamic' }
|
|
]"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Button Style"
|
|
v-model="action.variant"
|
|
:options="[
|
|
{ label: 'Default (Blue)', value: 'primary' },
|
|
{ label: 'Secondary (Gray)', value: 'secondary' },
|
|
{ label: 'Success (Green)', value: 'success' },
|
|
{ label: 'Warning (Orange)', value: 'warning' },
|
|
{ label: 'Danger (Red)', value: 'danger' },
|
|
{ label: 'Info (Blue)', value: 'info' }
|
|
]"
|
|
help="Matches the edit/delete button styling"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<div class="field-wrapper">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
|
Icon (Optional)
|
|
</label>
|
|
<IconBrowser
|
|
v-model="action.icon"
|
|
placeholder="Search Material Design icons..."
|
|
size="compact"
|
|
:show-preview="false"
|
|
/>
|
|
<div class="text-xs text-gray-600 mt-1">
|
|
Leave empty for automatic icon based on button style
|
|
</div>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Target"
|
|
v-model="action.target"
|
|
:options="[
|
|
{ label: 'Same Tab', value: '_self' },
|
|
{ label: 'New Tab', value: '_blank' }
|
|
]"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Confirmation Message (Optional)"
|
|
v-model="action.confirmMessage"
|
|
placeholder="e.g., Are you sure you want to proceed?"
|
|
help="Shows confirmation dialog before navigation"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!configModel.customActions || configModel.customActions.length === 0" class="text-center py-4 text-gray-500">
|
|
<Icon name="heroicons:cursor-arrow-rays" class="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
|
<p class="text-sm">No custom actions configured</p>
|
|
<p class="text-xs">Add custom actions to enable additional functionality in your table rows</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Repeating Group Configuration -->
|
|
<template v-if="component.type === 'repeating-group'">
|
|
<div class="space-y-6">
|
|
<!-- Group Limits -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Items"
|
|
name="minItems"
|
|
v-model="configModel.minItems"
|
|
help="Minimum number of groups required"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="0"
|
|
placeholder="1"
|
|
/>
|
|
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Items"
|
|
name="maxItems"
|
|
v-model="configModel.maxItems"
|
|
help="Maximum number of groups allowed"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:min="1"
|
|
placeholder="10"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Button Text Settings -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Add Button Text"
|
|
name="buttonText"
|
|
v-model="configModel.buttonText"
|
|
help="Text for the add button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Add Item"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Remove Button Text"
|
|
name="removeText"
|
|
v-model="configModel.removeText"
|
|
help="Text for the remove button"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Remove"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Container Information -->
|
|
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
|
|
<div class="flex items-start">
|
|
<Icon name="material-symbols:info" class="w-5 h-5 text-blue-600 mr-2 mt-0.5" />
|
|
<div>
|
|
<h4 class="font-medium text-sm text-blue-800 mb-1">Repeating Group Container</h4>
|
|
<p class="text-xs text-blue-700">
|
|
This is a container component. Drag and drop fields into it in the form builder to create the repeating group structure.
|
|
Each field you add will be repeated for every group item.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Children Count -->
|
|
<div class="border rounded-md p-3 bg-gray-50">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h5 class="text-sm font-medium text-gray-700">Group Fields</h5>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
{{ configModel.children?.length || 0 }} field(s) in this repeating group
|
|
</p>
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
<Icon name="material-symbols:drag-indicator" class="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Layout Grid Configuration -->
|
|
<template v-if="component.type === 'layout-grid'">
|
|
<div class="space-y-6">
|
|
<!-- Grid Layout Settings -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Grid Areas (Vertical)"
|
|
name="rows"
|
|
v-model="configModel.rows"
|
|
help="How many vertical sections you want"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
min="1"
|
|
max="6"
|
|
/>
|
|
<FormKit
|
|
type="number"
|
|
label="Grid Areas (Horizontal)"
|
|
name="columns"
|
|
v-model="configModel.columns"
|
|
help="How many horizontal sections you want"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
min="1"
|
|
max="6"
|
|
/>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="range"
|
|
label="Spacing Between Areas"
|
|
name="gap"
|
|
v-model="configModel.gap"
|
|
help="Space between each grid area"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
min="0"
|
|
max="32"
|
|
step="4"
|
|
/>
|
|
|
|
<!-- Cell Spanning Controls -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Cell Spanning</h5>
|
|
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<p class="text-sm text-blue-800 mb-3">
|
|
<Icon name="heroicons:information-circle" class="w-4 h-4 inline mr-1" />
|
|
Configure how cells span across rows and columns
|
|
</p>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(cell, index) in configModel.cells" :key="index" class="cell-span-control">
|
|
<div class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded">
|
|
<div class="flex items-center space-x-3">
|
|
<span class="text-sm font-medium text-gray-700">Cell {{ index + 1 }}</span>
|
|
<span class="text-xs text-gray-500">({{ cell.row + 1 }},{{ cell.col + 1 }})</span>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<div class="flex items-center space-x-1">
|
|
<label class="text-xs text-gray-600">Rows:</label>
|
|
<input
|
|
type="number"
|
|
v-model="cell.rowSpan"
|
|
min="1"
|
|
max="6"
|
|
class="w-12 px-2 py-1 text-xs border border-gray-300 rounded"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-1">
|
|
<label class="text-xs text-gray-600">Cols:</label>
|
|
<input
|
|
type="number"
|
|
v-model="cell.colSpan"
|
|
min="1"
|
|
max="6"
|
|
class="w-12 px-2 py-1 text-xs border border-gray-300 rounded"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
@click="removeCell(index)"
|
|
class="p-1 text-red-400 hover:text-red-600 rounded"
|
|
title="Remove cell"
|
|
>
|
|
<Icon name="heroicons:trash" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
@click="addCell"
|
|
class="w-full py-2 px-3 text-sm text-blue-600 bg-blue-50 border border-blue-200 rounded hover:bg-blue-100 transition-colors"
|
|
>
|
|
<Icon name="heroicons:plus" class="w-4 h-4 inline mr-1" />
|
|
Add Cell
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout Presets -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Quick Layout Presets</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<button
|
|
@click="applyPreset('sidebar-right')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('sidebar-right') }"
|
|
>
|
|
<div class="preset-preview sidebar-right-preview"></div>
|
|
<span class="preset-label">Sidebar Right</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('sidebar-left')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('sidebar-left') }"
|
|
>
|
|
<div class="preset-preview sidebar-left-preview"></div>
|
|
<span class="preset-label">Sidebar Left</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('header-content')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('header-content') }"
|
|
>
|
|
<div class="preset-preview header-content-preview"></div>
|
|
<span class="preset-label">Header + Content</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('three-column')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('three-column') }"
|
|
>
|
|
<div class="preset-preview three-column-preview"></div>
|
|
<span class="preset-label">Three Column</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('two-by-two')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('two-by-two') }"
|
|
>
|
|
<div class="preset-preview two-by-two-preview"></div>
|
|
<span class="preset-label">2x2 Grid</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('form-layout')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('form-layout') }"
|
|
>
|
|
<div class="preset-preview form-layout-preview"></div>
|
|
<span class="preset-label">Form Layout</span>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('header-sidebar')"
|
|
class="preset-button"
|
|
:class="{ 'preset-active': isCurrentPreset('header-sidebar') }"
|
|
>
|
|
<div class="preset-preview header-sidebar-preview"></div>
|
|
<span class="preset-label">Header + Sidebar</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grid Layout Management -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Layout Management</h5>
|
|
|
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-800">
|
|
Components in grid: {{ getTotalComponents() }}
|
|
</p>
|
|
<p class="text-xs text-gray-500">
|
|
Drag form components into the grid areas to position them
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-xs text-gray-600 mt-2">
|
|
<p>• In the form builder, drag components directly into the grid areas</p>
|
|
<p>• Components will be positioned exactly where you drop them</p>
|
|
<p>• Adjust grid dimensions above to create more areas</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Form Section Configuration -->
|
|
<template v-if="component.type === 'form-section'">
|
|
<div class="space-y-6">
|
|
<!-- Section Content -->
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<FormKit
|
|
type="textarea"
|
|
label="Section Description"
|
|
name="description"
|
|
v-model="configModel.description"
|
|
help="Optional description text below the section title"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="Describe this section..."
|
|
rows="2"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Section Header Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Header Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="switch"
|
|
label="Show Header"
|
|
name="showHeader"
|
|
v-model="configModel.showHeader"
|
|
help="Display the section header"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="configModel.showHeader"
|
|
type="select"
|
|
label="Header Size"
|
|
name="headerSize"
|
|
v-model="configModel.headerSize"
|
|
:options="[
|
|
{ label: 'Small', value: 'small' },
|
|
{ label: 'Medium', value: 'medium' },
|
|
{ label: 'Large', value: 'large' }
|
|
]"
|
|
help="Size of the section header"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="configModel.showHeader"
|
|
type="switch"
|
|
label="Collapsible"
|
|
name="collapsible"
|
|
v-model="configModel.collapsible"
|
|
help="Allow users to collapse this section"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<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' }"
|
|
/>
|
|
|
|
<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>
|
|
|
|
<!-- Visual Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Visual Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="switch"
|
|
label="Show Border"
|
|
name="showBorder"
|
|
v-model="configModel.showBorder"
|
|
help="Display border around the section"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Border Style"
|
|
name="borderStyle"
|
|
v-model="configModel.borderStyle"
|
|
:options="[
|
|
{ label: 'Solid', value: 'solid' },
|
|
{ label: 'Dashed', value: 'dashed' },
|
|
{ label: 'Dotted', value: 'dotted' }
|
|
]"
|
|
help="Style of the section border"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:disabled="!configModel.showBorder"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Spacing"
|
|
name="spacing"
|
|
v-model="configModel.spacing"
|
|
:options="[
|
|
{ label: 'Compact', value: 'compact' },
|
|
{ label: 'Normal', value: 'normal' },
|
|
{ label: 'Relaxed', value: 'relaxed' }
|
|
]"
|
|
help="Internal padding of the section"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Color Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Color Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="color"
|
|
label="Background Color"
|
|
name="backgroundColor"
|
|
v-model="configModel.backgroundColor"
|
|
help="Background color of the section"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="color"
|
|
label="Header Background"
|
|
name="headerBackground"
|
|
v-model="configModel.headerBackground"
|
|
help="Background color of the header"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
:disabled="!configModel.showHeader"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nested Components Management -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Nested Components</h5>
|
|
|
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-800">
|
|
Components in this section: {{ (configModel.children || []).length }}
|
|
</p>
|
|
<p class="text-xs text-gray-500">
|
|
Drag form components into the section to group them here
|
|
</p>
|
|
</div>
|
|
<div v-if="(configModel.children || []).length > 0">
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
{{ (configModel.children || []).length }} {{ (configModel.children || []).length === 1 ? 'component' : 'components' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List of nested components -->
|
|
<div v-if="(configModel.children || []).length > 0" class="space-y-2">
|
|
<div
|
|
v-for="(child, index) in configModel.children"
|
|
:key="child.id"
|
|
class="flex items-center justify-between bg-white border border-gray-200 rounded px-3 py-2"
|
|
>
|
|
<div class="flex items-center space-x-2">
|
|
<Icon :name="getComponentIcon(child.type)" class="w-4 h-4 text-gray-500" />
|
|
<span class="text-sm text-gray-700">
|
|
{{ child.props.label || getComponentTypeName(child.type) }}
|
|
</span>
|
|
<span class="text-xs text-gray-400">
|
|
({{ child.type }})
|
|
</span>
|
|
</div>
|
|
<button
|
|
@click="removeNestedComponent(index)"
|
|
class="text-red-500 hover:text-red-700 text-xs"
|
|
title="Remove from section"
|
|
>
|
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-else class="text-center py-4">
|
|
<Icon name="material-symbols:inbox-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
|
<p class="text-sm text-gray-500 mb-1">No components in this section</p>
|
|
<p class="text-xs text-gray-400">
|
|
Drag components from the sidebar into the section area
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Custom HTML Configuration -->
|
|
<template v-if="component.type === 'customHtml'">
|
|
<div class="space-y-6">
|
|
<!-- Code Editor Tabs -->
|
|
<div class="code-editor-tabs">
|
|
<div class="tabs-header border-b border-gray-200">
|
|
<div class="flex">
|
|
<button
|
|
@click="customHtmlActiveTab = 'html'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="customHtmlActiveTab === 'html' ? 'text-blue-600 border-b-2 border-blue-500 bg-blue-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'"
|
|
>
|
|
<span class="flex items-center">
|
|
<Icon name="material-symbols:code" class="w-4 h-4 mr-2" />
|
|
HTML
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="customHtmlActiveTab = 'css'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="customHtmlActiveTab === 'css' ? 'text-blue-600 border-b-2 border-blue-500 bg-blue-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'"
|
|
>
|
|
<span class="flex items-center">
|
|
<Icon name="material-symbols:format-color-fill" class="w-4 h-4 mr-2" />
|
|
CSS
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="customHtmlActiveTab = 'js'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="customHtmlActiveTab === 'js' ? 'text-blue-600 border-b-2 border-blue-500 bg-blue-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'"
|
|
>
|
|
<span class="flex items-center">
|
|
<Icon name="material-symbols:code-blocks" class="w-4 h-4 mr-2" />
|
|
JavaScript
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="customHtmlActiveTab = 'preview'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors ml-auto"
|
|
:class="customHtmlActiveTab === 'preview' ? 'text-green-600 border-b-2 border-green-500 bg-green-50' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'"
|
|
>
|
|
<span class="flex items-center">
|
|
<Icon name="material-symbols:preview" class="w-4 h-4 mr-2" />
|
|
Preview
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HTML Tab -->
|
|
<div v-show="customHtmlActiveTab === 'html'" class="tab-content py-4">
|
|
<div class="html-editor-container">
|
|
<RsCodeMirror
|
|
v-model="configModel.htmlContent"
|
|
language="html"
|
|
height="300px"
|
|
class="html-editor border border-gray-200 rounded"
|
|
placeholder="<!-- Example 1: Price Calculator -->
|
|
<div class='price-calculator'>
|
|
<h3>Order Calculator</h3>
|
|
<div class='calc-row'>
|
|
<label>Quantity:</label>
|
|
<input type='number' id='qty' value='1' min='1' class='calc-input' placeholder='Enter quantity'>
|
|
</div>
|
|
<div class='calc-row'>
|
|
<label>Unit Price:</label>
|
|
<input type='number' id='price' value='10.00' step='0.01' class='calc-input' placeholder='Enter price'>
|
|
</div>
|
|
<div class='calc-row'>
|
|
<label>Total:</label>
|
|
<div id='total' class='total-display'>$10.00</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Example 2: Progress Tracker -->
|
|
<div class='progress-tracker'>
|
|
<h3>Application Progress</h3>
|
|
<div class='steps'>
|
|
<div class='step' data-step='1'>Personal Info</div>
|
|
<div class='step' data-step='2'>Documents</div>
|
|
<div class='step' data-step='3'>Review</div>
|
|
</div>
|
|
<div class='progress-bar'>
|
|
<div id='progress-fill' class='progress-fill'></div>
|
|
</div>
|
|
<div class='text-center text-sm text-gray-600 mt-3'>
|
|
<span id='progress-percentage'>33%</span> Complete
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Example 3: File Upload Preview -->
|
|
<div class='file-upload'>
|
|
<h3>Document Upload</h3>
|
|
<div id='drop-zone' class='drop-zone'>
|
|
<div class='text-4xl mb-3'>📁</div>
|
|
<p>Drag files here or click to browse</p>
|
|
<p class='text-xs text-gray-500 mt-2'>Supports PDF, DOC, images up to 5MB</p>
|
|
<input type='file' id='file-input' multiple accept='.pdf,.doc,.docx,.jpg,.jpeg,.png' style='display: none;'>
|
|
</div>
|
|
<div id='file-list' class='file-list'></div>
|
|
</div>"
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2 flex items-start">
|
|
<Icon name="material-symbols:lightbulb-outline" class="w-4 h-4 mr-1 mt-0.5 flex-shrink-0" />
|
|
<span>Create your custom HTML structure. Use standard HTML5 elements and attributes. Use <code class="bg-gray-100 px-1 rounded text-xs">this.getValue('fieldName')</code> and <code class="bg-gray-100 px-1 rounded text-xs">this.setValue('fieldName', value)</code> to interact with form fields.</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- CSS Tab -->
|
|
<div v-show="customHtmlActiveTab === 'css'" class="tab-content py-4">
|
|
<div class="css-editor-container">
|
|
<RsCodeMirror
|
|
v-model="configModel.cssContent"
|
|
language="css"
|
|
height="300px"
|
|
class="css-editor border border-gray-200 rounded"
|
|
placeholder="/* Example 1: Price Calculator Styles */
|
|
.price-calculator {
|
|
padding: 24px;
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.price-calculator h3 {
|
|
color: #111827;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.calc-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.calc-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.calc-input {
|
|
width: 120px;
|
|
padding: 8px 12px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.calc-input:focus {
|
|
outline: none;
|
|
border-color: #6b7280;
|
|
box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
|
|
}
|
|
|
|
.total-display {
|
|
font-weight: 600;
|
|
font-size: 18px;
|
|
color: #111827;
|
|
background: #f9fafb;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid #e5e7eb;
|
|
}
|
|
|
|
/* Example 2: Progress Tracker Styles */
|
|
.progress-tracker {
|
|
padding: 24px;
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.progress-tracker h3 {
|
|
color: #111827;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.steps {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
position: relative;
|
|
}
|
|
|
|
.steps::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: #e5e7eb;
|
|
transform: translateY(-50%);
|
|
z-index: 1;
|
|
}
|
|
|
|
.step {
|
|
position: relative;
|
|
z-index: 2;
|
|
padding: 8px 16px;
|
|
background: #ffffff;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.step.active {
|
|
background: #6b7280;
|
|
border-color: #6b7280;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.step.completed {
|
|
background: #9ca3af;
|
|
border-color: #9ca3af;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 6px;
|
|
background: #f3f4f6;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #6b7280;
|
|
width: 33%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* Example 3: File Upload Styles */
|
|
.file-upload {
|
|
padding: 24px;
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.file-upload h3 {
|
|
color: #111827;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.drop-zone {
|
|
border: 2px dashed #d1d5db;
|
|
border-radius: 8px;
|
|
padding: 32px 24px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.drop-zone:hover {
|
|
border-color: #6b7280;
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.drop-zone.dragover {
|
|
border-color: #9ca3af;
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.drop-zone p {
|
|
color: #6b7280;
|
|
font-size: 14px;
|
|
margin: 0;
|
|
}
|
|
|
|
.file-list {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
margin-bottom: 8px;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.file-item:hover {
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
.file-item .valid {
|
|
color: #6b7280;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.file-item .invalid {
|
|
color: #ef4444;
|
|
font-weight: 500;
|
|
}"
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2 flex items-start">
|
|
<Icon name="material-symbols:lightbulb-outline" class="w-4 h-4 mr-1 mt-0.5 flex-shrink-0" />
|
|
<span>Add custom CSS styles. These styles will be automatically scoped to this component only, preventing conflicts with other form elements.</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- JavaScript Tab -->
|
|
<div v-show="customHtmlActiveTab === 'js'" class="tab-content py-4">
|
|
<div class="js-editor-container">
|
|
<RsCodeMirror
|
|
v-model="configModel.jsContent"
|
|
language="javascript"
|
|
height="300px"
|
|
class="js-editor border border-gray-200 rounded"
|
|
placeholder="// Example 1: Price Calculator JavaScript
|
|
console.log('Price calculator initialized');
|
|
|
|
function calculateTotal() {
|
|
const qtyInput = this.element.querySelector('#qty');
|
|
const priceInput = this.element.querySelector('#price');
|
|
const totalDisplay = this.element.querySelector('#total');
|
|
|
|
if (qtyInput && priceInput && totalDisplay) {
|
|
const qty = parseFloat(qtyInput.value) || 0;
|
|
const price = parseFloat(priceInput.value) || 0;
|
|
const total = qty * price;
|
|
|
|
totalDisplay.textContent = '$' + total.toFixed(2);
|
|
|
|
// Update form fields
|
|
this.setValue('quantity', qty);
|
|
this.setValue('unit_price', price);
|
|
this.setValue('total_price', total);
|
|
}
|
|
}
|
|
|
|
// Add event listeners for real-time calculation
|
|
const qtyInput = this.element.querySelector('#qty');
|
|
const priceInput = this.element.querySelector('#price');
|
|
|
|
if (qtyInput) qtyInput.addEventListener('input', calculateTotal.bind(this));
|
|
if (priceInput) priceInput.addEventListener('input', calculateTotal.bind(this));
|
|
|
|
// Example 2: Progress Tracker JavaScript
|
|
function updateProgress() {
|
|
const steps = this.element.querySelectorAll('.step');
|
|
const progressFill = this.element.querySelector('#progress-fill');
|
|
|
|
// Get current step from form data
|
|
const currentStep = this.getValue('current_step') || 1;
|
|
|
|
steps.forEach((step, index) => {
|
|
const stepNum = index + 1;
|
|
step.classList.remove('active');
|
|
|
|
if (stepNum <= currentStep) {
|
|
step.classList.add('active');
|
|
}
|
|
});
|
|
|
|
if (progressFill) {
|
|
const percentage = (currentStep / steps.length) * 100;
|
|
progressFill.style.width = percentage + '%';
|
|
}
|
|
|
|
// Update form data
|
|
this.setValue('progress_percentage', percentage);
|
|
}
|
|
|
|
// Add click handlers to steps
|
|
const steps = this.element.querySelectorAll('.step');
|
|
steps.forEach((step, index) => {
|
|
step.addEventListener('click', () => {
|
|
const newStep = index + 1;
|
|
this.setValue('current_step', newStep);
|
|
updateProgress.call(this);
|
|
});
|
|
});
|
|
|
|
// Example 3: File Upload JavaScript
|
|
function setupFileUpload() {
|
|
const dropZone = this.element.querySelector('#drop-zone');
|
|
const fileInput = this.element.querySelector('#file-input');
|
|
const fileList = this.element.querySelector('#file-list');
|
|
|
|
if (!dropZone || !fileInput || !fileList) return;
|
|
|
|
function handleFiles(files) {
|
|
fileList.innerHTML = '';
|
|
let validCount = 0;
|
|
|
|
Array.from(files).forEach(file => {
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
|
|
// Validate file (example: only images and PDFs)
|
|
const isValid = file.type.startsWith('image/') || file.type === 'application/pdf';
|
|
if (isValid) validCount++;
|
|
|
|
fileItem.innerHTML = \`
|
|
<span>\${file.name}</span>
|
|
<span class='\${isValid ? 'valid' : 'invalid'}'>\${isValid ? '✓' : '✗'}</span>
|
|
\`;
|
|
|
|
fileList.appendChild(fileItem);
|
|
});
|
|
|
|
// Update form data
|
|
this.setValue('uploaded_files', Array.from(files).map(f => f.name));
|
|
this.setValue('valid_files_count', validCount);
|
|
}
|
|
|
|
// Event listeners
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('dragover');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
dropZone.classList.remove('dragover');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('dragover');
|
|
handleFiles.call(this, e.dataTransfer.files);
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
handleFiles.call(this, e.target.files);
|
|
});
|
|
}
|
|
|
|
// Initialize components
|
|
if (this.element.querySelector('.price-calculator')) {
|
|
calculateTotal.call(this);
|
|
}
|
|
|
|
if (this.element.querySelector('.progress-tracker')) {
|
|
updateProgress.call(this);
|
|
}
|
|
|
|
if (this.element.querySelector('.file-upload')) {
|
|
setupFileUpload.call(this);
|
|
}"
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2 flex items-start">
|
|
<Icon name="material-symbols:lightbulb-outline" class="w-4 h-4 mr-1 mt-0.5 flex-shrink-0" />
|
|
<span>Add interactive JavaScript functionality. Use <code class="bg-gray-100 px-1 rounded text-xs">this.element</code> to access the component, and the helper functions for form interaction. Scripts run in {{ configModel.previewMode === 'safe' ? 'Safe Mode (disabled)' : 'Advanced Mode' }}.</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Live Preview Tab -->
|
|
<div v-show="customHtmlActiveTab === 'preview'" class="tab-content py-4">
|
|
<div class="preview-section">
|
|
<div class="preview-container border border-gray-200 rounded-lg p-4 bg-white min-h-[200px] max-h-[400px] overflow-auto">
|
|
<div v-if="configModel.htmlContent" class="custom-html-preview">
|
|
<div v-html="getFullHtmlPreview(configModel.htmlContent, configModel.cssContent)"></div>
|
|
</div>
|
|
<div v-else class="text-gray-500 text-center py-12">
|
|
<Icon name="material-symbols:code" class="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
|
<p class="text-sm">No HTML content to preview</p>
|
|
<p class="text-xs text-gray-400 mt-1">Switch to the HTML tab to start coding</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Component Settings -->
|
|
<div class="space-y-4">
|
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Component Settings</h5>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="select"
|
|
label="Preview Mode"
|
|
name="previewMode"
|
|
v-model="configModel.previewMode"
|
|
help="Choose how to render the HTML content"
|
|
:options="[
|
|
{ label: 'Safe Mode (No Scripts)', value: 'safe' },
|
|
{ label: 'Advanced Mode (Full HTML)', value: 'advanced' }
|
|
]"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Allow Scripts"
|
|
name="allowScripts"
|
|
v-model="configModel.allowScripts"
|
|
help="Enable JavaScript execution (Advanced mode only)"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="switch"
|
|
label="Show in Preview"
|
|
name="showInPreview"
|
|
v-model="configModel.showInPreview"
|
|
help="Display this component in form preview mode"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Content Statistics -->
|
|
<div class="p-4 bg-gray-50 rounded-lg border">
|
|
<h5 class="text-sm font-medium text-gray-700 mb-2">Content Statistics</h5>
|
|
<div class="flex items-center space-x-6 text-xs text-gray-600">
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:html" class="w-4 h-4 mr-1" />
|
|
HTML: {{ (configModel.htmlContent || '').length }} characters
|
|
</div>
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:css" class="w-4 h-4 mr-1" />
|
|
CSS: {{ (configModel.cssContent || '').length }} characters
|
|
</div>
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:javascript" class="w-4 h-4 mr-1" />
|
|
JavaScript: {{ (configModel.jsContent || '').length }} characters
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Security Notice -->
|
|
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
|
<div class="flex items-start">
|
|
<Icon name="material-symbols:security" class="w-5 h-5 text-yellow-600 mr-2 mt-0.5" />
|
|
<div>
|
|
<h4 class="font-medium text-sm text-yellow-800 mb-1">Security Notice</h4>
|
|
<p class="text-xs text-yellow-700">
|
|
Custom HTML components can contain arbitrary code. In Safe Mode, scripts and event handlers are disabled.
|
|
Use Advanced Mode only with trusted content and enable "Allow Scripts" carefully.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Info Display Specific Settings -->
|
|
<template v-if="component.type === 'info-display'">
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Show Border"
|
|
name="showBorder"
|
|
v-model="configModel.showBorder"
|
|
help="Show border around the information display"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="color"
|
|
label="Background Color"
|
|
name="backgroundColor"
|
|
v-model="configModel.backgroundColor"
|
|
help="Background color for the information display"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Information Fields Management -->
|
|
<div class="info-fields-section">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div>
|
|
<h5 class="text-sm font-semibold text-gray-800">Information Fields</h5>
|
|
<p class="text-xs text-gray-500 mt-1">Configure the information that will be displayed</p>
|
|
</div>
|
|
<button
|
|
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
@click="addInfoField"
|
|
>
|
|
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
|
Add Field
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Fields List -->
|
|
<div v-if="configModel.fields && configModel.fields.length > 0" class="space-y-4">
|
|
<div
|
|
v-for="(field, index) in configModel.fields"
|
|
:key="index"
|
|
class="field-card bg-white border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors"
|
|
>
|
|
<!-- Field Header -->
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center">
|
|
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
|
<Icon
|
|
:name="field.type === 'button' ? 'material-symbols:smart-button' : 'material-symbols:text-fields'"
|
|
class="w-4 h-4 text-blue-600"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h6 class="text-sm font-medium text-gray-800">
|
|
{{ field.label || `Field ${index + 1}` }}
|
|
</h6>
|
|
<p class="text-xs text-gray-500">
|
|
{{ field.type === 'button' ? 'Button Field' : 'Text Field' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
|
|
@click="removeInfoField(index)"
|
|
title="Remove field"
|
|
>
|
|
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Field Configuration -->
|
|
<div class="space-y-3">
|
|
<!-- Basic Info -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<FormKit
|
|
type="text"
|
|
label="Display Label"
|
|
v-model="field.label"
|
|
placeholder="e.g., Customer Name"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
label="Field Key"
|
|
v-model="field.key"
|
|
placeholder="e.g., customer_name"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Field Type -->
|
|
<FormKit
|
|
type="select"
|
|
label="Field Type"
|
|
v-model="field.type"
|
|
:options="[
|
|
{ label: '📝 Text Information', value: 'text' },
|
|
{ label: '🔗 Action Button', value: 'button' }
|
|
]"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
|
|
<!-- Text Field Configuration -->
|
|
<div v-if="field.type === 'text'">
|
|
<FormKit
|
|
type="text"
|
|
label="Display Value"
|
|
v-model="field.value"
|
|
placeholder="e.g., John Doe, 555-123-4567"
|
|
help="The text that will be shown to users"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Button Field Configuration -->
|
|
<div v-if="field.type === 'button'" class="space-y-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
|
<div class="flex items-center mb-2">
|
|
<Icon name="material-symbols:settings" class="w-4 h-4 text-blue-600 mr-2" />
|
|
<span class="text-sm font-medium text-blue-800">Button Settings</span>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Button Text"
|
|
v-model="field.value"
|
|
placeholder="e.g., View Profile, Download PDF"
|
|
help="Text displayed on the button"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Button URL"
|
|
v-model="field.url"
|
|
placeholder="e.g., https://example.com/profile"
|
|
help="Where the button should redirect to"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Open in new tab"
|
|
v-model="field.openInNewTab"
|
|
help="Opens link in new window"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
|
|
<div class="mb-0">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Icon (optional)</label>
|
|
<IconBrowser
|
|
v-model="field.icon"
|
|
size="compact"
|
|
:show-preview="false"
|
|
placeholder="Search icons..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="empty-fields-state text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
|
<Icon name="material-symbols:info-outline" class="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
<h6 class="text-sm font-medium text-gray-600 mb-1">No information fields yet</h6>
|
|
<p class="text-xs text-gray-500 mb-4">Add fields to display information to your users</p>
|
|
<button
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
@click="addInfoField"
|
|
>
|
|
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
|
Add Your First Field
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options Tab (for select, radio, checkbox) -->
|
|
<div v-if="activeTab === 'options'" class="space-y-6">
|
|
<div class="settings-section">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:list-bullet" class="w-5 h-5 mr-2" />
|
|
Choice Options
|
|
</h4>
|
|
<p class="section-description">Configure the options users can choose from</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<div class="options-manager">
|
|
<div class="options-header">
|
|
<span class="text-sm font-medium text-gray-700">Options List</span>
|
|
<button @click="addOption" class="add-option-btn">
|
|
<Icon name="heroicons:plus-circle" class="w-4 h-4 mr-1" />
|
|
Add Option
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="configModel.options && configModel.options.length > 0" class="options-list">
|
|
<div
|
|
v-for="(option, index) in configModel.options"
|
|
:key="index"
|
|
class="option-item"
|
|
>
|
|
<div class="option-drag-handle">
|
|
<Icon name="heroicons:bars-3" class="w-4 h-4 text-gray-400" />
|
|
</div>
|
|
|
|
<div class="option-content">
|
|
<FormKit
|
|
type="text"
|
|
placeholder="What users will see"
|
|
v-model="option.label"
|
|
:classes="{ outer: 'option-field', input: 'option-input' }"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
placeholder="Internal value"
|
|
v-model="option.value"
|
|
:classes="{ outer: 'option-field', input: 'option-input' }"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
@click="removeOption(index)"
|
|
class="option-remove-btn"
|
|
title="Remove this option"
|
|
>
|
|
<Icon name="heroicons:trash" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-options">
|
|
<Icon name="heroicons:list-bullet" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
|
<p class="text-sm text-gray-500 text-center mb-3">No options added yet</p>
|
|
<button @click="addOption" class="empty-add-btn">
|
|
Add First Option
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout Tab -->
|
|
<div v-if="activeTab === 'layout'" class="space-y-6">
|
|
<div class="settings-section">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:squares-2x2" class="w-5 h-5 mr-2" />
|
|
Layout & Size
|
|
</h4>
|
|
<p class="section-description">Control how this field appears in your form</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-3">Field Width</label>
|
|
|
|
<!-- Compact Width Selector -->
|
|
<div class="width-selector-compact">
|
|
<!-- Button Group for Width Selection -->
|
|
<div class="width-buttons">
|
|
<button
|
|
v-for="option in widthOptions"
|
|
:key="option.value"
|
|
@click="setComponentWidth(option.value, option.gridColumns)"
|
|
type="button"
|
|
class="width-button"
|
|
:class="{
|
|
'active': getComponentWidthPercent() === option.value,
|
|
'recommended': isRecommendedWidth(option.type)
|
|
}"
|
|
:title="option.useCase"
|
|
>
|
|
<div class="width-visual">
|
|
<div class="width-bar" :style="{ width: option.value + '%' }"></div>
|
|
</div>
|
|
<span class="width-label">{{ option.name }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Simple Visual Preview -->
|
|
<div class="width-preview">
|
|
<div class="preview-container">
|
|
<div class="preview-field" :style="{ width: getComponentWidthPercent() + '%' }">
|
|
<div class="preview-input"></div>
|
|
</div>
|
|
</div>
|
|
<div class="preview-info">
|
|
{{ getCurrentWidthOption()?.name || 'Custom' }} ({{ getComponentWidthPercent() }}%)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Validation Tab -->
|
|
<div v-if="activeTab === 'validation'" class="space-y-6">
|
|
<div class="settings-section">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:shield-check" class="w-5 h-5 mr-2" />
|
|
Field Validation
|
|
</h4>
|
|
<p class="section-description">Set rules to ensure users enter valid information</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<div class="validation-options">
|
|
<!-- Required Field -->
|
|
<div class="validation-item">
|
|
<label class="validation-toggle" @click.stop>
|
|
<input type="checkbox" v-model="isRequired" />
|
|
<span class="toggle-slider"></span>
|
|
<div class="validation-info">
|
|
<span class="validation-title">Required Field</span>
|
|
<span class="validation-desc">Users must fill this field before submitting</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Email Validation -->
|
|
<div v-if="component.type === 'email' || component.type === 'text'" class="validation-item">
|
|
<label class="validation-toggle" @click.stop>
|
|
<input type="checkbox" v-model="isEmailValidation" />
|
|
<span class="toggle-slider"></span>
|
|
<div class="validation-info">
|
|
<span class="validation-title">Email Format</span>
|
|
<span class="validation-desc">Must be a valid email address</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- URL Validation -->
|
|
<div v-if="component.type === 'url' || component.type === 'text'" class="validation-item">
|
|
<label class="validation-toggle" @click.stop>
|
|
<input type="checkbox" v-model="isUrlValidation" />
|
|
<span class="toggle-slider"></span>
|
|
<div class="validation-info">
|
|
<span class="validation-title">URL Format</span>
|
|
<span class="validation-desc">Must be a valid website address</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Length Validation -->
|
|
<div v-if="isTextBasedField" class="validation-item">
|
|
<label class="validation-toggle" @click.stop>
|
|
<input type="checkbox" v-model="hasLengthValidation" />
|
|
<span class="toggle-slider"></span>
|
|
<div class="validation-info">
|
|
<span class="validation-title">Text Length Limits</span>
|
|
<span class="validation-desc">Set minimum and maximum text length</span>
|
|
</div>
|
|
</label>
|
|
|
|
<div v-if="hasLengthValidation" class="validation-details">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Length"
|
|
v-model="minLength"
|
|
:min="0"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Length"
|
|
v-model="maxLength"
|
|
:min="1"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Number Range Validation -->
|
|
<div v-if="component.type === 'number'" class="validation-item">
|
|
<label class="validation-toggle" @click.stop>
|
|
<input type="checkbox" v-model="hasNumberValidation" />
|
|
<span class="toggle-slider"></span>
|
|
<div class="validation-info">
|
|
<span class="validation-title">Number Range</span>
|
|
<span class="validation-desc">Set minimum and maximum values</span>
|
|
</div>
|
|
</label>
|
|
|
|
<div v-if="hasNumberValidation" class="validation-details">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="number"
|
|
label="Minimum Value"
|
|
v-model="minNumber"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
<FormKit
|
|
type="number"
|
|
label="Maximum Value"
|
|
v-model="maxNumber"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Validation -->
|
|
<div class="mt-6">
|
|
<FormKit
|
|
type="text"
|
|
label="Custom Validation Rules"
|
|
name="validation"
|
|
v-model="configModel.validation"
|
|
help="Advanced: Enter custom validation rules using the format below"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
placeholder="e.g., required|email|length:3,50"
|
|
/>
|
|
|
|
|
|
|
|
<!-- Validation Rules Guide -->
|
|
<ValidationRulesHelp />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conditional Logic Section -->
|
|
<div v-if="showField('conditionalLogic')" class="section-container mt-6">
|
|
<div class="section">
|
|
<div class="section-wrapper">
|
|
<div class="section-header">
|
|
<h4 class="section-title">
|
|
<Icon name="heroicons:adjustments-horizontal" class="w-5 h-5 mr-2" />
|
|
Conditional Logic
|
|
</h4>
|
|
<p class="section-description">Show, hide, or disable this field based on other field values</p>
|
|
</div>
|
|
|
|
<div class="section-content">
|
|
<!-- Enable Conditional Logic -->
|
|
<FormKit
|
|
type="switch"
|
|
label="Enable Conditional Logic"
|
|
name="conditionalLogicEnabled"
|
|
v-model="configModel.conditionalLogic.enabled"
|
|
help="Show or hide this field based on other fields"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Conditional Logic Configuration -->
|
|
<div v-if="configModel.conditionalLogic.enabled" class="mt-4 space-y-4">
|
|
<!-- Action Selection -->
|
|
<FormKit
|
|
type="select"
|
|
label="Action"
|
|
name="conditionalAction"
|
|
v-model="configModel.conditionalLogic.action"
|
|
:options="[
|
|
{ label: 'Show this field', value: 'show' },
|
|
{ label: 'Hide this field', value: 'hide' },
|
|
{ label: 'Enable this field', value: 'enable' },
|
|
{ label: 'Disable this field', value: 'disable' }
|
|
]"
|
|
help="What should happen when conditions are met"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Operator Selection (when multiple conditions) -->
|
|
<FormKit
|
|
v-if="configModel.conditionalLogic.conditions.length > 1"
|
|
type="select"
|
|
label="Logic Operator"
|
|
name="conditionalOperator"
|
|
v-model="configModel.conditionalLogic.operator"
|
|
:options="[
|
|
{ label: 'All conditions must be true (AND)', value: 'and' },
|
|
{ label: 'Any condition can be true (OR)', value: 'or' }
|
|
]"
|
|
help="How multiple conditions should be evaluated"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<!-- Conditions -->
|
|
<div class="conditions-section">
|
|
<h5 class="text-sm font-medium text-gray-700 mb-3">Conditions</h5>
|
|
|
|
<div v-if="configModel.conditionalLogic.conditions.length === 0" class="text-center py-4 text-gray-500 border-2 border-dashed border-gray-300 rounded-lg">
|
|
<Icon name="heroicons:plus-circle" class="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
|
<p class="text-sm">No conditions set. Add a condition to get started.</p>
|
|
</div>
|
|
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="(condition, index) in configModel.conditionalLogic.conditions"
|
|
:key="index"
|
|
class="condition-item bg-gray-50 p-4 rounded-lg border"
|
|
>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<FormKit
|
|
type="text"
|
|
label="Field Name"
|
|
:name="`condition_field_${index}`"
|
|
v-model="condition.field"
|
|
placeholder="field_name"
|
|
help="Name of the field to check"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Operator"
|
|
:name="`condition_operator_${index}`"
|
|
v-model="condition.operator"
|
|
:options="[
|
|
{ label: 'Equals', value: 'equals' },
|
|
{ label: 'Not equals', value: 'not_equals' },
|
|
{ label: 'Contains', value: 'contains' },
|
|
{ label: 'Does not contain', value: 'not_contains' },
|
|
{ label: 'Is empty', value: 'is_empty' },
|
|
{ label: 'Is not empty', value: 'is_not_empty' },
|
|
{ label: 'Greater than', value: 'greater_than' },
|
|
{ label: 'Less than', value: 'less_than' }
|
|
]"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Value"
|
|
:name="`condition_value_${index}`"
|
|
v-model="condition.value"
|
|
placeholder="Value to compare"
|
|
help="Leave empty for 'is empty' conditions"
|
|
:classes="{ outer: 'field-wrapper' }"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-3">
|
|
<button
|
|
@click="removeCondition(index)"
|
|
type="button"
|
|
class="text-red-600 hover:text-red-800 text-sm flex items-center"
|
|
>
|
|
<Icon name="heroicons:trash" class="w-4 h-4 mr-1" />
|
|
Remove Condition
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
@click="addCondition"
|
|
type="button"
|
|
class="mt-3 inline-flex items-center px-3 py-2 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
|
>
|
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
|
Add Condition
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Preview JavaScript Code -->
|
|
<div v-if="configModel.conditionalLogic.conditions.length > 0" class="mt-4">
|
|
<h5 class="text-sm font-medium text-gray-700 mb-2">Generated Code Preview</h5>
|
|
<div class="bg-gray-900 text-green-400 p-3 rounded text-xs font-mono">
|
|
{{ generateConditionalLogicCode() }}
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-1">This JavaScript will be automatically executed to handle the conditional logic</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-between items-center">
|
|
<RsButton
|
|
@click="handleClose"
|
|
variant="secondary-outline"
|
|
>
|
|
Cancel
|
|
</RsButton>
|
|
|
|
<div class="flex space-x-3">
|
|
<RsButton
|
|
@click="showResetConfirmation"
|
|
variant="warning-outline"
|
|
>
|
|
<Icon name="heroicons:arrow-path" class="w-4 h-4 mr-1" />
|
|
Reset to Default
|
|
</RsButton>
|
|
|
|
<RsButton
|
|
@click="handleSave"
|
|
variant="primary"
|
|
>
|
|
<Icon name="heroicons:check" class="w-4 h-4 mr-1" />
|
|
Apply Changes
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</RsModal>
|
|
|
|
<!-- Reset Confirmation Modal -->
|
|
<RsModal v-model="showResetModal" title="Confirm Reset" size="md" position="center">
|
|
<div class="p-4">
|
|
<div class="flex items-center mb-4">
|
|
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
|
<div>
|
|
<p class="text-gray-600 font-medium mb-1">Reset to Default Settings</p>
|
|
<p class="text-gray-600 text-sm">
|
|
Are you sure you want to reset this component to its default settings? This action will overwrite all current configuration and cannot be undone.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<RsButton @click="cancelReset" variant="tertiary">
|
|
Cancel
|
|
</RsButton>
|
|
<RsButton @click="confirmReset" variant="warning">
|
|
<Icon name="heroicons:arrow-path" class="w-4 h-4 mr-1" />
|
|
Reset to Default
|
|
</RsButton>
|
|
</div>
|
|
</template>
|
|
</RsModal>
|
|
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, nextTick } from 'vue'
|
|
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue'
|
|
import IconBrowser from '~/components/IconBrowser.vue'
|
|
|
|
const props = defineProps({
|
|
modelValue: Boolean,
|
|
component: Object
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'update-component', 'close'])
|
|
|
|
// Modal state
|
|
const isOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
})
|
|
|
|
const activeTab = ref('basic')
|
|
const customHtmlActiveTab = ref('html')
|
|
const configModel = ref({
|
|
conditionalLogic: {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
})
|
|
|
|
// Type changing state
|
|
const pendingTypeChange = ref(null)
|
|
const showTypeChangePreview = ref(false)
|
|
const showResetModal = ref(false)
|
|
|
|
// Component info helpers
|
|
const modalTitle = computed(() => {
|
|
if (!props.component) return 'Field Settings'
|
|
return `Configure ${getComponentTypeName(props.component.type)}`
|
|
})
|
|
|
|
const getComponentIcon = (type) => {
|
|
const icons = {
|
|
text: 'heroicons:document-text',
|
|
textarea: 'heroicons:document-text',
|
|
number: 'heroicons:hashtag',
|
|
email: 'heroicons:envelope',
|
|
password: 'heroicons:key',
|
|
url: 'heroicons:link',
|
|
tel: 'heroicons:device-phone-mobile',
|
|
mask: 'heroicons:pencil-square',
|
|
hidden: 'heroicons:eye-slash',
|
|
select: 'heroicons:chevron-down',
|
|
searchSelect: 'heroicons:magnifying-glass',
|
|
checkbox: 'heroicons:check-badge',
|
|
radio: 'heroicons:radio',
|
|
switch: 'material-symbols:toggle-on',
|
|
date: 'heroicons:calendar-days',
|
|
time: 'heroicons:clock',
|
|
'datetime-local': 'heroicons:calendar',
|
|
range: 'heroicons:adjustments-horizontal',
|
|
color: 'heroicons:swatch',
|
|
file: 'heroicons:document-arrow-up',
|
|
otp: 'heroicons:key',
|
|
dropzone: 'heroicons:cloud-arrow-up',
|
|
button: 'heroicons:cursor-arrow-rays',
|
|
heading: 'heroicons:h1',
|
|
paragraph: 'heroicons:document-text',
|
|
divider: 'heroicons:minus',
|
|
'form-section': 'material-symbols:view-module-outline',
|
|
'layout-grid': 'material-symbols:grid-on',
|
|
'info-display': 'heroicons:information-circle',
|
|
'dynamic-list': 'heroicons:list-bullet',
|
|
'repeating-table': 'heroicons:table-cells',
|
|
'repeating-group': 'heroicons:list-bullet',
|
|
'customHtml': 'material-symbols:code'
|
|
}
|
|
return icons[type] || 'heroicons:square-3-stack-3d'
|
|
}
|
|
|
|
const getComponentTypeName = (type) => {
|
|
const names = {
|
|
text: 'Text Field',
|
|
textarea: 'Text Area',
|
|
number: 'Number Field',
|
|
email: 'Email Field',
|
|
password: 'Password Field',
|
|
url: 'URL Field',
|
|
tel: 'Phone Field',
|
|
mask: 'Masked Input',
|
|
hidden: 'Hidden Field',
|
|
select: 'Dropdown Menu',
|
|
searchSelect: 'Searchable Dropdown',
|
|
checkbox: 'Checkboxes',
|
|
radio: 'Radio Buttons',
|
|
switch: 'Switch Toggle',
|
|
date: 'Date Picker',
|
|
time: 'Time Picker',
|
|
'datetime-local': 'Date & Time',
|
|
range: 'Range Slider',
|
|
color: 'Color Picker',
|
|
file: 'File Upload',
|
|
otp: 'Verification Code',
|
|
dropzone: 'File Drop Zone',
|
|
button: 'Action Button',
|
|
heading: 'Heading Text',
|
|
paragraph: 'Paragraph Text',
|
|
divider: 'Divider Line',
|
|
'form-section': 'Form Section',
|
|
'layout-grid': 'Layout Grid',
|
|
'info-display': 'Information Display',
|
|
'dynamic-list': 'Dynamic List',
|
|
'repeating-table': 'Data Table',
|
|
'repeating-group': 'Repeating Group',
|
|
'customHtml': 'Custom HTML'
|
|
}
|
|
return names[type] || 'Form Field'
|
|
}
|
|
|
|
const getComponentDescription = (type) => {
|
|
const descriptions = {
|
|
text: 'Single line text input for names, titles, and short text',
|
|
textarea: 'Multi-line text area for longer content and descriptions',
|
|
number: 'Numeric input with validation for quantities, prices, and counts',
|
|
email: 'Email address input with format validation',
|
|
password: 'Secure password input with hidden characters',
|
|
url: 'Website URL input with link validation',
|
|
tel: 'Phone number input for contact information',
|
|
mask: 'Formatted text input with custom patterns like phone numbers',
|
|
hidden: 'Hidden field for storing data not visible to users',
|
|
select: 'Dropdown menu for choosing one option from a list',
|
|
searchSelect: 'Searchable dropdown menu with type-to-filter functionality',
|
|
checkbox: 'Multiple checkboxes for selecting multiple options',
|
|
radio: 'Radio buttons for choosing one option from a group',
|
|
switch: 'Toggle switch for enabling/disabling options',
|
|
date: 'Date picker for selecting dates',
|
|
time: 'Time picker for selecting times',
|
|
'datetime-local': 'Combined date and time selection',
|
|
range: 'Slider for selecting values within a range',
|
|
color: 'Color picker for selecting colors',
|
|
file: 'Standard file upload button',
|
|
otp: 'One-time password input for verification codes',
|
|
dropzone: 'Drag and drop file upload area',
|
|
button: 'Action button for forms and interactions',
|
|
heading: 'Section heading to organize form content',
|
|
paragraph: 'Text content for instructions and descriptions',
|
|
divider: 'Visual separator to organize form sections',
|
|
'form-section': 'Visual container to group related form fields into sections',
|
|
'layout-grid': 'Create custom grid layouts for precise component positioning',
|
|
'info-display': 'Read-only information display in organized format',
|
|
'dynamic-list': 'Dynamic list for displaying and managing items',
|
|
'repeating-table': 'Structured table for collecting multiple records with forms',
|
|
'repeating-group': 'Collect multiple entries of the same data structure',
|
|
'customHtml': 'Create custom designs using HTML, CSS, and JavaScript'
|
|
}
|
|
return descriptions[type] || 'Configure this form field'
|
|
}
|
|
|
|
// Tab management
|
|
const availableTabs = computed(() => {
|
|
const tabs = [
|
|
{ id: 'basic', label: 'Basic Settings', icon: 'heroicons:cog-6-tooth' }
|
|
]
|
|
|
|
// Add layout tab for components that support width settings
|
|
if (showField('width')) {
|
|
tabs.push({ id: 'layout', label: 'Layout', icon: 'heroicons:squares-2x2' })
|
|
}
|
|
|
|
if (hasOptions.value) {
|
|
tabs.push({ id: 'options', label: 'Options', icon: 'heroicons:list-bullet' })
|
|
}
|
|
|
|
// Only add validation tab for components that support validation
|
|
if (supportsValidation.value) {
|
|
tabs.push({ id: 'validation', label: 'Validation', icon: 'heroicons:shield-check' })
|
|
}
|
|
|
|
return tabs
|
|
})
|
|
|
|
// Field visibility helpers
|
|
const showField = (fieldName) => {
|
|
if (!props.component) return false
|
|
|
|
const fieldConfig = {
|
|
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
|
|
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
|
|
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'dynamic-list'],
|
|
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
|
|
value: ['heading', 'paragraph', 'hidden'],
|
|
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
|
|
rows: ['textarea'],
|
|
options: ['select', 'searchSelect', 'checkbox', 'radio'],
|
|
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
|
|
readonly: ['text', 'number', 'email', 'password', 'textarea', 'mask', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch']
|
|
}
|
|
|
|
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
|
}
|
|
|
|
const hasOptions = computed(() => showField('options'))
|
|
const hasSpecificSettings = computed(() => {
|
|
if (!props.component) return false
|
|
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display', 'layout-grid']
|
|
return specificTypes.includes(props.component.type)
|
|
})
|
|
|
|
// Components that support validation
|
|
const supportsValidation = computed(() => {
|
|
if (!props.component) return false
|
|
const validationSupportedTypes = [
|
|
'text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select',
|
|
'searchSelect', 'checkbox', 'radio', 'date', 'time', 'datetime-local', 'range', 'color',
|
|
'file', 'otp', 'dropzone', 'mask'
|
|
]
|
|
return validationSupportedTypes.includes(props.component.type)
|
|
})
|
|
|
|
// Validation helpers
|
|
const isRequired = computed({
|
|
get: () => configModel.value.validation?.includes('required') || false,
|
|
set: (value) => updateValidation('required', value)
|
|
})
|
|
|
|
const isEmailValidation = computed({
|
|
get: () => configModel.value.validation?.includes('email') || false,
|
|
set: (value) => updateValidation('email', value)
|
|
})
|
|
|
|
const isUrlValidation = computed({
|
|
get: () => configModel.value.validation?.includes('url') || false,
|
|
set: (value) => updateValidation('url', value)
|
|
})
|
|
|
|
const hasLengthValidation = ref(false)
|
|
const minLength = ref(null)
|
|
const maxLength = ref(null)
|
|
|
|
const hasNumberValidation = ref(false)
|
|
const minNumber = ref(null)
|
|
const maxNumber = ref(null)
|
|
|
|
const isTextBasedField = computed(() => {
|
|
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
|
|
})
|
|
|
|
// Default Value Field Helpers
|
|
const showDefaultValueField = () => {
|
|
if (!props.component) return false
|
|
|
|
// List of components that support default values
|
|
const supportedTypes = [
|
|
'text', 'textarea', 'number', 'email', 'password', 'url', 'tel',
|
|
'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch',
|
|
'date', 'time', 'datetime-local', 'range', 'color', 'hidden'
|
|
]
|
|
|
|
return supportedTypes.includes(props.component.type)
|
|
}
|
|
|
|
const getDefaultValueFieldType = () => {
|
|
if (!props.component) return 'text'
|
|
|
|
const fieldTypeMap = {
|
|
'text': 'text',
|
|
'textarea': 'textarea',
|
|
'number': 'number',
|
|
'email': 'email',
|
|
'password': 'text',
|
|
'url': 'url',
|
|
'tel': 'tel',
|
|
'mask': 'text',
|
|
'select': 'select',
|
|
'searchSelect': 'select',
|
|
'checkbox': 'checkbox',
|
|
'radio': 'select',
|
|
'switch': 'switch',
|
|
'date': 'date',
|
|
'time': 'time',
|
|
'datetime-local': 'datetime-local',
|
|
'range': 'number',
|
|
'color': 'color',
|
|
'hidden': 'text'
|
|
}
|
|
|
|
return fieldTypeMap[props.component.type] || 'text'
|
|
}
|
|
|
|
const getDefaultValueHelp = () => {
|
|
if (!props.component) return 'Initial value when the form loads'
|
|
|
|
const helpTexts = {
|
|
'select': 'Choose the default selected option',
|
|
'searchSelect': 'Choose the default selected option',
|
|
'checkbox': 'Select the default checked options',
|
|
'radio': 'Choose the default selected option',
|
|
'switch': 'Set the default on/off state',
|
|
'date': 'Set the default date (YYYY-MM-DD)',
|
|
'time': 'Set the default time (HH:MM)',
|
|
'datetime-local': 'Set the default date and time',
|
|
'range': 'Set the default slider value',
|
|
'color': 'Choose the default color',
|
|
'number': 'Set the default numeric value'
|
|
}
|
|
|
|
return helpTexts[props.component.type] || 'Initial value when the form loads'
|
|
}
|
|
|
|
const getDefaultValuePlaceholder = () => {
|
|
if (!props.component) return ''
|
|
|
|
const placeholders = {
|
|
'text': 'e.g., John Doe',
|
|
'email': 'e.g., user@example.com',
|
|
'url': 'e.g., https://example.com',
|
|
'tel': 'e.g., +1234567890',
|
|
'number': 'e.g., 100',
|
|
'date': 'YYYY-MM-DD',
|
|
'time': 'HH:MM',
|
|
'datetime-local': 'YYYY-MM-DDTHH:MM'
|
|
}
|
|
|
|
return placeholders[props.component.type] || ''
|
|
}
|
|
|
|
// Safe HTML preview for custom HTML component
|
|
const getSafeHtmlPreview = (htmlContent) => {
|
|
if (!htmlContent) return ''
|
|
|
|
// Remove script tags and event handlers for safe preview
|
|
return htmlContent
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '')
|
|
.replace(/javascript:/gi, '')
|
|
}
|
|
|
|
// Full HTML preview with CSS included
|
|
const getFullHtmlPreview = (htmlContent, cssContent) => {
|
|
if (!htmlContent) return ''
|
|
|
|
const safeHtml = getSafeHtmlPreview(htmlContent)
|
|
|
|
if (cssContent) {
|
|
// Create a unique ID for scoping
|
|
const previewId = `preview-${Date.now()}`
|
|
|
|
// Scope the CSS to the preview container
|
|
const scopedCss = cssContent.replace(
|
|
/([^@{}]+)\s*{/g,
|
|
`#${previewId} $1 {`
|
|
)
|
|
|
|
return `
|
|
<style>
|
|
#${previewId} {
|
|
/* Preview container styles */
|
|
position: relative;
|
|
}
|
|
${scopedCss}
|
|
</style>
|
|
<div id="${previewId}">${safeHtml}</div>
|
|
`
|
|
}
|
|
|
|
return safeHtml
|
|
}
|
|
|
|
// Custom HTML tab integration
|
|
const getTotalContentLength = () => {
|
|
const html = configModel.value.htmlContent || '';
|
|
const css = configModel.value.cssContent || '';
|
|
const js = configModel.value.jsContent || '';
|
|
return html.length + css.length + js.length;
|
|
}
|
|
|
|
// Published processes for button linking
|
|
const publishedProcesses = ref([])
|
|
|
|
// Fetch published processes when component is a button
|
|
const fetchPublishedProcesses = async () => {
|
|
if (props.component?.type === 'button') {
|
|
try {
|
|
const response = await $fetch('/api/process?status=published&limit=1000')
|
|
if (response.success) {
|
|
publishedProcesses.value = response.data.processes.map(process => ({
|
|
label: process.processName,
|
|
value: process.processUUID
|
|
}))
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching published processes:', error)
|
|
publishedProcesses.value = []
|
|
}
|
|
}
|
|
}
|
|
|
|
// Watch for component changes to fetch processes when needed
|
|
watch(() => props.component, (newComponent) => {
|
|
if (newComponent?.type === 'button') {
|
|
fetchPublishedProcesses()
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Generate iframe URL preview
|
|
const getIframeUrlPreview = () => {
|
|
if (!configModel.value.linkProcessId) {
|
|
return 'Select a process first...'
|
|
}
|
|
|
|
const baseUrl = `${window.location.origin}/workflow/${configModel.value.linkProcessId}`
|
|
const params = new URLSearchParams()
|
|
|
|
// Add debug parameter (false = iframe mode, true = debug mode)
|
|
if (configModel.value.iframeDebug !== undefined) {
|
|
params.append('debug', configModel.value.iframeDebug ? 'true' : 'false')
|
|
}
|
|
|
|
// Add hideComplete parameter
|
|
if (configModel.value.iframeHideComplete) {
|
|
params.append('hideComplete', 'true')
|
|
}
|
|
|
|
// Add theme parameter
|
|
if (configModel.value.iframeTheme) {
|
|
params.append('theme', configModel.value.iframeTheme)
|
|
}
|
|
|
|
// Add custom parameters
|
|
if (configModel.value.iframeCustomParams) {
|
|
const customParams = new URLSearchParams(configModel.value.iframeCustomParams)
|
|
customParams.forEach((value, key) => {
|
|
params.append(key, value)
|
|
})
|
|
}
|
|
|
|
const queryString = params.toString()
|
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
|
}
|
|
|
|
// Generate custom button styles for preview
|
|
const getCustomButtonStyles = () => {
|
|
const styles = {
|
|
backgroundColor: configModel.value.customBackgroundColor || '#3b82f6',
|
|
color: configModel.value.customTextColor || '#ffffff',
|
|
border: 'none',
|
|
cursor: 'pointer'
|
|
}
|
|
|
|
// Add border if specified
|
|
if (configModel.value.customBorderColor && configModel.value.customBorderWidth) {
|
|
styles.border = `${configModel.value.customBorderWidth}px solid ${configModel.value.customBorderColor}`
|
|
}
|
|
|
|
// Add border radius
|
|
if (configModel.value.customBorderRadius) {
|
|
styles.borderRadius = `${configModel.value.customBorderRadius}px`
|
|
}
|
|
|
|
// Add hover effects
|
|
const hoverEffect = configModel.value.customHoverEffect
|
|
if (hoverEffect && hoverEffect !== 'none') {
|
|
switch (hoverEffect) {
|
|
case 'darken':
|
|
styles[':hover'] = { filter: 'brightness(0.9)' }
|
|
break
|
|
case 'lighten':
|
|
styles[':hover'] = { filter: 'brightness(1.1)' }
|
|
break
|
|
case 'scale':
|
|
styles[':hover'] = { transform: 'scale(1.05)' }
|
|
break
|
|
case 'glow':
|
|
styles[':hover'] = {
|
|
boxShadow: `0 0 10px ${configModel.value.customBackgroundColor || '#3b82f6'}`
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return styles
|
|
}
|
|
|
|
// Type changing functionality
|
|
const compatibilityGroups = {
|
|
// Text-based inputs (can switch between each other)
|
|
textInputs: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask'],
|
|
|
|
// Selection inputs (can switch between each other)
|
|
selectionInputs: ['select', 'searchSelect', 'radio', 'checkbox'],
|
|
|
|
// Date/time inputs (can switch between each other)
|
|
dateTimeInputs: ['date', 'time', 'datetime-local'],
|
|
|
|
// Number-based inputs
|
|
numberInputs: ['number', 'range'],
|
|
|
|
// File inputs
|
|
fileInputs: ['file', 'dropzone'],
|
|
|
|
// Layout elements
|
|
layoutElements: ['heading', 'paragraph'],
|
|
|
|
// Advanced inputs (each unique but can have some compatibility)
|
|
advancedInputs: ['switch', 'color', 'otp', 'hidden']
|
|
}
|
|
|
|
const availableTypeChanges = computed(() => {
|
|
if (!props.component) return []
|
|
|
|
const currentType = props.component.type
|
|
|
|
// Find which group the current type belongs to
|
|
for (const [groupName, types] of Object.entries(compatibilityGroups)) {
|
|
if (types.includes(currentType)) {
|
|
// Return other types in the same group
|
|
return types.filter(type => type !== currentType)
|
|
}
|
|
}
|
|
|
|
return []
|
|
})
|
|
|
|
// Get preserved properties when changing types
|
|
const getPreservedProperties = (fromType, toType) => {
|
|
const commonProperties = ['label', 'name', 'help', 'validation', 'width', 'gridColumn']
|
|
const preserved = [...commonProperties]
|
|
|
|
// Check for specific preserved properties based on compatibility groups
|
|
if (compatibilityGroups.textInputs.includes(fromType) && compatibilityGroups.textInputs.includes(toType)) {
|
|
preserved.push('placeholder')
|
|
}
|
|
|
|
if (compatibilityGroups.selectionInputs.includes(fromType) && compatibilityGroups.selectionInputs.includes(toType)) {
|
|
preserved.push('options')
|
|
}
|
|
|
|
if (compatibilityGroups.dateTimeInputs.includes(fromType) && compatibilityGroups.dateTimeInputs.includes(toType)) {
|
|
preserved.push('min', 'max')
|
|
}
|
|
|
|
if (compatibilityGroups.numberInputs.includes(fromType) && compatibilityGroups.numberInputs.includes(toType)) {
|
|
preserved.push('min', 'max', 'step')
|
|
}
|
|
|
|
if (compatibilityGroups.fileInputs.includes(fromType) && compatibilityGroups.fileInputs.includes(toType)) {
|
|
preserved.push('accept', 'multiple')
|
|
}
|
|
|
|
if (compatibilityGroups.layoutElements.includes(fromType) && compatibilityGroups.layoutElements.includes(toType)) {
|
|
preserved.push('value')
|
|
}
|
|
|
|
return preserved.filter((prop, index, self) => self.indexOf(prop) === index) // Remove duplicates
|
|
}
|
|
|
|
// File size helper
|
|
const configModelMaxSizeMB = computed({
|
|
get: () => configModel.value.maxSize ? Math.round(configModel.value.maxSize / (1024 * 1024)) : 5,
|
|
set: (value) => {
|
|
configModel.value.maxSize = value * 1024 * 1024
|
|
}
|
|
})
|
|
|
|
// Width options with smart recommendations
|
|
const widthOptions = [
|
|
{
|
|
name: 'Narrow',
|
|
value: 25,
|
|
gridColumns: 3,
|
|
type: 'narrow',
|
|
description: 'Quarter width',
|
|
useCase: 'Small inputs like age, zip code, or short codes'
|
|
},
|
|
{
|
|
name: 'Small',
|
|
value: 33,
|
|
gridColumns: 4,
|
|
type: 'small',
|
|
description: 'One-third width',
|
|
useCase: 'Short text fields, city names, or grouped inputs'
|
|
},
|
|
{
|
|
name: 'Medium',
|
|
value: 50,
|
|
gridColumns: 6,
|
|
type: 'medium',
|
|
description: 'Half width',
|
|
useCase: 'Names, phone numbers, or paired inputs'
|
|
},
|
|
{
|
|
name: 'Wide',
|
|
value: 75,
|
|
gridColumns: 9,
|
|
type: 'wide',
|
|
description: 'Three-quarter width',
|
|
useCase: 'Addresses, longer text, or prominent fields'
|
|
},
|
|
{
|
|
name: 'Full',
|
|
value: 100,
|
|
gridColumns: 12,
|
|
type: 'full',
|
|
description: 'Full width',
|
|
useCase: 'Long text, paragraphs, or single-column layout'
|
|
}
|
|
]
|
|
|
|
// Smart recommendations based on field type
|
|
const getRecommendedWidth = (fieldType) => {
|
|
const recommendations = {
|
|
// Narrow fields (25% - 3 columns)
|
|
'number': 'narrow',
|
|
'date': 'narrow',
|
|
'time': 'narrow',
|
|
'color': 'narrow',
|
|
'otp': 'narrow',
|
|
|
|
// Small fields (33% - 4 columns)
|
|
'text': 'small',
|
|
|
|
// Medium fields (50% - 6 columns)
|
|
'email': 'medium',
|
|
'tel': 'medium',
|
|
'password': 'medium',
|
|
'mask': 'medium',
|
|
'select': 'medium',
|
|
'datetime-local': 'medium',
|
|
|
|
// Wide fields (75% - 9 columns)
|
|
'url': 'wide',
|
|
'file': 'wide',
|
|
'dropzone': 'wide',
|
|
|
|
// Full width fields (100% - 12 columns)
|
|
'textarea': 'full',
|
|
'heading': 'full',
|
|
'paragraph': 'full',
|
|
'checkbox': 'full',
|
|
'radio': 'full',
|
|
'range': 'full',
|
|
'switch': 'full',
|
|
'button': 'full',
|
|
'info-display': 'full',
|
|
'dynamic-list': 'full',
|
|
'repeating-table': 'full'
|
|
}
|
|
|
|
return recommendations[fieldType] || 'full'
|
|
}
|
|
|
|
const isRecommendedWidth = (widthType) => {
|
|
return getRecommendedWidth(props.component?.type) === widthType
|
|
}
|
|
|
|
const getCurrentWidthOption = () => {
|
|
const currentPercent = getComponentWidthPercent()
|
|
return widthOptions.find(option => option.value === currentPercent)
|
|
}
|
|
|
|
const getCurrentGridColumns = () => {
|
|
const currentPercent = getComponentWidthPercent()
|
|
const option = widthOptions.find(opt => opt.value === currentPercent)
|
|
return option ? option.gridColumns : 12
|
|
}
|
|
|
|
// Watch for component changes
|
|
watch(() => props.component, (newComponent) => {
|
|
if (newComponent) {
|
|
configModel.value = {
|
|
...newComponent.props,
|
|
// Ensure conditionalLogic is properly initialized
|
|
conditionalLogic: newComponent.props.conditionalLogic || {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
activeTab.value = 'basic'
|
|
resetValidationState()
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Methods - Define resetValidationState first before it's used above
|
|
const resetValidationState = () => {
|
|
hasLengthValidation.value = false
|
|
hasNumberValidation.value = false
|
|
minLength.value = null
|
|
maxLength.value = null
|
|
minNumber.value = null
|
|
maxNumber.value = null
|
|
}
|
|
|
|
const updateValidation = (rule, enabled) => {
|
|
let current = configModel.value.validation || ''
|
|
const rules = current.split('|').filter(r => r && r !== rule)
|
|
|
|
if (enabled) {
|
|
rules.push(rule)
|
|
}
|
|
|
|
configModel.value.validation = rules.join('|')
|
|
}
|
|
|
|
const setComponentWidth = (percentage, gridColumns) => {
|
|
configModel.value.width = `${percentage}%`
|
|
configModel.value.gridColumn = `span ${gridColumns}`
|
|
}
|
|
|
|
const getComponentWidthPercent = () => {
|
|
const width = configModel.value.width || '100%'
|
|
return parseInt(width.replace('%', ''))
|
|
}
|
|
|
|
const addOption = () => {
|
|
if (!configModel.value.options) {
|
|
configModel.value.options = []
|
|
}
|
|
configModel.value.options.push({
|
|
label: `Option ${configModel.value.options.length + 1}`,
|
|
value: `option_${configModel.value.options.length + 1}`
|
|
})
|
|
}
|
|
|
|
const removeOption = (index) => {
|
|
if (configModel.value.options && configModel.value.options.length > index) {
|
|
configModel.value.options.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Default items management for dynamic-list
|
|
const addDefaultItem = () => {
|
|
if (!configModel.value.defaultItems) {
|
|
configModel.value.defaultItems = []
|
|
}
|
|
configModel.value.defaultItems.push('')
|
|
}
|
|
|
|
const removeDefaultItem = (index) => {
|
|
if (configModel.value.defaultItems) {
|
|
configModel.value.defaultItems.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Conditional logic management
|
|
const addCondition = () => {
|
|
if (!configModel.value.conditionalLogic) {
|
|
configModel.value.conditionalLogic = {
|
|
enabled: true,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
|
|
if (!configModel.value.conditionalLogic.conditions) {
|
|
configModel.value.conditionalLogic.conditions = []
|
|
}
|
|
|
|
configModel.value.conditionalLogic.conditions.push({
|
|
field: '',
|
|
operator: 'equals',
|
|
value: ''
|
|
})
|
|
}
|
|
|
|
const removeCondition = (index) => {
|
|
if (configModel.value.conditionalLogic?.conditions) {
|
|
configModel.value.conditionalLogic.conditions.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Info Display field management
|
|
const addInfoField = () => {
|
|
if (!configModel.value.fields) {
|
|
configModel.value.fields = []
|
|
}
|
|
|
|
configModel.value.fields.push({
|
|
label: `Field ${configModel.value.fields.length + 1}`,
|
|
value: 'Value',
|
|
key: `field_${configModel.value.fields.length + 1}`,
|
|
type: 'text', // text, button
|
|
url: '', // for button type
|
|
openInNewTab: false, // for button type
|
|
icon: '' // for button type
|
|
})
|
|
}
|
|
|
|
const removeInfoField = (index) => {
|
|
if (configModel.value.fields && configModel.value.fields.length > index) {
|
|
configModel.value.fields.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const generateConditionalLogicCode = () => {
|
|
if (!configModel.value.conditionalLogic || !configModel.value.conditionalLogic.conditions.length) {
|
|
return '// No conditions defined'
|
|
}
|
|
|
|
const { conditions, action, operator } = configModel.value.conditionalLogic
|
|
const fieldName = configModel.value.name || 'this_field'
|
|
|
|
// Helper function to map operators to valid JavaScript
|
|
const getJavaScriptOperator = (operator) => {
|
|
switch (operator) {
|
|
case 'and':
|
|
case 'AND':
|
|
return '&&'
|
|
case 'or':
|
|
case 'OR':
|
|
return '||'
|
|
default:
|
|
return '&&' // Default to AND if unknown operator
|
|
}
|
|
}
|
|
|
|
// Helper function to generate condition check
|
|
const generateConditionCheck = (condition) => {
|
|
const { field, operator: condOp, value } = condition
|
|
|
|
switch (condOp) {
|
|
case 'equals':
|
|
return `getField('${field}') === '${value}'`
|
|
case 'not_equals':
|
|
return `getField('${field}') !== '${value}'`
|
|
case 'contains':
|
|
return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
|
|
case 'not_contains':
|
|
return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
|
|
case 'is_empty':
|
|
return `!getField('${field}') || getField('${field}') === ''`
|
|
case 'is_not_empty':
|
|
return `getField('${field}') && getField('${field}') !== ''`
|
|
case 'greater_than':
|
|
return `Number(getField('${field}')) > ${Number(value) || 0}`
|
|
case 'less_than':
|
|
return `Number(getField('${field}')) < ${Number(value) || 0}`
|
|
default:
|
|
return `getField('${field}') === '${value}'`
|
|
}
|
|
}
|
|
|
|
// Helper function to generate action code
|
|
const generateActionCode = (action, fieldName) => {
|
|
switch (action) {
|
|
case 'show': return `showField('${fieldName}')`
|
|
case 'hide': return `hideField('${fieldName}')`
|
|
case 'enable': return `enableField('${fieldName}')`
|
|
case 'disable': return `disableField('${fieldName}')`
|
|
default: return `showField('${fieldName}')`
|
|
}
|
|
}
|
|
|
|
// Helper function to generate inverse action code
|
|
const generateInverseActionCode = (action, fieldName) => {
|
|
switch (action) {
|
|
case 'show': return `hideField('${fieldName}')`
|
|
case 'hide': return `showField('${fieldName}')`
|
|
case 'enable': return `disableField('${fieldName}')`
|
|
case 'disable': return `enableField('${fieldName}')`
|
|
default: return `hideField('${fieldName}')`
|
|
}
|
|
}
|
|
|
|
// Generate condition checks
|
|
const conditionChecks = conditions.map(generateConditionCheck).join(` ${getJavaScriptOperator(operator)} `)
|
|
|
|
// Generate action functions
|
|
const actionCode = generateActionCode(action, fieldName)
|
|
const inverseActionCode = generateInverseActionCode(action, fieldName)
|
|
|
|
// Generate field change listeners - note about optimization
|
|
const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))]
|
|
|
|
// Add helpful comment about optimization when multiple fields watch the same condition
|
|
const optimizationNote = watchedFields.length > 1 ?
|
|
`// NOTE: When multiple fields use the same trigger field, the FormBuilder automatically\n// optimizes by grouping these handlers to avoid duplicate onFieldChange listeners.\n\n` :
|
|
''
|
|
|
|
const listenerCode = watchedFields.map(field =>
|
|
`onFieldChange('${field}', function() {
|
|
if (${conditionChecks}) {
|
|
${actionCode};
|
|
} else {
|
|
${inverseActionCode};
|
|
}
|
|
});`
|
|
).join('\n\n')
|
|
|
|
return optimizationNote + (listenerCode || '// Invalid configuration')
|
|
}
|
|
|
|
const handleSave = () => {
|
|
emit('update-component', { ...props.component, props: configModel.value })
|
|
handleClose()
|
|
}
|
|
|
|
const showResetConfirmation = () => {
|
|
showResetModal.value = true;
|
|
};
|
|
|
|
const cancelReset = () => {
|
|
showResetModal.value = false;
|
|
};
|
|
|
|
const confirmReset = () => {
|
|
handleReset();
|
|
showResetModal.value = false;
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (props.component) {
|
|
// Reset to default props from the component definition
|
|
const defaultComponent = {
|
|
...props.component,
|
|
props: props.component.defaultProps || {}
|
|
}
|
|
configModel.value = {
|
|
...defaultComponent.props,
|
|
// Ensure conditionalLogic is properly initialized
|
|
conditionalLogic: defaultComponent.props.conditionalLogic || {
|
|
enabled: false,
|
|
conditions: [],
|
|
action: 'show',
|
|
operator: 'and'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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')
|
|
}
|
|
|
|
// Table column management for repeating-table
|
|
const addTableColumn = () => {
|
|
if (!configModel.value.columns) {
|
|
configModel.value.columns = []
|
|
}
|
|
configModel.value.columns.push({
|
|
name: `column_${configModel.value.columns.length + 1}`,
|
|
label: `Column ${configModel.value.columns.length + 1}`,
|
|
type: 'text',
|
|
required: false,
|
|
placeholder: 'Enter value...',
|
|
validation: '',
|
|
width: '200px',
|
|
options: []
|
|
})
|
|
}
|
|
|
|
const removeTableColumn = (index) => {
|
|
if (configModel.value.columns) {
|
|
configModel.value.columns.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const addColumnOption = (columnIndex) => {
|
|
if (!configModel.value.columns[columnIndex].options) {
|
|
configModel.value.columns[columnIndex].options = []
|
|
}
|
|
configModel.value.columns[columnIndex].options.push({
|
|
label: `Option ${configModel.value.columns[columnIndex].options.length + 1}`,
|
|
value: `option_${configModel.value.columns[columnIndex].options.length + 1}`
|
|
})
|
|
}
|
|
|
|
const removeColumnOption = (columnIndex, optionIndex) => {
|
|
if (configModel.value.columns[columnIndex].options) {
|
|
configModel.value.columns[columnIndex].options.splice(optionIndex, 1)
|
|
}
|
|
}
|
|
|
|
// Custom actions management for repeating-table
|
|
const addCustomAction = () => {
|
|
if (!configModel.value.customActions) {
|
|
configModel.value.customActions = []
|
|
}
|
|
configModel.value.customActions.push({
|
|
label: 'New Action',
|
|
url: '/action/{id}',
|
|
urlType: 'dynamic',
|
|
variant: 'primary',
|
|
target: '_self',
|
|
icon: '',
|
|
confirmMessage: ''
|
|
})
|
|
}
|
|
|
|
const removeCustomAction = (index) => {
|
|
if (configModel.value.customActions) {
|
|
configModel.value.customActions.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const removeNestedComponent = (index) => {
|
|
if (configModel.value.children) {
|
|
configModel.value.children.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Repeating group field management
|
|
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'
|
|
})
|
|
}
|
|
|
|
const removeGroupField = (index) => {
|
|
if (configModel.value.fields) {
|
|
configModel.value.fields.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const addFieldOption = (field) => {
|
|
if (!field.options) {
|
|
field.options = []
|
|
}
|
|
field.options.push({
|
|
label: `Option ${field.options.length + 1}`,
|
|
value: `option_${field.options.length + 1}`
|
|
})
|
|
}
|
|
|
|
const removeFieldOption = (field, optionIndex) => {
|
|
if (field.options) {
|
|
field.options.splice(optionIndex, 1)
|
|
}
|
|
}
|
|
|
|
// Type changing methods
|
|
const handleTypeChange = (newType) => {
|
|
if (newType === props.component.type) return
|
|
|
|
pendingTypeChange.value = newType
|
|
showTypeChangePreview.value = true
|
|
}
|
|
|
|
const confirmTypeChange = () => {
|
|
if (!pendingTypeChange.value) return
|
|
|
|
const newType = pendingTypeChange.value
|
|
const preservedProps = getPreservedProperties(props.component.type, newType)
|
|
|
|
// Create new component configuration with preserved properties
|
|
const newProps = {}
|
|
|
|
// Preserve common properties
|
|
preservedProps.forEach(prop => {
|
|
if (configModel.value[prop] !== undefined) {
|
|
newProps[prop] = configModel.value[prop]
|
|
}
|
|
})
|
|
|
|
// Add default properties for the new type that aren't being preserved
|
|
const newTypeDefaults = getDefaultPropsForType(newType)
|
|
Object.keys(newTypeDefaults).forEach(prop => {
|
|
if (newProps[prop] === undefined) {
|
|
newProps[prop] = newTypeDefaults[prop]
|
|
}
|
|
})
|
|
|
|
// Update the component type and properties
|
|
const updatedComponent = {
|
|
...props.component,
|
|
type: newType,
|
|
props: newProps
|
|
}
|
|
|
|
// Emit the updated component
|
|
emit('update-component', updatedComponent)
|
|
|
|
// Reset state
|
|
pendingTypeChange.value = null
|
|
showTypeChangePreview.value = false
|
|
|
|
// Close modal after change
|
|
handleClose()
|
|
}
|
|
|
|
const cancelTypeChange = () => {
|
|
pendingTypeChange.value = null
|
|
showTypeChangePreview.value = false
|
|
}
|
|
|
|
// Get default properties for a specific component type
|
|
// Layout Grid Presets
|
|
const layoutPresets = {
|
|
'sidebar-right': {
|
|
rows: 2,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-1', row: 0, col: 1, rowSpan: 2, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 1, col: 0, rowSpan: 1, colSpan: 1, component: null }
|
|
]
|
|
},
|
|
'sidebar-left': {
|
|
rows: 2,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 2, colSpan: 1, component: null },
|
|
{ id: 'cell-1', row: 0, col: 1, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 1, col: 1, rowSpan: 1, colSpan: 1, component: null }
|
|
]
|
|
},
|
|
'header-content': {
|
|
rows: 2,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 2, component: null },
|
|
{ id: 'cell-1', row: 1, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 1, col: 1, rowSpan: 1, colSpan: 1, component: null }
|
|
]
|
|
},
|
|
'three-column': {
|
|
rows: 1,
|
|
columns: 3,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-1', row: 0, col: 1, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 0, col: 2, rowSpan: 1, colSpan: 1, component: null }
|
|
]
|
|
},
|
|
'two-by-two': {
|
|
rows: 2,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-1', row: 0, col: 1, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 1, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-3', row: 1, col: 1, rowSpan: 1, colSpan: 1, component: null }
|
|
]
|
|
},
|
|
'form-layout': {
|
|
rows: 3,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-1', row: 0, col: 1, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 1, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-3', row: 1, col: 1, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-4', row: 2, col: 0, rowSpan: 1, colSpan: 2, component: null }
|
|
]
|
|
},
|
|
'header-sidebar': {
|
|
rows: 3,
|
|
columns: 2,
|
|
cells: [
|
|
{ id: 'cell-0', row: 0, col: 0, rowSpan: 1, colSpan: 2, component: null },
|
|
{ id: 'cell-1', row: 1, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-2', row: 2, col: 0, rowSpan: 1, colSpan: 1, component: null },
|
|
{ id: 'cell-3', row: 1, col: 1, rowSpan: 2, colSpan: 1, component: null }
|
|
]
|
|
}
|
|
}
|
|
|
|
const currentPreset = ref('')
|
|
|
|
// Apply a layout preset
|
|
const applyPreset = (presetName) => {
|
|
if (!props.component || props.component.type !== 'layout-grid') return
|
|
|
|
const preset = layoutPresets[presetName]
|
|
if (!preset) return
|
|
|
|
// Update the component configuration
|
|
configModel.value.rows = preset.rows
|
|
configModel.value.columns = preset.columns
|
|
configModel.value.cells = [...preset.cells] // Create a copy to avoid reference issues
|
|
|
|
currentPreset.value = presetName
|
|
}
|
|
|
|
// Check if current layout matches a preset
|
|
const isCurrentPreset = (presetName) => {
|
|
if (!props.component || props.component.type !== 'layout-grid') return false
|
|
|
|
const preset = layoutPresets[presetName]
|
|
if (!preset) return false
|
|
|
|
return (
|
|
configModel.value.rows === preset.rows &&
|
|
configModel.value.columns === preset.columns &&
|
|
JSON.stringify(configModel.value.cells) === JSON.stringify(preset.cells)
|
|
)
|
|
}
|
|
|
|
// Layout Grid Cell Management
|
|
const addCell = () => {
|
|
if (!configModel.value.cells) {
|
|
configModel.value.cells = []
|
|
}
|
|
|
|
// Find the next available position
|
|
const rows = configModel.value.rows || 2
|
|
const cols = configModel.value.columns || 2
|
|
let newRow = 0
|
|
let newCol = 0
|
|
|
|
// Find first empty position
|
|
for (let row = 0; row < rows; row++) {
|
|
for (let col = 0; col < cols; col++) {
|
|
const isOccupied = configModel.value.cells.some(cell =>
|
|
cell.row === row && cell.col === col
|
|
)
|
|
if (!isOccupied) {
|
|
newRow = row
|
|
newCol = col
|
|
break
|
|
}
|
|
}
|
|
if (newRow !== 0 || newCol !== 0) break
|
|
}
|
|
|
|
const newCell = {
|
|
id: `cell-${Date.now()}`,
|
|
row: newRow,
|
|
col: newCol,
|
|
rowSpan: 1,
|
|
colSpan: 1,
|
|
component: null
|
|
}
|
|
|
|
configModel.value.cells.push(newCell)
|
|
}
|
|
|
|
const removeCell = (index) => {
|
|
if (configModel.value.cells && index >= 0 && index < configModel.value.cells.length) {
|
|
configModel.value.cells.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
// Get total components in layout grid
|
|
const getTotalComponents = () => {
|
|
if (props.component && props.component.type === 'layout-grid' && props.component.props.cells) {
|
|
return props.component.props.cells.filter(cell => cell.component).length
|
|
}
|
|
return 0
|
|
}
|
|
|
|
const getDefaultPropsForType = (type) => {
|
|
const defaults = {
|
|
text: {
|
|
type: 'text',
|
|
placeholder: 'Enter text...',
|
|
validation: ''
|
|
},
|
|
textarea: {
|
|
type: 'textarea',
|
|
placeholder: 'Enter text...',
|
|
validation: '',
|
|
rows: 3
|
|
},
|
|
email: {
|
|
type: 'email',
|
|
placeholder: 'email@example.com',
|
|
validation: 'email'
|
|
},
|
|
password: {
|
|
type: 'password',
|
|
placeholder: 'Enter password...',
|
|
validation: ''
|
|
},
|
|
url: {
|
|
type: 'url',
|
|
placeholder: 'https://example.com',
|
|
validation: 'url'
|
|
},
|
|
tel: {
|
|
type: 'tel',
|
|
placeholder: '+1 (555) 123-4567',
|
|
validation: ''
|
|
},
|
|
mask: {
|
|
type: 'mask',
|
|
placeholder: 'Enter value...',
|
|
mask: '###-###-####',
|
|
validation: ''
|
|
},
|
|
select: {
|
|
type: 'select',
|
|
placeholder: 'Select an option',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' }
|
|
],
|
|
validation: ''
|
|
},
|
|
searchSelect: {
|
|
type: 'searchSelect',
|
|
placeholder: 'Search and select an option',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' },
|
|
{ label: 'Option 3', value: 'option_3' }
|
|
],
|
|
validation: ''
|
|
},
|
|
radio: {
|
|
type: 'radio',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' }
|
|
],
|
|
validation: ''
|
|
},
|
|
checkbox: {
|
|
type: 'checkbox',
|
|
options: [
|
|
{ label: 'Option 1', value: 'option_1' },
|
|
{ label: 'Option 2', value: 'option_2' }
|
|
],
|
|
validation: ''
|
|
},
|
|
date: {
|
|
type: 'date',
|
|
validation: ''
|
|
},
|
|
time: {
|
|
type: 'time',
|
|
validation: ''
|
|
},
|
|
'datetime-local': {
|
|
type: 'datetime-local',
|
|
validation: ''
|
|
},
|
|
number: {
|
|
type: 'number',
|
|
placeholder: '0',
|
|
validation: ''
|
|
},
|
|
range: {
|
|
type: 'range',
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
value: 50,
|
|
validation: ''
|
|
},
|
|
file: {
|
|
type: 'file',
|
|
accept: '*/*',
|
|
validation: ''
|
|
},
|
|
dropzone: {
|
|
type: 'dropzone',
|
|
accept: 'image/*,.pdf,.doc,.docx',
|
|
multiple: true,
|
|
maxSize: 5242880,
|
|
maxFiles: 5,
|
|
validation: ''
|
|
},
|
|
switch: {
|
|
type: 'switch',
|
|
value: false,
|
|
validation: ''
|
|
},
|
|
color: {
|
|
type: 'color',
|
|
value: '#3b82f6',
|
|
validation: ''
|
|
},
|
|
otp: {
|
|
type: 'otp',
|
|
digits: 6,
|
|
validation: 'required'
|
|
},
|
|
hidden: {
|
|
type: 'hidden',
|
|
value: ''
|
|
},
|
|
heading: {
|
|
value: 'Section Heading',
|
|
level: 2
|
|
},
|
|
paragraph: {
|
|
value: 'Enter some descriptive text here.'
|
|
},
|
|
button: {
|
|
type: 'button',
|
|
buttonType: 'button',
|
|
variant: 'primary',
|
|
size: 'md',
|
|
showLabel: false,
|
|
showButtonText: true,
|
|
buttonText: '',
|
|
icon: '',
|
|
linkType: 'none', // 'none', 'url', 'process'
|
|
linkUrl: '',
|
|
linkProcessId: '',
|
|
linkTarget: '_self', // '_self', '_blank'
|
|
iframeDebug: false, // Show/hide debug UI
|
|
iframeHideComplete: false, // Hide completion message
|
|
iframeTheme: '', // Custom theme
|
|
iframeCustomParams: '' // Additional URL parameters
|
|
}
|
|
}
|
|
|
|
// Ensure width and gridColumn are included for all types
|
|
const baseDefaults = {
|
|
width: '100%',
|
|
gridColumn: 'span 12'
|
|
}
|
|
|
|
return { ...baseDefaults, ...(defaults[type] || {}) }
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.field-settings-modal {
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Section Styling */
|
|
.settings-section {
|
|
@apply border border-gray-200 rounded-lg overflow-hidden;
|
|
}
|
|
|
|
.section-header {
|
|
@apply bg-gray-50 px-4 py-3 border-b border-gray-200;
|
|
}
|
|
|
|
.section-title {
|
|
@apply text-sm font-semibold text-gray-800 flex items-center;
|
|
}
|
|
|
|
.section-description {
|
|
@apply text-xs text-gray-600 mt-1;
|
|
}
|
|
|
|
.section-content {
|
|
@apply p-4 space-y-4;
|
|
}
|
|
|
|
/* Width Selection */
|
|
.width-selector {
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.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 {
|
|
@apply flex-shrink-0;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.option-info {
|
|
@apply flex-1 text-left min-w-0;
|
|
}
|
|
|
|
.option-name {
|
|
@apply text-base font-semibold text-gray-800 mb-1 flex items-center;
|
|
}
|
|
|
|
.option-description {
|
|
@apply text-sm text-gray-600 mb-1;
|
|
}
|
|
|
|
.option-use-case {
|
|
@apply text-xs text-gray-500 italic;
|
|
}
|
|
|
|
.recommended-badge {
|
|
@apply inline-flex items-center px-2 py-0.5 text-xs font-medium text-green-800 bg-green-100 rounded-full ml-2;
|
|
}
|
|
|
|
.current-selection-feedback {
|
|
@apply mt-4 p-3 bg-gray-50 rounded-lg border;
|
|
}
|
|
|
|
.feedback-row {
|
|
@apply flex justify-between items-center py-1;
|
|
}
|
|
|
|
/* Compact Width Selection - New Styles */
|
|
.width-selector-compact {
|
|
@apply space-y-4;
|
|
}
|
|
|
|
.width-buttons {
|
|
@apply flex flex-wrap gap-2;
|
|
}
|
|
|
|
.width-button {
|
|
@apply flex-1 min-w-0 px-3 py-2 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 cursor-pointer text-center relative;
|
|
}
|
|
|
|
.width-button.active {
|
|
@apply border-blue-500 bg-blue-50 ring-1 ring-blue-200;
|
|
}
|
|
|
|
.width-button.recommended::before {
|
|
content: '';
|
|
@apply absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full;
|
|
}
|
|
|
|
.width-button.active.recommended {
|
|
@apply border-green-500 bg-green-50 ring-1 ring-green-200;
|
|
}
|
|
|
|
.width-visual {
|
|
@apply w-full h-1.5 bg-gray-100 rounded-sm mb-2 overflow-hidden;
|
|
}
|
|
|
|
.width-bar {
|
|
@apply h-full bg-blue-500 rounded-sm transition-all duration-300;
|
|
}
|
|
|
|
.width-button.active .width-bar {
|
|
@apply bg-blue-600;
|
|
}
|
|
|
|
.width-button.recommended .width-bar {
|
|
@apply bg-green-500;
|
|
}
|
|
|
|
.width-button.active.recommended .width-bar {
|
|
@apply bg-green-600;
|
|
}
|
|
|
|
.width-label {
|
|
@apply text-sm font-medium text-gray-700;
|
|
}
|
|
|
|
.width-button.active .width-label {
|
|
@apply text-blue-700;
|
|
}
|
|
|
|
.width-button.recommended .width-label {
|
|
@apply text-green-700;
|
|
}
|
|
|
|
.width-preview {
|
|
@apply mt-4 p-3 bg-gray-50 rounded-lg border;
|
|
}
|
|
|
|
.preview-container {
|
|
@apply w-full bg-white rounded border p-2 mb-2;
|
|
}
|
|
|
|
.preview-field {
|
|
@apply transition-all duration-300;
|
|
}
|
|
|
|
.preview-input {
|
|
@apply w-full h-8 border border-gray-100 rounded px-2 bg-blue-600;
|
|
}
|
|
|
|
.preview-info {
|
|
@apply text-sm text-gray-600 text-center;
|
|
}
|
|
|
|
.feedback-label {
|
|
@apply text-sm font-medium text-gray-600;
|
|
}
|
|
|
|
.feedback-value {
|
|
@apply text-sm font-semibold text-gray-800;
|
|
}
|
|
|
|
.current-width-visual {
|
|
@apply mt-3 pt-3 border-t border-gray-200;
|
|
}
|
|
|
|
.visual-grid {
|
|
@apply grid grid-cols-12 gap-1 w-full mb-2;
|
|
}
|
|
|
|
.visual-cell {
|
|
@apply h-2 rounded-sm transition-colors duration-200;
|
|
}
|
|
|
|
.visual-cell.filled {
|
|
@apply bg-blue-500;
|
|
}
|
|
|
|
.visual-cell.empty {
|
|
@apply bg-gray-200;
|
|
}
|
|
|
|
.visual-label {
|
|
@apply text-center text-xs text-gray-600;
|
|
}
|
|
|
|
/* Options Management */
|
|
.options-manager {
|
|
@apply border border-gray-200 rounded-lg;
|
|
}
|
|
|
|
.options-header {
|
|
@apply flex justify-between items-center p-3 bg-gray-50 border-b border-gray-200;
|
|
}
|
|
|
|
.add-option-btn {
|
|
@apply inline-flex items-center px-3 py-1 text-sm font-medium text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 transition-colors;
|
|
}
|
|
|
|
.options-list {
|
|
@apply divide-y divide-gray-200;
|
|
}
|
|
|
|
.option-item {
|
|
@apply flex items-center p-3 hover:bg-gray-50;
|
|
}
|
|
|
|
.option-drag-handle {
|
|
@apply mr-3 cursor-move;
|
|
}
|
|
|
|
.option-content {
|
|
@apply flex-1 grid grid-cols-2 gap-3;
|
|
}
|
|
|
|
.option-field {
|
|
@apply mb-0;
|
|
}
|
|
|
|
.option-input {
|
|
@apply text-sm;
|
|
}
|
|
|
|
.option-remove-btn {
|
|
@apply ml-3 p-1 text-red-400 hover:text-red-600 rounded transition-colors;
|
|
}
|
|
|
|
.empty-options {
|
|
@apply text-center py-8;
|
|
}
|
|
|
|
.empty-add-btn {
|
|
@apply inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors;
|
|
}
|
|
|
|
/* Validation Styling */
|
|
.validation-options {
|
|
@apply space-y-4;
|
|
}
|
|
|
|
.validation-item {
|
|
@apply border border-gray-200 rounded-lg p-4;
|
|
}
|
|
|
|
.validation-toggle {
|
|
@apply flex items-center cursor-pointer;
|
|
}
|
|
|
|
.validation-toggle input[type="checkbox"] {
|
|
@apply sr-only;
|
|
}
|
|
|
|
.toggle-slider {
|
|
@apply relative inline-block w-10 h-6 mr-3 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out;
|
|
}
|
|
|
|
.validation-toggle input:checked + .toggle-slider {
|
|
@apply bg-blue-600;
|
|
}
|
|
|
|
.toggle-slider::before {
|
|
@apply absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform duration-200 ease-in-out;
|
|
content: '';
|
|
}
|
|
|
|
.validation-toggle input:checked + .toggle-slider::before {
|
|
@apply transform translate-x-4;
|
|
}
|
|
|
|
.validation-info {
|
|
@apply flex-1;
|
|
}
|
|
|
|
.validation-title {
|
|
@apply block text-sm font-medium text-gray-900;
|
|
}
|
|
|
|
.validation-desc {
|
|
@apply block text-xs text-gray-500 mt-1;
|
|
}
|
|
|
|
.validation-details {
|
|
@apply mt-4 pt-4 border-t border-gray-200;
|
|
}
|
|
|
|
/* Layout Grid Presets */
|
|
.preset-button {
|
|
@apply flex flex-col items-center p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-all duration-200;
|
|
}
|
|
|
|
.preset-button.preset-active {
|
|
@apply border-blue-500 bg-blue-100;
|
|
}
|
|
|
|
.preset-preview {
|
|
@apply w-full h-16 mb-2 bg-gray-100 rounded border border-gray-200 relative;
|
|
}
|
|
|
|
.preset-label {
|
|
@apply text-xs font-medium text-gray-700;
|
|
}
|
|
|
|
/* Preset Preview Layouts */
|
|
.sidebar-right-preview {
|
|
background: linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 50%, #d1d5db 50%, #d1d5db 100%);
|
|
background-size: 100% 50%;
|
|
background-position: 0 0, 0 100%;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.sidebar-left-preview {
|
|
background: linear-gradient(90deg, #d1d5db 0%, #d1d5db 50%, #e5e7eb 50%, #e5e7eb 100%);
|
|
background-size: 100% 50%;
|
|
background-position: 0 0, 0 100%;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.header-content-preview {
|
|
background: linear-gradient(180deg, #d1d5db 0%, #d1d5db 50%, #e5e7eb 50%, #e5e7eb 100%);
|
|
}
|
|
|
|
.three-column-preview {
|
|
background: linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 33.33%, #d1d5db 33.33%, #d1d5db 66.66%, #f3f4f6 66.66%, #f3f4f6 100%);
|
|
}
|
|
|
|
.two-by-two-preview {
|
|
background:
|
|
linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 50%, #d1d5db 50%, #d1d5db 100%) 0 0,
|
|
linear-gradient(90deg, #d1d5db 0%, #d1d5db 50%, #e5e7eb 50%, #e5e7eb 100%) 0 100%;
|
|
background-size: 100% 50%;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.form-layout-preview {
|
|
background:
|
|
linear-gradient(90deg, #d1d5db 0%, #d1d5db 100%) 0 0,
|
|
linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 50%, #d1d5db 50%, #d1d5db 100%) 0 33.33%,
|
|
linear-gradient(90deg, #f3f4f6 0%, #f3f4f6 100%) 0 66.66%;
|
|
background-size: 100% 33.33%;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
.header-sidebar-preview {
|
|
background:
|
|
linear-gradient(90deg, #d1d5db 0%, #d1d5db 100%) 0 0,
|
|
linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 50%, #f3f4f6 50%, #f3f4f6 100%) 0 33.33%,
|
|
linear-gradient(90deg, #d1d5db 0%, #d1d5db 50%, #f3f4f6 50%, #f3f4f6 100%) 0 66.66%;
|
|
background-size: 100% 33.33%;
|
|
background-repeat: no-repeat;
|
|
}
|
|
|
|
/* Layout Grid Cell Spanning Controls */
|
|
.cell-span-control {
|
|
@apply transition-all duration-200;
|
|
}
|
|
|
|
.cell-span-control:hover {
|
|
@apply transform scale-[1.02];
|
|
}
|
|
|
|
.cell-span-control input[type="number"] {
|
|
@apply text-center;
|
|
}
|
|
|
|
.cell-span-control input[type="number"]:focus {
|
|
@apply border-blue-500 ring-1 ring-blue-500;
|
|
}
|
|
|
|
/* Toggle Switch */
|
|
.toggle-switch {
|
|
@apply flex items-center;
|
|
}
|
|
|
|
.toggle-switch input[type="checkbox"] {
|
|
@apply sr-only;
|
|
}
|
|
|
|
.toggle-switch .toggle-slider {
|
|
@apply relative inline-block w-10 h-6 mr-3 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out;
|
|
}
|
|
|
|
.toggle-switch input:checked + .toggle-slider {
|
|
@apply bg-green-600;
|
|
}
|
|
|
|
.toggle-switch .toggle-slider::before {
|
|
@apply absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform duration-200 ease-in-out;
|
|
content: '';
|
|
}
|
|
|
|
.toggle-switch input:checked + .toggle-slider::before {
|
|
@apply transform translate-x-4;
|
|
}
|
|
|
|
.toggle-label {
|
|
@apply text-sm font-medium text-gray-700;
|
|
}
|
|
|
|
/* Button Styling */
|
|
.cancel-btn {
|
|
@apply px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors;
|
|
}
|
|
|
|
.reset-btn {
|
|
@apply inline-flex items-center px-4 py-2 text-sm font-medium text-orange-700 bg-orange-50 border border-orange-200 rounded-md hover:bg-orange-100 transition-colors;
|
|
}
|
|
|
|
.save-btn {
|
|
@apply inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 transition-colors;
|
|
}
|
|
|
|
/* Field Wrapper */
|
|
.field-wrapper {
|
|
@apply mb-0;
|
|
}
|
|
|
|
/* Type Changer Styles */
|
|
.type-changer-section {
|
|
@apply mb-6 p-4 border-2 border-dashed border-blue-200 rounded-lg bg-blue-50;
|
|
}
|
|
|
|
.type-change-preview {
|
|
@apply mt-4 p-4 bg-white border border-blue-300 rounded-lg;
|
|
}
|
|
|
|
.preview-header {
|
|
@apply flex items-center space-x-2 mb-3 pb-2 border-b border-gray-200;
|
|
}
|
|
|
|
.preview-content {
|
|
@apply space-y-2 mb-4;
|
|
}
|
|
|
|
.preview-item {
|
|
@apply flex items-center justify-between text-sm;
|
|
}
|
|
|
|
.preview-label {
|
|
@apply font-medium text-gray-600;
|
|
}
|
|
|
|
.preview-value {
|
|
@apply text-gray-800 bg-gray-100 px-2 py-1 rounded text-xs;
|
|
}
|
|
|
|
.preview-actions {
|
|
@apply flex items-center space-x-2 pt-3 border-t border-gray-200;
|
|
}
|
|
|
|
/* Mask Examples */
|
|
.mask-examples {
|
|
@apply mt-3 p-3 bg-gray-50 rounded-lg;
|
|
}
|
|
|
|
.example-button {
|
|
@apply text-left p-2 text-xs bg-white border border-gray-200 rounded hover:border-blue-300 hover:bg-blue-50 transition-colors;
|
|
}
|
|
|
|
/* Validation Guide Styles */
|
|
.validation-rule {
|
|
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
|
|
}
|
|
|
|
.rule-code {
|
|
@apply font-mono text-xs font-semibold text-blue-800 bg-blue-100 px-2 py-1 rounded;
|
|
}
|
|
|
|
.rule-desc {
|
|
@apply text-gray-600;
|
|
}
|
|
|
|
.example-item {
|
|
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
|
|
}
|
|
|
|
.example-code {
|
|
@apply font-mono text-xs font-semibold text-green-800 bg-green-100 px-2 py-1 rounded;
|
|
}
|
|
|
|
.example-desc {
|
|
@apply text-gray-600;
|
|
}
|
|
</style> |