mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 09:43:49 +02:00
replace el-input with InputGroup
This commit is contained in:
47
src/components/ui/input-group/InputGroupAction.vue
Normal file
47
src/components/ui/input-group/InputGroupAction.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { useAttrs } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import InputGroupField from './InputGroupField.vue';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
showPassword: { type: Boolean, default: false },
|
||||
showCount: { type: Boolean, default: false },
|
||||
maxlength: { type: Number, required: false },
|
||||
size: { type: String, default: 'default' }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroupField
|
||||
v-model="modelValue"
|
||||
:class="props.class"
|
||||
:input-class="props.inputClass"
|
||||
:clearable="props.clearable"
|
||||
:show-password="props.showPassword"
|
||||
:show-count="props.showCount"
|
||||
:maxlength="props.maxlength"
|
||||
:size="props.size"
|
||||
v-bind="attrs">
|
||||
<template v-if="$slots.leading" #leading>
|
||||
<slot name="leading" />
|
||||
</template>
|
||||
<template v-if="$slots.actions" #trailing>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</InputGroupField>
|
||||
</template>
|
||||
50
src/components/ui/input-group/InputGroupAffix.vue
Normal file
50
src/components/ui/input-group/InputGroupAffix.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { computed, useAttrs, useSlots } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import InputGroupField from './InputGroupField.vue';
|
||||
import { InputGroupText } from '.';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
prefixText: { type: String, default: '' },
|
||||
suffixText: { type: String, default: '' },
|
||||
clearable: { type: Boolean, default: false },
|
||||
size: { type: String, default: 'default' }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
const slots = useSlots();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
|
||||
const hasLeading = computed(() => Boolean(props.prefixText) || Boolean(slots.leading));
|
||||
const hasTrailing = computed(() => Boolean(props.suffixText) || Boolean(slots.trailing));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroupField
|
||||
v-model="modelValue"
|
||||
:class="props.class"
|
||||
:input-class="props.inputClass"
|
||||
:clearable="props.clearable"
|
||||
:size="props.size"
|
||||
v-bind="attrs">
|
||||
<template v-if="hasLeading" #leading>
|
||||
<InputGroupText v-if="props.prefixText">{{ props.prefixText }}</InputGroupText>
|
||||
<slot name="leading" />
|
||||
</template>
|
||||
<template v-if="hasTrailing" #trailing>
|
||||
<slot name="trailing" />
|
||||
<InputGroupText v-if="props.suffixText">{{ props.suffixText }}</InputGroupText>
|
||||
</template>
|
||||
</InputGroupField>
|
||||
</template>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script setup>
|
||||
import { Hash, X } from 'lucide-vue-next';
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea
|
||||
} from '.';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
maxlength: { type: Number, required: true },
|
||||
multiline: { type: Boolean, default: false },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
countClass: { type: null, required: false },
|
||||
iconClass: { type: null, required: false },
|
||||
clearable: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
|
||||
const valueLength = computed(() => String(modelValue.value ?? '').length);
|
||||
const remaining = computed(() => Math.max(props.maxlength - valueLength.value, 0));
|
||||
const wrapperClass = computed(() => cn(props.class, attrs.class));
|
||||
const inputClass = computed(() => cn(props.inputClass));
|
||||
const inputAttrs = computed(() => {
|
||||
const { class: _class, style: _style, ...rest } = attrs;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
|
||||
|
||||
function clearValue() {
|
||||
if (isDisabled.value) return;
|
||||
modelValue.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroup :class="wrapperClass" :style="attrs.style">
|
||||
<InputGroupAddon v-if="$slots.leading" align="inline-start">
|
||||
<slot name="leading" />
|
||||
</InputGroupAddon>
|
||||
<component
|
||||
:is="props.multiline ? InputGroupTextarea : InputGroupInput"
|
||||
v-model="modelValue"
|
||||
:maxlength="props.maxlength"
|
||||
:class="inputClass"
|
||||
v-bind="inputAttrs" />
|
||||
<InputGroupAddon v-if="$slots.trailing" align="inline-end">
|
||||
<slot name="trailing" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="props.clearable && valueLength > 0" align="inline-end">
|
||||
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="clearValue">
|
||||
<X class="size-3.5" />
|
||||
<span class="sr-only">Clear</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon :align="props.multiline ? 'block-end' : 'inline-end'" v-if="valueLength > 0">
|
||||
<InputGroupText :class="cn('gap-1 tabular-nums text-xs', props.multiline && 'ml-auto', props.countClass)">
|
||||
<span>{{ valueLength }}</span>
|
||||
<span class="text-muted-foreground/70">/ {{ props.maxlength }}</span>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</template>
|
||||
124
src/components/ui/input-group/InputGroupField.vue
Normal file
124
src/components/ui/input-group/InputGroupField.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script setup>
|
||||
import { computed, ref, useAttrs } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Eye, EyeOff, X } from 'lucide-vue-next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from '.';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
showPassword: { type: Boolean, default: false },
|
||||
showCount: { type: Boolean, default: false },
|
||||
maxlength: { type: Number, required: false },
|
||||
type: { type: String, required: false },
|
||||
size: { type: String, default: 'default' }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
|
||||
const reveal = ref(false);
|
||||
const valueLength = computed(() => String(modelValue.value ?? '').length);
|
||||
const maxLength = computed(() => props.maxlength ?? attrs.maxlength);
|
||||
const wrapperClass = computed(() =>
|
||||
cn(props.class, attrs.class, props.size === 'sm' && 'h-8')
|
||||
);
|
||||
const inputClass = computed(() => cn(props.inputClass));
|
||||
|
||||
const inputType = computed(() => {
|
||||
const rawType = props.type ?? attrs.type;
|
||||
if (props.showPassword) {
|
||||
return reveal.value ? 'text' : rawType || 'password';
|
||||
}
|
||||
return rawType;
|
||||
});
|
||||
|
||||
const inputAttrs = computed(() => {
|
||||
const {
|
||||
class: _class,
|
||||
style: _style,
|
||||
type: _type,
|
||||
maxlength: _maxlength,
|
||||
onInput: _onInput,
|
||||
onChange: _onChange,
|
||||
...rest
|
||||
} = attrs;
|
||||
return {
|
||||
...rest,
|
||||
type: inputType.value,
|
||||
maxlength: maxLength.value
|
||||
};
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
|
||||
const showCount = computed(() => Boolean(maxLength.value) && props.showCount);
|
||||
|
||||
function clearValue() {
|
||||
if (isDisabled.value) return;
|
||||
modelValue.value = '';
|
||||
emit('input', '');
|
||||
emit('change', '');
|
||||
}
|
||||
|
||||
function toggleReveal() {
|
||||
if (isDisabled.value) return;
|
||||
reveal.value = !reveal.value;
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
const value = event?.target?.value ?? '';
|
||||
emit('input', value);
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
const value = event?.target?.value ?? '';
|
||||
emit('change', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroup :class="wrapperClass" :style="attrs.style" :data-disabled="isDisabled ? 'true' : undefined">
|
||||
<InputGroupAddon v-if="$slots.leading" align="inline-start">
|
||||
<slot name="leading" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
v-model="modelValue"
|
||||
:class="inputClass"
|
||||
v-bind="inputAttrs"
|
||||
@input="handleInput"
|
||||
@change="handleChange" />
|
||||
<InputGroupAddon v-if="$slots.trailing" align="inline-end">
|
||||
<slot name="trailing" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="props.showPassword" align="inline-end">
|
||||
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="toggleReveal">
|
||||
<Eye v-if="!reveal" class="size-3.5" />
|
||||
<EyeOff v-else class="size-3.5" />
|
||||
<span class="sr-only">Toggle password</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="props.clearable && valueLength > 0" align="inline-end">
|
||||
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="clearValue">
|
||||
<X class="size-3.5" />
|
||||
<span class="sr-only">Clear</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="showCount && valueLength > 0" align="inline-end">
|
||||
<InputGroupText class="gap-1 tabular-nums text-xs">
|
||||
<span>{{ valueLength }}</span>
|
||||
<span class="text-muted-foreground/70">/ {{ maxLength }}</span>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</template>
|
||||
42
src/components/ui/input-group/InputGroupSearch.vue
Normal file
42
src/components/ui/input-group/InputGroupSearch.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { useAttrs } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Search } from 'lucide-vue-next';
|
||||
|
||||
import InputGroupField from './InputGroupField.vue';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
clearable: { type: Boolean, default: true },
|
||||
size: { type: String, default: 'default' }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroupField
|
||||
v-model="modelValue"
|
||||
:class="props.class"
|
||||
:input-class="props.inputClass"
|
||||
:clearable="props.clearable"
|
||||
:size="props.size"
|
||||
v-bind="attrs">
|
||||
<template #leading>
|
||||
<Search class="size-4" />
|
||||
</template>
|
||||
<template v-if="$slots.trailing" #trailing>
|
||||
<slot name="trailing" />
|
||||
</template>
|
||||
</InputGroupField>
|
||||
</template>
|
||||
144
src/components/ui/input-group/InputGroupTextareaField.vue
Normal file
144
src/components/ui/input-group/InputGroupTextareaField.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, useAttrs, watch } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { X } from 'lucide-vue-next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea } from '.';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
class: { type: null, required: false },
|
||||
inputClass: { type: null, required: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
showCount: { type: Boolean, default: false },
|
||||
maxlength: { type: Number, required: false },
|
||||
autosize: { type: [Boolean, Object], default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||
passive: true,
|
||||
defaultValue: props.modelValue
|
||||
});
|
||||
|
||||
const textareaRef = ref(null);
|
||||
const valueLength = computed(() => String(modelValue.value ?? '').length);
|
||||
const maxLength = computed(() => props.maxlength ?? attrs.maxlength);
|
||||
const wrapperClass = computed(() => cn(props.class, attrs.class));
|
||||
const inputClass = computed(() => cn(props.inputClass));
|
||||
const showCount = computed(() => Boolean(maxLength.value) && props.showCount);
|
||||
const autosizeConfig = computed(() => {
|
||||
if (!props.autosize) return null;
|
||||
return typeof props.autosize === 'object' ? props.autosize : {};
|
||||
});
|
||||
|
||||
const inputAttrs = computed(() => {
|
||||
const {
|
||||
class: _class,
|
||||
style: _style,
|
||||
maxlength: _maxlength,
|
||||
onInput: _onInput,
|
||||
onChange: _onChange,
|
||||
...rest
|
||||
} = attrs;
|
||||
return {
|
||||
...rest,
|
||||
maxlength: maxLength.value
|
||||
};
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
|
||||
|
||||
function resolveTextareaEl() {
|
||||
const instance = textareaRef.value;
|
||||
if (!instance) return null;
|
||||
return instance.$el ?? instance;
|
||||
}
|
||||
|
||||
function resizeTextarea() {
|
||||
if (!autosizeConfig.value) return;
|
||||
const el = resolveTextareaEl();
|
||||
if (!el) return;
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight) || 16;
|
||||
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
|
||||
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
||||
const minRows = autosizeConfig.value.minRows ?? Number(attrs.rows) || 1;
|
||||
const maxRows = autosizeConfig.value.maxRows ?? Number.POSITIVE_INFINITY;
|
||||
const minHeight = lineHeight * minRows + paddingTop + paddingBottom;
|
||||
const maxHeight = lineHeight * maxRows + paddingTop + paddingBottom;
|
||||
|
||||
el.style.height = 'auto';
|
||||
const nextHeight = Math.min(maxHeight, Math.max(el.scrollHeight, minHeight));
|
||||
el.style.height = `${nextHeight}px`;
|
||||
}
|
||||
|
||||
function clearValue() {
|
||||
if (isDisabled.value) return;
|
||||
modelValue.value = '';
|
||||
emit('input', '');
|
||||
emit('change', '');
|
||||
nextTick(resizeTextarea);
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
const value = event?.target?.value ?? '';
|
||||
emit('input', value);
|
||||
resizeTextarea();
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
const value = event?.target?.value ?? '';
|
||||
emit('change', value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (autosizeConfig.value) {
|
||||
nextTick(resizeTextarea);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
() => {
|
||||
if (autosizeConfig.value) {
|
||||
nextTick(resizeTextarea);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputGroup :class="wrapperClass" :style="attrs.style" :data-disabled="isDisabled ? 'true' : undefined">
|
||||
<InputGroupAddon v-if="$slots.leading" align="block-start">
|
||||
<slot name="leading" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupTextarea
|
||||
ref="textareaRef"
|
||||
v-model="modelValue"
|
||||
:class="inputClass"
|
||||
v-bind="inputAttrs"
|
||||
@input="handleInput"
|
||||
@change="handleChange" />
|
||||
<InputGroupAddon v-if="$slots.trailing" align="block-end">
|
||||
<slot name="trailing" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="props.clearable && valueLength > 0" align="inline-end">
|
||||
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="clearValue">
|
||||
<X class="size-3.5" />
|
||||
<span class="sr-only">Clear</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon v-if="showCount && valueLength > 0" align="block-end">
|
||||
<InputGroupText class="gap-1 tabular-nums text-xs">
|
||||
<span>{{ valueLength }}</span>
|
||||
<span class="text-muted-foreground/70">/ {{ maxLength }}</span>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</template>
|
||||
@@ -6,7 +6,11 @@ export { default as InputGroupButton } from './InputGroupButton.vue';
|
||||
export { default as InputGroupInput } from './InputGroupInput.vue';
|
||||
export { default as InputGroupText } from './InputGroupText.vue';
|
||||
export { default as InputGroupTextarea } from './InputGroupTextarea.vue';
|
||||
export { default as InputGroupCharCount } from './InputGroupCharCount.vue';
|
||||
export { default as InputGroupField } from './InputGroupField.vue';
|
||||
export { default as InputGroupTextareaField } from './InputGroupTextareaField.vue';
|
||||
export { default as InputGroupSearch } from './InputGroupSearch.vue';
|
||||
export { default as InputGroupAffix } from './InputGroupAffix.vue';
|
||||
export { default as InputGroupAction } from './InputGroupAction.vue';
|
||||
|
||||
export const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||
|
||||
Reference in New Issue
Block a user