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:
Favour Olayinka
2024-04-15 18:39:03 +01:00
committed by GitHub
parent 8d35fc8b1a
commit bcd45ce8a6
228 changed files with 17036 additions and 8744 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
<script>
export default {
name: 'InputLabel',
name: "InputLabel",
props: {
nativeFor: { type: String, default: null },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}
}
}
},
},
}

View File

@@ -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">&times;</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>

View File

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

View File

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