index.taro.vue 8.9 KB

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