Files
2026-01-05 12:47:14 +08:00

274 lines
6.5 KiB
Vue

<script lang="ts" setup>
import type { InputOnBlurEvent, InputOnConfirmEvent, InputOnFocusEvent, InputOnInputEvent } from '@uni-helper/uni-app-types'
import { computed, defineComponent, nextTick, onMounted, ref, toRef, useSlots, watch } from 'vue'
import { BLUR_EVENT, CLEAR_EVENT, CLICK_EVENT, CONFIRM_EVENT, FOCUS_EVENT, INPUT_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass, isH5 } from '../_utils'
import { useFormDisabled } from '../form/form'
import NutIcon from '../icon/icon.vue'
import { inputEmits, inputProps } from './input'
import type { InputFormatTrigger, InputTarget } from './type'
import { formatNumber } from './util'
const props = defineProps(inputProps)
const emit = defineEmits(inputEmits)
const slots = useSlots()
function hasSlot(name: string) {
return Boolean(slots[name])
}
const formDisabled = useFormDisabled(toRef(props, 'disabled'))
function stringModelValue() {
if (props.modelValue == null)
return ''
return String(props.modelValue)
}
const innerValue = computed<string>(() => {
return stringModelValue()
})
const classes = computed(() => {
return getMainClass(props, componentName, {
[`${componentName}--disabled`]: formDisabled.value,
[`${componentName}--required`]: props.required,
[`${componentName}--error`]: props.error,
[`${componentName}--border`]: props.border,
})
})
const inputStyles = computed(() => {
return [props.inputStyle, {
textAlign: props.inputAlign,
}]
})
const innerMaxLength = computed(() => {
if (props.maxLength == null)
return -1
return Number(props.maxLength)
})
function updateValue(value: string, trigger: InputFormatTrigger = 'onChange') {
if (innerMaxLength.value > 0 && value.length > innerMaxLength.value)
value = value.slice(0, innerMaxLength.value)
if (props.type === 'number')
value = formatNumber(value, false, false)
if (props.type === 'digit')
value = formatNumber(value, true, true)
if (props.formatter && trigger === props.formatTrigger)
value = props.formatter(value)
emit(UPDATE_MODEL_EVENT, value)
}
function _onInput(evt: InputOnInputEvent) {
updateValue(evt.detail.value)
nextTick(() => {
emit(INPUT_EVENT, innerValue.value, evt)
})
}
function handleInput(evt: InputOnInputEvent) {
if (isH5) {
const target = evt.target as InputTarget
if (!target.composing)
_onInput(evt)
}
else {
_onInput(evt)
}
}
function handleClick(evt: any) {
emit(CLICK_EVENT, evt)
}
function handleClickInput(evt: any) {
if (formDisabled.value)
return
emit('clickInput', evt)
}
const active = ref(false)
const clearing = ref(false)
function handleFocus(evt: InputOnFocusEvent) {
if (formDisabled.value || props.readonly)
return
emit(FOCUS_EVENT, evt)
active.value = true
}
function handleBlur(evt: InputOnBlurEvent) {
if (formDisabled.value || props.readonly)
return
emit(BLUR_EVENT, evt)
setTimeout(() => {
active.value = false
}, 200)
if (clearing.value) {
clearing.value = false
return
}
updateValue(evt.detail.value, 'onBlur')
}
function handleConfirm(evt: InputOnConfirmEvent) {
emit(CONFIRM_EVENT, evt)
}
function handleClear(evt: any) {
if (formDisabled.value)
return
emit(UPDATE_MODEL_EVENT, '', evt)
emit(CLEAR_EVENT)
clearing.value = true
}
function startComposing(evt: any) {
if (isH5) {
const target = evt.target as InputTarget
target.composing = true
}
}
function endComposing(evt: any) {
if (isH5) {
const target = evt.target as InputTarget
if (target.composing) {
target.composing = false
target.dispatchEvent(new Event('input'))
}
}
}
watch(
() => props.modelValue,
(value) => {
if (value === innerValue.value)
return
updateValue(stringModelValue())
},
)
onMounted(() => {
updateValue(stringModelValue(), props.formatTrigger)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-input`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle" @click="handleClick">
<view v-if="hasSlot('left')" class="nut-input__left">
<slot name="left" />
</view>
<view class="nut-input__value">
<input
class="nut-input__input"
:class="props.inputClass"
:style="inputStyles"
:value="innerValue"
:type="props.type as any"
:placeholder="props.placeholder"
:placeholder-style="props.placeholderStyle"
:placeholder-class="props.placeholderClass"
:disabled="formDisabled"
:readonly="props.readonly"
:focus="props.autofocus"
:maxlength="innerMaxLength"
:format-trigger="props.formatTrigger"
:auto-blur="props.autofocus ? true : undefined"
:confirm-type="props.confirmType"
:adjust-position="props.adjustPosition"
:always-system="props.alwaysSystem"
:inputmode="props.inputMode"
:cursor-spacing="props.cursorSpacing"
:always-embed="props.alwaysEmbed"
:confirm-hold="props.confirmHold"
:cursor="props.cursor"
:selection-start="props.selectionStart"
:selection-end="props.selectionEnd"
:hold-keyboard="props.holdKeyboard"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@click="handleClickInput"
@change="endComposing"
@compositionstart="startComposing"
@compositionend="endComposing"
@confirm="handleConfirm"
>
<view v-if="props.readonly" class="nut-input__mask" @click="handleClickInput" />
<view v-if="props.showWordLimit && innerMaxLength > 0" class="nut-input__word-limit">
<text class="nut-input__word-num">
{{ innerValue.length }}
</text>/{{ innerMaxLength }}
</view>
</view>
<view
v-if="props.clearable && !props.readonly"
class="nut-input__clear"
:class="{ 'nut-hidden': !((active || props.showClearIcon) && innerValue.length > 0) }"
@click.stop="handleClear"
>
<slot v-if="hasSlot('clear')" name="clear" />
<NutIcon
v-else
name="mask-close"
custom-class="nut-input__clear-icon"
:size="props.clearSize"
:width="props.clearSize"
:height="props.clearSize"
/>
</view>
<view v-if="hasSlot('right')" class="nut-input__right">
<slot name="right" />
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>