Add Searchable Select Component and Update Styles
- Introduced a new 'searchSelect' component to the form builder, allowing users to search and select options from a dropdown. - Updated FormBuilderComponents.vue to include the new searchable select component with default properties and options. - Enhanced form validation rules in various components to support the new searchable select input type. - Adjusted z-index values in multiple components and styles for improved layering and visibility. - Refined CSS styles for modal and header components to ensure consistent appearance across the application.
This commit is contained in:
parent
b29c035370
commit
eab2ca3647
@ -3,6 +3,7 @@ import OneTimePassword from "~/components/formkit/OneTimePassword.vue";
|
||||
import MaskText from "~/components/formkit/TextMask.vue";
|
||||
import FileDropzone from "~/components/formkit/FileDropzone.vue";
|
||||
import Switch from "~/components/formkit/Switch.vue";
|
||||
import SearchSelect from "~/components/formkit/SearchSelect.vue";
|
||||
|
||||
export default {
|
||||
otp: createInput(OneTimePassword, {
|
||||
@ -17,4 +18,7 @@ export default {
|
||||
switch: createInput(Switch, {
|
||||
props: ["value", "disabled", "name", "id"],
|
||||
}),
|
||||
searchSelect: createInput(SearchSelect, {
|
||||
props: ["options", "placeholder"],
|
||||
}),
|
||||
};
|
||||
|
@ -51,6 +51,13 @@ const rangeClassification = {
|
||||
input: "formkit-input-range",
|
||||
};
|
||||
|
||||
const searchSelectClassification = {
|
||||
label: "formkit-label-search-select",
|
||||
inner: "formkit-inner-search-select",
|
||||
input: "formkit-input-search-select",
|
||||
message: "formkit-message-search-select",
|
||||
};
|
||||
|
||||
// export our definitions using our above
|
||||
// templates and declare one-offs and
|
||||
// overrides as needed.
|
||||
@ -99,4 +106,5 @@ export default {
|
||||
inner: "formkit-inner-dropzone",
|
||||
dropzone: "formkit-dropzone",
|
||||
},
|
||||
searchSelect: searchSelectClassification,
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ body {
|
||||
}
|
||||
|
||||
.w-header {
|
||||
@apply z-20 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md;
|
||||
@apply z-[1000] fixed top-0 right-0 px-5 py-3 duration-300 shadow-md;
|
||||
background-color: rgb(var(--bg-2));
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* RS Modal Component */
|
||||
.modal {
|
||||
@apply fixed top-0 left-0 w-full h-full overflow-hidden z-[1000] duration-300;
|
||||
@apply fixed top-0 left-0 w-full h-full overflow-hidden z-[10000] duration-300;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
|
129
assets/style/css/form/searchSelect.css
Normal file
129
assets/style/css/form/searchSelect.css
Normal file
@ -0,0 +1,129 @@
|
||||
.formkit-label-search-select{
|
||||
@apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||
}
|
||||
|
||||
.formkit-label-searchSelect {
|
||||
@apply block mb-2 font-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||
}
|
||||
|
||||
.formkit-inner-searchSelect {
|
||||
@apply relative w-full;
|
||||
}
|
||||
|
||||
.formkit-input-searchSelect {
|
||||
@apply w-full min-h-[38px] px-3 py-2
|
||||
text-sm
|
||||
rounded-lg
|
||||
border
|
||||
border-[rgb(var(--fk-border-color))]
|
||||
bg-[rgb(var(--bg-2))]
|
||||
placeholder-secondary
|
||||
focus:border-primary
|
||||
focus:outline-none
|
||||
disabled:opacity-50
|
||||
disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.formkit-message-searchSelect {
|
||||
@apply formkit-invalid:text-red-500 dark:formkit-invalid:text-danger;
|
||||
}
|
||||
|
||||
/* Vue Select specific styling to match FormKit theme */
|
||||
.v-select {
|
||||
@apply relative w-full;
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-toggle {
|
||||
@apply flex items-center min-h-[38px] px-3 py-2
|
||||
bg-[rgb(var(--bg-2))]
|
||||
border border-[rgb(var(--fk-border-color))]
|
||||
rounded-lg
|
||||
cursor-pointer
|
||||
transition-all duration-150 ease-in-out;
|
||||
}
|
||||
|
||||
.v-select:not(.vs--disabled) .vs__dropdown-toggle:hover {
|
||||
@apply border-[rgb(var(--fk-border-hover-color))];
|
||||
}
|
||||
|
||||
.v-select.vs--open .vs__dropdown-toggle {
|
||||
@apply border-primary shadow-[0_0_0_1px_rgb(var(--fk-primary-color))];
|
||||
}
|
||||
|
||||
.v-select.vs--disabled .vs__dropdown-toggle {
|
||||
@apply bg-[rgb(var(--fk-disabled-bg))] cursor-not-allowed opacity-50;
|
||||
}
|
||||
|
||||
.v-select .vs__selected-options {
|
||||
@apply flex items-center flex-1 py-0 px-0 min-h-[1.5rem];
|
||||
}
|
||||
|
||||
.v-select .vs__selected {
|
||||
@apply text-[rgb(var(--fk-text-color))] text-sm m-0 p-0 border-none bg-transparent;
|
||||
}
|
||||
|
||||
.v-select .vs__search {
|
||||
@apply bg-transparent border-none outline-none text-sm text-[rgb(var(--fk-text-color))] m-0 p-0 w-auto max-w-full;
|
||||
}
|
||||
|
||||
.v-select .vs__search::placeholder {
|
||||
@apply text-[rgb(var(--fk-placeholder-color))];
|
||||
}
|
||||
|
||||
.v-select .vs__actions {
|
||||
@apply flex items-center px-1;
|
||||
}
|
||||
|
||||
.v-select .vs__open-indicator {
|
||||
@apply text-[rgb(var(--fk-text-secondary-color))] cursor-pointer transition-transform duration-200 ease-in-out;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.v-select.vs--open .vs__open-indicator {
|
||||
@apply rotate-180;
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-menu {
|
||||
@apply absolute top-full left-0 right-0 z-[1050] mt-1
|
||||
bg-[rgb(var(--bg-2))]
|
||||
border border-[rgb(var(--fk-border-color))]
|
||||
rounded-lg
|
||||
shadow-lg
|
||||
max-h-[300px]
|
||||
overflow-y-auto
|
||||
py-1;
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-option {
|
||||
@apply px-3 py-2 cursor-pointer text-sm text-[rgb(var(--fk-text-color))] transition-colors duration-150 ease-in-out leading-5;
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-option--highlight {
|
||||
@apply bg-[rgb(var(--fk-hover-bg))] text-[rgb(var(--fk-text-color))];
|
||||
}
|
||||
|
||||
.v-select .vs__dropdown-option--selected {
|
||||
@apply bg-primary text-white font-medium;
|
||||
}
|
||||
|
||||
.v-select .vs__no-options {
|
||||
@apply px-3 py-3 text-[rgb(var(--fk-text-secondary-color))] italic text-center text-sm;
|
||||
}
|
||||
|
||||
/* Hide the clear button since we set clearable to false */
|
||||
.v-select .vs__clear {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing and alignment */
|
||||
.v-select .vs__dropdown-toggle {
|
||||
@apply px-3 py-2;
|
||||
}
|
||||
|
||||
.v-select .vs__selected-options {
|
||||
@apply p-0 m-0;
|
||||
}
|
||||
|
||||
.v-select .vs__actions {
|
||||
@apply px-1;
|
||||
}
|
@ -1105,7 +1105,8 @@ const isInputType = computed(() => {
|
||||
'text', 'textarea', 'number', 'email', 'password',
|
||||
'date', 'time', 'datetime-local', 'url', 'tel',
|
||||
'select', 'checkbox', 'radio', 'file', 'range',
|
||||
'color', 'hidden', 'mask', 'otp', 'dropzone', 'switch'
|
||||
'color', 'hidden', 'mask', 'otp', 'dropzone', 'switch',
|
||||
'searchSelect' // Add our new searchable select component
|
||||
];
|
||||
|
||||
return inputTypes.includes(props.component.type);
|
||||
|
@ -49,7 +49,7 @@
|
||||
<!-- Selection Indicator -->
|
||||
<div
|
||||
v-if="selectedComponentId === element.id"
|
||||
class="absolute top-2 left-2 flex items-center space-x-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-20"
|
||||
class="absolute top-2 left-2 flex items-center space-x-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-[1000]"
|
||||
>
|
||||
<Icon name="heroicons:check-circle" class="w-3 h-3" />
|
||||
<span>Selected</span>
|
||||
@ -302,7 +302,7 @@ onUnmounted(() => {
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row; /* Changed: Remove 'dense' to preserve intentional spacing */
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
row-gap: 12px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
@ -304,6 +304,31 @@ const availableComponents = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'searchSelect',
|
||||
name: 'Searchable Select',
|
||||
category: 'Selection Inputs',
|
||||
icon: 'material-symbols:search',
|
||||
description: 'Dropdown with search functionality',
|
||||
defaultProps: {
|
||||
type: 'searchSelect',
|
||||
placeholder: 'Search and select an option',
|
||||
help: 'Type to search for options',
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' },
|
||||
{ label: 'Option 3', value: 'option_3' }
|
||||
],
|
||||
validation: '',
|
||||
// Conditional Logic Properties
|
||||
conditionalLogic: {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
action: 'show',
|
||||
operator: 'and'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'Checkbox Group',
|
||||
|
@ -1855,132 +1855,7 @@ if (name && email) {
|
||||
/>
|
||||
|
||||
<!-- Validation Rules Guide -->
|
||||
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h5 class="text-sm font-medium text-blue-800 mb-3 flex items-center">
|
||||
<Icon name="heroicons:information-circle" class="w-4 h-4 mr-2" />
|
||||
Validation Rules Guide
|
||||
</h5>
|
||||
|
||||
<div class="space-y-4 text-sm">
|
||||
<!-- Basic Rules -->
|
||||
<div>
|
||||
<h6 class="font-medium text-blue-700 mb-2">Basic Rules</h6>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">required</code>
|
||||
<span class="rule-desc">Field must be filled</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">email</code>
|
||||
<span class="rule-desc">Must be valid email</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">url</code>
|
||||
<span class="rule-desc">Must be valid URL</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">number</code>
|
||||
<span class="rule-desc">Must be numeric</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Length Rules -->
|
||||
<div>
|
||||
<h6 class="font-medium text-blue-700 mb-2">Length Rules</h6>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">length:5</code>
|
||||
<span class="rule-desc">Exactly 5 characters</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">length:3,20</code>
|
||||
<span class="rule-desc">Between 3-20 characters</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">min:3</code>
|
||||
<span class="rule-desc">Minimum 3 characters</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">max:50</code>
|
||||
<span class="rule-desc">Maximum 50 characters</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number Rules -->
|
||||
<div>
|
||||
<h6 class="font-medium text-blue-700 mb-2">Number Rules</h6>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">between:1,100</code>
|
||||
<span class="rule-desc">Between 1 and 100</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">min_value:0</code>
|
||||
<span class="rule-desc">Minimum value 0</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">max_value:999</code>
|
||||
<span class="rule-desc">Maximum value 999</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">integer</code>
|
||||
<span class="rule-desc">Must be whole number</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern Rules -->
|
||||
<div>
|
||||
<h6 class="font-medium text-blue-700 mb-2">Pattern Rules</h6>
|
||||
<div class="grid grid-cols-1 gap-2 text-xs">
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">alpha</code>
|
||||
<span class="rule-desc">Only letters (A-Z, a-z)</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">alpha_numeric</code>
|
||||
<span class="rule-desc">Letters and numbers only</span>
|
||||
</div>
|
||||
<div class="validation-rule">
|
||||
<code class="rule-code">alpha_spaces</code>
|
||||
<span class="rule-desc">Letters and spaces only</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Examples Section -->
|
||||
<div class="mt-4 pt-4 border-t border-blue-200">
|
||||
<h6 class="font-medium text-blue-700 mb-2">Examples</h6>
|
||||
<div class="space-y-2 text-xs">
|
||||
<div class="example-item">
|
||||
<code class="example-code">required|email</code>
|
||||
<span class="example-desc">Required email field</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<code class="example-code">required|length:3,50</code>
|
||||
<span class="example-desc">Required text, 3-50 characters</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<code class="example-code">number|between:1,100</code>
|
||||
<span class="example-desc">Number between 1-100</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<code class="example-code">required|alpha_numeric|min:5</code>
|
||||
<span class="example-desc">Required alphanumeric, min 5 chars</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Tips -->
|
||||
<div class="mt-3 pt-3 border-t border-blue-200">
|
||||
<p class="text-xs text-blue-600">
|
||||
<strong>💡 Tips:</strong> Separate rules with | (pipe) • Order doesn't matter • Leave empty for no validation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ValidationRulesHelp />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2170,6 +2045,7 @@ if (name && email) {
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
@ -2216,6 +2092,7 @@ const getComponentIcon = (type) => {
|
||||
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',
|
||||
@ -2252,6 +2129,7 @@ const getComponentTypeName = (type) => {
|
||||
mask: 'Masked Input',
|
||||
hidden: 'Hidden Field',
|
||||
select: 'Dropdown Menu',
|
||||
searchSelect: 'Searchable Dropdown',
|
||||
checkbox: 'Checkboxes',
|
||||
radio: 'Radio Buttons',
|
||||
switch: 'Switch Toggle',
|
||||
@ -2288,6 +2166,7 @@ const getComponentDescription = (type) => {
|
||||
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',
|
||||
@ -2322,7 +2201,10 @@ const availableTabs = computed(() => {
|
||||
tabs.push({ id: 'options', label: 'Options', icon: 'heroicons:list-bullet' })
|
||||
}
|
||||
|
||||
tabs.push({ id: 'validation', label: 'Validation', icon: 'heroicons:shield-check' })
|
||||
// Only add validation tab for components that support validation
|
||||
if (supportsValidation.value) {
|
||||
tabs.push({ id: 'validation', label: 'Validation', icon: 'heroicons:shield-check' })
|
||||
}
|
||||
|
||||
return tabs
|
||||
})
|
||||
@ -2332,15 +2214,15 @@ const showField = (fieldName) => {
|
||||
if (!props.component) return false
|
||||
|
||||
const fieldConfig = {
|
||||
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'dynamic-list'],
|
||||
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||
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'],
|
||||
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'],
|
||||
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'],
|
||||
value: ['heading', 'paragraph', 'hidden'],
|
||||
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', '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'],
|
||||
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'],
|
||||
rows: ['textarea'],
|
||||
options: ['select', 'checkbox', 'radio'],
|
||||
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||
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'],
|
||||
readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel']
|
||||
}
|
||||
|
||||
@ -2354,6 +2236,17 @@ const hasSpecificSettings = computed(() => {
|
||||
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,
|
||||
@ -2495,7 +2388,7 @@ const compatibilityGroups = {
|
||||
textInputs: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask'],
|
||||
|
||||
// Selection inputs (can switch between each other)
|
||||
selectionInputs: ['select', 'radio', 'checkbox'],
|
||||
selectionInputs: ['select', 'searchSelect', 'radio', 'checkbox'],
|
||||
|
||||
// Date/time inputs (can switch between each other)
|
||||
dateTimeInputs: ['date', 'time', 'datetime-local'],
|
||||
@ -3166,6 +3059,16 @@ const getDefaultPropsForType = (type) => {
|
||||
],
|
||||
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: [
|
||||
|
105
components/ValidationRulesHelp.vue
Normal file
105
components/ValidationRulesHelp.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="validation-rules-help">
|
||||
<div class="bg-blue-50 border border-blue-200 text-blue-800 p-3 rounded mb-4">
|
||||
<div class="flex items-start">
|
||||
<Icon name="material-symbols:info" class="w-5 h-5 mr-2 mt-0.5" />
|
||||
<div>
|
||||
<h4 class="font-medium text-sm">Validation Rules</h4>
|
||||
<p class="text-xs mt-1">
|
||||
Use the pipe character (|) to separate multiple validation rules.
|
||||
For rules with parameters, use a colon (:) followed by the parameter value.
|
||||
Example: <code>required|email|minLength:5</code>
|
||||
</p>
|
||||
<p class="text-xs mt-1 text-blue-700 font-medium">
|
||||
Note: Not all components support validation. Components like Form Section, Repeating Group,
|
||||
Info Display, and other structural/display components do not use validation rules.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Basic Validation</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>required</code> - Field must not be empty</li>
|
||||
<li><code>required:trim</code> - Field must not be empty (ignores whitespace)</li>
|
||||
<li><code>email</code> - Must be a valid email address</li>
|
||||
<li><code>url</code> - Must be a valid URL</li>
|
||||
<li><code>number</code> - Must be a valid number</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Length & Size</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>minLength:5</code> - Minimum length of 5 characters</li>
|
||||
<li><code>maxLength:20</code> - Maximum length of 20 characters</li>
|
||||
<li><code>length:5,20</code> - Length between 5 and 20 characters</li>
|
||||
<li><code>min:10</code> - Minimum value of 10 (for numbers)</li>
|
||||
<li><code>max:100</code> - Maximum value of 100 (for numbers)</li>
|
||||
<li><code>between:1,100</code> - Value between 1 and 100</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Character Types</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>alpha</code> - Only alphabetical characters</li>
|
||||
<li><code>alphanumeric</code> - Only letters and numbers</li>
|
||||
<li><code>alpha_spaces</code> - Only letters and spaces</li>
|
||||
<li><code>contains_lowercase</code> - Has at least one lowercase letter</li>
|
||||
<li><code>contains_uppercase</code> - Has at least one uppercase letter</li>
|
||||
<li><code>contains_numeric</code> - Has at least one number</li>
|
||||
<li><code>contains_symbol</code> - Has at least one symbol</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Special Validation</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>confirm:password</code> - Must match the field named 'password'</li>
|
||||
<li><code>matches:/^[A-Z]+$/</code> - Must match the regex pattern</li>
|
||||
<li><code>starts_with:https://</code> - Must start with specific text</li>
|
||||
<li><code>ends_with:.com</code> - Must end with specific text</li>
|
||||
<li><code>is:option1,option2</code> - Must be one of the listed values</li>
|
||||
<li><code>not:admin,root</code> - Must not be one of the listed values</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Date Validation</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>date_before:2023-12-31</code> - Date before specified date</li>
|
||||
<li><code>date_after:2023-01-01</code> - Date after specified date</li>
|
||||
<li><code>date_between:2023-01-01,2023-12-31</code> - Date in range</li>
|
||||
<li><code>date_format:YYYY-MM-DD</code> - Date in specific format</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Validation Hints</h5>
|
||||
<ul class="text-xs space-y-1 text-gray-600">
|
||||
<li><code>*rule</code> - Force rule to run even if others fail</li>
|
||||
<li><code>+rule</code> - Run rule even when field is empty</li>
|
||||
<li><code>?rule</code> - Make rule optional (non-blocking)</li>
|
||||
<li><code>(200)rule</code> - Debounce rule by 200ms</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// No additional script needed
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.validation-rules-help code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
84
components/formkit/SearchSelect.vue
Normal file
84
components/formkit/SearchSelect.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="formkit-search-select">
|
||||
<VueSelect
|
||||
:value="_value"
|
||||
@input="handleChange"
|
||||
:options="context.options || []"
|
||||
:placeholder="context.placeholder || 'Search and select an option'"
|
||||
:disabled="context.disabled"
|
||||
:searchable="true"
|
||||
:clearable="false"
|
||||
label="label"
|
||||
:reduce="option => option.value"
|
||||
:class="[
|
||||
'vue-select-wrapper',
|
||||
{ 'vue-select-disabled': context.disabled }
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Hidden native select for FormKit value handling -->
|
||||
<select
|
||||
:value="_value"
|
||||
:name="context.node.name"
|
||||
:id="context.id"
|
||||
:disabled="context.disabled"
|
||||
:required="context.attrs.required"
|
||||
class="hidden-select"
|
||||
tabindex="-1"
|
||||
>
|
||||
<option v-if="context.placeholder" value="" disabled>{{ context.placeholder }}</option>
|
||||
<option
|
||||
v-for="option in context.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import VueSelect from 'vue-select'
|
||||
import 'vue-select/dist/vue-select.css'
|
||||
import '~/assets/style/css/form/searchSelect.css'
|
||||
|
||||
const props = defineProps({
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// Computed value that syncs with FormKit
|
||||
const _value = computed({
|
||||
get: () => props.context.node._value,
|
||||
set: (value) => props.context.node.input(value)
|
||||
})
|
||||
|
||||
// Handle value changes from vue3-select-component
|
||||
const handleChange = (value) => {
|
||||
props.context.node.input(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.formkit-search-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hidden-select {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
</style>
|
@ -3,7 +3,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="w-full h-14 z-20 bg-white dark:bg-slate-800 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md shadow-slate-200 dark:shadow-slate-900"
|
||||
class="w-full h-14 z-[1000] bg-white dark:bg-slate-800 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md shadow-slate-200 dark:shadow-slate-900"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
|
@ -89,7 +89,7 @@ const resetTheme = () => {
|
||||
:class="{
|
||||
'right-[-300px]': hideConfigMenu,
|
||||
}"
|
||||
class="h-full w-[300px] bg-white dark:bg-slate-800 fixed top-0 right-0 z-20 shadow-md shadow-slate-200 dark:shadow-slate-900 duration-300"
|
||||
class="h-full w-[300px] bg-white dark:bg-slate-800 fixed top-0 right-0 z-[1000] shadow-md shadow-slate-200 dark:shadow-slate-900 duration-300"
|
||||
>
|
||||
<div
|
||||
@click="hideConfigMenu = !hideConfigMenu"
|
||||
|
@ -90,6 +90,7 @@
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-click-away": "^1.2.4",
|
||||
"vue3-dropzone": "^2.0.1",
|
||||
"vue3-select-component": "^0.11.8",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
|
@ -35,9 +35,12 @@
|
||||
name="formName"
|
||||
placeholder="Form Name"
|
||||
v-model="formName"
|
||||
validation="required"
|
||||
validation="required|minLength:3"
|
||||
validation-visibility="live"
|
||||
:validation-messages="{ required: 'Please enter a form name' }"
|
||||
:validation-messages="{
|
||||
required: 'Please enter a form name',
|
||||
minLength: 'Form name must be at least 3 characters'
|
||||
}"
|
||||
class="form-name-input max-w-md"
|
||||
:classes="{
|
||||
outer: 'mb-0 w-full',
|
||||
@ -288,8 +291,8 @@
|
||||
<div
|
||||
class="grid-preview-container"
|
||||
:class="{
|
||||
'p-4': selectedDevice !== 'Desktop',
|
||||
'p-6': selectedDevice === 'Desktop'
|
||||
'px-4 pt-0': selectedDevice !== 'Desktop',
|
||||
'p-0': selectedDevice === 'Desktop'
|
||||
}"
|
||||
>
|
||||
<template
|
||||
@ -683,7 +686,12 @@
|
||||
type="text"
|
||||
label="Form Name"
|
||||
placeholder="Enter a name for your new form"
|
||||
validation="required"
|
||||
validation="required|minLength:3|maxLength:50"
|
||||
:validation-messages="{
|
||||
required: 'Please enter a form name',
|
||||
minLength: 'Form name must be at least 3 characters',
|
||||
maxLength: 'Form name must be less than 50 characters'
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
@ -692,15 +700,23 @@
|
||||
label="Description (Optional)"
|
||||
placeholder="Enter a description"
|
||||
:rows="3"
|
||||
validation="maxLength:200"
|
||||
:validation-messages="{
|
||||
maxLength: 'Description must be less than 200 characters'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="newFormCategory"
|
||||
type="select"
|
||||
label="Category (Optional)"
|
||||
label="Category"
|
||||
placeholder="Select a category"
|
||||
:options="categoryOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Please select a category'
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
@ -739,7 +755,12 @@
|
||||
label="Form Name"
|
||||
v-model="formStore.formName"
|
||||
help="Name of your form"
|
||||
validation="required"
|
||||
validation="required|minLength:3|maxLength:50"
|
||||
:validation-messages="{
|
||||
required: 'Please enter a form name',
|
||||
minLength: 'Form name must be at least 3 characters',
|
||||
maxLength: 'Form name must be less than 50 characters'
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
@ -748,6 +769,10 @@
|
||||
v-model="formStore.formDescription"
|
||||
help="Brief description of what this form is for"
|
||||
rows="3"
|
||||
validation="maxLength:200"
|
||||
:validation-messages="{
|
||||
maxLength: 'Description must be less than 200 characters'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -807,7 +832,12 @@
|
||||
label="Submit Button Text"
|
||||
v-model="formStore.submitButton.label"
|
||||
help="The text that appears on the submit button"
|
||||
validation="required"
|
||||
validation="required|minLength:2|maxLength:30"
|
||||
:validation-messages="{
|
||||
required: 'Button text is required',
|
||||
minLength: 'Button text must be at least 2 characters',
|
||||
maxLength: 'Button text must be less than 30 characters'
|
||||
}"
|
||||
placeholder="Submit"
|
||||
/>
|
||||
</div>
|
||||
@ -1471,6 +1501,7 @@ import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettings
|
||||
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||
import FormHistoryModal from '~/components/FormHistoryModal.vue';
|
||||
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue';
|
||||
|
||||
definePageMeta({
|
||||
title: "Form Builder",
|
||||
@ -3364,7 +3395,7 @@ const handleFormRestored = (restoredForm) => {
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: row dense;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
row-gap: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -3015,8 +3015,8 @@ const sendToBack = () => {
|
||||
<div
|
||||
v-show="showLeftPanel"
|
||||
:class="{
|
||||
'absolute inset-y-0 left-0 z-20 bg-white shadow-lg': isMobile,
|
||||
'absolute inset-y-0 left-0 z-10 bg-white shadow-md': isTablet,
|
||||
'absolute inset-y-0 left-0 z-[1000] bg-white shadow-lg': isMobile,
|
||||
'absolute inset-y-0 left-0 z-[980] bg-white shadow-md': isTablet,
|
||||
'relative w-60': !isMobile && !isTablet,
|
||||
'w-72': isMobile,
|
||||
'w-80': isTablet
|
||||
|
12
yarn.lock
12
yarn.lock
@ -2239,6 +2239,11 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.44.0":
|
||||
version "4.44.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz#597d40f60d4b15bedbbacf2491a69c5b67a58e93"
|
||||
integrity sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==
|
||||
|
||||
"@shimyshack/uid@^0.1.7":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@shimyshack/uid/-/uid-0.1.9.tgz#bd05eb244c18236f12a5eca29ae41a1c726bf661"
|
||||
@ -9583,6 +9588,13 @@ vue3-perfect-scrollbar@^1.6.1:
|
||||
perfect-scrollbar "^1.5.5"
|
||||
postcss-import "^12.0.0"
|
||||
|
||||
vue3-select-component@^0.11.8:
|
||||
version "0.11.8"
|
||||
resolved "https://registry.yarnpkg.com/vue3-select-component/-/vue3-select-component-0.11.8.tgz#519b6322591eb0943726bee73228d87f5f760cd0"
|
||||
integrity sha512-fNFZXg/fwrels/xYH3URXkV4df4mPxy4q35DZMjUth6u1JUGYHTci29ND5GgNmQncS2vQeMyeTzejqlQD16zOA==
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-linux-x64-gnu" "4.44.0"
|
||||
|
||||
vue@^2.5.16:
|
||||
version "2.7.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
|
||||
|
Loading…
x
Reference in New Issue
Block a user