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 MaskText from "~/components/formkit/TextMask.vue";
|
||||||
import FileDropzone from "~/components/formkit/FileDropzone.vue";
|
import FileDropzone from "~/components/formkit/FileDropzone.vue";
|
||||||
import Switch from "~/components/formkit/Switch.vue";
|
import Switch from "~/components/formkit/Switch.vue";
|
||||||
|
import SearchSelect from "~/components/formkit/SearchSelect.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
otp: createInput(OneTimePassword, {
|
otp: createInput(OneTimePassword, {
|
||||||
@ -17,4 +18,7 @@ export default {
|
|||||||
switch: createInput(Switch, {
|
switch: createInput(Switch, {
|
||||||
props: ["value", "disabled", "name", "id"],
|
props: ["value", "disabled", "name", "id"],
|
||||||
}),
|
}),
|
||||||
|
searchSelect: createInput(SearchSelect, {
|
||||||
|
props: ["options", "placeholder"],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
@ -51,6 +51,13 @@ const rangeClassification = {
|
|||||||
input: "formkit-input-range",
|
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
|
// export our definitions using our above
|
||||||
// templates and declare one-offs and
|
// templates and declare one-offs and
|
||||||
// overrides as needed.
|
// overrides as needed.
|
||||||
@ -99,4 +106,5 @@ export default {
|
|||||||
inner: "formkit-inner-dropzone",
|
inner: "formkit-inner-dropzone",
|
||||||
dropzone: "formkit-dropzone",
|
dropzone: "formkit-dropzone",
|
||||||
},
|
},
|
||||||
|
searchSelect: searchSelectClassification,
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.w-header {
|
.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));
|
background-color: rgb(var(--bg-2));
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* RS Modal Component */
|
/* RS Modal Component */
|
||||||
.modal {
|
.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);
|
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',
|
'text', 'textarea', 'number', 'email', 'password',
|
||||||
'date', 'time', 'datetime-local', 'url', 'tel',
|
'date', 'time', 'datetime-local', 'url', 'tel',
|
||||||
'select', 'checkbox', 'radio', 'file', 'range',
|
'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);
|
return inputTypes.includes(props.component.type);
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<!-- Selection Indicator -->
|
<!-- Selection Indicator -->
|
||||||
<div
|
<div
|
||||||
v-if="selectedComponentId === element.id"
|
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" />
|
<Icon name="heroicons:check-circle" class="w-3 h-3" />
|
||||||
<span>Selected</span>
|
<span>Selected</span>
|
||||||
@ -302,7 +302,7 @@ onUnmounted(() => {
|
|||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
grid-auto-flow: row; /* Changed: Remove 'dense' to preserve intentional spacing */
|
grid-auto-flow: row; /* Changed: Remove 'dense' to preserve intentional spacing */
|
||||||
column-gap: 16px;
|
column-gap: 16px;
|
||||||
row-gap: 16px;
|
row-gap: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
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',
|
type: 'checkbox',
|
||||||
name: 'Checkbox Group',
|
name: 'Checkbox Group',
|
||||||
|
@ -1855,132 +1855,7 @@ if (name && email) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Validation Rules Guide -->
|
<!-- Validation Rules Guide -->
|
||||||
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<ValidationRulesHelp />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2170,6 +2045,7 @@ if (name && email) {
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: Boolean,
|
modelValue: Boolean,
|
||||||
@ -2216,6 +2092,7 @@ const getComponentIcon = (type) => {
|
|||||||
mask: 'heroicons:pencil-square',
|
mask: 'heroicons:pencil-square',
|
||||||
hidden: 'heroicons:eye-slash',
|
hidden: 'heroicons:eye-slash',
|
||||||
select: 'heroicons:chevron-down',
|
select: 'heroicons:chevron-down',
|
||||||
|
searchSelect: 'heroicons:magnifying-glass',
|
||||||
checkbox: 'heroicons:check-badge',
|
checkbox: 'heroicons:check-badge',
|
||||||
radio: 'heroicons:radio',
|
radio: 'heroicons:radio',
|
||||||
switch: 'material-symbols:toggle-on',
|
switch: 'material-symbols:toggle-on',
|
||||||
@ -2252,6 +2129,7 @@ const getComponentTypeName = (type) => {
|
|||||||
mask: 'Masked Input',
|
mask: 'Masked Input',
|
||||||
hidden: 'Hidden Field',
|
hidden: 'Hidden Field',
|
||||||
select: 'Dropdown Menu',
|
select: 'Dropdown Menu',
|
||||||
|
searchSelect: 'Searchable Dropdown',
|
||||||
checkbox: 'Checkboxes',
|
checkbox: 'Checkboxes',
|
||||||
radio: 'Radio Buttons',
|
radio: 'Radio Buttons',
|
||||||
switch: 'Switch Toggle',
|
switch: 'Switch Toggle',
|
||||||
@ -2288,6 +2166,7 @@ const getComponentDescription = (type) => {
|
|||||||
mask: 'Formatted text input with custom patterns like phone numbers',
|
mask: 'Formatted text input with custom patterns like phone numbers',
|
||||||
hidden: 'Hidden field for storing data not visible to users',
|
hidden: 'Hidden field for storing data not visible to users',
|
||||||
select: 'Dropdown menu for choosing one option from a list',
|
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',
|
checkbox: 'Multiple checkboxes for selecting multiple options',
|
||||||
radio: 'Radio buttons for choosing one option from a group',
|
radio: 'Radio buttons for choosing one option from a group',
|
||||||
switch: 'Toggle switch for enabling/disabling options',
|
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: '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
|
return tabs
|
||||||
})
|
})
|
||||||
@ -2332,15 +2214,15 @@ const showField = (fieldName) => {
|
|||||||
if (!props.component) return false
|
if (!props.component) return false
|
||||||
|
|
||||||
const fieldConfig = {
|
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'],
|
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', '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', 'dynamic-list'],
|
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', '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'],
|
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'],
|
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'],
|
rows: ['textarea'],
|
||||||
options: ['select', 'checkbox', 'radio'],
|
options: ['select', 'searchSelect', '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'],
|
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']
|
readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel']
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2354,6 +2236,17 @@ const hasSpecificSettings = computed(() => {
|
|||||||
return specificTypes.includes(props.component.type)
|
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
|
// Validation helpers
|
||||||
const isRequired = computed({
|
const isRequired = computed({
|
||||||
get: () => configModel.value.validation?.includes('required') || false,
|
get: () => configModel.value.validation?.includes('required') || false,
|
||||||
@ -2495,7 +2388,7 @@ const compatibilityGroups = {
|
|||||||
textInputs: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask'],
|
textInputs: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask'],
|
||||||
|
|
||||||
// Selection inputs (can switch between each other)
|
// Selection inputs (can switch between each other)
|
||||||
selectionInputs: ['select', 'radio', 'checkbox'],
|
selectionInputs: ['select', 'searchSelect', 'radio', 'checkbox'],
|
||||||
|
|
||||||
// Date/time inputs (can switch between each other)
|
// Date/time inputs (can switch between each other)
|
||||||
dateTimeInputs: ['date', 'time', 'datetime-local'],
|
dateTimeInputs: ['date', 'time', 'datetime-local'],
|
||||||
@ -3166,6 +3059,16 @@ const getDefaultPropsForType = (type) => {
|
|||||||
],
|
],
|
||||||
validation: ''
|
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: {
|
radio: {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
options: [
|
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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<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 class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
@ -89,7 +89,7 @@ const resetTheme = () => {
|
|||||||
:class="{
|
:class="{
|
||||||
'right-[-300px]': hideConfigMenu,
|
'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
|
<div
|
||||||
@click="hideConfigMenu = !hideConfigMenu"
|
@click="hideConfigMenu = !hideConfigMenu"
|
||||||
|
@ -90,6 +90,7 @@
|
|||||||
"vue3-apexcharts": "^1.4.1",
|
"vue3-apexcharts": "^1.4.1",
|
||||||
"vue3-click-away": "^1.2.4",
|
"vue3-click-away": "^1.2.4",
|
||||||
"vue3-dropzone": "^2.0.1",
|
"vue3-dropzone": "^2.0.1",
|
||||||
|
"vue3-select-component": "^0.11.8",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
|
@ -35,9 +35,12 @@
|
|||||||
name="formName"
|
name="formName"
|
||||||
placeholder="Form Name"
|
placeholder="Form Name"
|
||||||
v-model="formName"
|
v-model="formName"
|
||||||
validation="required"
|
validation="required|minLength:3"
|
||||||
validation-visibility="live"
|
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"
|
class="form-name-input max-w-md"
|
||||||
:classes="{
|
:classes="{
|
||||||
outer: 'mb-0 w-full',
|
outer: 'mb-0 w-full',
|
||||||
@ -288,8 +291,8 @@
|
|||||||
<div
|
<div
|
||||||
class="grid-preview-container"
|
class="grid-preview-container"
|
||||||
:class="{
|
:class="{
|
||||||
'p-4': selectedDevice !== 'Desktop',
|
'px-4 pt-0': selectedDevice !== 'Desktop',
|
||||||
'p-6': selectedDevice === 'Desktop'
|
'p-0': selectedDevice === 'Desktop'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
@ -683,7 +686,12 @@
|
|||||||
type="text"
|
type="text"
|
||||||
label="Form Name"
|
label="Form Name"
|
||||||
placeholder="Enter a name for your new form"
|
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
|
<FormKit
|
||||||
@ -692,15 +700,23 @@
|
|||||||
label="Description (Optional)"
|
label="Description (Optional)"
|
||||||
placeholder="Enter a description"
|
placeholder="Enter a description"
|
||||||
:rows="3"
|
: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">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="newFormCategory"
|
v-model="newFormCategory"
|
||||||
type="select"
|
type="select"
|
||||||
label="Category (Optional)"
|
label="Category"
|
||||||
placeholder="Select a category"
|
placeholder="Select a category"
|
||||||
:options="categoryOptions"
|
:options="categoryOptions"
|
||||||
|
validation="required"
|
||||||
|
:validation-messages="{
|
||||||
|
required: 'Please select a category'
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
@ -739,7 +755,12 @@
|
|||||||
label="Form Name"
|
label="Form Name"
|
||||||
v-model="formStore.formName"
|
v-model="formStore.formName"
|
||||||
help="Name of your form"
|
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
|
<FormKit
|
||||||
@ -748,6 +769,10 @@
|
|||||||
v-model="formStore.formDescription"
|
v-model="formStore.formDescription"
|
||||||
help="Brief description of what this form is for"
|
help="Brief description of what this form is for"
|
||||||
rows="3"
|
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">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@ -807,7 +832,12 @@
|
|||||||
label="Submit Button Text"
|
label="Submit Button Text"
|
||||||
v-model="formStore.submitButton.label"
|
v-model="formStore.submitButton.label"
|
||||||
help="The text that appears on the submit button"
|
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"
|
placeholder="Submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1471,6 +1501,7 @@ import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettings
|
|||||||
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||||
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||||
import FormHistoryModal from '~/components/FormHistoryModal.vue';
|
import FormHistoryModal from '~/components/FormHistoryModal.vue';
|
||||||
|
import ValidationRulesHelp from '~/components/ValidationRulesHelp.vue';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
title: "Form Builder",
|
title: "Form Builder",
|
||||||
@ -3364,7 +3395,7 @@ const handleFormRestored = (restoredForm) => {
|
|||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
grid-auto-flow: row dense;
|
grid-auto-flow: row dense;
|
||||||
column-gap: 16px;
|
column-gap: 16px;
|
||||||
row-gap: 16px;
|
row-gap: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -3015,8 +3015,8 @@ const sendToBack = () => {
|
|||||||
<div
|
<div
|
||||||
v-show="showLeftPanel"
|
v-show="showLeftPanel"
|
||||||
:class="{
|
:class="{
|
||||||
'absolute inset-y-0 left-0 z-20 bg-white shadow-lg': isMobile,
|
'absolute inset-y-0 left-0 z-[1000] 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-[980] bg-white shadow-md': isTablet,
|
||||||
'relative w-60': !isMobile && !isTablet,
|
'relative w-60': !isMobile && !isTablet,
|
||||||
'w-72': isMobile,
|
'w-72': isMobile,
|
||||||
'w-80': isTablet
|
'w-80': isTablet
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -2239,6 +2239,11 @@
|
|||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
picomatch "^2.3.1"
|
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":
|
"@shimyshack/uid@^0.1.7":
|
||||||
version "0.1.9"
|
version "0.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/@shimyshack/uid/-/uid-0.1.9.tgz#bd05eb244c18236f12a5eca29ae41a1c726bf661"
|
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"
|
perfect-scrollbar "^1.5.5"
|
||||||
postcss-import "^12.0.0"
|
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:
|
vue@^2.5.16:
|
||||||
version "2.7.14"
|
version "2.7.14"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user