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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user