Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
13
components/formkit/DateTimePicker.vue
Normal file
13
components/formkit/DateTimePicker.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
function handleInput(e) {
|
||||
props.context.node.input(e.target.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input @input="handleInput" :value="props.context._value" />
|
||||
</template>
|
||||
139
components/formkit/FileDropzone.vue
Normal file
139
components/formkit/FileDropzone.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
/* eslint-disable */
|
||||
import { useDropzone } from "vue3-dropzone";
|
||||
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
const fileBase64 = ref([]);
|
||||
const files = ref([]);
|
||||
let err = ref(false);
|
||||
let errmsg = ref("");
|
||||
|
||||
const accept = props.context.accept;
|
||||
const multiple = props.context.multiple;
|
||||
const maxSize = props.context.maxSize;
|
||||
const minSize = props.context.minSize;
|
||||
const maxFiles = props.context.maxFiles;
|
||||
const disabled = props.context.disabled;
|
||||
|
||||
const toBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
async function onDrop(fileList, fileError, event) {
|
||||
if (fileError.length == 0) {
|
||||
err.value = false;
|
||||
errmsg.value = "";
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const base64 = await toBase64(fileList[i]);
|
||||
fileBase64.value.push({ data: fileList[i], base64 });
|
||||
files.value.push([fileList[i]]);
|
||||
}
|
||||
} else {
|
||||
err.value = true;
|
||||
errmsg.value = fileError[0].errors[0].message;
|
||||
}
|
||||
|
||||
updateNodeValue();
|
||||
}
|
||||
|
||||
async function removeFiles(index) {
|
||||
fileBase64.value.splice(index, 1);
|
||||
files.value.splice(index, 1);
|
||||
updateNodeValue();
|
||||
}
|
||||
|
||||
function updateNodeValue() {
|
||||
props.context.node.input(files.value);
|
||||
}
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept,
|
||||
multiple: multiple === "true" ? true : false,
|
||||
maxSize: maxSize ? parseInt(maxSize) : Infinity,
|
||||
minSize: minSize ? parseInt(minSize) : 0,
|
||||
maxFiles: maxFiles ? parseInt(maxFiles) : 0,
|
||||
disabled: disabled === "true" ? true : false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
|
||||
<div :class="context.classes.dropzone">
|
||||
<div v-bind="getRootProps()" class="cursor-pointer">
|
||||
<input v-bind="getInputProps()" />
|
||||
<div class="flex items-center justify-center h-36">
|
||||
<div>
|
||||
<Icon
|
||||
class="!block m-auto mb-3"
|
||||
size="30px"
|
||||
name="ic:outline-upload-file"
|
||||
/>
|
||||
<p class="text-center" v-if="isDragActive">Drop the files here ...</p>
|
||||
<p v-else>Drop files or click here to upload files</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="fileList"
|
||||
class="grid sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4"
|
||||
v-auto-animate
|
||||
>
|
||||
<div
|
||||
v-for="(file, index) in fileBase64"
|
||||
class="relative overflow-hidden w-full h-20 md:h-36 rounded-lg border-2 border-[rgb(var(--border-color))]"
|
||||
v-auto-animate
|
||||
>
|
||||
<img
|
||||
v-if="file.data.type.includes('image')"
|
||||
:src="file.base64"
|
||||
class="w-full h-20 md:h-36 object-cover object-center rounded-lg"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="h-full flex items-center justify-center opacity-50 text-primary font-semibold uppercase text-xl whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
file.data.name.slice(
|
||||
((file.data.name.lastIndexOf(".") - 1) >>> 0) + 2
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Icon
|
||||
name="ic:round-close"
|
||||
@click="removeFiles(index)"
|
||||
class="cursor-pointer absolute top-1 right-1 text-[rgb(var(--text-color))] bg-[rgb(var(--bg-2))] p-1 rounded-full"
|
||||
size="18"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-1 right-1 bg-[rgb(var(--bg-2))] px-2 rounded-lg"
|
||||
>
|
||||
<span class="font-semibold text-xs text-[rgb(var(--text-color))]">
|
||||
{{ file.data.path }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
v-if="err"
|
||||
class="formkit-messages list-none p-0 mt-1 mb-0 relative -bottom-5 -left-2"
|
||||
aria-live="polite"
|
||||
>
|
||||
<li
|
||||
class="formkit-message text-red-500 mb-1 text-xs formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
|
||||
id="input_9-rule_required"
|
||||
data-message-type="validation"
|
||||
>
|
||||
{{ errmsg }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
83
components/formkit/OneTimePassword.vue
Normal file
83
components/formkit/OneTimePassword.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
/* eslint-disable */
|
||||
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
const digits = Number(props.context.digits);
|
||||
const tmp = ref(props.context.value || "");
|
||||
|
||||
/**
|
||||
* Handle input, advancing or retreating focus.
|
||||
*/
|
||||
function handleInput(index, e) {
|
||||
const prev = tmp.value;
|
||||
|
||||
if (tmp.value.length <= index) {
|
||||
// If this is a new digit
|
||||
tmp.value = "" + tmp.value + e.target.value;
|
||||
} else {
|
||||
// If this digit is in the middle somewhere, cut the string into two
|
||||
// pieces at the index, and insert our new digit in.
|
||||
tmp.value =
|
||||
"" +
|
||||
tmp.value.substr(0, index) +
|
||||
e.target.value +
|
||||
tmp.value.substr(index + 1);
|
||||
}
|
||||
|
||||
// Get all the digit inputs
|
||||
const inputs = e.target.parentElement.querySelectorAll("input");
|
||||
|
||||
if (index < digits - 1 && tmp.value.length >= prev.length) {
|
||||
// If this is a new input and not at the end, focus the next input
|
||||
inputs.item(index + 1).focus();
|
||||
} else if (index > 0 && tmp.value.length < prev.length) {
|
||||
// in this case we deleted a value, focus backwards
|
||||
inputs.item(index - 1).focus();
|
||||
}
|
||||
|
||||
if (tmp.value.length === digits) {
|
||||
// If our input is complete, commit the value.
|
||||
props.context.node.input(tmp.value);
|
||||
} else if (tmp.value.length < digits && props.context.value !== "") {
|
||||
// If our input is incomplete, it should have no value.
|
||||
props.context.node.input("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On focus, select the text in our input.
|
||||
*/
|
||||
function handleFocus(e) {
|
||||
e.target.select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the paste event.
|
||||
*/
|
||||
function handlePaste(e) {
|
||||
const paste = e.clipboardData.getData("text");
|
||||
if (typeof paste === "string") {
|
||||
// If it is the right length, paste it.
|
||||
tmp.value = paste.substr(0, digits);
|
||||
const inputs = e.target.parentElement.querySelectorAll("input");
|
||||
// Focus on the last character
|
||||
inputs.item(tmp.value.length - 1).focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<input
|
||||
v-for="index in digits"
|
||||
maxlength="1"
|
||||
:class="context.classes.digit"
|
||||
:value="tmp[index - 1] || ''"
|
||||
@input="handleInput(index - 1, $event)"
|
||||
@focus="handleFocus"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
</template>
|
||||
23
components/formkit/TextMask.vue
Normal file
23
components/formkit/TextMask.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
/* eslint-disable */
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
const mask = String(props.context.mask);
|
||||
// console.log(props.context);
|
||||
|
||||
function handleInput(e) {
|
||||
props.context.node.input(e.target.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
@input="handleInput"
|
||||
:class="context.classes.input"
|
||||
:value="props.context._value"
|
||||
:placeholder="props.context.attrs.placeholder"
|
||||
v-maska="mask"
|
||||
/>
|
||||
</template>
|
||||
36
components/formkit/Toggle.vue
Normal file
36
components/formkit/Toggle.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
context: Object,
|
||||
});
|
||||
|
||||
function handleChange(event) {
|
||||
props.context.node.input(event.target.checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:class="context.classes.toggle"
|
||||
class="inline-flex items-center mb-5 cursor-pointer mt-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="context.value"
|
||||
:disabled="context.disabled"
|
||||
class="sr-only peer"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<div
|
||||
class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all peer-checked:bg-blue-600"
|
||||
></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900">
|
||||
{{
|
||||
context.onLabel || context.offLabel
|
||||
? context.value
|
||||
? context.onLabel
|
||||
: context.offLabel
|
||||
: context.label
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
Reference in New Issue
Block a user