index.taro.vue 8.9 KB

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