0351d front end linting (#377)
* feat: disable custom script for trial users * after lint fix * frontend linting --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
@@ -4,9 +4,18 @@
|
||||
<span />
|
||||
</template>
|
||||
|
||||
<v-checkbox :id="id?id:name" v-model="compVal" :disabled="disabled?true:null" :name="name">
|
||||
<v-checkbox
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
:name="name"
|
||||
>
|
||||
<slot name="label">
|
||||
{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
>*</span>
|
||||
</slot>
|
||||
</v-checkbox>
|
||||
|
||||
@@ -21,28 +30,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import VCheckbox from './components/VCheckbox.vue'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import VCheckbox from "./components/VCheckbox.vue"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: 'CheckboxInput',
|
||||
name: "CheckboxInput",
|
||||
|
||||
components: { InputWrapper, VCheckbox },
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
mounted() {
|
||||
if (!this.compVal) {
|
||||
this.compVal = false
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
@@ -11,12 +9,23 @@
|
||||
</template>
|
||||
|
||||
<div
|
||||
:class="[theme.CodeInput.input,{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200':disabled }]"
|
||||
:class="[
|
||||
theme.CodeInput.input,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<codemirror :id="id?id:name" v-model="compVal" :disabled="disabled?true:null"
|
||||
:extensions="extensions"
|
||||
:style="inputStyle" :name="name" :tab-size="4"
|
||||
:placeholder="placeholder"
|
||||
<codemirror
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
:extensions="extensions"
|
||||
:style="inputStyle"
|
||||
:name="name"
|
||||
:tab-size="4"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,25 +36,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Codemirror } from 'vue-codemirror'
|
||||
import { Codemirror } from "vue-codemirror"
|
||||
|
||||
import {html} from '@codemirror/lang-html'
|
||||
import { html } from "@codemirror/lang-html"
|
||||
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
components: { InputWrapper, Codemirror },
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
const extensions = [html()]
|
||||
return {
|
||||
...useFormInput(props, context),
|
||||
extensions
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
</template>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input :id="id?id:name" v-model="compVal" :disabled="disabled?true:null"
|
||||
type="color" class="mr-2"
|
||||
:name="name"
|
||||
<input
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
type="color"
|
||||
class="mr-2"
|
||||
:name="name"
|
||||
>
|
||||
<slot name="label">
|
||||
<span>{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span></span>
|
||||
<span>{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
>*</span></span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -25,21 +33,21 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
|
||||
export default {
|
||||
name: 'ColorInput',
|
||||
name: "ColorInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,60 +3,117 @@
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
<div v-if="cameraUpload && isInWebcam" class="hidden sm:block w-full min-h-40">
|
||||
<camera-upload v-if="cameraUpload" @uploadImage="cameraFileUpload" @stopWebcam="isInWebcam=false" :theme="theme"/>
|
||||
</div>
|
||||
<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]"
|
||||
@dragover.prevent="uploadDragoverEvent=true"
|
||||
@dragleave.prevent="uploadDragoverEvent=false"
|
||||
@drop.prevent="onUploadDropEvent"
|
||||
@click="openFileUpload"
|
||||
<div
|
||||
v-if="cameraUpload && isInWebcam"
|
||||
class="hidden sm:block w-full min-h-40"
|
||||
>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<Loader class="mx-auto h-6 w-6" />
|
||||
<p class="mt-2 text-center text-sm text-gray-500">
|
||||
Uploading your file...
|
||||
</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="text-center">
|
||||
<input ref="actual-input" class="hidden" :multiple="multiple" type="file" :name="name"
|
||||
:accept="acceptExtensions" @change="manualFileUpload">
|
||||
<div v-if="files.length" class="flex flex-wrap items-center justify-center gap-4">
|
||||
<uploaded-file v-for="file in files" :key="file.url" :file="file" :theme="theme"
|
||||
@remove="clearFile(file)" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="text-gray-500 w-full flex justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-500 font-semibold 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">
|
||||
Size limit: {{ mbLimit }}MB per file
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<camera-upload
|
||||
v-if="cameraUpload"
|
||||
:theme="theme"
|
||||
@upload-image="cameraFileUpload"
|
||||
@stop-webcam="isInWebcam=false"
|
||||
/>
|
||||
</div>
|
||||
<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]"
|
||||
@dragover.prevent="uploadDragoverEvent=true"
|
||||
@dragleave.prevent="uploadDragoverEvent=false"
|
||||
@drop.prevent="onUploadDropEvent"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<Loader class="mx-auto h-6 w-6" />
|
||||
<p class="mt-2 text-center text-sm text-gray-500">
|
||||
Uploading your file...
|
||||
</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="text-center">
|
||||
<input
|
||||
ref="actual-input"
|
||||
class="hidden"
|
||||
:multiple="multiple"
|
||||
type="file"
|
||||
:name="name"
|
||||
:accept="acceptExtensions"
|
||||
@change="manualFileUpload"
|
||||
>
|
||||
<div
|
||||
v-if="files.length"
|
||||
class="flex flex-wrap items-center justify-center gap-4"
|
||||
>
|
||||
<uploaded-file
|
||||
v-for="file in files"
|
||||
:key="file.url"
|
||||
:file="file"
|
||||
:theme="theme"
|
||||
@remove="clearFile(file)"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="text-gray-500 w-full flex justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-500 font-semibold 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">
|
||||
Size limit: {{ mbLimit }}MB per file
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-full items-center justify-center mt-2 hidden sm:flex">
|
||||
<open-form-button native-type="button" :loading="loading" :theme="theme" :color="color" class="py-2 p-1 px-2" @click.stop="openWebcam" v-if="cameraUpload">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
|
||||
<open-form-button
|
||||
v-if="cameraUpload"
|
||||
native-type="button"
|
||||
:loading="loading"
|
||||
:theme="theme"
|
||||
:color="color"
|
||||
class="py-2 p-1 px-2"
|
||||
@click.stop="openWebcam"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</open-form-button>
|
||||
</div>
|
||||
@@ -134,14 +191,14 @@ export default {
|
||||
deep: true,
|
||||
handler(newVal, oldVal) {
|
||||
if (!oldVal) {
|
||||
this.handleCompValChange();
|
||||
this.handleCompValChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.handleCompValChange();
|
||||
this.handleCompValChange()
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -199,13 +256,13 @@ export default {
|
||||
},
|
||||
openWebcam(){
|
||||
if(!this.cameraUpload){
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.isInWebcam = true;
|
||||
this.isInWebcam = true
|
||||
},
|
||||
cameraFileUpload(file){
|
||||
this.isInWebcam = false
|
||||
this.isUploading = false;
|
||||
this.isUploading = false
|
||||
this.uploadFileToServer(file)
|
||||
},
|
||||
uploadFileToServer(file) {
|
||||
@@ -254,7 +311,7 @@ export default {
|
||||
}
|
||||
const response = await fetch(url)
|
||||
const data = await response.blob()
|
||||
const name = url.replace(/^.*(\\|\/|\:)/, '')
|
||||
const name = url.replace(/^.*(\\|\/|:)/, '')
|
||||
return new File([data], name, {
|
||||
type: data.type || defaultType
|
||||
})
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<Loader v-if="loading" key="loader" 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="[theme.default.input,'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex',{ 'mb-2': index !== options.length,'!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200':disabled }]"
|
||||
@click="onSelect(option[optionKey])"
|
||||
<Loader
|
||||
v-if="loading"
|
||||
key="loader"
|
||||
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="[
|
||||
theme.default.input,
|
||||
'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 flex',
|
||||
{
|
||||
'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">
|
||||
<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" />
|
||||
<div
|
||||
v-if="isSelected(option[optionKey])"
|
||||
class="flex items-center"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,36 +58,36 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
/**
|
||||
* Options: {name,value} objects
|
||||
*/
|
||||
export default {
|
||||
name: 'FlatSelectInput',
|
||||
name: "FlatSelectInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: 'value' },
|
||||
emitKey: { type: String, default: 'value' },
|
||||
displayKey: { type: String, default: 'name' },
|
||||
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 }
|
||||
multiple: { type: Boolean, default: false },
|
||||
},
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onSelect (value) {
|
||||
onSelect(value) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
@@ -80,17 +107,17 @@ export default {
|
||||
emitValue.push(value)
|
||||
this.compVal = emitValue
|
||||
} else {
|
||||
this.compVal = (this.compVal === value) ? null : value
|
||||
this.compVal = this.compVal === value ? null : value
|
||||
}
|
||||
},
|
||||
isSelected (value) {
|
||||
isSelected(value) {
|
||||
if (!this.compVal) return false
|
||||
|
||||
if (this.multiple) {
|
||||
return this.compVal.includes(value)
|
||||
}
|
||||
return this.compVal === value
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,68 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
||||
class="cursor-pointer relative w-full" :class="[theme.default.input,{'ring-red-500 ring-2': hasError}]"
|
||||
:style="inputStyle" @click.prevent="showUploadModal=true"
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="true"
|
||||
aria-labelledby="listbox-label"
|
||||
class="cursor-pointer relative w-full"
|
||||
:class="[theme.default.input, { '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">
|
||||
Upload image <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<div
|
||||
v-if="currentUrl == null"
|
||||
class="h-6 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Upload image
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
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"
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
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>
|
||||
</div>
|
||||
<div v-else class="h-6 text-gray-600 dark:text-gray-400 flex">
|
||||
<div
|
||||
v-else
|
||||
class="h-6 text-gray-600 dark:text-gray-400 flex"
|
||||
>
|
||||
<div class="flex-grow">
|
||||
<img :src="currentUrl" class="h-6 rounded shadow-md"/>
|
||||
</div>
|
||||
<a href="#" class="hover:text-nt-blue flex" @click.prevent="clearUrl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<img
|
||||
:src="currentUrl"
|
||||
class="h-6 rounded shadow-md"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg></a>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
class="hover:text-nt-blue flex"
|
||||
@click.prevent="clearUrl"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/></svg></a>
|
||||
</div>
|
||||
</button>
|
||||
</span>
|
||||
@@ -44,13 +75,18 @@
|
||||
</template>
|
||||
|
||||
<!-- Modal -->
|
||||
<modal :show="showUploadModal" @close="showUploadModal=false">
|
||||
<modal
|
||||
:show="showUploadModal"
|
||||
@close="showUploadModal = false"
|
||||
>
|
||||
<h2 class="text-lg font-semibold">
|
||||
Upload an image
|
||||
</h2>
|
||||
|
||||
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||
<div class="sm:mt-5 sm:grid sm:grid-cols-1 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<div
|
||||
class="sm:mt-5 sm:grid sm:grid-cols-1 sm:gap-4 sm:items-start sm:pt-5"
|
||||
>
|
||||
<div class="mt-2 sm:mt-0 sm:col-span-2 mb-5">
|
||||
<div
|
||||
v-cloak
|
||||
@@ -58,7 +94,10 @@
|
||||
@dragover.prevent="onUploadDragoverEvent($event)"
|
||||
@drop.prevent="onUploadDropEvent($event)"
|
||||
>
|
||||
<div v-if="loading" class="text-gray-600 dark:text-gray-400">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<Loader class="h-6 w-6 mx-auto m-10" />
|
||||
<p class="text-center mt-6">
|
||||
Uploading your file...
|
||||
@@ -69,18 +108,30 @@
|
||||
class="absolute rounded-full bg-gray-100 h-20 w-20 z-10 transition-opacity duration-500 ease-in-out"
|
||||
:class="{
|
||||
'opacity-100': uploadDragoverTracking,
|
||||
'opacity-0': !uploadDragoverTracking
|
||||
'opacity-0': !uploadDragoverTracking,
|
||||
}"
|
||||
/>
|
||||
<div class="relative z-20 text-center">
|
||||
<input ref="actual-input" class="hidden" type="file" :name="name"
|
||||
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml" @change="manualFileUpload"
|
||||
<input
|
||||
ref="actual-input"
|
||||
class="hidden"
|
||||
type="file"
|
||||
:name="name"
|
||||
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
|
||||
@change="manualFileUpload"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-24 w-24 text-gray-200" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mx-auto h-24 w-24 text-gray-200"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
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"
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
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>
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
@@ -107,22 +158,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import Modal from '../global/Modal.vue'
|
||||
import {storeFile} from "~/lib/file-uploads.js"
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import Modal from "../global/Modal.vue"
|
||||
import { storeFile } from "~/lib/file-uploads.js"
|
||||
|
||||
export default {
|
||||
name: 'ImageInput',
|
||||
name: "ImageInput",
|
||||
components: { InputWrapper, Modal },
|
||||
mixins: [],
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -132,87 +183,96 @@ export default {
|
||||
file: [],
|
||||
uploadDragoverTracking: false,
|
||||
uploadDragoverEvent: false,
|
||||
loading: false
|
||||
loading: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
currentUrl () {
|
||||
currentUrl() {
|
||||
return this.compVal
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
showUploadModal: {
|
||||
handler (val) {
|
||||
handler() {
|
||||
if (import.meta.server) return
|
||||
document.removeEventListener('paste', this.onUploadPasteEvent)
|
||||
document.removeEventListener("paste", this.onUploadPasteEvent)
|
||||
if (this.showUploadModal) {
|
||||
document.addEventListener('paste', this.onUploadPasteEvent)
|
||||
document.addEventListener("paste", this.onUploadPasteEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clearUrl () {
|
||||
clearUrl() {
|
||||
this.form[this.name] = null
|
||||
},
|
||||
onUploadDragoverEvent (e) {
|
||||
onUploadDragoverEvent() {
|
||||
this.uploadDragoverEvent = true
|
||||
this.uploadDragoverTracking = true
|
||||
},
|
||||
onUploadDropEvent (e) {
|
||||
onUploadDropEvent(e) {
|
||||
this.uploadDragoverEvent = false
|
||||
this.uploadDragoverTracking = false
|
||||
this.droppedFiles(e.dataTransfer.files)
|
||||
},
|
||||
onUploadPasteEvent (e) {
|
||||
onUploadPasteEvent(e) {
|
||||
if (!this.showUploadModal) return
|
||||
this.uploadDragoverEvent = false
|
||||
this.uploadDragoverTracking = false
|
||||
this.droppedFiles(e.clipboardData.files)
|
||||
},
|
||||
droppedFiles (droppedFiles) {
|
||||
droppedFiles(droppedFiles) {
|
||||
if (!droppedFiles) return
|
||||
|
||||
this.file = droppedFiles[0]
|
||||
this.uploadFileToServer()
|
||||
},
|
||||
openFileUpload () {
|
||||
this.$refs['actual-input'].click()
|
||||
openFileUpload() {
|
||||
this.$refs["actual-input"].click()
|
||||
},
|
||||
manualFileUpload (e) {
|
||||
manualFileUpload(e) {
|
||||
this.file = e.target.files[0]
|
||||
this.uploadFileToServer()
|
||||
},
|
||||
uploadFileToServer () {
|
||||
uploadFileToServer() {
|
||||
this.loading = true
|
||||
// Store file in s3
|
||||
storeFile(this.file).then(response => {
|
||||
// Move file to permanent storage for form assets
|
||||
opnFetch('/open/forms/assets/upload', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
url: this.file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension
|
||||
}
|
||||
}).then(moveFileResponseData => {
|
||||
if (!this.multiple) {
|
||||
this.files = []
|
||||
}
|
||||
this.compVal = moveFileResponseData.url
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
}).catch((error) => {
|
||||
storeFile(this.file)
|
||||
.then((response) => {
|
||||
// Move file to permanent storage for form assets
|
||||
opnFetch("/open/forms/assets/upload", {
|
||||
method: "POST",
|
||||
body: {
|
||||
url:
|
||||
this.file.name.split(".").slice(0, -1).join(".") +
|
||||
"_" +
|
||||
response.uuid +
|
||||
"." +
|
||||
response.extension,
|
||||
},
|
||||
})
|
||||
.then((moveFileResponseData) => {
|
||||
if (!this.multiple) {
|
||||
this.files = []
|
||||
}
|
||||
this.compVal = moveFileResponseData.url
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.compVal = null
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
this.compVal = null
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.compVal = null
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,77 +1,110 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label"/>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<div :id="id ? id : name" :name="name" :style="inputStyle" class="flex items-start">
|
||||
<v-select v-model="selectedCountryCode" class="w-[130px]" dropdown-class="w-[300px]" input-class="rounded-r-none"
|
||||
:data="countries"
|
||||
:disabled="(disabled || countries.length===1)?true:null" :searchable="true" :search-keys="['name']"
|
||||
:option-key="'code'" :color="color"
|
||||
:has-error="hasError"
|
||||
:placeholder="'Select a country'" :uppercase-labels="true" :theme="theme"
|
||||
@update:model-value="onChangeCountryCode"
|
||||
<div
|
||||
:id="id ? id : name"
|
||||
:name="name"
|
||||
:style="inputStyle"
|
||||
class="flex items-start"
|
||||
>
|
||||
<v-select
|
||||
v-model="selectedCountryCode"
|
||||
class="w-[130px]"
|
||||
dropdown-class="w-[300px]"
|
||||
input-class="rounded-r-none"
|
||||
:data="countries"
|
||||
:disabled="disabled || countries.length === 1 ? true : null"
|
||||
:searchable="true"
|
||||
:search-keys="['name']"
|
||||
:option-key="'code'"
|
||||
:color="color"
|
||||
:has-error="hasError"
|
||||
:placeholder="'Select a country'"
|
||||
:uppercase-labels="true"
|
||||
:theme="theme"
|
||||
@update:model-value="onChangeCountryCode"
|
||||
>
|
||||
<template #option="props">
|
||||
<div class="flex items-center space-x-2 hover:text-white">
|
||||
<country-flag size="normal" class="!-mt-[9px]" :country="props.option.code"/>
|
||||
<country-flag
|
||||
size="normal"
|
||||
class="!-mt-[9px]"
|
||||
:country="props.option.code"
|
||||
/>
|
||||
<span class="grow">{{ props.option.name }}</span>
|
||||
<span>{{ props.option.dial_code }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #selected="props">
|
||||
<div class="flex items-center space-x-2 justify-center overflow-hidden">
|
||||
<country-flag size="normal" class="!-mt-[9px]" :country="props.option.code"/>
|
||||
<div
|
||||
class="flex items-center space-x-2 justify-center overflow-hidden"
|
||||
>
|
||||
<country-flag
|
||||
size="normal"
|
||||
class="!-mt-[9px]"
|
||||
:country="props.option.code"
|
||||
/>
|
||||
<span>{{ props.option.dial_code }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
<input v-model="inputVal" type="text" class="inline-flex-grow !border-l-0 !rounded-l-none"
|
||||
:disabled="disabled?true:null"
|
||||
:class="[theme.default.input, { '!ring-red-500 !ring-2': hasError, '!cursor-not-allowed !bg-gray-200': disabled }]"
|
||||
:placeholder="placeholder" :style="inputStyle" @input="onInput"
|
||||
<input
|
||||
v-model="inputVal"
|
||||
type="text"
|
||||
class="inline-flex-grow !border-l-0 !rounded-l-none"
|
||||
:disabled="disabled ? true : null"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
{
|
||||
'!ring-red-500 !ring-2': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
:placeholder="placeholder"
|
||||
:style="inputStyle"
|
||||
@input="onInput"
|
||||
>
|
||||
</div>
|
||||
|
||||
<template #help>
|
||||
<slot name="help"/>
|
||||
<slot name="help" />
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<slot name="error"/>
|
||||
<slot name="error" />
|
||||
</template>
|
||||
</input-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {inputProps, useFormInput} from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import countryCodes from '~/data/country_codes.json'
|
||||
import CountryFlag from 'vue-country-flag-next'
|
||||
import parsePhoneNumber from 'libphonenumber-js'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import countryCodes from "~/data/country_codes.json"
|
||||
import CountryFlag from "vue-country-flag-next"
|
||||
import parsePhoneNumber from "libphonenumber-js"
|
||||
|
||||
export default {
|
||||
phone: 'PhoneInput',
|
||||
components: {InputWrapper, CountryFlag},
|
||||
phone: "PhoneInput",
|
||||
components: { InputWrapper, CountryFlag },
|
||||
props: {
|
||||
...inputProps,
|
||||
canOnlyCountry: {type: Boolean, default: false},
|
||||
unavailableCountries: {type: Array, default: () => []}
|
||||
canOnlyCountry: { type: Boolean, default: false },
|
||||
unavailableCountries: { type: Array, default: () => [] },
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedCountryCode: null,
|
||||
inputVal: null
|
||||
inputVal: null,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -80,30 +113,42 @@ export default {
|
||||
return countryCodes.filter((item) => {
|
||||
return !this.unavailableCountries.includes(item.code)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
inputVal: {
|
||||
handler(val) {
|
||||
if (val && val.startsWith('0')) {
|
||||
if (val && val.startsWith("0")) {
|
||||
val = val.substring(1)
|
||||
}
|
||||
if (this.canOnlyCountry) {
|
||||
this.compVal = (val) ? this.selectedCountryCode.code + this.selectedCountryCode.dial_code + val : this.selectedCountryCode.code + this.selectedCountryCode.dial_code
|
||||
this.compVal = val
|
||||
? this.selectedCountryCode.code +
|
||||
this.selectedCountryCode.dial_code +
|
||||
val
|
||||
: this.selectedCountryCode.code +
|
||||
this.selectedCountryCode.dial_code
|
||||
} else {
|
||||
this.compVal = (val) ? this.selectedCountryCode.code + this.selectedCountryCode.dial_code + val : null
|
||||
this.compVal = val
|
||||
? this.selectedCountryCode.code +
|
||||
this.selectedCountryCode.dial_code +
|
||||
val
|
||||
: null
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
compVal(newVal, oldVal) {
|
||||
compVal() {
|
||||
this.initState()
|
||||
},
|
||||
selectedCountryCode(newVal, oldVal) {
|
||||
if (this.compVal && newVal && oldVal) {
|
||||
this.compVal = this.compVal.replace(oldVal.code + oldVal.dial_code, newVal.code + newVal.dial_code)
|
||||
this.compVal = this.compVal.replace(
|
||||
oldVal.code + oldVal.dial_code,
|
||||
newVal.code + newVal.dial_code,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@@ -119,40 +164,51 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
getCountryBy(code = 'US', type = 'code') {
|
||||
if (!code) code = 'US' // Default US
|
||||
return this.countries.find((item) => {
|
||||
return item[type] === code
|
||||
}) ?? null
|
||||
getCountryBy(code = "US", type = "code") {
|
||||
if (!code) code = "US" // Default US
|
||||
return (
|
||||
this.countries.find((item) => {
|
||||
return item[type] === code
|
||||
}) ?? null
|
||||
)
|
||||
},
|
||||
onInput(event) {
|
||||
this.inputVal = event?.target?.value.replace(/[^0-9]/g, '')
|
||||
|
||||
this.inputVal = event?.target?.value.replace(/[^0-9]/g, "")
|
||||
},
|
||||
onChangeCountryCode() {
|
||||
if (!this.selectedCountryCode && this.countries.length > 0) {
|
||||
this.selectedCountryCode = this.countries[0]
|
||||
}
|
||||
if (this.canOnlyCountry && (this.inputVal === null || this.inputVal === '' || !this.inputVal)) {
|
||||
this.compVal = this.selectedCountryCode.code + this.selectedCountryCode.dial_code
|
||||
if (
|
||||
this.canOnlyCountry &&
|
||||
(this.inputVal === null || this.inputVal === "" || !this.inputVal)
|
||||
) {
|
||||
this.compVal =
|
||||
this.selectedCountryCode.code + this.selectedCountryCode.dial_code
|
||||
}
|
||||
},
|
||||
initState() {
|
||||
if (this.compVal === null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!this.compVal?.startsWith('+')) {
|
||||
this.selectedCountryCode = this.getCountryBy(this.compVal.substring(2, 0))
|
||||
if (!this.compVal?.startsWith("+")) {
|
||||
this.selectedCountryCode = this.getCountryBy(
|
||||
this.compVal.substring(2, 0),
|
||||
)
|
||||
}
|
||||
|
||||
const phoneObj = parsePhoneNumber(this.compVal)
|
||||
if (phoneObj !== undefined && phoneObj) {
|
||||
if (!this.selectedCountryCode && phoneObj.country !== undefined && phoneObj.country) {
|
||||
if (
|
||||
!this.selectedCountryCode &&
|
||||
phoneObj.country !== undefined &&
|
||||
phoneObj.country
|
||||
) {
|
||||
this.selectedCountryCode = this.getCountryBy(phoneObj.country)
|
||||
}
|
||||
this.inputVal = phoneObj.nationalNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<div class="stars-outer">
|
||||
<div v-for="i in starsCount" :key="i"
|
||||
class="cursor-pointer inline-block text-gray-200 dark:text-gray-800"
|
||||
:class="{'!text-yellow-400 active-star':i<=compVal, '!text-yellow-200 !dark:text-yellow-800 hover-star':i>compVal && i<=hoverRating, '!cursor-not-allowed':disabled}"
|
||||
role="button" @click="setRating(i)"
|
||||
@mouseenter="onMouseHover(i)"
|
||||
@mouseleave="hoverRating = -1"
|
||||
<div
|
||||
v-for="i in starsCount"
|
||||
:key="i"
|
||||
class="cursor-pointer inline-block text-gray-200 dark:text-gray-800"
|
||||
:class="{
|
||||
'!text-yellow-400 active-star': i <= compVal,
|
||||
'!text-yellow-200 !dark:text-yellow-800 hover-star':
|
||||
i > compVal && i <= hoverRating,
|
||||
'!cursor-not-allowed': disabled,
|
||||
}"
|
||||
role="button"
|
||||
@click="setRating(i)"
|
||||
@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">
|
||||
<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"
|
||||
/>
|
||||
@@ -32,37 +43,27 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: 'RatingInput',
|
||||
name: "RatingInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
numberOfStars: { type: Number, default: 5 }
|
||||
numberOfStars: { type: Number, default: 5 },
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hoverRating: -1
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (!this.compVal) this.compVal = 0
|
||||
},
|
||||
|
||||
updated () {
|
||||
if (!this.compVal) {
|
||||
this.compVal = 0
|
||||
hoverRating: -1,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -72,14 +73,24 @@ export default {
|
||||
return 5
|
||||
}
|
||||
return this.numberOfStars
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!this.compVal) this.compVal = 0
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (!this.compVal) {
|
||||
this.compVal = 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMouseHover (i) {
|
||||
this.hoverRating = (this.disabled) ? -1 : i
|
||||
onMouseHover(i) {
|
||||
this.hoverRating = this.disabled ? -1 : i
|
||||
},
|
||||
setRating (val) {
|
||||
setRating(val) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
@@ -88,7 +99,7 @@ export default {
|
||||
} else {
|
||||
this.compVal = val
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<vue-editor :id="id?id:name" ref="editor" v-model="compVal" :disabled="disabled?true:null"
|
||||
:placeholder="placeholder" :class="[{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200':disabled }, theme.RichTextAreaInput.input]"
|
||||
:editor-toolbar="editorToolbar" class="rich-editor resize-y"
|
||||
:style="inputStyle"
|
||||
<vue-editor
|
||||
:id="id ? id : name"
|
||||
ref="editor"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
:placeholder="placeholder"
|
||||
:class="[
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
theme.RichTextAreaInput.input,
|
||||
]"
|
||||
:editor-toolbar="editorToolbar"
|
||||
class="rich-editor resize-y"
|
||||
:style="inputStyle"
|
||||
/>
|
||||
|
||||
<template #help>
|
||||
@@ -22,14 +32,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { VueEditor, Quill } from 'vue3-editor'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import { VueEditor, Quill } from "vue3-editor"
|
||||
|
||||
Quill.imports['formats/link'].PROTOCOL_WHITELIST.push('notion')
|
||||
Quill.imports["formats/link"].PROTOCOL_WHITELIST.push("notion")
|
||||
|
||||
export default {
|
||||
name: 'RichTextAreaInput',
|
||||
name: "RichTextAreaInput",
|
||||
components: { InputWrapper, VueEditor },
|
||||
|
||||
props: {
|
||||
@@ -39,20 +49,19 @@ export default {
|
||||
default: () => {
|
||||
return [
|
||||
[{ header: 1 }, { header: 2 }],
|
||||
['bold', 'italic', 'underline', 'link'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }]
|
||||
["bold", "italic", "underline", "link"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ color: [] }],
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,7 +83,20 @@ export default {
|
||||
border-left: 0px !important;
|
||||
}
|
||||
|
||||
.ql-snow .ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar button.ql-active, .ql-snow .ql-toolbar button:focus, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar button.ql-active, .ql-snow.ql-toolbar button:focus, .ql-snow.ql-toolbar button:hover {
|
||||
.ql-snow .ql-toolbar .ql-picker-item.ql-selected,
|
||||
.ql-snow .ql-toolbar .ql-picker-item:hover,
|
||||
.ql-snow .ql-toolbar .ql-picker-label.ql-active,
|
||||
.ql-snow .ql-toolbar .ql-picker-label:hover,
|
||||
.ql-snow .ql-toolbar button.ql-active,
|
||||
.ql-snow .ql-toolbar button:focus,
|
||||
.ql-snow .ql-toolbar button:hover,
|
||||
.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
|
||||
.ql-snow.ql-toolbar .ql-picker-item:hover,
|
||||
.ql-snow.ql-toolbar .ql-picker-label.ql-active,
|
||||
.ql-snow.ql-toolbar .ql-picker-label:hover,
|
||||
.ql-snow.ql-toolbar button.ql-active,
|
||||
.ql-snow.ql-toolbar button:focus,
|
||||
.ql-snow.ql-toolbar button:hover {
|
||||
@apply text-nt-blue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<div class="rectangle-outer grid grid-cols-5 gap-2">
|
||||
<div v-for="i in scaleList" :key="i"
|
||||
:class="[{'font-semibold':compVal===i},theme.ScaleInput.button, compVal!==i ? unselectedButtonClass: '']"
|
||||
:style="btnStyle(i===compVal)"
|
||||
role="button" @click="setScale(i)"
|
||||
<div
|
||||
v-for="i in scaleList"
|
||||
:key="i"
|
||||
:class="[
|
||||
{ 'font-semibold': compVal === i },
|
||||
theme.ScaleInput.button,
|
||||
compVal !== i ? unselectedButtonClass : '',
|
||||
]"
|
||||
:style="btnStyle(i === compVal)"
|
||||
role="button"
|
||||
@click="setScale(i)"
|
||||
>
|
||||
{{ i }}
|
||||
</div>
|
||||
@@ -26,43 +31,44 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: 'ScaleInput',
|
||||
name: "ScaleInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
minScale: { type: Number, default: 1 },
|
||||
maxScale: { type: Number, default: 5 },
|
||||
stepScale: { type: Number, default: 1 }
|
||||
stepScale: { type: Number, default: 1 },
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {
|
||||
scaleList () {
|
||||
scaleList() {
|
||||
const list = []
|
||||
for (let i = this.minScale; i <= this.maxScale; i += this.stepScale) {
|
||||
list.push(i)
|
||||
}
|
||||
return list
|
||||
},
|
||||
unselectedButtonClass () {
|
||||
unselectedButtonClass() {
|
||||
return this.theme.ScaleInput.unselectedButton
|
||||
},
|
||||
textColor () {
|
||||
const color = (this.color.charAt(0) === '#') ? this.color.substring(1, 7) : this.color
|
||||
textColor() {
|
||||
const color =
|
||||
this.color.charAt(0) === "#" ? this.color.substring(1, 7) : this.color
|
||||
const r = parseInt(color.substring(0, 2), 16) // hexToR
|
||||
const g = parseInt(color.substring(2, 4), 16) // hexToG
|
||||
const b = parseInt(color.substring(4, 6), 16) // hexToB
|
||||
@@ -73,26 +79,26 @@ export default {
|
||||
}
|
||||
return Math.pow((col + 0.055) / 1.055, 2.4)
|
||||
})
|
||||
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2])
|
||||
return (L > 0.55) ? '#000000' : '#FFFFFF'
|
||||
}
|
||||
const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
return L > 0.55 ? "#000000" : "#FFFFFF"
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.compVal && typeof this.compVal === 'string') {
|
||||
mounted() {
|
||||
if (this.compVal && typeof this.compVal === "string") {
|
||||
this.compVal = parseInt(this.compVal)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
btnStyle (isSelected) {
|
||||
btnStyle(isSelected) {
|
||||
if (!isSelected) return {}
|
||||
return {
|
||||
color: this.textColor,
|
||||
backgroundColor: this.color
|
||||
backgroundColor: this.color,
|
||||
}
|
||||
},
|
||||
setScale (val) {
|
||||
setScale(val) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
@@ -101,7 +107,7 @@ export default {
|
||||
} else {
|
||||
this.compVal = val
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,17 +3,38 @@
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
<v-select v-model="compVal" :data="finalOptions" :label="label" :option-key="optionKey" :emit-key="emitKey"
|
||||
:required="required" :multiple="multiple" :searchable="searchable" :loading="loading" :color="color"
|
||||
:placeholder="placeholder" :uppercase-labels="uppercaseLabels" :theme="theme" :has-error="hasError"
|
||||
:allow-creation="allowCreation" :disabled="disabled ? true : null" :help="help" :help-position="helpPosition"
|
||||
@update-options="updateOptions" @update:model-value="updateModelValue">
|
||||
<v-select
|
||||
v-model="compVal"
|
||||
:data="finalOptions"
|
||||
:label="label"
|
||||
:option-key="optionKey"
|
||||
:emit-key="emitKey"
|
||||
:required="required"
|
||||
:multiple="multiple"
|
||||
:searchable="searchable"
|
||||
:loading="loading"
|
||||
:color="color"
|
||||
:placeholder="placeholder"
|
||||
:uppercase-labels="uppercaseLabels"
|
||||
:theme="theme"
|
||||
:has-error="hasError"
|
||||
:allow-creation="allowCreation"
|
||||
:disabled="disabled ? true : null"
|
||||
:help="help"
|
||||
:help-position="helpPosition"
|
||||
@update-options="updateOptions"
|
||||
@update:model-value="updateModelValue"
|
||||
>
|
||||
<template #selected="{ option }">
|
||||
<slot name="selected" :option="option" :optionName="getOptionName(option)">
|
||||
<slot
|
||||
name="selected"
|
||||
:option="option"
|
||||
:option-name="getOptionName(option)"
|
||||
>
|
||||
<template v-if="multiple">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<span class="truncate">
|
||||
{{ getOptionNames(selectedValues).join(', ') }}
|
||||
{{ getOptionNames(selectedValues).join(", ") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,16 +46,29 @@
|
||||
</slot>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<slot name="option" :option="option" :selected="selected">
|
||||
<slot
|
||||
name="option"
|
||||
:option="option"
|
||||
:selected="selected"
|
||||
>
|
||||
<span class="flex group-hover:text-white">
|
||||
<p class="flex-grow group-hover:text-white">
|
||||
{{ option.name }}
|
||||
</p>
|
||||
<span v-if="selected" class="absolute inset-y-0 right-0 flex items-center pr-4 dark:text-white">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4 dark:text-white"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
@@ -53,31 +87,31 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
/**
|
||||
* Options: {name,value} objects
|
||||
*/
|
||||
export default {
|
||||
name: 'SelectInput',
|
||||
name: "SelectInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: 'value' },
|
||||
emitKey: { type: String, default: 'value' },
|
||||
displayKey: { type: String, default: 'name' },
|
||||
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 },
|
||||
allowCreation: { type: Boolean, default: false }
|
||||
allowCreation: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -91,7 +125,7 @@ export default {
|
||||
computed: {
|
||||
finalOptions() {
|
||||
return this.options.concat(this.additionalOptions)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getOptionName(val) {
|
||||
@@ -102,7 +136,7 @@ export default {
|
||||
return null
|
||||
},
|
||||
getOptionNames(values) {
|
||||
return values.map(val => {
|
||||
return values.map((val) => {
|
||||
return this.getOptionName(val)
|
||||
})
|
||||
},
|
||||
@@ -114,7 +148,7 @@ export default {
|
||||
if (newItem) {
|
||||
this.additionalOptions.push(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<VueSignaturePad ref="signaturePad"
|
||||
:class="[theme.default.input,{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200':disabled }]"
|
||||
height="150px"
|
||||
:name="name"
|
||||
:options="{ onEnd }"
|
||||
<VueSignaturePad
|
||||
ref="signaturePad"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
height="150px"
|
||||
:name="name"
|
||||
:options="{ onEnd }"
|
||||
/>
|
||||
|
||||
<template #bottom_after_help>
|
||||
<small :class="theme.default.help">
|
||||
<a :class="theme.default.help" href="#" @click.prevent="clear">Clear</a>
|
||||
<a
|
||||
:class="theme.default.help"
|
||||
href="#"
|
||||
@click.prevent="clear"
|
||||
>Clear</a>
|
||||
</small>
|
||||
</template>
|
||||
|
||||
@@ -26,37 +35,38 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { VueSignaturePad } from 'vue-signature-pad'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
import { VueSignaturePad } from "vue-signature-pad"
|
||||
|
||||
export default {
|
||||
name: 'SignatureInput',
|
||||
name: "SignatureInput",
|
||||
components: { InputWrapper, VueSignaturePad },
|
||||
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clear () {
|
||||
clear() {
|
||||
this.$refs.signaturePad.clearSignature()
|
||||
this.onEnd()
|
||||
},
|
||||
onEnd () {
|
||||
onEnd() {
|
||||
if (this.disabled) {
|
||||
this.$refs.signaturePad.clearSignature()
|
||||
} else {
|
||||
/* eslint-disable-next-line */
|
||||
const { isEmpty, data } = this.$refs.signaturePad?.saveSignature()
|
||||
this.form[this.name] = (!isEmpty && data) ? data : null
|
||||
this.form[this.name] = !isEmpty && data ? data : null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,30 +5,28 @@
|
||||
</template>
|
||||
<div class="flex space-x-2">
|
||||
<div class="flex-1 relative">
|
||||
<div class="font-medium text-sm absolute -top-[6px]"
|
||||
:style="labelStyle"
|
||||
<div
|
||||
class="font-medium text-sm absolute -top-[6px]"
|
||||
:style="labelStyle"
|
||||
>
|
||||
<div class="">
|
||||
{{ compVal }}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="compVal"
|
||||
type="range"
|
||||
class="w-full mt-3"
|
||||
:disabled="disabled"
|
||||
:min="minSlider"
|
||||
:max="maxSlider"
|
||||
:step="stepSlider"
|
||||
v-model="compVal"
|
||||
/>
|
||||
>
|
||||
<div class="grid grid-cols-3 gap-2 -mt-1">
|
||||
<div
|
||||
v-for="i in sliderLabelsList"
|
||||
:key="i"
|
||||
:class="[
|
||||
theme.SliderInput.stepLabel,
|
||||
i.style,
|
||||
]"
|
||||
:class="[theme.SliderInput.stepLabel, i.style]"
|
||||
>
|
||||
{{ i.label }}
|
||||
</div>
|
||||
@@ -46,8 +44,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from "./useFormInput.js";
|
||||
import InputWrapper from "./components/InputWrapper.vue";
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: "SliderInput",
|
||||
@@ -63,18 +61,20 @@ export default {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context),
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelStyle() {
|
||||
const ratio = ((this.compVal-this.minSlider) / (this.maxSlider-this.minSlider)) * 100
|
||||
const ratio =
|
||||
((this.compVal - this.minSlider) / (this.maxSlider - this.minSlider)) *
|
||||
100
|
||||
return {
|
||||
left: `${ratio}%`,
|
||||
marginLeft: `-${ratio/100*15}px`
|
||||
marginLeft: `-${(ratio / 100) * 15}px`,
|
||||
}
|
||||
},
|
||||
sliderLabelsList() {
|
||||
const midPoint = (this.maxSlider - this.minSlider) / 2 + this.minSlider;
|
||||
const midPoint = (this.maxSlider - this.minSlider) / 2 + this.minSlider
|
||||
const labels = [
|
||||
{
|
||||
label: `${this.minSlider}`,
|
||||
@@ -88,14 +88,12 @@ export default {
|
||||
label: `${this.maxSlider}`,
|
||||
style: "flex items-center justify-end",
|
||||
},
|
||||
];
|
||||
return labels;
|
||||
]
|
||||
return labels
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.compVal = parseInt(this.compVal ?? this.minSlider);
|
||||
this.compVal = parseInt(this.compVal ?? this.minSlider)
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<textarea :id="id?id:name" v-model="compVal" :disabled="disabled?true:null"
|
||||
:class="[theme.default.input,{ '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200':disabled }]"
|
||||
class="resize-y"
|
||||
:name="name" :style="inputStyle"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxCharLimit"
|
||||
<textarea
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
class="resize-y"
|
||||
:name="name"
|
||||
:style="inputStyle"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxCharLimit"
|
||||
/>
|
||||
|
||||
<template v-if="maxCharLimit && showCharLimit" #bottom_after_help>
|
||||
<template
|
||||
v-if="maxCharLimit && showCharLimit"
|
||||
#bottom_after_help
|
||||
>
|
||||
<small :class="theme.default.help">
|
||||
{{ charCount }}/{{ maxCharLimit }}
|
||||
</small>
|
||||
@@ -27,30 +38,30 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: 'TextAreaInput',
|
||||
name: "TextAreaInput",
|
||||
components: { InputWrapper },
|
||||
mixins: [],
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
maxCharLimit: { type: Number, required: false, default: null },
|
||||
showCharLimit: { type: Boolean, required: false, default: false }
|
||||
showCharLimit: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
charCount () {
|
||||
return (this.compVal) ? this.compVal.length : 0
|
||||
}
|
||||
}
|
||||
charCount() {
|
||||
return this.compVal ? this.compVal.length : 0
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
<template>
|
||||
<input-wrapper
|
||||
v-bind="inputWrapperProps"
|
||||
>
|
||||
<input-wrapper v-bind="inputWrapperProps">
|
||||
<template #label>
|
||||
<slot name="label" />
|
||||
</template>
|
||||
|
||||
<input :id="id?id:name" v-model="compVal" :disabled="disabled?true:null"
|
||||
:type="nativeType" :autocomplete="autocomplete"
|
||||
:pattern="pattern"
|
||||
:style="inputStyle"
|
||||
:class="[theme.default.input, { '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled }]"
|
||||
:name="name" :accept="accept"
|
||||
:placeholder="placeholder" :min="min" :max="max" :maxlength="maxCharLimit"
|
||||
@change="onChange" @keydown.enter.prevent="onEnterPress"
|
||||
<input
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
:disabled="disabled ? true : null"
|
||||
:type="nativeType"
|
||||
:autocomplete="autocomplete"
|
||||
:pattern="pattern"
|
||||
:style="inputStyle"
|
||||
:class="[
|
||||
theme.default.input,
|
||||
{
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
]"
|
||||
:name="name"
|
||||
:accept="accept"
|
||||
:placeholder="placeholder"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:maxlength="maxCharLimit"
|
||||
@change="onChange"
|
||||
@keydown.enter.prevent="onEnterPress"
|
||||
>
|
||||
|
||||
<template #help>
|
||||
@@ -36,28 +49,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
|
||||
export default {
|
||||
name: 'TextInput',
|
||||
name: "TextInput",
|
||||
components: { InputWrapper },
|
||||
|
||||
props: {
|
||||
...inputProps,
|
||||
nativeType: { type: String, default: 'text' },
|
||||
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: { 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 }
|
||||
pattern: { type: String, default: null },
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
const onChange = (event) => {
|
||||
if (props.nativeType !== 'file') return
|
||||
if (props.nativeType !== "file") return
|
||||
|
||||
const file = event.target.files[0]
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
@@ -70,15 +83,19 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
...useFormInput(props, context, props.nativeType === 'file' ? 'file-' : null),
|
||||
...useFormInput(
|
||||
props,
|
||||
context,
|
||||
props.nativeType === "file" ? "file-" : null,
|
||||
),
|
||||
onEnterPress,
|
||||
onChange
|
||||
onChange,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
charCount () {
|
||||
return (this.compVal) ? this.compVal.length : 0
|
||||
}
|
||||
}
|
||||
charCount() {
|
||||
return this.compVal ? this.compVal.length : 0
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,9 +5,18 @@
|
||||
</template>
|
||||
|
||||
<div class="flex">
|
||||
<v-switch :id="id?id:name" v-model="compVal" class="inline-block mr-2" :disabled="disabled?true:null" />
|
||||
<v-switch
|
||||
:id="id ? id : name"
|
||||
v-model="compVal"
|
||||
class="inline-block mr-2"
|
||||
:disabled="disabled ? true : null"
|
||||
/>
|
||||
<slot name="label">
|
||||
<span>{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span></span>
|
||||
<span>{{ label }}
|
||||
<span
|
||||
v-if="required"
|
||||
class="text-red-500 required-dot"
|
||||
>*</span></span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -22,25 +31,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inputProps, useFormInput } from './useFormInput.js'
|
||||
import VSwitch from './components/VSwitch.vue'
|
||||
import InputWrapper from './components/InputWrapper.vue'
|
||||
import { inputProps, useFormInput } from "./useFormInput.js"
|
||||
import VSwitch from "./components/VSwitch.vue"
|
||||
import InputWrapper from "./components/InputWrapper.vue"
|
||||
export default {
|
||||
name: 'ToggleSwitchInput',
|
||||
name: "ToggleSwitchInput",
|
||||
|
||||
components: { InputWrapper, VSwitch },
|
||||
props: {
|
||||
...inputProps
|
||||
...inputProps,
|
||||
},
|
||||
|
||||
setup (props, context) {
|
||||
setup(props, context) {
|
||||
return {
|
||||
...useFormInput(props, context)
|
||||
...useFormInput(props, context),
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.compVal = !!this.compVal
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,143 +1,218 @@
|
||||
<template>
|
||||
<div class="relative border">
|
||||
<video id="webcam" autoplay playsinline :class="[{ 'hidden': !isCapturing },theme.fileInput.cameraInput]" width="1280" height="720"></video>
|
||||
<canvas id="canvas" :class="{ 'hidden': !capturedImage }"></canvas>
|
||||
<div v-if="cameraPermissionStatus === 'allowed'" class="absolute inset-x-0 grid place-content-center bottom-2">
|
||||
<div class=" p-2 px-4 flex items-center justify-center text-xs space-x-2" v-if="isCapturing">
|
||||
<span class="cursor-pointer rounded-full w-14 h-14 border-2 grid place-content-center"
|
||||
@click="processCapturedImage">
|
||||
<span class="cursor-pointer bg-gray-100 rounded-full w-10 h-10 grid place-content-center">
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-white cursor-pointer" @click="cancelCamera">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-8 h-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="cameraPermissionStatus === 'blocked'"
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
@click="openCameraUpload">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
|
||||
</svg>
|
||||
<p class="text-center font-bold">
|
||||
Allow Camera Permission
|
||||
</p>
|
||||
<p class="text-xs">You need to allow camera permission before you can take pictures. Go to browser settings to enable camera permission on this page.</p>
|
||||
<button class="text-xs p-1 px-2 bg-blue-600 rounded" type="button" @click.stop="cancelCamera">Got it!</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="cameraPermissionStatus === 'loading'"
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
>
|
||||
<div class="w-6 h-6">
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
@click="openCameraUpload">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M12 18.75H4.5a2.25 2.25 0 0 1-2.25-2.25V9m12.841 9.091L16.5 19.5m-1.409-1.409c.407-.407.659-.97.659-1.591v-9a2.25 2.25 0 0 0-2.25-2.25h-9c-.621 0-1.184.252-1.591.659m12.182 12.182L2.909 5.909M1.5 4.5l1.409 1.409" />
|
||||
</svg>
|
||||
|
||||
<p class="text-center font-bold">
|
||||
Camera Device Error
|
||||
</p>
|
||||
<p class="text-xs">An unknown error occurred when trying to start Webcam device.</p>
|
||||
<button class="text-xs p-1 px-2 bg-blue-600 rounded" type="button" @click.stop="cancelCamera">Go back</button>
|
||||
</div>
|
||||
|
||||
<div class="relative border">
|
||||
<video
|
||||
id="webcam"
|
||||
autoplay
|
||||
playsinline
|
||||
:class="[{ hidden: !isCapturing }, theme.fileInput.cameraInput]"
|
||||
width="1280"
|
||||
height="720"
|
||||
/>
|
||||
<canvas
|
||||
id="canvas"
|
||||
:class="{ hidden: !capturedImage }"
|
||||
/>
|
||||
<div
|
||||
v-if="cameraPermissionStatus === 'allowed'"
|
||||
class="absolute inset-x-0 grid place-content-center bottom-2"
|
||||
>
|
||||
<div
|
||||
v-if="isCapturing"
|
||||
class="p-2 px-4 flex items-center justify-center text-xs space-x-2"
|
||||
>
|
||||
<span
|
||||
class="cursor-pointer rounded-full w-14 h-14 border-2 grid place-content-center"
|
||||
@click="processCapturedImage"
|
||||
>
|
||||
<span
|
||||
class="cursor-pointer bg-gray-100 rounded-full w-10 h-10 grid place-content-center"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="text-white cursor-pointer"
|
||||
@click="cancelCamera"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="cameraPermissionStatus === 'blocked'"
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
@click="openCameraUpload"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-center font-bold">
|
||||
Allow Camera Permission
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
You need to allow camera permission before you can take pictures. Go to
|
||||
browser settings to enable camera permission on this page.
|
||||
</p>
|
||||
<button
|
||||
class="text-xs p-1 px-2 bg-blue-600 rounded"
|
||||
type="button"
|
||||
@click.stop="cancelCamera"
|
||||
>
|
||||
Got it!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="cameraPermissionStatus === 'loading'"
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
>
|
||||
<div class="w-6 h-6">
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="absolute p-5 top-0 inset-x-0 flex flex-col items-center justify-center space-y-4 text-center rounded border border-gray-400/30 h-full"
|
||||
@click="openCameraUpload"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M12 18.75H4.5a2.25 2.25 0 0 1-2.25-2.25V9m12.841 9.091L16.5 19.5m-1.409-1.409c.407-.407.659-.97.659-1.591v-9a2.25 2.25 0 0 0-2.25-2.25h-9c-.621 0-1.184.252-1.591.659m12.182 12.182L2.909 5.909M1.5 4.5l1.409 1.409"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p class="text-center font-bold">
|
||||
Camera Device Error
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
An unknown error occurred when trying to start Webcam device.
|
||||
</p>
|
||||
<button
|
||||
class="text-xs p-1 px-2 bg-blue-600 rounded"
|
||||
type="button"
|
||||
@click.stop="cancelCamera"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Webcam from 'webcam-easy';
|
||||
import { themes } from '~/lib/forms/form-themes.js'
|
||||
import Webcam from "webcam-easy"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
export default {
|
||||
name: 'FileInput',
|
||||
props:{
|
||||
theme: { type: Object, default: () => themes.default }
|
||||
name: "FileInput",
|
||||
props: {
|
||||
theme: { type: Object, default: () => themes.default },
|
||||
},
|
||||
emits: ['stopWebcam', 'uploadImage'],
|
||||
data: () => ({
|
||||
webcam: null,
|
||||
isCapturing: false,
|
||||
capturedImage: null,
|
||||
cameraPermissionStatus: "loading",
|
||||
}),
|
||||
computed: {
|
||||
videoDisplay() {
|
||||
return this.isCapturing ? "" : "hidden"
|
||||
},
|
||||
data: () => ({
|
||||
webcam: null,
|
||||
isCapturing: false,
|
||||
capturedImage: null,
|
||||
cameraPermissionStatus: 'loading',
|
||||
}),
|
||||
computed: {
|
||||
videoDisplay() {
|
||||
return this.isCapturing ? '' : 'hidden';
|
||||
},
|
||||
canvasDisplay() {
|
||||
return (!this.isCapturing && this.capturedImage) ? '' : 'hidden'
|
||||
}
|
||||
canvasDisplay() {
|
||||
return !this.isCapturing && this.capturedImage ? "" : "hidden"
|
||||
},
|
||||
mounted() {
|
||||
const webcamElement = document.getElementById('webcam');
|
||||
const canvasElement = document.getElementById('canvas');
|
||||
this.webcam = new Webcam(webcamElement, 'user', canvasElement);
|
||||
this.openCameraUpload()
|
||||
},
|
||||
mounted() {
|
||||
const webcamElement = document.getElementById("webcam")
|
||||
const canvasElement = document.getElementById("canvas")
|
||||
this.webcam = new Webcam(webcamElement, "user", canvasElement)
|
||||
this.openCameraUpload()
|
||||
},
|
||||
|
||||
methods: {
|
||||
openCameraUpload() {
|
||||
this.isCapturing = true
|
||||
this.capturedImage = null
|
||||
this.webcam
|
||||
.start()
|
||||
.then(() => {
|
||||
this.cameraPermissionStatus = "allowed"
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
if (err.toString() === "NotAllowedError: Permission denied") {
|
||||
this.cameraPermissionStatus = "blocked"
|
||||
return
|
||||
}
|
||||
this.cameraPermissionStatus = "unknown"
|
||||
})
|
||||
},
|
||||
cancelCamera() {
|
||||
this.isCapturing = false
|
||||
this.capturedImage = null
|
||||
this.webcam.stop()
|
||||
this.$emit("stopWebcam")
|
||||
},
|
||||
processCapturedImage() {
|
||||
this.capturedImage = this.webcam.snap()
|
||||
this.isCapturing = false
|
||||
this.webcam.stop()
|
||||
const byteCharacters = atob(this.capturedImage.split(",")[1])
|
||||
const byteArrays = []
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512)
|
||||
|
||||
methods: {
|
||||
openCameraUpload() {
|
||||
this.isCapturing = true;
|
||||
this.capturedImage = null;
|
||||
this.webcam.start()
|
||||
.then(result => {
|
||||
this.cameraPermissionStatus = 'allowed';
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
if(err.toString() === 'NotAllowedError: Permission denied'){
|
||||
this.cameraPermissionStatus = 'blocked';
|
||||
return;
|
||||
}
|
||||
this.cameraPermissionStatus = 'unknown';
|
||||
});
|
||||
},
|
||||
cancelCamera() {
|
||||
this.isCapturing = false;
|
||||
this.capturedImage = null;
|
||||
this.webcam.stop()
|
||||
this.$emit('stopWebcam')
|
||||
},
|
||||
processCapturedImage() {
|
||||
this.capturedImage = this.webcam.snap();
|
||||
this.isCapturing = false;
|
||||
this.webcam.stop()
|
||||
const byteCharacters = atob(this.capturedImage.split(',')[1]);
|
||||
const byteArrays = [];
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
// Create Blob from binary data
|
||||
const blob = new Blob(byteArrays, { type: 'image/png' });
|
||||
const filename = Date.now()
|
||||
// Create a File object from the Blob
|
||||
const file = new File([blob], `${filename}.png`, { type: 'image/png' });
|
||||
this.$emit('uploadImage', file)
|
||||
const byteNumbers = new Array(slice.length)
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
|
||||
}
|
||||
// Create Blob from binary data
|
||||
const blob = new Blob(byteArrays, { type: "image/png" })
|
||||
const filename = Date.now()
|
||||
// Create a File object from the Blob
|
||||
const file = new File([blob], `${filename}.png`, { type: "image/png" })
|
||||
this.$emit("uploadImage", file)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
<span
|
||||
v-if="help"
|
||||
class="field-help"
|
||||
v-html="help"/>
|
||||
v-html="help"
|
||||
/>
|
||||
</slot>
|
||||
</small>
|
||||
<slot name="after-help">
|
||||
<small class="flex-grow"/>
|
||||
<small class="flex-grow" />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
defineProps({
|
||||
helpClasses: {type: String, default: 'text-gray-400 dark:text-gray-500'},
|
||||
help: {type: String, required: false}
|
||||
helpClasses: { type: String, default: "text-gray-400 dark:text-gray-500" },
|
||||
help: { type: String, required: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputLabel',
|
||||
name: "InputLabel",
|
||||
|
||||
props: {
|
||||
nativeFor: { type: String, default: null },
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InputLabel from './InputLabel.vue'
|
||||
import InputHelp from './InputHelp.vue'
|
||||
import InputLabel from "./InputLabel.vue"
|
||||
import InputHelp from "./InputHelp.vue"
|
||||
|
||||
defineProps({
|
||||
id: { type: String, required: false },
|
||||
@@ -62,7 +62,7 @@ defineProps({
|
||||
wrapperClass: { type: String, required: false },
|
||||
inputStyle: { type: Object, required: false },
|
||||
help: { type: String, required: false },
|
||||
helpPosition: { type: String, default: 'below_input' },
|
||||
helpPosition: { type: String, default: "below_input" },
|
||||
uppercaseLabels: { type: Boolean, default: true },
|
||||
hideFieldName: { type: Boolean, default: true },
|
||||
required: { type: Boolean, default: false },
|
||||
|
||||
@@ -3,15 +3,32 @@
|
||||
:class="[theme.fileInput.uploadedFile, 'overflow-hidden']"
|
||||
:title="file.file.name"
|
||||
>
|
||||
<div v-if="file.src && !isImageHide" class="h-20 overflow-hidden flex">
|
||||
<img class="block object-cover object-center w-full" :src="file.src" @error="isImageHide=true"/>
|
||||
</div>
|
||||
<div v-else class="h-20 flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="0.8" stroke="currentColor"
|
||||
<div
|
||||
v-if="file.src && !isImageHide"
|
||||
class="h-20 overflow-hidden flex"
|
||||
>
|
||||
<img
|
||||
class="block object-cover object-center w-full"
|
||||
:src="file.src"
|
||||
@error="isImageHide = true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="h-20 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-10 h-10 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="0.8"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -47,17 +64,17 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UploadedFile',
|
||||
name: "UploadedFile",
|
||||
|
||||
props: {
|
||||
file: { default: null },
|
||||
theme: { type: Object }
|
||||
file: { type:Object, default: null },
|
||||
theme: { type: Object },
|
||||
},
|
||||
|
||||
emits: ['remove'],
|
||||
data: () => ({
|
||||
isImageHide: false
|
||||
isImageHide: false,
|
||||
}),
|
||||
|
||||
computed: {}
|
||||
computed: {},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,21 +27,21 @@ import {
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
} from "vue"
|
||||
|
||||
defineOptions({
|
||||
name: 'VCheckbox',
|
||||
name: "VCheckbox",
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: null },
|
||||
name: { type: String, default: 'checkbox' },
|
||||
name: { type: String, default: "checkbox" },
|
||||
modelValue: { type: [Boolean, String], default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
sizeClasses: { type: String, default: 'w-4 h-4' },
|
||||
sizeClasses: { type: String, default: "w-4 h-4" },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'click'])
|
||||
const emit = defineEmits(["update:modelValue", "click"])
|
||||
|
||||
const internalValue = ref(props.modelValue)
|
||||
|
||||
@@ -62,18 +62,14 @@ watch(
|
||||
watch(
|
||||
() => internalValue.value,
|
||||
(val, oldVal) => {
|
||||
if (val === 0 || val === '0')
|
||||
val = false
|
||||
if (val === 1 || val === '1')
|
||||
val = true
|
||||
if (val === 0 || val === "0") val = false
|
||||
if (val === 1 || val === "1") val = true
|
||||
|
||||
if (val !== oldVal)
|
||||
emit('update:modelValue', val)
|
||||
if (val !== oldVal) emit("update:modelValue", val)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (internalValue.value === null)
|
||||
internalValue.value = false
|
||||
if (internalValue.value === null) internalValue.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,16 @@
|
||||
aria-labelledby="listbox-label"
|
||||
class="cursor-pointer"
|
||||
:style="inputStyle"
|
||||
:class="[theme.SelectInput.input, { 'py-2': !multiple || loading, 'py-1': multiple, '!ring-red-500 !ring-2 !border-transparent': hasError, '!cursor-not-allowed !bg-gray-200': disabled }, inputClass]"
|
||||
:class="[
|
||||
theme.SelectInput.input,
|
||||
{
|
||||
'py-2': !multiple || loading,
|
||||
'py-1': multiple,
|
||||
'!ring-red-500 !ring-2 !border-transparent': hasError,
|
||||
'!cursor-not-allowed !bg-gray-200': disabled,
|
||||
},
|
||||
inputClass,
|
||||
]"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<div :class="{ 'h-6': !multiple, 'min-h-8': multiple && !loading }">
|
||||
@@ -43,14 +52,17 @@
|
||||
<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 }"
|
||||
>
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
@@ -67,13 +79,32 @@
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<collapsible v-model="isOpen" @click-away="onClickAway"
|
||||
class="absolute mt-1 rounded-md bg-white dark:bg-notion-dark-light shadow-xl z-10" :class="dropdownClass">
|
||||
<ul tabindex="-1" role="listbox"
|
||||
<collapsible
|
||||
v-model="isOpen"
|
||||
class="absolute mt-1 rounded-md bg-white dark:bg-notion-dark-light shadow-xl z-10"
|
||||
:class="dropdownClass"
|
||||
@click-away="onClickAway"
|
||||
>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
role="listbox"
|
||||
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
||||
:class="{ 'max-h-42 py-1': !isSearchable, 'max-h-48 pb-1': isSearchable }">
|
||||
<div v-if="isSearchable" class="px-2 pt-2 sticky top-0 bg-white dark-bg-notion-dark-light z-10">
|
||||
<text-input v-model="searchTerm" name="search" :color="color" :theme="theme" placeholder="Search..." />
|
||||
:class="{
|
||||
'max-h-42 py-1': !isSearchable,
|
||||
'max-h-48 pb-1': isSearchable,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="isSearchable"
|
||||
class="px-2 pt-2 sticky top-0 bg-white dark-bg-notion-dark-light z-10"
|
||||
>
|
||||
<text-input
|
||||
v-model="searchTerm"
|
||||
name="search"
|
||||
:color="color"
|
||||
:theme="theme"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
@@ -89,7 +120,8 @@
|
||||
:style="optionStyle"
|
||||
:class="{ 'px-3 pr-9': multiple, 'px-3': !multiple }"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
|
||||
@click="select(item)">
|
||||
@click="select(item)"
|
||||
>
|
||||
<slot
|
||||
name="option"
|
||||
:option="item"
|
||||
@@ -101,7 +133,11 @@
|
||||
v-else-if="!loading && !(allowCreation && searchTerm)"
|
||||
class="w-full text-gray-500 text-center py-2"
|
||||
>
|
||||
{{ (allowCreation ? 'Type something to add an option' : 'No option available') }}.
|
||||
{{
|
||||
allowCreation
|
||||
? "Type something to add an option"
|
||||
: "No option available"
|
||||
}}.
|
||||
</p>
|
||||
<li
|
||||
v-if="allowCreation && searchTerm"
|
||||
@@ -109,8 +145,12 @@
|
||||
:style="optionStyle"
|
||||
:class="{ 'px-3 pr-9': multiple, 'px-3': !multiple }"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white dark:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
|
||||
@click="createOption(searchTerm)">
|
||||
Create <b class="px-1 bg-gray-300 rounded group-hover-text-black">{{ searchTerm }}</b>
|
||||
@click="createOption(searchTerm)"
|
||||
>
|
||||
Create
|
||||
<b class="px-1 bg-gray-300 rounded group-hover-text-black">{{
|
||||
searchTerm
|
||||
}}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</collapsible>
|
||||
@@ -118,54 +158,54 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Collapsible from '~/components/global/transitions/Collapsible.vue'
|
||||
import { themes} from "~/lib/forms/form-themes.js"
|
||||
import TextInput from '../TextInput.vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
import Fuse from 'fuse.js'
|
||||
import Collapsible from "~/components/global/transitions/Collapsible.vue"
|
||||
import { themes } from "~/lib/forms/form-themes.js"
|
||||
import TextInput from "../TextInput.vue"
|
||||
import debounce from "lodash/debounce"
|
||||
import Fuse from "fuse.js"
|
||||
|
||||
export default {
|
||||
name: 'VSelect',
|
||||
name: "VSelect",
|
||||
components: { Collapsible, TextInput },
|
||||
directives: {},
|
||||
props: {
|
||||
data: Array,
|
||||
modelValue: { default: null, type: [String, Number, Array, Object] },
|
||||
inputClass: { type: String, default: null },
|
||||
dropdownClass: { type: String, default: 'w-full' },
|
||||
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 },
|
||||
hasError: { type: Boolean, default: false },
|
||||
remote: { type: Function, default: null },
|
||||
searchKeys: { type: Array, default: () => ['name'] },
|
||||
optionKey: { type: String, default: 'id' },
|
||||
searchKeys: { type: Array, default: () => ["name"] },
|
||||
optionKey: { type: String, default: "id" },
|
||||
emitKey: { type: String, default: null },
|
||||
color: { type: String, default: '#3B82F6' },
|
||||
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 }
|
||||
disabled: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['update:modelValue', 'update-options'],
|
||||
emits: ["update:modelValue", "update-options"],
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
searchTerm: '',
|
||||
defaultValue: this.modelValue ?? null
|
||||
searchTerm: "",
|
||||
defaultValue: this.modelValue ?? null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionStyle() {
|
||||
return {
|
||||
'--bg-form-color': this.color
|
||||
"--bg-form-color": this.color,
|
||||
}
|
||||
},
|
||||
inputStyle() {
|
||||
return {
|
||||
'--tw-ring-color': this.color
|
||||
"--tw-ring-color": this.color,
|
||||
}
|
||||
},
|
||||
debouncedRemote() {
|
||||
@@ -176,13 +216,13 @@ export default {
|
||||
},
|
||||
filteredOptions() {
|
||||
if (!this.data) return []
|
||||
if (!this.searchable || this.remote || this.searchTerm === '') {
|
||||
if (!this.searchable || this.remote || this.searchTerm === "") {
|
||||
return this.data
|
||||
}
|
||||
|
||||
// Fuse search
|
||||
const fuzeOptions = {
|
||||
keys: this.searchKeys
|
||||
keys: this.searchKeys,
|
||||
}
|
||||
const fuse = new Fuse(this.data, fuzeOptions)
|
||||
return fuse.search(this.searchTerm).map((res) => {
|
||||
@@ -191,15 +231,19 @@ export default {
|
||||
},
|
||||
isSearchable() {
|
||||
return this.searchable || this.remote !== null || this.allowCreation
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm(val) {
|
||||
if (!this.debouncedRemote) return
|
||||
if ((this.remote && val) || (val === '' && !this.modelValue) || (val === '' && this.isOpen)) {
|
||||
if (
|
||||
(this.remote && val) ||
|
||||
(val === "" && !this.modelValue) ||
|
||||
(val === "" && this.isOpen)
|
||||
) {
|
||||
return this.debouncedRemote(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickAway(event) {
|
||||
@@ -227,7 +271,7 @@ export default {
|
||||
this.isOpen = !this.isOpen
|
||||
}
|
||||
if (!this.isOpen) {
|
||||
this.searchTerm = ''
|
||||
this.searchTerm = ""
|
||||
}
|
||||
},
|
||||
select(value) {
|
||||
@@ -241,25 +285,33 @@ export default {
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
const emitValue = Array.isArray(this.modelValue) ? [...this.modelValue] : []
|
||||
const emitValue = Array.isArray(this.modelValue)
|
||||
? [...this.modelValue]
|
||||
: []
|
||||
|
||||
if (this.isSelected(value)) {
|
||||
this.$emit('update:modelValue', emitValue.filter((item) => {
|
||||
if (this.emitKey) {
|
||||
return item !== value
|
||||
}
|
||||
return item[this.optionKey] !== value && item[this.optionKey] !== value[this.optionKey]
|
||||
}))
|
||||
this.$emit(
|
||||
"update:modelValue",
|
||||
emitValue.filter((item) => {
|
||||
if (this.emitKey) {
|
||||
return item !== value
|
||||
}
|
||||
return (
|
||||
item[this.optionKey] !== value &&
|
||||
item[this.optionKey] !== value[this.optionKey]
|
||||
)
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
emitValue.push(value)
|
||||
this.$emit('update:modelValue', emitValue)
|
||||
this.$emit("update:modelValue", emitValue)
|
||||
} else {
|
||||
if (this.modelValue === value) {
|
||||
this.$emit('update:modelValue', this.defaultValue ?? null)
|
||||
this.$emit("update:modelValue", this.defaultValue ?? null)
|
||||
} else {
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit("update:modelValue", value)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -268,13 +320,13 @@ export default {
|
||||
const newItem = {
|
||||
name: newOption,
|
||||
value: newOption,
|
||||
id: newOption
|
||||
id: newOption,
|
||||
}
|
||||
this.$emit('update-options', newItem)
|
||||
this.$emit("update-options", newItem)
|
||||
this.select(newItem)
|
||||
this.searchTerm = ''
|
||||
this.searchTerm = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,17 +16,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, defineProps } from 'vue'
|
||||
import { defineEmits, defineProps } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
function onClick() {
|
||||
if (props.disabled)
|
||||
return
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
if (props.disabled) return
|
||||
emit("update:modelValue", !props.modelValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
40
client/components/forms/useFormInput.js
vendored
40
client/components/forms/useFormInput.js
vendored
@@ -1,8 +1,8 @@
|
||||
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 { 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"
|
||||
|
||||
export const inputProps = {
|
||||
id: { type: String, default: null },
|
||||
@@ -17,22 +17,26 @@ export const inputProps = {
|
||||
uppercaseLabels: { type: Boolean, default: false },
|
||||
hideFieldName: { 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' }
|
||||
helpPosition: { type: String, default: "below_input" },
|
||||
color: { type: String, default: "#3B82F6" },
|
||||
wrapperClass: { type: String, default: "relative mb-3" },
|
||||
}
|
||||
|
||||
export function useFormInput (props, context, formPrefixKey = null) {
|
||||
export function useFormInput(props, context, formPrefixKey = null) {
|
||||
const content = ref(props.modelValue)
|
||||
|
||||
const inputStyle = computed(() => {
|
||||
return {
|
||||
'--tw-ring-color': props.color
|
||||
"--tw-ring-color": props.color,
|
||||
}
|
||||
})
|
||||
|
||||
const hasValidation = computed(() => {
|
||||
return props.form !== null && props.form !== undefined && _has(props.form, 'errors')
|
||||
return (
|
||||
props.form !== null &&
|
||||
props.form !== undefined &&
|
||||
_has(props.form, "errors")
|
||||
)
|
||||
})
|
||||
|
||||
const hasError = computed(() => {
|
||||
@@ -42,13 +46,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, (formPrefixKey || "") + props.name)
|
||||
}
|
||||
return content.value
|
||||
},
|
||||
set: (val) => {
|
||||
if (props.form) {
|
||||
_set(props.form, (formPrefixKey || '') + props.name, val)
|
||||
_set(props.form, (formPrefixKey || "") + props.name, val)
|
||||
} else {
|
||||
content.value = val
|
||||
}
|
||||
@@ -57,14 +61,14 @@ export function useFormInput (props, context, formPrefixKey = null) {
|
||||
props.form.errors.clear(props.name)
|
||||
}
|
||||
|
||||
context.emit('update:modelValue', compVal.value)
|
||||
}
|
||||
context.emit("update:modelValue", compVal.value)
|
||||
},
|
||||
})
|
||||
|
||||
const inputWrapperProps = computed(() => {
|
||||
const wrapperProps = {}
|
||||
Object.keys(inputProps).forEach((key) => {
|
||||
if (!['modelValue', 'disabled', 'placeholder', 'color'].includes(key)) {
|
||||
if (!["modelValue", "disabled", "placeholder", "color"].includes(key)) {
|
||||
wrapperProps[key] = props[key]
|
||||
}
|
||||
})
|
||||
@@ -78,7 +82,7 @@ export function useFormInput (props, context, formPrefixKey = null) {
|
||||
if (content.value !== newValue) {
|
||||
content.value = newValue
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -86,6 +90,6 @@ export function useFormInput (props, context, formPrefixKey = null) {
|
||||
inputStyle,
|
||||
hasValidation,
|
||||
hasError,
|
||||
inputWrapperProps
|
||||
inputWrapperProps,
|
||||
}
|
||||
}
|
||||
|
||||
12
client/components/forms/validation/Alert.js
vendored
12
client/components/forms/validation/Alert.js
vendored
@@ -2,20 +2,20 @@ export default {
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
|
||||
dismissible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
dismiss () {
|
||||
dismiss() {
|
||||
if (this.dismissible) {
|
||||
this.form.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
<template>
|
||||
<div v-if="form.errors.any()" class="alert alert-danger alert-dismissible" role="alert">
|
||||
<button v-if="dismissible" type="button" class="close" aria-label="Close" @click="dismiss">
|
||||
<div
|
||||
v-if="form.errors.any()"
|
||||
class="alert alert-danger alert-dismissible"
|
||||
role="alert"
|
||||
>
|
||||
<button
|
||||
v-if="dismissible"
|
||||
type="button"
|
||||
class="close"
|
||||
aria-label="Close"
|
||||
@click="dismiss"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
<slot>
|
||||
<div v-if="form.errors.has('error')" v-html="form.errors.get('error')"/>
|
||||
<div v-else v-html="message"/>
|
||||
<div
|
||||
v-if="form.errors.has('error')"
|
||||
v-html="form.errors.get('error')"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-html="message"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Alert from './Alert.js'
|
||||
import Alert from "./Alert.js"
|
||||
|
||||
export default {
|
||||
name: 'AlertError',
|
||||
name: "AlertError",
|
||||
|
||||
extends: Alert,
|
||||
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: 'There were some problems with your input.'
|
||||
}
|
||||
}
|
||||
default: "There were some problems with your input.",
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="form.successful" class="bg-green-200 border-green-600 text-green-600 border-l-4 p-4 relative rounded-lg"
|
||||
role="alert">
|
||||
<button v-if="dismissible"
|
||||
type="button"
|
||||
@click.prevent="dismiss()"
|
||||
class="absolute right-2 top-0 -mr-1 flex-shrink-0 flex p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 sm:-mr-2">
|
||||
<span class="sr-only">
|
||||
Dismiss
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="h-6 w-6 text-green-500"
|
||||
viewBox="0 0 1792 1792">
|
||||
<div
|
||||
v-if="form.successful"
|
||||
class="bg-green-200 border-green-600 text-green-600 border-l-4 p-4 relative rounded-lg"
|
||||
role="alert"
|
||||
>
|
||||
<button
|
||||
v-if="dismissible"
|
||||
type="button"
|
||||
class="absolute right-2 top-0 -mr-1 flex-shrink-0 flex p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 sm:-mr-2"
|
||||
@click.prevent="dismiss()"
|
||||
>
|
||||
<span class="sr-only"> Dismiss </span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="h-6 w-6 text-green-500"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z">
|
||||
</path>
|
||||
d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="font-bold">
|
||||
Success
|
||||
</p>
|
||||
<div v-html="message"/>
|
||||
<div v-html="message" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Alert from './Alert.js'
|
||||
import Alert from "./Alert.js"
|
||||
|
||||
export default {
|
||||
name: 'AlertSuccess',
|
||||
name: "AlertSuccess",
|
||||
extends: Alert,
|
||||
props: {
|
||||
message: { type: String, default: '' }
|
||||
}
|
||||
message: { type: String, default: "" },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="errorMessage" class="has-error text-sm text-red-500 -bottom-3"
|
||||
v-html="errorMessage"
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="has-error text-sm text-red-500 -bottom-3"
|
||||
v-html="errorMessage"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HasError',
|
||||
name: "HasError",
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
errorMessage () {
|
||||
if (!this.form || !this.form.errors || !this.form.errors.any()) return null
|
||||
const subErrorsKeys = Object.keys(this.form.errors.all()).filter((key) => {
|
||||
return key.startsWith(this.field) && key !== this.field
|
||||
})
|
||||
const baseError = this.form.errors.get(this.field) ?? (subErrorsKeys.length ? 'This field has some errors:' : null)
|
||||
errorMessage() {
|
||||
if (!this.form || !this.form.errors || !this.form.errors.any())
|
||||
return null
|
||||
const subErrorsKeys = Object.keys(this.form.errors.all()).filter(
|
||||
(key) => {
|
||||
return key.startsWith(this.field) && key !== this.field
|
||||
},
|
||||
)
|
||||
const baseError =
|
||||
this.form.errors.get(this.field) ??
|
||||
(subErrorsKeys.length ? "This field has some errors:" : null)
|
||||
// If no error and no sub errors, return
|
||||
if (!baseError) return null
|
||||
|
||||
return `<p class="text-red-500">${baseError}</p><ul class="list-disc list-inside">${subErrorsKeys.map((key) => {
|
||||
return '<li>' + this.getSubError(key) + '</li>'
|
||||
})}</ul>`
|
||||
}
|
||||
return `<p class="text-red-500">${baseError}</p><ul class="list-disc list-inside">${subErrorsKeys.map(
|
||||
(key) => {
|
||||
return "<li>" + this.getSubError(key) + "</li>"
|
||||
},
|
||||
)}</ul>`
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSubError (subErrorKey) {
|
||||
return this.form.errors.get(subErrorKey).replace(subErrorKey, 'item')
|
||||
}
|
||||
}
|
||||
getSubError(subErrorKey) {
|
||||
return this.form.errors.get(subErrorKey).replace(subErrorKey, "item")
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user