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:
Afiq 2025-08-06 21:34:56 +08:00
parent b29c035370
commit eab2ca3647
17 changed files with 456 additions and 153 deletions

View File

@ -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"],
}),
};

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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);
}

View 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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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',

View File

@ -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: [

View 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>

View 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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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;
}

View File

@ -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

View File

@ -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"