mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-11-21 01:21:54 +08:00
rework inputs, again...
hopefully this is the last time, but code wise its nicer now and structure wise we can now have elements "inside" of the input for counter, loading, etc
This commit is contained in:
parent
58a2eb77c3
commit
7af707538f
@ -2,6 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", date: string): void;
|
||||
@ -14,6 +15,7 @@ const props = defineProps<{
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
@ -22,6 +24,8 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, date, pr
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy -->
|
||||
<input v-model="date" type="date" v-bind="$attrs" :disabled="disabled" />
|
||||
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="date">
|
||||
<!-- todo make fancy -->
|
||||
<input v-model="date" type="date" v-bind="$attrs" :disabled="disabled" :class="slotProps.class" />
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", file: string): void;
|
||||
@ -15,6 +16,7 @@ const props = defineProps<{
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
showSize?: boolean;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
@ -23,6 +25,8 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, file, pr
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy, implement functionality -->
|
||||
<input type="file" v-bind="$attrs" :disabled="disabled" />
|
||||
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="file">
|
||||
<!-- todo make fancy, implement functionality -->
|
||||
<input type="file" v-bind="$attrs" :disabled="disabled" :class="slotProps.class" />
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: object | string | boolean | number | null | undefined): void;
|
||||
@ -26,6 +25,7 @@ const props = withDefaults(
|
||||
itemText?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>(),
|
||||
@ -34,6 +34,7 @@ const props = withDefaults(
|
||||
itemValue: "value",
|
||||
itemText: "text",
|
||||
label: "",
|
||||
loading: false,
|
||||
errorMessages: () => [],
|
||||
rules: () => [],
|
||||
}
|
||||
@ -43,14 +44,11 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, internal
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: internalVal, error: hasError }">
|
||||
<select v-model="internalVal" :disabled="disabled" :class="inputClasses" @blur="v.$touch()">
|
||||
<option v-for="val in values" :key="val[itemValue] || val" :value="val[itemValue] || val" class="dark:bg-[#191e28]">
|
||||
{{ val[itemText] || val }}
|
||||
</option>
|
||||
</select>
|
||||
<floating-label :label="label" />
|
||||
</label>
|
||||
</ErrorTooltip>
|
||||
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="internalVal">
|
||||
<select v-model="internalVal" :disabled="disabled" :class="slotProps.class" @blur="v.$touch()">
|
||||
<option v-for="val in values" :key="val[itemValue] || val" :value="val[itemValue] || val" class="dark:bg-[#191e28]">
|
||||
{{ val[itemText] || val }}
|
||||
</option>
|
||||
</select>
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const tag = ref<string>("");
|
||||
const emit = defineEmits<{
|
||||
@ -18,6 +17,7 @@ const props = defineProps<{
|
||||
label?: string;
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
@ -40,18 +40,27 @@ function add() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<div class="relative flex items-center pointer-events-none text-left" :class="{ filled: (modelValue && modelValue.length) || tag, error: hasError }">
|
||||
<div :class="inputClasses" class="flex">
|
||||
<span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
|
||||
{{ t }}
|
||||
<button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
|
||||
</span>
|
||||
<input v-model="tag" type="text" class="pointer-events-auto outline-none bg-transparent flex-grow" @keydown.enter="add" @blur="v.$touch()" />
|
||||
<floating-label :label="label" />
|
||||
</div>
|
||||
<span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ tags?.length || 0 }}</span>
|
||||
</div>
|
||||
</ErrorTooltip>
|
||||
<InputWrapper
|
||||
v-slot="slotProps"
|
||||
:errors="errors"
|
||||
:has-error="hasError"
|
||||
:counter="counter"
|
||||
:maxlength="maxlength"
|
||||
:loading="loading || v.$pending"
|
||||
:label="label"
|
||||
:value="tags"
|
||||
>
|
||||
<span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
|
||||
{{ t }}
|
||||
<button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
|
||||
</span>
|
||||
<input
|
||||
v-model="tag"
|
||||
type="text"
|
||||
class="pointer-events-auto outline-none bg-transparent flex-grow"
|
||||
:class="slotProps.class"
|
||||
@keydown.enter="add"
|
||||
@blur="v.$touch()"
|
||||
/>
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value?: string): void;
|
||||
@ -17,6 +16,7 @@ const props = defineProps<{
|
||||
label?: string;
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
@ -25,11 +25,16 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, value, p
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
|
||||
<input v-model="value" type="text" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
|
||||
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ value?.length || 0 }}</span>
|
||||
<floating-label :label="label" /> </label
|
||||
></ErrorTooltip>
|
||||
<InputWrapper
|
||||
v-slot="slotProps"
|
||||
:errors="errors"
|
||||
:has-error="hasError"
|
||||
:counter="counter"
|
||||
:maxlength="maxlength"
|
||||
:loading="loading || v.$pending"
|
||||
:label="label"
|
||||
:value="value"
|
||||
>
|
||||
<input v-model="value" type="text" v-bind="$attrs" :maxlength="maxlength" :class="slotProps.class" @blur="v.$touch()" />
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import InputWrapper from "~/components/ui/InputWrapper.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
@ -17,6 +16,7 @@ const props = defineProps<{
|
||||
label?: string;
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
loading?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
@ -25,14 +25,16 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, value, p
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
|
||||
<textarea v-model="value" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
|
||||
<floating-label :label="label" />
|
||||
</label>
|
||||
<div v-if="counter" class="mt-1 mb-2">
|
||||
<span v-if="maxlength">{{ value?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else>{{ value?.length || 0 }}</span>
|
||||
</div>
|
||||
</ErrorTooltip>
|
||||
<InputWrapper
|
||||
v-slot="slotProps"
|
||||
:errors="errors"
|
||||
:has-error="hasError"
|
||||
:counter="counter"
|
||||
:maxlength="maxlength"
|
||||
:loading="loading || v.$pending"
|
||||
:label="label"
|
||||
:value="value"
|
||||
>
|
||||
<textarea v-model="value" v-bind="$attrs" :maxlength="maxlength" :class="slotProps.class" @blur="v.$touch()" />
|
||||
</InputWrapper>
|
||||
</template>
|
||||
|
47
frontend-new/src/components/ui/InputWrapper.vue
Normal file
47
frontend-new/src/components/ui/InputWrapper.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
errors?: string[];
|
||||
hasError: boolean;
|
||||
label?: string;
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
loading?: boolean;
|
||||
value: unknown;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errors" class="w-full" :class="{ filled: value, error: hasError }">
|
||||
<label
|
||||
:class="[
|
||||
'relative flex w-full outline-none p-2 border-bottom-1px rounded',
|
||||
'bg-primary-light-200 border-gray-400 dark:(bg-primary-300/8 border-gray-500)',
|
||||
'focus:(bg-primary-200/5 dark:bg-primary-200/10 border-primary-400)',
|
||||
'error:(border-red-400) disabled:(bg-black-15 text-black-50)',
|
||||
'transition duration-200 ease',
|
||||
]"
|
||||
>
|
||||
<slot class="outline-none flex-grow bg-transparent"></slot>
|
||||
<span class="flex pl-2">
|
||||
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ value?.length || 0 }}</span>
|
||||
<!-- todo proper loading indicator -->
|
||||
<span v-if="loading">Loading...</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="label"
|
||||
:class="[
|
||||
'absolute origin-top-left left-2 italic',
|
||||
'input-focused:(transform scale-62 opacity-100 not-italic) filled:(transform scale-62 text-black-50 not-italic)',
|
||||
'opacity-80 error:(!text-red-400) input-focused:(text-primary-300)',
|
||||
'top-10px input-focused:(top-0) filled:(top-0)',
|
||||
'transition duration-250 ease',
|
||||
]"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</label>
|
||||
</ErrorTooltip>
|
||||
</template>
|
@ -1,31 +0,0 @@
|
||||
import { defineComponent, h } from "vue";
|
||||
|
||||
export const inputClasses =
|
||||
"w-full outline-none p-2 border-bottom-1px rounded " +
|
||||
"bg-primary-light-200 border-gray-400 dark:(bg-primary-300/8 border-gray-500) " +
|
||||
"focus:(bg-primary-200/5 dark:bg-primary-200/10 border-primary-400) " +
|
||||
"error:(border-red-400) " +
|
||||
"disabled:(bg-black-15 text-black-50) " +
|
||||
"transition duration-200 ease";
|
||||
|
||||
export const labelClasses = [
|
||||
"absolute origin-top-left left-2 italic",
|
||||
"input-focused:(transform scale-62 opacity-100 not-italic) filled:(transform scale-62 text-black-50 not-italic)",
|
||||
"opacity-80 error:(!text-red-400) input-focused:(text-primary-300)",
|
||||
"top-10px input-focused:(top-0) filled:(top-0)",
|
||||
"transition duration-250 ease",
|
||||
];
|
||||
|
||||
export const FloatingLabel = defineComponent({
|
||||
name: "FloatingLabel",
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.label ? h("p", { class: labelClasses }, this.label) : [];
|
||||
},
|
||||
});
|
@ -4,6 +4,7 @@ import * as validators from "@vuelidate/validators";
|
||||
import { createI18nMessage, helpers, ValidatorWrapper } from "@vuelidate/validators";
|
||||
import { I18n } from "~/i18n";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
export function isErrorObject(errorObject: string | ErrorObject): errorObject is ErrorObject {
|
||||
return (<ErrorObject>errorObject).$message !== undefined;
|
||||
@ -32,7 +33,7 @@ export function useValidation<T>(
|
||||
}
|
||||
return e;
|
||||
});
|
||||
const hasError = computed(() => (errorMessages && errorMessages.length) || v.value.$error);
|
||||
const hasError = computed(() => (errorMessages && errorMessages.length > 0) || v.value.$error);
|
||||
|
||||
return { v, errors, hasError };
|
||||
}
|
||||
@ -82,7 +83,7 @@ export const validName = withOverrideMessage(
|
||||
value: value,
|
||||
});
|
||||
return { $valid: true };
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return !e.response?.data.isHangarApiException ? { $valid: false } : { $valid: false, $message: e.response?.data.message };
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user