input.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template>
  2. <div :class="[
  3. type === 'textarea' ? 'el-textarea' : 'el-input',
  4. inputSize ? 'el-input--' + inputSize : '',
  5. {
  6. 'is-disabled': inputDisabled,
  7. 'is-exceed': inputExceed,
  8. 'el-input-group': $slots.prepend || $slots.append,
  9. 'el-input-group--append': $slots.append,
  10. 'el-input-group--prepend': $slots.prepend,
  11. 'el-input--prefix': $slots.prefix || prefixIcon,
  12. 'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
  13. }
  14. ]"
  15. @mouseenter="hovering = true"
  16. @mouseleave="hovering = false"
  17. >
  18. <template v-if="type !== 'textarea'">
  19. <!-- 前置元素 -->
  20. <div class="el-input-group__prepend" v-if="$slots.prepend">
  21. <slot name="prepend"></slot>
  22. </div>
  23. <input
  24. :tabindex="tabindex"
  25. v-if="type !== 'textarea'"
  26. class="el-input__inner"
  27. v-bind="$attrs"
  28. :type="showPassword ? (passwordVisible ? 'text': 'password') : type"
  29. :disabled="inputDisabled"
  30. :readonly="readonly"
  31. :autocomplete="autoComplete || autocomplete"
  32. ref="input"
  33. @compositionstart="handleCompositionStart"
  34. @compositionupdate="handleCompositionUpdate"
  35. @compositionend="handleCompositionEnd"
  36. @input="handleInput"
  37. @focus="handleFocus"
  38. @blur="handleBlur"
  39. @change="handleChange"
  40. :aria-label="label"
  41. >
  42. <!-- 前置内容 -->
  43. <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
  44. <slot name="prefix"></slot>
  45. <i class="el-input__icon"
  46. v-if="prefixIcon"
  47. :class="prefixIcon">
  48. </i>
  49. </span>
  50. <!-- 后置内容 -->
  51. <span
  52. class="el-input__suffix"
  53. v-if="getSuffixVisible()">
  54. <span class="el-input__suffix-inner">
  55. <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
  56. <slot name="suffix"></slot>
  57. <i class="el-input__icon"
  58. v-if="suffixIcon"
  59. :class="suffixIcon">
  60. </i>
  61. </template>
  62. <i v-if="showClear"
  63. class="el-input__icon el-icon-circle-close el-input__clear"
  64. @mousedown.prevent
  65. @click="clear"
  66. ></i>
  67. <i v-if="showPwdVisible"
  68. class="el-input__icon el-icon-view el-input__clear"
  69. @click="handlePasswordVisible"
  70. ></i>
  71. <span v-if="isWordLimitVisible" class="el-input__count">
  72. <span class="el-input__count-inner">
  73. {{ textLength }}/{{ upperLimit }}
  74. </span>
  75. </span>
  76. </span>
  77. <i class="el-input__icon"
  78. v-if="validateState"
  79. :class="['el-input__validateIcon', validateIcon]">
  80. </i>
  81. </span>
  82. <!-- 后置元素 -->
  83. <div class="el-input-group__append" v-if="$slots.append">
  84. <slot name="append"></slot>
  85. </div>
  86. </template>
  87. <textarea
  88. v-else
  89. :tabindex="tabindex"
  90. class="el-textarea__inner"
  91. @compositionstart="handleCompositionStart"
  92. @compositionupdate="handleCompositionUpdate"
  93. @compositionend="handleCompositionEnd"
  94. @input="handleInput"
  95. ref="textarea"
  96. v-bind="$attrs"
  97. :disabled="inputDisabled"
  98. :readonly="readonly"
  99. :autocomplete="autoComplete || autocomplete"
  100. :style="textareaStyle"
  101. @focus="handleFocus"
  102. @blur="handleBlur"
  103. @change="handleChange"
  104. :aria-label="label"
  105. >
  106. </textarea>
  107. <span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
  108. </div>
  109. </template>
  110. <script>
  111. import emitter from 'element-ui/src/mixins/emitter';
  112. import Migrating from 'element-ui/src/mixins/migrating';
  113. import calcTextareaHeight from './calcTextareaHeight';
  114. import merge from 'element-ui/src/utils/merge';
  115. import {isKorean} from 'element-ui/src/utils/shared';
  116. export default {
  117. name: 'ElInput',
  118. componentName: 'ElInput',
  119. mixins: [emitter, Migrating],
  120. inheritAttrs: false,
  121. inject: {
  122. elForm: {
  123. default: ''
  124. },
  125. elFormItem: {
  126. default: ''
  127. }
  128. },
  129. data() {
  130. return {
  131. textareaCalcStyle: {},
  132. hovering: false,
  133. focused: false,
  134. isComposing: false,
  135. passwordVisible: false
  136. };
  137. },
  138. props: {
  139. value: [String, Number],
  140. size: String,
  141. resize: String,
  142. form: String,
  143. disabled: Boolean,
  144. readonly: Boolean,
  145. type: {
  146. type: String,
  147. default: 'text'
  148. },
  149. autosize: {
  150. type: [Boolean, Object],
  151. default: false
  152. },
  153. autocomplete: {
  154. type: String,
  155. default: 'off'
  156. },
  157. /** @Deprecated in next major version */
  158. autoComplete: {
  159. type: String,
  160. validator(val) {
  161. process.env.NODE_ENV !== 'production' &&
  162. console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
  163. return true;
  164. }
  165. },
  166. validateEvent: {
  167. type: Boolean,
  168. default: true
  169. },
  170. suffixIcon: String,
  171. prefixIcon: String,
  172. label: String,
  173. clearable: {
  174. type: Boolean,
  175. default: false
  176. },
  177. showPassword: {
  178. type: Boolean,
  179. default: false
  180. },
  181. showWordLimit: {
  182. type: Boolean,
  183. default: false
  184. },
  185. tabindex: String
  186. },
  187. computed: {
  188. _elFormItemSize() {
  189. return (this.elFormItem || {}).elFormItemSize;
  190. },
  191. validateState() {
  192. return this.elFormItem ? this.elFormItem.validateState : '';
  193. },
  194. needStatusIcon() {
  195. return this.elForm ? this.elForm.statusIcon : false;
  196. },
  197. validateIcon() {
  198. return {
  199. validating: 'el-icon-loading',
  200. success: 'el-icon-circle-check',
  201. error: 'el-icon-circle-close'
  202. }[this.validateState];
  203. },
  204. textareaStyle() {
  205. return merge({}, this.textareaCalcStyle, { resize: this.resize });
  206. },
  207. inputSize() {
  208. return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
  209. },
  210. inputDisabled() {
  211. return this.disabled || (this.elForm || {}).disabled;
  212. },
  213. nativeInputValue() {
  214. return this.value === null || this.value === undefined ? '' : String(this.value);
  215. },
  216. showClear() {
  217. return this.clearable &&
  218. !this.inputDisabled &&
  219. !this.readonly &&
  220. this.nativeInputValue &&
  221. (this.focused || this.hovering);
  222. },
  223. showPwdVisible() {
  224. return this.showPassword &&
  225. !this.inputDisabled &&
  226. !this.readonly &&
  227. (!!this.nativeInputValue || this.focused);
  228. },
  229. isWordLimitVisible() {
  230. return this.showWordLimit &&
  231. this.$attrs.maxlength &&
  232. (this.type === 'text' || this.type === 'textarea') &&
  233. !this.inputDisabled &&
  234. !this.readonly &&
  235. !this.showPassword;
  236. },
  237. upperLimit() {
  238. return this.$attrs.maxlength;
  239. },
  240. textLength() {
  241. if (typeof this.value === 'number') {
  242. return String(this.value).length;
  243. }
  244. return (this.value || '').length;
  245. },
  246. inputExceed() {
  247. // show exceed style if length of initial value greater then maxlength
  248. return this.isWordLimitVisible &&
  249. (this.textLength > this.upperLimit);
  250. }
  251. },
  252. watch: {
  253. value(val) {
  254. this.$nextTick(this.resizeTextarea);
  255. if (this.validateEvent) {
  256. this.dispatch('ElFormItem', 'el.form.change', [val]);
  257. }
  258. },
  259. // native input value is set explicitly
  260. // do not use v-model / :value in template
  261. // see: https://github.com/ElemeFE/element/issues/14521
  262. nativeInputValue() {
  263. this.setNativeInputValue();
  264. },
  265. // when change between <input> and <textarea>,
  266. // update DOM dependent value and styles
  267. // https://github.com/ElemeFE/element/issues/14857
  268. type() {
  269. this.$nextTick(() => {
  270. this.setNativeInputValue();
  271. this.resizeTextarea();
  272. this.updateIconOffset();
  273. });
  274. }
  275. },
  276. methods: {
  277. focus() {
  278. this.getInput().focus();
  279. },
  280. blur() {
  281. this.getInput().blur();
  282. },
  283. getMigratingConfig() {
  284. return {
  285. props: {
  286. 'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
  287. 'on-icon-click': 'on-icon-click is removed.'
  288. },
  289. events: {
  290. 'click': 'click is removed.'
  291. }
  292. };
  293. },
  294. handleBlur(event) {
  295. this.focused = false;
  296. this.$emit('blur', event);
  297. if (this.validateEvent) {
  298. this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
  299. }
  300. },
  301. select() {
  302. this.getInput().select();
  303. },
  304. resizeTextarea() {
  305. if (this.$isServer) return;
  306. const { autosize, type } = this;
  307. if (type !== 'textarea') return;
  308. if (!autosize) {
  309. this.textareaCalcStyle = {
  310. minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
  311. };
  312. return;
  313. }
  314. const minRows = autosize.minRows;
  315. const maxRows = autosize.maxRows;
  316. this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
  317. },
  318. setNativeInputValue() {
  319. const input = this.getInput();
  320. if (!input) return;
  321. if (input.value === this.nativeInputValue) return;
  322. input.value = this.nativeInputValue;
  323. },
  324. handleFocus(event) {
  325. this.focused = true;
  326. this.$emit('focus', event);
  327. },
  328. handleCompositionStart(event) {
  329. this.$emit('compositionstart', event);
  330. this.isComposing = true;
  331. },
  332. handleCompositionUpdate(event) {
  333. this.$emit('compositionupdate', event);
  334. const text = event.target.value;
  335. const lastCharacter = text[text.length - 1] || '';
  336. this.isComposing = !isKorean(lastCharacter);
  337. },
  338. handleCompositionEnd(event) {
  339. this.$emit('compositionend', event);
  340. if (this.isComposing) {
  341. this.isComposing = false;
  342. this.handleInput(event);
  343. }
  344. },
  345. handleInput(event) {
  346. // should not emit input during composition
  347. // see: https://github.com/ElemeFE/element/issues/10516
  348. if (this.isComposing) return;
  349. // hack for https://github.com/ElemeFE/element/issues/8548
  350. // should remove the following line when we don't support IE
  351. if (event.target.value === this.nativeInputValue) return;
  352. this.$emit('input', event.target.value);
  353. // ensure native input value is controlled
  354. // see: https://github.com/ElemeFE/element/issues/12850
  355. this.$nextTick(this.setNativeInputValue);
  356. },
  357. handleChange(event) {
  358. this.$emit('change', event.target.value);
  359. },
  360. calcIconOffset(place) {
  361. let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
  362. if (!elList.length) return;
  363. let el = null;
  364. for (let i = 0; i < elList.length; i++) {
  365. if (elList[i].parentNode === this.$el) {
  366. el = elList[i];
  367. break;
  368. }
  369. }
  370. if (!el) return;
  371. const pendantMap = {
  372. suffix: 'append',
  373. prefix: 'prepend'
  374. };
  375. const pendant = pendantMap[place];
  376. if (this.$slots[pendant]) {
  377. el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
  378. } else {
  379. el.removeAttribute('style');
  380. }
  381. },
  382. updateIconOffset() {
  383. this.calcIconOffset('prefix');
  384. this.calcIconOffset('suffix');
  385. },
  386. clear() {
  387. this.$emit('input', '');
  388. this.$emit('change', '');
  389. this.$emit('clear');
  390. },
  391. handlePasswordVisible() {
  392. this.passwordVisible = !this.passwordVisible;
  393. this.$nextTick(() => {
  394. this.focus();
  395. });
  396. },
  397. getInput() {
  398. return this.$refs.input || this.$refs.textarea;
  399. },
  400. getSuffixVisible() {
  401. return this.$slots.suffix ||
  402. this.suffixIcon ||
  403. this.showClear ||
  404. this.showPassword ||
  405. this.isWordLimitVisible ||
  406. (this.validateState && this.needStatusIcon);
  407. }
  408. },
  409. created() {
  410. this.$on('inputSelect', this.select);
  411. },
  412. mounted() {
  413. this.setNativeInputValue();
  414. this.resizeTextarea();
  415. this.updateIconOffset();
  416. },
  417. updated() {
  418. this.$nextTick(this.updateIconOffset);
  419. }
  420. };
  421. </script>