index.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <template>
  2. <view :class="classes">
  3. <view class="nut-input-value">
  4. <view class="nut-input-inner">
  5. <view class="nut-input-box">
  6. <component
  7. :is="renderInput(type)"
  8. class="input-text"
  9. ref="inputRef"
  10. :style="styles"
  11. :maxlength="maxLength"
  12. :placeholder="placeholder"
  13. :disabled="disabled"
  14. :readonly="readonly"
  15. :value="modelValue"
  16. :formatTrigger="formatTrigger"
  17. :autofocus="autofocus"
  18. :enterkeyhint="confirmType"
  19. @input="onInput"
  20. @focus="onFocus"
  21. @blur="onBlur"
  22. @click="onClickInput"
  23. @change="endComposing"
  24. @compositionend="endComposing"
  25. @compositionstart="startComposing"
  26. ></component>
  27. <view v-if="showWordLimit && maxLength" class="nut-input-word-limit">
  28. <span class="nut-input-word-num">{{ modelValue ? modelValue.length : 0 }}</span
  29. >/{{ maxLength }}
  30. </view>
  31. </view>
  32. <view class="nut-input-clear-box" v-if="clearable && !readonly" v-show="active && modelValue.length > 0">
  33. <slot name="clear">
  34. <MaskClose
  35. class="nut-input-clear"
  36. v-bind="$attrs"
  37. :size="clearSize"
  38. :width="clearSize"
  39. :height="clearSize"
  40. @click="clear"
  41. >
  42. </MaskClose>
  43. </slot>
  44. </view>
  45. </view>
  46. </view>
  47. </view>
  48. </template>
  49. <!-- eslint-disable @typescript-eslint/no-non-null-assertion -->
  50. <script lang="ts">
  51. import { PropType, ref, reactive, computed, onMounted, watch, ComputedRef, InputHTMLAttributes, h } from 'vue';
  52. import { createComponent } from '@/packages/utils/create';
  53. import { formatNumber } from './util';
  54. import { MaskClose } from '@nutui/icons-vue';
  55. const { componentName, create } = createComponent('input');
  56. export type InputType = InputHTMLAttributes['type'];
  57. export type InputAlignType = 'left' | 'center' | 'right'; // text-align
  58. export type InputFormatTrigger = 'onChange' | 'onBlur'; // onChange: 在输入时执行格式化 ; onBlur: 在失焦时执行格式化
  59. export type InputRule = {
  60. pattern?: RegExp;
  61. message?: string;
  62. required?: boolean;
  63. };
  64. export type ConfirmTextType = 'send' | 'search' | 'next' | 'go' | 'done';
  65. export interface InputTarget extends HTMLInputElement {
  66. composing: boolean;
  67. }
  68. export default create({
  69. props: {
  70. type: {
  71. type: String as PropType<InputType>,
  72. default: 'text'
  73. },
  74. modelValue: {
  75. type: String,
  76. default: ''
  77. },
  78. placeholder: {
  79. type: String,
  80. default: ''
  81. },
  82. inputAlign: {
  83. type: String,
  84. default: 'left'
  85. },
  86. required: {
  87. type: Boolean,
  88. default: false
  89. },
  90. disabled: {
  91. type: Boolean,
  92. default: false
  93. },
  94. readonly: {
  95. type: Boolean,
  96. default: false
  97. },
  98. maxLength: {
  99. type: [String, Number],
  100. default: ''
  101. },
  102. clearable: {
  103. type: Boolean,
  104. default: false
  105. },
  106. clearSize: {
  107. type: [String, Number],
  108. default: '14'
  109. },
  110. border: {
  111. type: Boolean,
  112. default: true
  113. },
  114. formatTrigger: {
  115. type: String as PropType<InputFormatTrigger>,
  116. default: 'onChange'
  117. },
  118. formatter: {
  119. type: Function as PropType<(value: string) => string>,
  120. default: null
  121. },
  122. showWordLimit: {
  123. type: Boolean,
  124. default: false
  125. },
  126. autofocus: {
  127. type: Boolean,
  128. default: false
  129. },
  130. confirmType: {
  131. type: String as PropType<ConfirmTextType>,
  132. default: 'done'
  133. },
  134. error: {
  135. type: Boolean,
  136. default: false
  137. }
  138. },
  139. components: { MaskClose },
  140. emits: ['update:modelValue', 'blur', 'focus', 'clear', 'keypress', 'click-input'],
  141. setup(props, { emit, slots }) {
  142. const active = ref(false);
  143. const inputRef = ref();
  144. const getModelValue = () => String(props.modelValue ?? '');
  145. const renderInput = (type: InputType) => {
  146. return h('input', {
  147. style: styles,
  148. type: type != 'textarea' && inputType(type)
  149. });
  150. };
  151. const state = reactive({
  152. focused: false,
  153. validateFailed: false, // 校验失败
  154. validateMessage: '' // 校验信息
  155. });
  156. const classes = computed(() => {
  157. const prefixCls = componentName;
  158. return {
  159. [prefixCls]: true,
  160. [`${prefixCls}--disabled`]: props.disabled,
  161. [`${prefixCls}--required`]: props.required,
  162. [`${prefixCls}--error`]: props.error,
  163. [`${prefixCls}--border`]: props.border
  164. };
  165. });
  166. const styles: ComputedRef = computed(() => {
  167. return {
  168. textAlign: props.inputAlign
  169. };
  170. });
  171. const inputType = (type: InputType) => {
  172. if (type === 'number') {
  173. return 'text';
  174. } else if (type === 'digit') {
  175. return 'tel';
  176. } else {
  177. return type;
  178. }
  179. };
  180. const onInput = (event: Event) => {
  181. if (!(event.target as InputTarget)!.composing) {
  182. const input = event.target as HTMLInputElement;
  183. let value = input.value;
  184. if (props.maxLength && value.length > Number(props.maxLength)) {
  185. value = value.slice(0, Number(props.maxLength));
  186. }
  187. updateValue(value);
  188. }
  189. };
  190. const updateValue = (value: string, trigger: InputFormatTrigger = 'onChange') => {
  191. if (props.type === 'digit') {
  192. value = formatNumber(value, false, false);
  193. }
  194. if (props.type === 'number') {
  195. value = formatNumber(value, true, true);
  196. }
  197. if (props.formatter && trigger === props.formatTrigger) {
  198. value = props.formatter(value);
  199. }
  200. if (inputRef?.value?.value !== value) {
  201. inputRef.value.value = value;
  202. }
  203. if (value !== props.modelValue) {
  204. emit('update:modelValue', value);
  205. // emit('change', value);
  206. }
  207. };
  208. const onFocus = (event: Event) => {
  209. if (props.disabled || props.readonly) {
  210. return;
  211. }
  212. const input = event.target as HTMLInputElement;
  213. let value = input.value;
  214. active.value = true;
  215. emit('focus', event);
  216. emit('update:modelValue', value);
  217. };
  218. const onBlur = (event: Event) => {
  219. if (props.disabled || props.readonly) {
  220. return;
  221. }
  222. setTimeout(() => {
  223. active.value = false;
  224. }, 200);
  225. const input = event.target as HTMLInputElement;
  226. let value = input.value;
  227. if (props.maxLength && value.length > Number(props.maxLength)) {
  228. value = value.slice(0, Number(props.maxLength));
  229. }
  230. updateValue(getModelValue(), 'onBlur');
  231. emit('blur', event);
  232. emit('update:modelValue', value);
  233. };
  234. const clear = (event: Event) => {
  235. event.stopPropagation();
  236. if (props.disabled) return;
  237. emit('update:modelValue', '', event);
  238. // emit('change', '', event);
  239. emit('clear', '', event);
  240. };
  241. const resetValidation = () => {
  242. if (state.validateFailed) {
  243. state.validateFailed = false;
  244. state.validateMessage = '';
  245. }
  246. };
  247. const onClickInput = (event: MouseEvent) => {
  248. if (props.disabled) {
  249. return;
  250. }
  251. emit('click-input', event);
  252. };
  253. const startComposing = ({ target }: Event) => {
  254. (target as InputTarget)!.composing = true;
  255. };
  256. const endComposing = ({ target }: Event) => {
  257. if ((target as InputTarget)!.composing) {
  258. (target as InputTarget)!.composing = false;
  259. (target as InputTarget)!.dispatchEvent(new Event('input'));
  260. }
  261. };
  262. watch(
  263. () => props.modelValue,
  264. () => {
  265. updateValue(getModelValue());
  266. resetValidation();
  267. }
  268. );
  269. onMounted(() => {
  270. updateValue(getModelValue(), props.formatTrigger);
  271. });
  272. return {
  273. renderInput,
  274. inputRef,
  275. active,
  276. classes,
  277. styles,
  278. inputType,
  279. onInput,
  280. onFocus,
  281. onBlur,
  282. clear,
  283. startComposing,
  284. endComposing,
  285. onClickInput
  286. };
  287. }
  288. });
  289. </script>