274 lines
6.5 KiB
Vue
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>
|