initial
This commit is contained in:
329
layers/ui/components/input/index.vue
Normal file
329
layers/ui/components/input/index.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
cn.b(),
|
||||
cn.m(nativeType),
|
||||
cn.is('invalid', invalid),
|
||||
cn.is('disabled', disabled),
|
||||
cn.is('focused', focused),
|
||||
cn.is('readonly', readonly),
|
||||
cn.has('value', isFilled),
|
||||
]"
|
||||
>
|
||||
<label :class="[cn.e('wrapper')]">
|
||||
<div :class="[cn.e('content')]">
|
||||
<input
|
||||
ref="inputRef"
|
||||
:inputmode="inputmode"
|
||||
:name="id"
|
||||
:class="[cn.e('control')]"
|
||||
:type="inputType"
|
||||
:autocomplete="autocomplete"
|
||||
:disabled="disabled"
|
||||
:value="inputValue"
|
||||
:readonly="readonly"
|
||||
:tabindex="readonly ? -1 : 0"
|
||||
@input="handleInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
>
|
||||
|
||||
<p :class="[cn.e('label')]">{{ label }}</p>
|
||||
|
||||
<div
|
||||
v-if="renderSuffixSlot"
|
||||
:class="[cn.e('suffix')]"
|
||||
>
|
||||
<slot name="suffix">
|
||||
<UiButton
|
||||
v-if="showPasswordToggler"
|
||||
type="ghost"
|
||||
color="secondary"
|
||||
size="small"
|
||||
:icon="showPassword ? 'non-visibility' : 'visibility'"
|
||||
@click="togglePassword"
|
||||
@mousedown.prevent
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-else-if="clearable"
|
||||
icon="cross"
|
||||
type="ghost"
|
||||
color="secondary"
|
||||
size="small"
|
||||
@click="handleChange('', false)"
|
||||
@mousedown.prevent
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-else-if="copyable"
|
||||
type="ghost"
|
||||
color="secondary"
|
||||
size="small"
|
||||
icon="s-copy"
|
||||
@click="copy"
|
||||
@mousedown.prevent
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="!readonly && (!!rules || !!caption || $slots.caption)"
|
||||
:class="[cn.e('bottom')]"
|
||||
>
|
||||
<span
|
||||
v-if="invalid"
|
||||
:class="[cn.e('validation-message')]"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
|
||||
<slot
|
||||
v-else-if="caption || $slots.caption"
|
||||
name="caption"
|
||||
>
|
||||
<span :class="[cn.e('caption')]">
|
||||
{{ caption }}
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useField } from 'vee-validate'
|
||||
import IMask from 'imask'
|
||||
import { isEqual } from 'lodash-es'
|
||||
|
||||
import type { InputHTMLAttributes } from 'vue'
|
||||
import type { RuleExpression } from 'vee-validate'
|
||||
import type { FactoryArg as IMaskOptions, InputMask } from 'imask'
|
||||
|
||||
export interface Props {
|
||||
id: string
|
||||
nativeType?: 'text' | 'password'
|
||||
label: string
|
||||
disabled?: boolean
|
||||
caption?: string
|
||||
copyable?: boolean
|
||||
clearable?: boolean
|
||||
autofocus?: boolean
|
||||
readonly?: boolean
|
||||
autocomplete?: InputHTMLAttributes['autocomplete']
|
||||
inputmode?: InputHTMLAttributes['inputmode']
|
||||
rules?: RuleExpression<string>
|
||||
mask?: IMaskOptions
|
||||
modelValue?: string
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'UiInput',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
nativeType: 'text',
|
||||
disabled: false,
|
||||
copyable: false,
|
||||
clearable: false,
|
||||
autofocus: false,
|
||||
readonly: false,
|
||||
autocomplete: 'off',
|
||||
inputmode: 'text',
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': []
|
||||
focus: []
|
||||
blur: []
|
||||
}>()
|
||||
|
||||
function isEmptyValue(value: string) {
|
||||
return [null, undefined, ''].includes(value)
|
||||
}
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const cn = useClassname('ui-input')
|
||||
|
||||
const { value, errorMessage, setValue, handleChange, handleBlur } = useField(
|
||||
toRef(props, 'id'),
|
||||
toRef(props, 'rules'),
|
||||
{
|
||||
validateOnValueUpdate: false,
|
||||
initialValue: !isEmptyValue(props.modelValue)
|
||||
? props.modelValue
|
||||
: undefined,
|
||||
syncVModel: true,
|
||||
},
|
||||
)
|
||||
|
||||
const inputRef = ref(null)
|
||||
const maskInstance = shallowRef<InputMask<IMaskOptions>>()
|
||||
const maskOptions = toRef(props, 'mask')
|
||||
const showPassword = ref(false)
|
||||
const focused = ref(false)
|
||||
const maskedValue = ref()
|
||||
|
||||
let valueEvent = false
|
||||
let acceptEvent = false
|
||||
let updateOptionsEvent = false
|
||||
|
||||
const active = computed(() => !props.disabled)
|
||||
const invalid = computed(() => active.value && !!errorMessage.value)
|
||||
|
||||
const inputValue = computed(() => {
|
||||
return maskInstance.value ? maskedValue.value : value.value
|
||||
})
|
||||
const isFilled = computed(() => !isEmptyValue(inputValue.value))
|
||||
|
||||
const isPassword = computed(() => props.nativeType === 'password')
|
||||
const showPasswordToggler = computed(() => isPassword.value && active.value)
|
||||
const copyable = computed(() => {
|
||||
if (isPassword.value)
|
||||
return false
|
||||
|
||||
return props.copyable && isFilled.value
|
||||
})
|
||||
const clearable = computed(
|
||||
() => active.value && props.clearable && isFilled.value,
|
||||
)
|
||||
|
||||
const renderSuffixSlot = computed(() => {
|
||||
return (
|
||||
slots.suffix
|
||||
|| showPasswordToggler.value
|
||||
|| copyable.value
|
||||
|| clearable.value
|
||||
)
|
||||
})
|
||||
|
||||
const inputType = computed(() => {
|
||||
return !isPassword.value || showPassword.value ? 'text' : props.nativeType
|
||||
})
|
||||
|
||||
function togglePassword() {
|
||||
showPassword.value = !showPassword.value
|
||||
}
|
||||
|
||||
function focus() {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
if (!maskInstance.value)
|
||||
handleChange(event, !!errorMessage.value)
|
||||
}
|
||||
|
||||
function initializeMask() {
|
||||
if (props.mask) {
|
||||
if (!inputRef.value)
|
||||
return
|
||||
if (maskInstance.value)
|
||||
destroyMask()
|
||||
|
||||
maskInstance.value = IMask(inputRef.value, props.mask).on('accept', () => {
|
||||
if (valueEvent || updateOptionsEvent)
|
||||
return
|
||||
|
||||
acceptEvent = true
|
||||
|
||||
handleChange(maskInstance.value.unmaskedValue, !!errorMessage.value)
|
||||
maskedValue.value = maskInstance.value.value
|
||||
|
||||
nextTick(() => (acceptEvent = false))
|
||||
})
|
||||
|
||||
maskInstance.value.unmaskedValue = value.value?.toString() || ''
|
||||
maskedValue.value = maskInstance.value.value
|
||||
}
|
||||
}
|
||||
|
||||
function destroyMask() {
|
||||
maskInstance.value?.destroy()
|
||||
maskInstance.value = null
|
||||
}
|
||||
|
||||
function onChange(event: Event) {
|
||||
if (!errorMessage.value)
|
||||
handleChange(maskInstance.value ? maskInstance.value.unmaskedValue : event)
|
||||
}
|
||||
|
||||
function copy() {
|
||||
navigator.clipboard?.writeText(inputValue.value)
|
||||
}
|
||||
|
||||
watch(value, (value) => {
|
||||
if (maskInstance.value) {
|
||||
if (acceptEvent)
|
||||
return
|
||||
|
||||
valueEvent = true
|
||||
maskInstance.value.unmaskedValue = value?.toString() || ''
|
||||
maskedValue.value = maskInstance.value.value
|
||||
|
||||
nextTick(() => (valueEvent = false))
|
||||
}
|
||||
})
|
||||
|
||||
watch(maskOptions, (newOptions, prevValue) => {
|
||||
if (newOptions) {
|
||||
if (!maskInstance.value) {
|
||||
initializeMask()
|
||||
}
|
||||
else {
|
||||
if (isEqual(newOptions, prevValue))
|
||||
return
|
||||
|
||||
updateOptionsEvent = true
|
||||
|
||||
maskInstance.value.updateOptions(toRaw(newOptions))
|
||||
maskInstance.value.unmaskedValue = value.value?.toString() || ''
|
||||
maskedValue.value = maskInstance.value.value
|
||||
|
||||
nextTick(() => (updateOptionsEvent = false))
|
||||
}
|
||||
}
|
||||
else {
|
||||
destroyMask()
|
||||
}
|
||||
})
|
||||
|
||||
function onFocus(e: Event) {
|
||||
focused.value = true
|
||||
}
|
||||
|
||||
function onBlur(e: Event) {
|
||||
focused.value = false
|
||||
|
||||
handleBlur(e)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isEmptyValue(props.modelValue) && !props.disabled)
|
||||
setValue(props.modelValue)
|
||||
|
||||
initializeMask()
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (!maskInstance.value)
|
||||
setValue(inputRef.value.value, false)
|
||||
|
||||
if (props.autofocus)
|
||||
focus()
|
||||
})
|
||||
|
||||
onUnmounted(destroyMask)
|
||||
|
||||
defineExpose({
|
||||
focused,
|
||||
maskInstance,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'styles';
|
||||
</style>
|
||||
Reference in New Issue
Block a user