Better form themes (#465)
* Working on custom radius + input size * Fix date input clear vertical align * Moslty finished implementing small size * Polishing larger theme * Finish large theme * Added size/radius options in form editor * Darken help text, improve switch input help location * Slight form editor improvement * Fix styling * Polish of the form editor
This commit is contained in:
@@ -10,9 +10,16 @@
|
||||
:disabled="disabled ? true : null"
|
||||
:name="name"
|
||||
:color="color"
|
||||
:theme="theme"
|
||||
>
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
<slot
|
||||
name="label"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
theme.SelectInput.fontSize,
|
||||
]"
|
||||
>{{ label }}</span>
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div
|
||||
:class="[
|
||||
theme.CodeInput.input,
|
||||
theme.CodeInput.borderRadius,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
:popper="{ placement: 'bottom-start' }"
|
||||
>
|
||||
<button
|
||||
ref="datepicker"
|
||||
class="cursor-pointer overflow-hidden"
|
||||
:class="inputClasses"
|
||||
:disabled="props.disabled"
|
||||
ref="datepicker"
|
||||
>
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="flex items-stretch min-w-0">
|
||||
<div
|
||||
class="flex-grow min-w-0 flex items-center gap-x-2"
|
||||
:class="[
|
||||
props.theme.default.inputSpacing.vertical,
|
||||
props.theme.default.inputSpacing.horizontal,
|
||||
{'hover:bg-gray-50 dark:hover:bg-gray-900': !props.disabled}
|
||||
props.theme.DateInput.spacing.horizontal,
|
||||
props.theme.DateInput.spacing.vertical,
|
||||
props.theme.DateInput.fontSize,
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
@@ -29,16 +29,24 @@
|
||||
class="w-4 h-4 flex-shrink-0"
|
||||
dynamic
|
||||
/>
|
||||
<div class="flex-grow truncate overflow-hidden">
|
||||
<p class="flex-grow truncate h-[24px]">
|
||||
<div class="flex-grow truncate overflow-hidden flex items-center">
|
||||
<p
|
||||
v-if="formattedDatePreview"
|
||||
class="flex-grow truncate"
|
||||
>
|
||||
{{ formattedDatePreview }}
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-transparent"
|
||||
>
|
||||
-
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="fromDate && !props.disabled"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
|
||||
:class="[props.theme.default.inputSpacing.vertical]"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2 flex items-center"
|
||||
@click.prevent="clear()"
|
||||
>
|
||||
<Icon
|
||||
@@ -133,7 +141,7 @@ const modeledValue = computed({
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
const classes = [props.theme.DateInput.input, 'w-full']
|
||||
const classes = [props.theme.DateInput.input, props.theme.DateInput.borderRadius]
|
||||
if (props.disabled) {
|
||||
classes.push('!cursor-not-allowed dark:!bg-gray-600 !bg-gray-200')
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
</template>
|
||||
<div
|
||||
v-if="cameraUpload && isInWebcam"
|
||||
class="hidden sm:block w-full min-h-40"
|
||||
class="hidden sm:block w-full"
|
||||
:class="[
|
||||
theme.fileInput.minHeight
|
||||
]"
|
||||
>
|
||||
<camera-upload
|
||||
v-if="cameraUpload"
|
||||
@@ -17,9 +20,17 @@
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col w-full items-center justify-center transition-colors duration-40"
|
||||
:class="[{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled,
|
||||
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
|
||||
['hover:'+theme.fileInput.inputHover.light +' dark:hover:'+theme.fileInput.inputHover.dark]: !loading}, theme.fileInput.input]"
|
||||
:class="[
|
||||
{'!cursor-not-allowed':disabled, 'cursor-pointer':!disabled,
|
||||
[theme.fileInput.inputHover.light + ' dark:'+theme.fileInput.inputHover.dark]: uploadDragoverEvent,
|
||||
['hover:'+theme.fileInput.inputHover.light +' dark:hover:'+theme.fileInput.inputHover.dark]: !loading},
|
||||
theme.fileInput.input,
|
||||
theme.fileInput.borderRadius,
|
||||
theme.fileInput.spacing.horizontal,
|
||||
theme.fileInput.spacing.vertical,
|
||||
theme.fileInput.fontSize,
|
||||
theme.fileInput.minHeight
|
||||
]"
|
||||
@dragover.prevent="uploadDragoverEvent=true"
|
||||
@dragleave.prevent="uploadDragoverEvent=false"
|
||||
@drop.prevent="onUploadDropEvent"
|
||||
@@ -66,7 +77,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@@ -76,7 +87,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-500 font-semibold select-none">
|
||||
<p class="mt-2 text-sm text-gray-500 font-medium select-none">
|
||||
Click to choose {{ multiple ? 'file(s)' : 'a file' }} or drag here
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-600 select-none">
|
||||
@@ -129,24 +140,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import {inputProps, useFormInput} from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import UploadedFile from './components/UploadedFile.vue'
|
||||
import OpenFormButton from '../open/forms/OpenFormButton.vue'
|
||||
import CameraUpload from './components/CameraUpload.vue'
|
||||
import { storeFile } from "~/lib/file-uploads.js"
|
||||
import {storeFile} from "~/lib/file-uploads.js"
|
||||
|
||||
export default {
|
||||
name: 'FileInput',
|
||||
components: { InputWrapper, UploadedFile, OpenFormButton },
|
||||
components: {InputWrapper, UploadedFile, OpenFormButton},
|
||||
mixins: [],
|
||||
props: {
|
||||
...inputProps,
|
||||
multiple: { type: Boolean, default: true },
|
||||
cameraUpload: { type: Boolean, default: false },
|
||||
mbLimit: { type: Number, default: 5 },
|
||||
accept: { type: String, default: '' },
|
||||
moveToFormAssets: { type: Boolean, default: false }
|
||||
multiple: {type: Boolean, default: true},
|
||||
cameraUpload: {type: Boolean, default: false},
|
||||
mbLimit: {type: Number, default: 5},
|
||||
accept: {type: String, default: ''},
|
||||
moveToFormAssets: {type: Boolean, default: false}
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
@@ -159,7 +170,7 @@ export default {
|
||||
files: [],
|
||||
uploadDragoverEvent: false,
|
||||
loading: false,
|
||||
isInWebcam:false
|
||||
isInWebcam: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
@@ -254,13 +265,13 @@ export default {
|
||||
this.uploadFileToServer(files.item(i))
|
||||
}
|
||||
},
|
||||
openWebcam(){
|
||||
if(!this.cameraUpload){
|
||||
openWebcam() {
|
||||
if (!this.cameraUpload) {
|
||||
return
|
||||
}
|
||||
this.isInWebcam = true
|
||||
},
|
||||
cameraFileUpload(file){
|
||||
cameraFileUpload(file) {
|
||||
this.isInWebcam = false
|
||||
this.isUploading = false
|
||||
this.uploadFileToServer(file)
|
||||
|
||||
@@ -10,41 +10,74 @@
|
||||
class="h-6 w-6 text-nt-blue mx-auto"
|
||||
/>
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
v-else
|
||||
:key="option[optionKey]"
|
||||
role="button"
|
||||
class="relative overflow-hidden"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex',
|
||||
theme.default.borderRadius,
|
||||
{
|
||||
'mb-2': index !== options.length,
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
@click="onSelect(option[optionKey])"
|
||||
>
|
||||
<p class="flex-grow">
|
||||
{{ option[displayKey] }}
|
||||
</p>
|
||||
<div
|
||||
v-if="isSelected(option[optionKey])"
|
||||
class="flex items-center"
|
||||
v-for="(option, index) in options"
|
||||
v-if="options && options.length"
|
||||
:key="option[optionKey]"
|
||||
:role="multiple?'checkbox':'radio'"
|
||||
:aria-checked="isSelected(option[optionKey])"
|
||||
:class="[
|
||||
theme.FlatSelectInput.spacing.vertical,
|
||||
theme.FlatSelectInput.fontSize,
|
||||
theme.FlatSelectInput.option,
|
||||
]"
|
||||
@click="onSelect(option[optionKey])"
|
||||
>
|
||||
<svg
|
||||
:color="color"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
<template v-if="multiple">
|
||||
<Icon
|
||||
v-if="isSelected(option[optionKey])"
|
||||
name="material-symbols:check-box"
|
||||
class="text-inherit"
|
||||
:color="color"
|
||||
:class="[theme.FlatSelectInput.icon]"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
v-else
|
||||
name="material-symbols:check-box-outline-blank"
|
||||
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon
|
||||
v-if="isSelected(option[optionKey])"
|
||||
name="material-symbols:radio-button-checked-outline"
|
||||
class="text-inherit"
|
||||
:color="color"
|
||||
:class="[theme.FlatSelectInput.icon]"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="material-symbols:radio-button-unchecked"
|
||||
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
|
||||
/>
|
||||
</template>
|
||||
<p class="flex-grow">
|
||||
{{ option[displayKey] }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
theme.FlatSelectInput.spacing.horizontal,
|
||||
theme.FlatSelectInput.spacing.vertical,
|
||||
theme.FlatSelectInput.fontSize,
|
||||
theme.FlatSelectInput.option,
|
||||
'!text-gray-500 !cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
No options available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +91,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import {inputProps, useFormInput} from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
/**
|
||||
@@ -66,16 +99,16 @@ import InputWrapper from "./components/InputWrapper.vue"
|
||||
*/
|
||||
export default {
|
||||
name: "FlatSelectInput",
|
||||
components: { InputWrapper },
|
||||
components: {InputWrapper},
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: "value" },
|
||||
emitKey: { type: String, default: "value" },
|
||||
displayKey: { type: String, default: "name" },
|
||||
loading: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
options: {type: Array, required: true},
|
||||
optionKey: {type: String, default: "value"},
|
||||
emitKey: {type: String, default: "value"},
|
||||
displayKey: {type: String, default: "name"},
|
||||
loading: {type: Boolean, default: false},
|
||||
multiple: {type: Boolean, default: false},
|
||||
},
|
||||
setup(props, context) {
|
||||
return {
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
aria-expanded="true"
|
||||
aria-labelledby="listbox-label"
|
||||
class="cursor-pointer relative w-full"
|
||||
:class="[theme.default.input, { 'ring-red-500 ring-2': hasError }]"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
theme.default.spacing.horizontal,
|
||||
theme.default.spacing.vertical,
|
||||
theme.default.fontSize,
|
||||
theme.default.borderRadius,
|
||||
{ 'ring-red-500 ring-2': hasError }
|
||||
]"
|
||||
:style="inputStyle"
|
||||
@click.prevent="showUploadModal = true"
|
||||
>
|
||||
<div
|
||||
v-if="currentUrl == null"
|
||||
class="h-6 text-gray-600 dark:text-gray-400"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Upload image
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 inline"
|
||||
class="h-5 w-5 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -34,6 +40,7 @@
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
Upload image
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
:id="id ? id : name"
|
||||
:name="name"
|
||||
:style="inputStyle"
|
||||
class="flex items-start"
|
||||
class="flex items-stretch"
|
||||
>
|
||||
<v-select
|
||||
v-model="selectedCountryCode"
|
||||
class="w-[130px]"
|
||||
dropdown-class="w-[300px]"
|
||||
dropdown-class="max-w-[300px]"
|
||||
input-class="rounded-r-none"
|
||||
:data="countries"
|
||||
:disabled="disabled || countries.length === 1 ? true : null"
|
||||
@@ -28,13 +28,13 @@
|
||||
@update:model-value="onChangeCountryCode"
|
||||
>
|
||||
<template #option="props">
|
||||
<div class="flex items-center space-x-2 hover:text-white">
|
||||
<div class="flex items-center space-x-2 max-w-full">
|
||||
<country-flag
|
||||
size="normal"
|
||||
class="!-mt-[9px]"
|
||||
class="!-mt-[9px] rounded"
|
||||
:country="props.option.code"
|
||||
/>
|
||||
<span class="grow">{{ props.option.name }}</span>
|
||||
<span class="grow truncate">{{ props.option.name }}</span>
|
||||
<span>{{ props.option.dial_code }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,7 +44,7 @@
|
||||
>
|
||||
<country-flag
|
||||
size="normal"
|
||||
class="!-mt-[9px]"
|
||||
class="!-mt-[9px] rounded"
|
||||
:country="props.option.code"
|
||||
/>
|
||||
<span>{{ props.option.dial_code }}</span>
|
||||
@@ -58,6 +58,10 @@
|
||||
:disabled="disabled ? true : null"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
theme.default.spacing.horizontal,
|
||||
theme.default.spacing.vertical,
|
||||
theme.default.fontSize,
|
||||
theme.default.borderRadius,
|
||||
{
|
||||
'!ring-red-500 !ring-2': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
|
||||
@@ -20,16 +20,10 @@
|
||||
@mouseenter="onMouseHover(i)"
|
||||
@mouseleave="hoverRating = -1"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||
/>
|
||||
</svg>
|
||||
<Icon
|
||||
name="heroicons:star-20-solid"
|
||||
:class="theme.RatingInput.size"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
theme.RichTextAreaInput.input,
|
||||
theme.RichTextAreaInput.borderRadius,
|
||||
]"
|
||||
:editor-toolbar="editorToolbar"
|
||||
class="rich-editor resize-y"
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
:class="[
|
||||
{ 'font-semibold': compVal === i },
|
||||
theme.ScaleInput.button,
|
||||
theme.ScaleInput.borderRadius,
|
||||
theme.ScaleInput.spacing.horizontal,
|
||||
theme.ScaleInput.spacing.vertical,
|
||||
theme.ScaleInput.fontSize,
|
||||
compVal !== i ? unselectedButtonClass : '',
|
||||
]"
|
||||
:style="btnStyle(i === compVal)"
|
||||
|
||||
@@ -33,7 +33,12 @@
|
||||
<template #selected="{ option }">
|
||||
<template v-if="multiple">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<span class="truncate">
|
||||
<span
|
||||
class="truncate"
|
||||
:class="[
|
||||
theme.SelectInput.fontSize,
|
||||
]"
|
||||
>
|
||||
{{ getOptionNames(selectedValues).join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -45,7 +50,13 @@
|
||||
:option-name="getOptionName(option)"
|
||||
>
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<div>{{ getOptionName(option) }}</div>
|
||||
<div
|
||||
:class="[
|
||||
theme.SelectInput.fontSize,
|
||||
]"
|
||||
>
|
||||
{{ getOptionName(option) }}
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
@@ -57,7 +68,12 @@
|
||||
:selected="selected"
|
||||
>
|
||||
<span class="flex">
|
||||
<p class="flex-grow">
|
||||
<p
|
||||
class="flex-grow"
|
||||
:class="[
|
||||
theme.SelectInput.fontSize,
|
||||
]"
|
||||
>
|
||||
{{ option.name }}
|
||||
</p>
|
||||
<span
|
||||
@@ -85,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import {inputProps, useFormInput} from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
|
||||
/**
|
||||
@@ -93,41 +109,41 @@ import InputWrapper from './components/InputWrapper.vue'
|
||||
*/
|
||||
export default {
|
||||
name: 'SelectInput',
|
||||
components: { InputWrapper },
|
||||
components: {InputWrapper},
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: 'value' },
|
||||
emitKey: { type: String, default: 'value' },
|
||||
displayKey: { type: String, default: 'name' },
|
||||
loading: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
searchable: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
allowCreation: { type: Boolean, default: false },
|
||||
dropdownClass: { type: String, default: 'w-full' },
|
||||
remote: { type: Function, default: null }
|
||||
options: {type: Array, required: true},
|
||||
optionKey: {type: String, default: 'value'},
|
||||
emitKey: {type: String, default: 'value'},
|
||||
displayKey: {type: String, default: 'name'},
|
||||
loading: {type: Boolean, default: false},
|
||||
multiple: {type: Boolean, default: false},
|
||||
searchable: {type: Boolean, default: false},
|
||||
clearable: {type: Boolean, default: false},
|
||||
allowCreation: {type: Boolean, default: false},
|
||||
dropdownClass: {type: String, default: 'w-full'},
|
||||
remote: {type: Function, default: null}
|
||||
},
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
additionalOptions: [],
|
||||
selectedValues: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalOptions () {
|
||||
finalOptions() {
|
||||
return this.options.concat(this.additionalOptions)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
compVal: {
|
||||
handler (newVal, oldVal) {
|
||||
handler(newVal, oldVal) {
|
||||
if (!oldVal) {
|
||||
this.handleCompValChanged()
|
||||
}
|
||||
@@ -135,32 +151,32 @@ export default {
|
||||
immediate: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.handleCompValChanged()
|
||||
},
|
||||
methods: {
|
||||
getOptionName (val) {
|
||||
getOptionName(val) {
|
||||
const option = this.finalOptions.find((optionCandidate) => {
|
||||
return optionCandidate[this.optionKey] === val
|
||||
})
|
||||
if (option) return option[this.displayKey]
|
||||
return null
|
||||
},
|
||||
getOptionNames (values) {
|
||||
getOptionNames(values) {
|
||||
return values.map(val => {
|
||||
return this.getOptionName(val)
|
||||
})
|
||||
},
|
||||
updateModelValue (newValues) {
|
||||
updateModelValue(newValues) {
|
||||
if (newValues === null) newValues = []
|
||||
this.selectedValues = newValues
|
||||
},
|
||||
updateOptions (newItem) {
|
||||
updateOptions(newItem) {
|
||||
if (newItem) {
|
||||
this.additionalOptions.push(newItem)
|
||||
}
|
||||
},
|
||||
handleCompValChanged () {
|
||||
handleCompValChanged() {
|
||||
if (this.compVal) {
|
||||
this.selectedValues = this.compVal
|
||||
}
|
||||
|
||||
@@ -7,13 +7,17 @@
|
||||
<VueSignaturePad
|
||||
ref="signaturePad"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
theme.SignatureInput.input,
|
||||
theme.SignatureInput.spacing.horizontal,
|
||||
theme.SignatureInput.spacing.vertical,
|
||||
theme.SignatureInput.fontSize,
|
||||
theme.SignatureInput.borderRadius,
|
||||
theme.SignatureInput.minHeight,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
height="150px"
|
||||
:name="name"
|
||||
:options="{ onEnd }"
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
:disabled="disabled ? true : null"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
theme.default.borderRadius,
|
||||
theme.default.spacing.horizontal,
|
||||
theme.default.spacing.vertical,
|
||||
theme.default.fontSize,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
@@ -48,18 +52,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import {inputProps, useFormInput} from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: "TextAreaInput",
|
||||
components: { InputWrapper },
|
||||
components: {InputWrapper},
|
||||
mixins: [],
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
maxCharLimit: { type: Number, required: false, default: null },
|
||||
showCharLimit: { type: Boolean, required: false, default: false },
|
||||
maxCharLimit: {type: Number, required: false, default: null},
|
||||
showCharLimit: {type: Boolean, required: false, default: false},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
:style="inputStyle"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
theme.default.borderRadius,
|
||||
theme.default.spacing.horizontal,
|
||||
theme.default.spacing.vertical,
|
||||
theme.default.fontSize,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
@@ -55,23 +59,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import {inputProps, useFormInput} from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: "TextInput",
|
||||
components: { InputWrapper },
|
||||
components: {InputWrapper},
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
nativeType: { type: String, default: "text" },
|
||||
accept: { type: String, default: null },
|
||||
min: { type: Number, required: false, default: null },
|
||||
max: { type: Number, required: false, default: null },
|
||||
autocomplete: { type:[Boolean, String, Object], default: null },
|
||||
maxCharLimit: { type: Number, required: false, default: null },
|
||||
showCharLimit: { type: Boolean, required: false, default: false },
|
||||
pattern: { type: String, default: null },
|
||||
nativeType: {type: String, default: "text"},
|
||||
accept: {type: String, default: null},
|
||||
min: {type: Number, required: false, default: null},
|
||||
max: {type: Number, required: false, default: null},
|
||||
autocomplete: {type: [Boolean, String, Object], default: null},
|
||||
maxCharLimit: {type: Number, required: false, default: null},
|
||||
showCharLimit: {type: Boolean, required: false, default: false},
|
||||
pattern: {type: String, default: null},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
@@ -92,10 +96,12 @@ export default {
|
||||
...useFormInput(
|
||||
props,
|
||||
context,
|
||||
props.nativeType === "file" ? "file-" : null,
|
||||
{
|
||||
formPrefixKey: props.nativeType === "file" ? "file-" : null
|
||||
},
|
||||
),
|
||||
onEnterPress,
|
||||
onChange,
|
||||
onChange
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -4,25 +4,43 @@
|
||||
<span />
|
||||
</template>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<v-switch
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
class="inline-block mr-2"
|
||||
:disabled="disabled ? true : null"
|
||||
:color="color"
|
||||
:theme="theme"
|
||||
/>
|
||||
<slot name="label">
|
||||
<span>{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
>*</span></span>
|
||||
</slot>
|
||||
<div>
|
||||
<slot name="label">
|
||||
<label
|
||||
:aria-label="id ? id : name"
|
||||
:for="id ? id : name"
|
||||
:class="theme.default.fontSize"
|
||||
>
|
||||
{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
>*</span>
|
||||
</label>
|
||||
</slot>
|
||||
<slot name="help">
|
||||
<InputHelp
|
||||
:help="help"
|
||||
:help-classes="theme.default.help"
|
||||
>
|
||||
<template #after-help>
|
||||
<slot name="bottom_after_help" />
|
||||
</template>
|
||||
</InputHelp>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #help>
|
||||
<slot name="help" />
|
||||
<span class="hidden" />
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
@@ -32,13 +50,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import {inputProps, useFormInput} from "./useFormInput.js"
|
||||
import VSwitch from "./components/VSwitch.vue"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import InputHelp from "~/components/forms/components/InputHelp.vue"
|
||||
|
||||
export default {
|
||||
name: "ToggleSwitchInput",
|
||||
|
||||
components: { InputWrapper, VSwitch },
|
||||
components: {InputHelp, InputWrapper, VSwitch},
|
||||
props: {
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
20
client/components/forms/VForm.vue
Normal file
20
client/components/forms/VForm.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<form>
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* Used to pass props to children input components
|
||||
*/
|
||||
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
|
||||
|
||||
const props = defineProps({
|
||||
themeName: { type: String, default: 'default' },
|
||||
size: { type: String, default: "md" },
|
||||
})
|
||||
|
||||
const theme = computed(() => (new ThemeBuilder(props.themeName, {size: props.size})).getAllComponents())
|
||||
provide('theme', theme)
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
id="webcam"
|
||||
autoplay
|
||||
playsinline
|
||||
:class="[{ hidden: !isCapturing }, theme.fileInput.cameraInput]"
|
||||
:class="[{ hidden: !isCapturing }, theme.fileInput.minHeight, theme.fileInput.borderRadius]"
|
||||
width="1280"
|
||||
height="720"
|
||||
/>
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
<script>
|
||||
import Webcam from "webcam-easy"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import { themes } from "~/lib/forms/themes/form-themes.js"
|
||||
export default {
|
||||
name: "FileInput",
|
||||
props: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:class="wrapperClass"
|
||||
:class="[ twMerge(theme.default.wrapper,wrapperClass)]"
|
||||
:style="inputStyle"
|
||||
>
|
||||
<slot name="label">
|
||||
@@ -54,20 +54,21 @@
|
||||
<script setup>
|
||||
import InputLabel from "./InputLabel.vue"
|
||||
import InputHelp from "./InputHelp.vue"
|
||||
import {twMerge} from "tailwind-merge"
|
||||
|
||||
defineProps({
|
||||
id: { type: String, required: false },
|
||||
name: { type: String, required: false },
|
||||
label: { type: String, required: false },
|
||||
form: { type: Object, required: false },
|
||||
theme: { type: Object, required: true },
|
||||
wrapperClass: { type: String, required: false },
|
||||
inputStyle: { type: Object, required: false },
|
||||
help: { type: String, required: false },
|
||||
helpPosition: { type: String, default: "below_input" },
|
||||
uppercaseLabels: { type: Boolean, default: true },
|
||||
hideFieldName: { type: Boolean, default: true },
|
||||
required: { type: Boolean, default: false },
|
||||
hasValidation: { type: Boolean, default: true },
|
||||
id: {type: String, required: false},
|
||||
name: {type: String, required: false},
|
||||
label: {type: String, required: false},
|
||||
form: {type: Object, required: false},
|
||||
theme: {type: Object, required: true},
|
||||
wrapperClass: {type: String, required: false},
|
||||
inputStyle: {type: Object, required: false},
|
||||
help: {type: String, required: false},
|
||||
helpPosition: {type: String, default: "below_input"},
|
||||
uppercaseLabels: {type: Boolean, default: true},
|
||||
hideFieldName: {type: Boolean, default: true},
|
||||
required: {type: Boolean, default: false},
|
||||
hasValidation: {type: Boolean, default: true},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
v-model="internalValue"
|
||||
:name="name"
|
||||
type="checkbox"
|
||||
:class="sizeClasses"
|
||||
class="rounded border-gray-500 cursor-pointer checkbox"
|
||||
class="rounded border-gray-500 w-10 h-10 cursor-pointer checkbox"
|
||||
:class="theme.CheckboxInput.size"
|
||||
:style="{ '--accent-color': color }"
|
||||
:disabled="disabled ? true : null"
|
||||
>
|
||||
@@ -32,7 +32,7 @@ const props = defineProps({
|
||||
name: { type: String, default: "checkbox" },
|
||||
modelValue: { type: [Boolean, String], default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
sizeClasses: { type: String, default: "w-4 h-4" },
|
||||
theme: { type: Object },
|
||||
color: { type: String, default: null },
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
<div
|
||||
class="inline-block w-full flex overflow-hidden"
|
||||
:style="inputStyle"
|
||||
:class="[theme.SelectInput.input, { '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled }, inputClass]"
|
||||
:class="[
|
||||
theme.SelectInput.input,
|
||||
theme.SelectInput.borderRadius,
|
||||
{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled },
|
||||
inputClass
|
||||
]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -15,10 +20,18 @@
|
||||
aria-expanded="true"
|
||||
aria-labelledby="listbox-label"
|
||||
class="cursor-pointer w-full flex-grow relative"
|
||||
:class="[{'py-2': !multiple || loading, 'py-1': multiple},theme.default.inputSpacing.horizontal]"
|
||||
:class="[
|
||||
theme.SelectInput.spacing.horizontal,
|
||||
theme.SelectInput.spacing.vertical
|
||||
]"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<div :class="{ 'h-6': !multiple, 'min-h-8': multiple && !loading }">
|
||||
<div
|
||||
class="flex items-center"
|
||||
:class="[
|
||||
theme.SelectInput.minHeight
|
||||
]"
|
||||
>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in"
|
||||
@@ -32,7 +45,6 @@
|
||||
v-else-if="modelValue"
|
||||
key="value"
|
||||
class="flex"
|
||||
:class="{ 'min-h-8': multiple }"
|
||||
>
|
||||
<slot
|
||||
name="selected"
|
||||
@@ -47,7 +59,10 @@
|
||||
<slot name="placeholder">
|
||||
<div
|
||||
class="text-gray-400 dark:text-gray-500 w-full text-left truncate pr-3"
|
||||
:class="{ 'py-1': multiple && !loading }"
|
||||
:class="[
|
||||
{ 'py-1': multiple && !loading },
|
||||
theme.SelectInput.fontSize
|
||||
]"
|
||||
>
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
@@ -65,7 +80,7 @@
|
||||
<button
|
||||
v-if="clearable && !isEmpty"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-900 border-l px-2"
|
||||
:class="[theme.default.inputSpacing.vertical]"
|
||||
:class="[theme.SelectInput.spacing.vertical]"
|
||||
@click.prevent="clear()"
|
||||
>
|
||||
<Icon
|
||||
@@ -78,15 +93,18 @@
|
||||
</div>
|
||||
<collapsible
|
||||
v-model="isOpen"
|
||||
class="absolute mt-1 bg-white overflow-auto dark:bg-notion-dark-light shadow-xl z-10"
|
||||
:class="[dropdownClass,theme.SelectInput.dropdown]"
|
||||
class="absolute mt-1 bg-white overflow-auto dark:bg-notion-dark-light shadow-xl z-30"
|
||||
:class="[dropdownClass,theme.SelectInput.dropdown, theme.SelectInput.borderRadius]"
|
||||
@click-away="onClickAway"
|
||||
>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
role="listbox"
|
||||
class="text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
||||
:class="{ 'max-h-42': !isSearchable, 'max-h-48': isSearchable }"
|
||||
class="leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
||||
:class="[
|
||||
{ 'max-h-42': !isSearchable, 'max-h-48': isSearchable },
|
||||
theme.SelectInput.fontSize
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="isSearchable"
|
||||
@@ -95,15 +113,29 @@
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
type="text"
|
||||
class="flex-grow pl-3 pr-7 py-3 w-full focus:outline-none dark:text-white"
|
||||
class="flex-grow pl-3 pr-7 py-2 w-full focus:outline-none dark:text-white"
|
||||
placeholder="Search"
|
||||
>
|
||||
<div class="flex absolute right-0 inset-y-0 items-center px-2 justify-center pointer-events-none">
|
||||
<div
|
||||
v-if="!searchTerm"
|
||||
class="flex absolute right-0 inset-y-0 items-center px-2 justify-center pointer-events-none"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:magnifying-glass-solid"
|
||||
class="h-5 w-5 text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
role="button"
|
||||
class="flex absolute right-0 inset-y-0 items-center px-2 justify-center"
|
||||
@click="searchTerm = ''"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:backspace"
|
||||
class="h-5 w-5 text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
@@ -120,8 +152,14 @@
|
||||
:key="item[optionKey]"
|
||||
role="option"
|
||||
:style="optionStyle"
|
||||
:class="[{ 'px-3 pr-9': multiple, 'px-3': !multiple },dropdownClass,theme.SelectInput.option]"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
|
||||
:class="[
|
||||
dropdownClass,
|
||||
theme.SelectInput.option,
|
||||
theme.SelectInput.spacing.horizontal,
|
||||
theme.SelectInput.spacing.vertical,
|
||||
{ 'pr-9': multiple},
|
||||
]"
|
||||
class="text-gray-900 select-none relative cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
|
||||
@click="select(item)"
|
||||
>
|
||||
<slot
|
||||
@@ -145,10 +183,11 @@
|
||||
role="option"
|
||||
:style="optionStyle"
|
||||
:class="[{ 'px-3 pr-9': multiple, 'px-3': !multiple },dropdownClass,theme.SelectInput.option]"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
|
||||
class="text-gray-900 select-none relative py-2 cursor-pointer group hover:bg-gray-100 dark:hover:bg-gray-900 rounded focus:outline-none"
|
||||
@click="createOption(searchTerm)"
|
||||
>
|
||||
Create <span class="px-2 bg-gray-100 border border-gray-300 rounded group-hover-text-black">{{ searchTerm
|
||||
Create <span class="px-2 bg-gray-100 border border-gray-300 rounded group-hover-text-black">{{
|
||||
searchTerm
|
||||
}}</span>
|
||||
</li>
|
||||
</div>
|
||||
@@ -159,38 +198,38 @@
|
||||
|
||||
<script>
|
||||
import Collapsible from '~/components/global/transitions/Collapsible.vue'
|
||||
import { themes } from '../../../lib/forms/form-themes.js'
|
||||
import {themes} from '../../../lib/forms/themes/form-themes.js'
|
||||
import debounce from 'debounce'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
export default {
|
||||
name: 'VSelect',
|
||||
components: { Collapsible },
|
||||
components: {Collapsible},
|
||||
directives: {},
|
||||
props: {
|
||||
data: Array,
|
||||
modelValue: { default: null, type: [String, Number, Array, Object] },
|
||||
inputClass: { type: String, default: null },
|
||||
dropdownClass: { type: String, default: 'w-full' },
|
||||
loading: { type: Boolean, default: false },
|
||||
required: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
searchable: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
hasError: { type: Boolean, default: false },
|
||||
remote: { type: Function, default: null },
|
||||
searchKeys: { type: Array, default: () => ['name'] },
|
||||
optionKey: { type: String, default: 'id' },
|
||||
emitKey: { type: String, default: null },
|
||||
color: { type: String, default: '#3B82F6' },
|
||||
placeholder: { type: String, default: null },
|
||||
uppercaseLabels: { type: Boolean, default: true },
|
||||
theme: { type: Object, default: () => themes.default },
|
||||
allowCreation: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false }
|
||||
modelValue: {default: null, type: [String, Number, Array, Object]},
|
||||
inputClass: {type: String, default: null},
|
||||
dropdownClass: {type: String, default: 'w-full'},
|
||||
loading: {type: Boolean, default: false},
|
||||
required: {type: Boolean, default: false},
|
||||
multiple: {type: Boolean, default: false},
|
||||
searchable: {type: Boolean, default: false},
|
||||
clearable: {type: Boolean, default: false},
|
||||
hasError: {type: Boolean, default: false},
|
||||
remote: {type: Function, default: null},
|
||||
searchKeys: {type: Array, default: () => ['name']},
|
||||
optionKey: {type: String, default: 'id'},
|
||||
emitKey: {type: String, default: null},
|
||||
color: {type: String, default: '#3B82F6'},
|
||||
placeholder: {type: String, default: null},
|
||||
uppercaseLabels: {type: Boolean, default: true},
|
||||
theme: {type: Object, default: () => themes.default},
|
||||
allowCreation: {type: Boolean, default: false},
|
||||
disabled: {type: Boolean, default: false}
|
||||
},
|
||||
emits: ['update:modelValue', 'update-options'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
searchTerm: '',
|
||||
@@ -198,46 +237,46 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionStyle () {
|
||||
optionStyle() {
|
||||
return {
|
||||
'--bg-form-color': this.color
|
||||
}
|
||||
},
|
||||
inputStyle () {
|
||||
inputStyle() {
|
||||
return {
|
||||
'--tw-ring-color': this.color
|
||||
}
|
||||
},
|
||||
debouncedRemote () {
|
||||
debouncedRemote() {
|
||||
if (this.remote) {
|
||||
return debounce(this.remote, 300)
|
||||
}
|
||||
return null
|
||||
},
|
||||
filteredOptions () {
|
||||
filteredOptions() {
|
||||
if (!this.data) return []
|
||||
if (!this.searchable || this.remote || this.searchTerm === '') {
|
||||
return this.data
|
||||
}
|
||||
|
||||
// Fuse search
|
||||
const fuzeOptions = {
|
||||
keys: this.searchKeys
|
||||
}
|
||||
const fuse = new Fuse(this.data, fuzeOptions)
|
||||
return fuse.search(this.searchTerm).map((res) => {
|
||||
const fuse = new Fuse(this.data, {
|
||||
keys: this.searchKeys,
|
||||
includeScore: true
|
||||
})
|
||||
return fuse.search(this.searchTerm).filter((res) => res.score < 0.5).map((res) => {
|
||||
return res.item
|
||||
})
|
||||
},
|
||||
isSearchable () {
|
||||
isSearchable() {
|
||||
return this.searchable || this.remote !== null || this.allowCreation
|
||||
},
|
||||
isEmpty () {
|
||||
isEmpty() {
|
||||
return this.multiple ? !this.modelValue || this.modelValue.length === 0 : !this.modelValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchTerm (val) {
|
||||
searchTerm(val) {
|
||||
if (!this.debouncedRemote) return
|
||||
if ((this.remote && val) || (val === '' && !this.modelValue) || (val === '' && this.isOpen)) {
|
||||
return this.debouncedRemote(val)
|
||||
@@ -245,13 +284,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClickAway (event) {
|
||||
onClickAway(event) {
|
||||
// Check that event target isn't children of dropdown
|
||||
if (this.$refs.select && !this.$refs.select.contains(event.target)) {
|
||||
this.isOpen = false
|
||||
}
|
||||
},
|
||||
isSelected (value) {
|
||||
isSelected(value) {
|
||||
if (!this.modelValue) return false
|
||||
|
||||
if (this.emitKey && value[this.emitKey]) {
|
||||
@@ -263,7 +302,7 @@ export default {
|
||||
}
|
||||
return this.modelValue === value
|
||||
},
|
||||
toggleDropdown () {
|
||||
toggleDropdown() {
|
||||
if (this.disabled) {
|
||||
this.isOpen = false
|
||||
} else {
|
||||
@@ -273,7 +312,7 @@ export default {
|
||||
this.searchTerm = ''
|
||||
}
|
||||
},
|
||||
select (value) {
|
||||
select(value) {
|
||||
if (!this.multiple) {
|
||||
// Close after select
|
||||
this.toggleDropdown()
|
||||
@@ -306,10 +345,10 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
clear () {
|
||||
clear() {
|
||||
this.$emit('update:modelValue', this.multiple ? [] : null)
|
||||
},
|
||||
createOption (newOption) {
|
||||
createOption(newOption) {
|
||||
if (newOption) {
|
||||
const newItem = {
|
||||
name: newOption,
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
:id="id || name"
|
||||
:aria-labelledby="id || name"
|
||||
role="checkbox"
|
||||
:aria-checked="props.modelValue"
|
||||
class="flex"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center h-6 w-12 p-1 bg-gray-300 border rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100"
|
||||
:class="{ 'toggle-switch': props.modelValue }"
|
||||
class="inline-flex items-center bg-gray-300 rounded-full cursor-pointer focus:outline-none transition-all transform ease-in-out duration-100"
|
||||
:class="[{ 'toggle-switch': props.modelValue }, theme.SwitchInput.containerSize]"
|
||||
:style="{ '--accent-color': props.color }"
|
||||
|
||||
>
|
||||
<div
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-all transform ease-in-out duration-150 rounded-2xl scale-100"
|
||||
:class="{ 'translate-x-5.5': props.modelValue }"
|
||||
class="inline-block h-4 w-4 rounded-full bg-white transition-all transform ease-in-out duration-150 scale-100"
|
||||
:class="{ [theme.SwitchInput.translatedClass]: props.modelValue}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,9 +24,12 @@
|
||||
import { defineEmits, defineProps } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: null },
|
||||
name: { type: String, default: "checkbox" },
|
||||
modelValue: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
color: { type: String, default: '#3B82F6' },
|
||||
theme: { type: Object },
|
||||
})
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
|
||||
60
client/components/forms/useFormInput.js
vendored
60
client/components/forms/useFormInput.js
vendored
@@ -1,29 +1,41 @@
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import { default as _get } from "lodash/get"
|
||||
import { default as _set } from "lodash/set"
|
||||
import { default as _has } from "lodash/has"
|
||||
import {ref, computed, watch} from "vue"
|
||||
import {default as _get} from "lodash/get"
|
||||
import {default as _set} from "lodash/set"
|
||||
import {default as _has} from "lodash/has"
|
||||
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
|
||||
|
||||
export const inputProps = {
|
||||
id: { type: String, default: null },
|
||||
name: { type: String, required: true },
|
||||
label: { type: String, required: false },
|
||||
form: { type: Object, required: false },
|
||||
theme: { type: Object, default: () => themes.default },
|
||||
modelValue: { required: false },
|
||||
required: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: null },
|
||||
uppercaseLabels: { type: Boolean, default: false },
|
||||
hideFieldName: { type: Boolean, default: false },
|
||||
showCharLimit: { type: Boolean, default: false },
|
||||
help: { type: String, default: null },
|
||||
helpPosition: { type: String, default: "below_input" },
|
||||
color: { type: String, default: "#3B82F6" },
|
||||
wrapperClass: { type: String, default: "relative mb-3" },
|
||||
id: {type: String, default: null},
|
||||
name: {type: String, required: true},
|
||||
label: {type: String, required: false},
|
||||
form: {type: Object, required: false},
|
||||
theme: {
|
||||
type: Object, default: () => {
|
||||
const theme = inject("theme", null)
|
||||
if (theme) {
|
||||
return theme.value
|
||||
}
|
||||
return CachedDefaultTheme.getInstance()
|
||||
}
|
||||
},
|
||||
modelValue: {required: false},
|
||||
required: {type: Boolean, default: false},
|
||||
disabled: {type: Boolean, default: false},
|
||||
placeholder: {type: String, default: null},
|
||||
uppercaseLabels: {type: Boolean, default: false},
|
||||
hideFieldName: {type: Boolean, default: false},
|
||||
showCharLimit: {type: Boolean, default: false},
|
||||
help: {type: String, default: null},
|
||||
helpPosition: {type: String, default: "below_input"},
|
||||
color: {type: String, default: "#3B82F6"},
|
||||
wrapperClass: {type: String, default: ""},
|
||||
}
|
||||
|
||||
export function useFormInput(props, context, formPrefixKey = null) {
|
||||
export function useFormInput(props, context, options = {}) {
|
||||
const composableOptions = {
|
||||
formPrefixKey: null,
|
||||
...options
|
||||
}
|
||||
const content = ref(props.modelValue)
|
||||
|
||||
const inputStyle = computed(() => {
|
||||
@@ -47,13 +59,13 @@ export function useFormInput(props, context, formPrefixKey = null) {
|
||||
const compVal = computed({
|
||||
get: () => {
|
||||
if (props.form) {
|
||||
return _get(props.form, (formPrefixKey || "") + props.name)
|
||||
return _get(props.form, (composableOptions.formPrefixKey || "") + props.name)
|
||||
}
|
||||
return content.value
|
||||
},
|
||||
set: (val) => {
|
||||
if (props.form) {
|
||||
_set(props.form, (formPrefixKey || "") + props.name, val)
|
||||
_set(props.form, (composableOptions.formPrefixKey || "") + props.name, val)
|
||||
} else {
|
||||
content.value = val
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from "vue"
|
||||
import OpenForm from "../forms/OpenForm.vue"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import { themes } from "~/lib/forms/themes/form-themes.js"
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, required: true },
|
||||
form: { type: Object, required: true },
|
||||
|
||||
@@ -193,13 +193,12 @@
|
||||
<script>
|
||||
import OpenForm from './OpenForm.vue'
|
||||
import OpenFormButton from './OpenFormButton.vue'
|
||||
import { themes } from '~/lib/forms/form-themes.js'
|
||||
import VButton from '~/components/global/VButton.vue'
|
||||
import FormCleanings from '../../pages/forms/show/FormCleanings.vue'
|
||||
import VTransition from '~/components/global/transitions/VTransition.vue'
|
||||
import {pendingSubmission} from "~/composables/forms/pendingSubmission.js"
|
||||
import clonedeep from "clone-deep"
|
||||
import { default as _has } from 'lodash/has'
|
||||
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
|
||||
|
||||
export default {
|
||||
components: { VTransition, VButton, OpenFormButton, OpenForm, FormCleanings },
|
||||
@@ -227,7 +226,6 @@ export default {
|
||||
return {
|
||||
loading: false,
|
||||
submitted: false,
|
||||
themes: themes,
|
||||
passwordForm: useForm({
|
||||
password: null
|
||||
}),
|
||||
@@ -241,7 +239,10 @@ export default {
|
||||
return import.meta.client && window.location.href.includes('popup=true')
|
||||
},
|
||||
theme () {
|
||||
return this.themes[_has(this.themes, this.form.theme) ? this.form.theme : 'default']
|
||||
return new ThemeBuilder(this.form.theme, {
|
||||
size: this.form.size,
|
||||
borderRadius: this.form.border_radius
|
||||
}).getAllComponents()
|
||||
},
|
||||
isPublicFormPage () {
|
||||
return this.$route.name === 'forms-slug'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
:type="nativeType"
|
||||
:disabled="loading ? true : null"
|
||||
:class="`py-${sizes['p-y']} px-${sizes['p-x']} text-${sizes['font']} ${theme.Button.body}`"
|
||||
:class="[`py-${sizes['p-y']} px-${sizes['p-x']} text-${sizes['font']}`,theme.Button.body,theme.Button.borderRadius]"
|
||||
:style="buttonStyle"
|
||||
class="btn"
|
||||
>
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import {themes} from "~/lib/forms/themes/form-themes.js"
|
||||
|
||||
export default {
|
||||
name: "OpenFormButton",
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
|
||||
theme: { type: Object, default: () => themes.default },
|
||||
theme: {type: Object, default: () => themes.default},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
@@ -91,14 +91,19 @@
|
||||
allow you to preview your form changes.
|
||||
</div>
|
||||
|
||||
<form-information />
|
||||
<form-structure />
|
||||
<form-customization />
|
||||
<form-about-submission />
|
||||
<form-access />
|
||||
<form-security-privacy />
|
||||
<form-custom-seo />
|
||||
<form-custom-code />
|
||||
<VForm
|
||||
size="sm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<form-information />
|
||||
<form-structure />
|
||||
<form-customization />
|
||||
<form-about-submission />
|
||||
<form-access />
|
||||
<form-security-privacy />
|
||||
<form-custom-seo />
|
||||
<form-custom-code />
|
||||
</VForm>
|
||||
</div>
|
||||
|
||||
<form-editor-preview />
|
||||
|
||||
@@ -55,9 +55,11 @@
|
||||
<p class="flex-grow truncate">
|
||||
{{ field.name }}
|
||||
</p>
|
||||
<v-switch
|
||||
<ToggleSwitchInput
|
||||
v-model="displayColumns[field.id]"
|
||||
class="float-right"
|
||||
wrapper-class="mb-0"
|
||||
label=""
|
||||
name="field.id"
|
||||
@update:model-value="onChangeDisplayColumns"
|
||||
/>
|
||||
</div>
|
||||
@@ -77,9 +79,11 @@
|
||||
<p class="flex-grow truncate">
|
||||
{{ field.name }}
|
||||
</p>
|
||||
<v-switch
|
||||
<ToggleSwitchInput
|
||||
v-model="displayColumns[field.id]"
|
||||
class="float-right"
|
||||
wrapper-class="mb-0"
|
||||
label=""
|
||||
name="field.id"
|
||||
@update:model-value="onChangeDisplayColumns"
|
||||
/>
|
||||
</div>
|
||||
@@ -98,12 +102,14 @@
|
||||
class="flex flex-wrap items-end"
|
||||
>
|
||||
<div class="flex-grow">
|
||||
<text-input
|
||||
class="w-64"
|
||||
:form="searchForm"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<VForm size="sm">
|
||||
<text-input
|
||||
class="w-64"
|
||||
:form="searchForm"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</VForm>
|
||||
</div>
|
||||
<div class="font-semibold flex gap-4">
|
||||
<p class="float-right text-xs uppercase mb-2">
|
||||
|
||||
@@ -35,8 +35,10 @@
|
||||
help="Gives user a unique url to update their submission"
|
||||
>
|
||||
<template #label>
|
||||
Editable submissions
|
||||
<pro-tag class="ml-1" />
|
||||
<span class="text-sm">
|
||||
Editable submissions
|
||||
</span>
|
||||
<pro-tag class="-mt-1 ml-1" />
|
||||
</template>
|
||||
</toggle-switch-input>
|
||||
<text-input
|
||||
@@ -138,7 +140,7 @@
|
||||
help="Show a message, or redirect to a URL"
|
||||
>
|
||||
<template #selected="{ option, optionName }">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<div class="flex items-center text-sm truncate mr-6">
|
||||
{{ optionName }}
|
||||
<pro-tag
|
||||
v-if="option === 'redirect'"
|
||||
@@ -147,8 +149,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex hover:text-white">
|
||||
<p class="flex-grow hover:text-white">
|
||||
<span class="flex">
|
||||
<p class="flex-grow">
|
||||
{{ option.name }}
|
||||
<template v-if="option.value === 'redirect'"><pro-tag /></template>
|
||||
</p>
|
||||
|
||||
@@ -53,9 +53,34 @@
|
||||
label="Form Theme"
|
||||
/>
|
||||
|
||||
<div class="flex space-x-4 justify-stretch">
|
||||
<select-input
|
||||
name="size"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'Small', value: 'sm' },
|
||||
{ name: 'Medium', value: 'md' },
|
||||
{ name: 'Large', value: 'lg' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Size"
|
||||
/>
|
||||
|
||||
<select-input
|
||||
name="border_radius"
|
||||
class="flex-grow"
|
||||
:options="[
|
||||
{ name: 'None', value: 'none' },
|
||||
{ name: 'Small', value: 'small' },
|
||||
{ name: 'Full', value: 'full' },
|
||||
]"
|
||||
:form="form"
|
||||
label="Input Roundness"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select-input
|
||||
name="dark_mode"
|
||||
class="mt-4"
|
||||
help="To see changes, save your form and open it"
|
||||
:options="[
|
||||
{ name: 'Auto - use Device System Preferences', value: 'auto' },
|
||||
@@ -68,7 +93,6 @@
|
||||
|
||||
<select-input
|
||||
name="width"
|
||||
class="mt-4"
|
||||
:options="[
|
||||
{ name: 'Centered', value: 'centered' },
|
||||
{ name: 'Full Width', value: 'full' },
|
||||
@@ -80,7 +104,6 @@
|
||||
|
||||
<image-input
|
||||
name="cover_picture"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Cover Picture"
|
||||
help="Not visible when form is embedded"
|
||||
@@ -89,7 +112,6 @@
|
||||
|
||||
<image-input
|
||||
name="logo_picture"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Logo"
|
||||
help="Not visible when form is embedded"
|
||||
@@ -98,49 +120,44 @@
|
||||
|
||||
<color-input
|
||||
name="color"
|
||||
class="mt-4"
|
||||
:form="form"
|
||||
label="Color (for buttons & inputs border)"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="hide_title"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Hide Title"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="no_branding"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #label>
|
||||
Remove OpnForm Branding
|
||||
<pro-tag class="ml-1" />
|
||||
<span class="text-sm">
|
||||
Remove OpnForm Branding
|
||||
</span>
|
||||
<pro-tag class="-mt-1" />
|
||||
</template>
|
||||
</toggle-switch-input>
|
||||
<toggle-switch-input
|
||||
name="show_progress_bar"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Show progress bar"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="uppercase_labels"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Uppercase Input Labels"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="transparent_background"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Transparent Background"
|
||||
help="Only applies when form is embedded"
|
||||
/>
|
||||
<toggle-switch-input
|
||||
name="confetti_on_submission"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
label="Confetti on successful submisison"
|
||||
@update:model-value="onChangeConfettiOnSubmission"
|
||||
/>
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
<editor-right-sidebar
|
||||
:show="form && (showEditFieldSidebar || showAddFieldSidebar)"
|
||||
>
|
||||
<transition mode="out-in">
|
||||
<form-field-edit
|
||||
v-if="showEditFieldSidebar"
|
||||
:key="editFieldIndex"
|
||||
v-motion-fade="'fade'"
|
||||
/>
|
||||
<add-form-block
|
||||
v-else-if="showAddFieldSidebar"
|
||||
v-motion-fade="'fade'"
|
||||
/>
|
||||
</transition>
|
||||
<VForm
|
||||
size="sm"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<transition mode="out-in">
|
||||
<form-field-edit
|
||||
v-if="showEditFieldSidebar"
|
||||
:key="editFieldIndex"
|
||||
v-motion-fade="'fade'"
|
||||
/>
|
||||
<add-form-block
|
||||
v-else-if="showAddFieldSidebar"
|
||||
v-motion-fade="'fade'"
|
||||
/>
|
||||
</transition>
|
||||
</VForm>
|
||||
</editor-right-sidebar>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
name="tags"
|
||||
label="Tags"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
help="To organize your forms (hidden to respondents)"
|
||||
placeholder="Select Tag(s)"
|
||||
:multiple="true"
|
||||
@@ -47,7 +46,6 @@
|
||||
name="visibility"
|
||||
label="Visibility"
|
||||
:form="form"
|
||||
class="mt-4"
|
||||
help="Only public form will be accessible"
|
||||
placeholder="Select Visibility"
|
||||
:required="true"
|
||||
@@ -56,7 +54,7 @@
|
||||
<v-button
|
||||
v-if="copyFormOptions.length > 0"
|
||||
color="light-gray"
|
||||
class="w-full mt-4"
|
||||
class="w-full"
|
||||
@click="showCopyFormSettingsModal = true"
|
||||
>
|
||||
<svg
|
||||
@@ -98,20 +96,13 @@
|
||||
</svg>
|
||||
</template>
|
||||
<template #title>
|
||||
Copy Settings from another form
|
||||
Import Settings from another form
|
||||
</template>
|
||||
<div class="p-4 min-h-[450px]">
|
||||
<p class="text-gray-600">
|
||||
If you already have another form that you like to use as a base for
|
||||
this form, you can do that here. Select another form, confirm, and we
|
||||
will copy all of the other form settings (except the form structure)
|
||||
to this form.
|
||||
</p>
|
||||
<div>
|
||||
<select-input
|
||||
v-model="copyFormId"
|
||||
name="copy_form_id"
|
||||
label="Copy Settings From"
|
||||
class="mt-3 mb-6"
|
||||
placeholder="Choose a form"
|
||||
:searchable="copyFormOptions.length > 5"
|
||||
:options="copyFormOptions"
|
||||
|
||||
@@ -31,14 +31,13 @@
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="relative flex items-center my-5">
|
||||
<v-checkbox
|
||||
<CheckboxInput
|
||||
v-model="remember"
|
||||
class="w-full md:w-1/2"
|
||||
name="remember"
|
||||
size="small"
|
||||
>
|
||||
Remember me
|
||||
</v-checkbox>
|
||||
label="Remember me"
|
||||
/>
|
||||
|
||||
<div class="w-full md:w-1/2 text-right">
|
||||
<a
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<script>
|
||||
import FormUrlPrefill from "../../../open/forms/components/FormUrlPrefill.vue"
|
||||
import OpenForm from "../../../open/forms/OpenForm.vue"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import { themes } from "~/lib/forms/themes/form-themes.js"
|
||||
|
||||
export default {
|
||||
name: "UrlFormPrefill",
|
||||
|
||||
Reference in New Issue
Block a user