index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import _mergeJSXProps2 from "@vue/babel-helper-vue-jsx-merge-props";
  2. import _mergeJSXProps from "@vue/babel-helper-vue-jsx-merge-props";
  3. import _extends from "@babel/runtime/helpers/esm/extends";
  4. // Utils
  5. import { resetScroll } from '../utils/dom/reset-scroll';
  6. import { formatNumber } from '../utils/format/number';
  7. import { preventDefault } from '../utils/dom/event';
  8. import { getRootScrollTop, setRootScrollTop } from '../utils/dom/scroll';
  9. import { isDef, addUnit, isObject, isPromise, isFunction, createNamespace } from '../utils'; // Components
  10. import Icon from '../icon';
  11. import Cell from '../cell';
  12. import { cellProps } from '../cell/shared';
  13. var _createNamespace = createNamespace('field'),
  14. createComponent = _createNamespace[0],
  15. bem = _createNamespace[1];
  16. export default createComponent({
  17. inheritAttrs: false,
  18. provide: function provide() {
  19. return {
  20. vanField: this
  21. };
  22. },
  23. inject: {
  24. vanForm: {
  25. default: null
  26. }
  27. },
  28. props: _extends({}, cellProps, {
  29. name: String,
  30. rules: Array,
  31. disabled: {
  32. type: Boolean,
  33. default: null
  34. },
  35. readonly: {
  36. type: Boolean,
  37. default: null
  38. },
  39. autosize: [Boolean, Object],
  40. leftIcon: String,
  41. rightIcon: String,
  42. clearable: Boolean,
  43. formatter: Function,
  44. maxlength: [Number, String],
  45. labelWidth: [Number, String],
  46. labelClass: null,
  47. labelAlign: String,
  48. inputAlign: String,
  49. placeholder: String,
  50. errorMessage: String,
  51. errorMessageAlign: String,
  52. showWordLimit: Boolean,
  53. value: {
  54. type: [Number, String],
  55. default: ''
  56. },
  57. type: {
  58. type: String,
  59. default: 'text'
  60. },
  61. error: {
  62. type: Boolean,
  63. default: null
  64. },
  65. colon: {
  66. type: Boolean,
  67. default: null
  68. },
  69. clearTrigger: {
  70. type: String,
  71. default: 'focus'
  72. },
  73. formatTrigger: {
  74. type: String,
  75. default: 'onChange'
  76. }
  77. }),
  78. data: function data() {
  79. return {
  80. focused: false,
  81. validateFailed: false,
  82. validateMessage: ''
  83. };
  84. },
  85. watch: {
  86. value: function value() {
  87. this.updateValue(this.value);
  88. this.resetValidation();
  89. this.validateWithTrigger('onChange');
  90. this.$nextTick(this.adjustSize);
  91. }
  92. },
  93. mounted: function mounted() {
  94. this.updateValue(this.value, this.formatTrigger);
  95. this.$nextTick(this.adjustSize);
  96. if (this.vanForm) {
  97. this.vanForm.addField(this);
  98. }
  99. },
  100. beforeDestroy: function beforeDestroy() {
  101. if (this.vanForm) {
  102. this.vanForm.removeField(this);
  103. }
  104. },
  105. computed: {
  106. showClear: function showClear() {
  107. var readonly = this.getProp('readonly');
  108. if (this.clearable && !readonly) {
  109. var hasValue = isDef(this.value) && this.value !== '';
  110. var trigger = this.clearTrigger === 'always' || this.clearTrigger === 'focus' && this.focused;
  111. return hasValue && trigger;
  112. }
  113. },
  114. showError: function showError() {
  115. if (this.error !== null) {
  116. return this.error;
  117. }
  118. if (this.vanForm && this.vanForm.showError && this.validateFailed) {
  119. return true;
  120. }
  121. },
  122. listeners: function listeners() {
  123. return _extends({}, this.$listeners, {
  124. blur: this.onBlur,
  125. focus: this.onFocus,
  126. input: this.onInput,
  127. click: this.onClickInput,
  128. keypress: this.onKeypress
  129. });
  130. },
  131. labelStyle: function labelStyle() {
  132. var labelWidth = this.getProp('labelWidth');
  133. if (labelWidth) {
  134. return {
  135. width: addUnit(labelWidth)
  136. };
  137. }
  138. },
  139. formValue: function formValue() {
  140. if (this.children && (this.$scopedSlots.input || this.$slots.input)) {
  141. return this.children.value;
  142. }
  143. return this.value;
  144. }
  145. },
  146. methods: {
  147. // @exposed-api
  148. focus: function focus() {
  149. if (this.$refs.input) {
  150. this.$refs.input.focus();
  151. }
  152. },
  153. // @exposed-api
  154. blur: function blur() {
  155. if (this.$refs.input) {
  156. this.$refs.input.blur();
  157. }
  158. },
  159. runValidator: function runValidator(value, rule) {
  160. return new Promise(function (resolve) {
  161. var returnVal = rule.validator(value, rule);
  162. if (isPromise(returnVal)) {
  163. return returnVal.then(resolve);
  164. }
  165. resolve(returnVal);
  166. });
  167. },
  168. isEmptyValue: function isEmptyValue(value) {
  169. if (Array.isArray(value)) {
  170. return !value.length;
  171. }
  172. if (value === 0) {
  173. return false;
  174. }
  175. return !value;
  176. },
  177. runSyncRule: function runSyncRule(value, rule) {
  178. if (rule.required && this.isEmptyValue(value)) {
  179. return false;
  180. }
  181. if (rule.pattern && !rule.pattern.test(value)) {
  182. return false;
  183. }
  184. return true;
  185. },
  186. getRuleMessage: function getRuleMessage(value, rule) {
  187. var message = rule.message;
  188. if (isFunction(message)) {
  189. return message(value, rule);
  190. }
  191. return message;
  192. },
  193. runRules: function runRules(rules) {
  194. var _this = this;
  195. return rules.reduce(function (promise, rule) {
  196. return promise.then(function () {
  197. if (_this.validateFailed) {
  198. return;
  199. }
  200. var value = _this.formValue;
  201. if (rule.formatter) {
  202. value = rule.formatter(value, rule);
  203. }
  204. if (!_this.runSyncRule(value, rule)) {
  205. _this.validateFailed = true;
  206. _this.validateMessage = _this.getRuleMessage(value, rule);
  207. return;
  208. }
  209. if (rule.validator) {
  210. return _this.runValidator(value, rule).then(function (result) {
  211. if (result === false) {
  212. _this.validateFailed = true;
  213. _this.validateMessage = _this.getRuleMessage(value, rule);
  214. }
  215. });
  216. }
  217. });
  218. }, Promise.resolve());
  219. },
  220. validate: function validate(rules) {
  221. var _this2 = this;
  222. if (rules === void 0) {
  223. rules = this.rules;
  224. }
  225. return new Promise(function (resolve) {
  226. if (!rules) {
  227. resolve();
  228. }
  229. _this2.resetValidation();
  230. _this2.runRules(rules).then(function () {
  231. if (_this2.validateFailed) {
  232. resolve({
  233. name: _this2.name,
  234. message: _this2.validateMessage
  235. });
  236. } else {
  237. resolve();
  238. }
  239. });
  240. });
  241. },
  242. validateWithTrigger: function validateWithTrigger(trigger) {
  243. if (this.vanForm && this.rules) {
  244. var defaultTrigger = this.vanForm.validateTrigger === trigger;
  245. var rules = this.rules.filter(function (rule) {
  246. if (rule.trigger) {
  247. return rule.trigger === trigger;
  248. }
  249. return defaultTrigger;
  250. });
  251. if (rules.length) {
  252. this.validate(rules);
  253. }
  254. }
  255. },
  256. resetValidation: function resetValidation() {
  257. if (this.validateFailed) {
  258. this.validateFailed = false;
  259. this.validateMessage = '';
  260. }
  261. },
  262. updateValue: function updateValue(value, trigger) {
  263. if (trigger === void 0) {
  264. trigger = 'onChange';
  265. }
  266. value = isDef(value) ? String(value) : ''; // native maxlength have incorrect line-break counting
  267. // see: https://github.com/youzan/vant/issues/5033
  268. var maxlength = this.maxlength;
  269. if (isDef(maxlength) && value.length > maxlength) {
  270. if (this.value && this.value.length === +maxlength) {
  271. value = this.value;
  272. } else {
  273. value = value.slice(0, maxlength);
  274. }
  275. }
  276. if (this.type === 'number' || this.type === 'digit') {
  277. var isNumber = this.type === 'number';
  278. value = formatNumber(value, isNumber, isNumber);
  279. }
  280. if (this.formatter && trigger === this.formatTrigger) {
  281. value = this.formatter(value);
  282. }
  283. var input = this.$refs.input;
  284. if (input && value !== input.value) {
  285. input.value = value;
  286. }
  287. if (value !== this.value) {
  288. this.$emit('input', value);
  289. }
  290. },
  291. onInput: function onInput(event) {
  292. // not update v-model when composing
  293. if (event.target.composing) {
  294. return;
  295. }
  296. this.updateValue(event.target.value);
  297. },
  298. onFocus: function onFocus(event) {
  299. this.focused = true;
  300. this.$emit('focus', event); // readonly not work in legacy mobile safari
  301. /* istanbul ignore if */
  302. var readonly = this.getProp('readonly');
  303. if (readonly) {
  304. this.blur();
  305. }
  306. },
  307. onBlur: function onBlur(event) {
  308. this.focused = false;
  309. this.updateValue(this.value, 'onBlur');
  310. this.$emit('blur', event);
  311. this.validateWithTrigger('onBlur');
  312. resetScroll();
  313. },
  314. onClick: function onClick(event) {
  315. this.$emit('click', event);
  316. },
  317. onClickInput: function onClickInput(event) {
  318. this.$emit('click-input', event);
  319. },
  320. onClickLeftIcon: function onClickLeftIcon(event) {
  321. this.$emit('click-left-icon', event);
  322. },
  323. onClickRightIcon: function onClickRightIcon(event) {
  324. this.$emit('click-right-icon', event);
  325. },
  326. onClear: function onClear(event) {
  327. preventDefault(event);
  328. this.$emit('input', '');
  329. this.$emit('clear', event);
  330. },
  331. onKeypress: function onKeypress(event) {
  332. var ENTER_CODE = 13;
  333. if (event.keyCode === ENTER_CODE) {
  334. var submitOnEnter = this.getProp('submitOnEnter');
  335. if (!submitOnEnter && this.type !== 'textarea') {
  336. preventDefault(event);
  337. } // trigger blur after click keyboard search button
  338. if (this.type === 'search') {
  339. this.blur();
  340. }
  341. }
  342. this.$emit('keypress', event);
  343. },
  344. adjustSize: function adjustSize() {
  345. var input = this.$refs.input;
  346. if (!(this.type === 'textarea' && this.autosize) || !input) {
  347. return;
  348. }
  349. var scrollTop = getRootScrollTop();
  350. input.style.height = 'auto';
  351. var height = input.scrollHeight;
  352. if (isObject(this.autosize)) {
  353. var _this$autosize = this.autosize,
  354. maxHeight = _this$autosize.maxHeight,
  355. minHeight = _this$autosize.minHeight;
  356. if (maxHeight) {
  357. height = Math.min(height, maxHeight);
  358. }
  359. if (minHeight) {
  360. height = Math.max(height, minHeight);
  361. }
  362. }
  363. if (height) {
  364. input.style.height = height + 'px'; // https://github.com/youzan/vant/issues/9178
  365. setRootScrollTop(scrollTop);
  366. }
  367. },
  368. genInput: function genInput() {
  369. var h = this.$createElement;
  370. var type = this.type;
  371. var disabled = this.getProp('disabled');
  372. var readonly = this.getProp('readonly');
  373. var inputSlot = this.slots('input');
  374. var inputAlign = this.getProp('inputAlign');
  375. if (inputSlot) {
  376. return h("div", {
  377. "class": bem('control', [inputAlign, 'custom']),
  378. "on": {
  379. "click": this.onClickInput
  380. }
  381. }, [inputSlot]);
  382. }
  383. var inputProps = {
  384. ref: 'input',
  385. class: bem('control', inputAlign),
  386. domProps: {
  387. value: this.value
  388. },
  389. attrs: _extends({}, this.$attrs, {
  390. name: this.name,
  391. disabled: disabled,
  392. readonly: readonly,
  393. placeholder: this.placeholder
  394. }),
  395. on: this.listeners,
  396. // add model directive to skip IME composition
  397. directives: [{
  398. name: 'model',
  399. value: this.value
  400. }]
  401. };
  402. if (type === 'textarea') {
  403. return h("textarea", _mergeJSXProps([{}, inputProps]));
  404. }
  405. var inputType = type;
  406. var inputMode; // type="number" is weird in iOS, and can't prevent dot in Android
  407. // so use inputmode to set keyboard in modern browsers
  408. if (type === 'number') {
  409. inputType = 'text';
  410. inputMode = 'decimal';
  411. }
  412. if (type === 'digit') {
  413. inputType = 'tel';
  414. inputMode = 'numeric';
  415. }
  416. return h("input", _mergeJSXProps2([{
  417. "attrs": {
  418. "type": inputType,
  419. "inputmode": inputMode
  420. }
  421. }, inputProps]));
  422. },
  423. genLeftIcon: function genLeftIcon() {
  424. var h = this.$createElement;
  425. var showLeftIcon = this.slots('left-icon') || this.leftIcon;
  426. if (showLeftIcon) {
  427. return h("div", {
  428. "class": bem('left-icon'),
  429. "on": {
  430. "click": this.onClickLeftIcon
  431. }
  432. }, [this.slots('left-icon') || h(Icon, {
  433. "attrs": {
  434. "name": this.leftIcon,
  435. "classPrefix": this.iconPrefix
  436. }
  437. })]);
  438. }
  439. },
  440. genRightIcon: function genRightIcon() {
  441. var h = this.$createElement;
  442. var slots = this.slots;
  443. var showRightIcon = slots('right-icon') || this.rightIcon;
  444. if (showRightIcon) {
  445. return h("div", {
  446. "class": bem('right-icon'),
  447. "on": {
  448. "click": this.onClickRightIcon
  449. }
  450. }, [slots('right-icon') || h(Icon, {
  451. "attrs": {
  452. "name": this.rightIcon,
  453. "classPrefix": this.iconPrefix
  454. }
  455. })]);
  456. }
  457. },
  458. genWordLimit: function genWordLimit() {
  459. var h = this.$createElement;
  460. if (this.showWordLimit && this.maxlength) {
  461. var count = (this.value || '').length;
  462. return h("div", {
  463. "class": bem('word-limit')
  464. }, [h("span", {
  465. "class": bem('word-num')
  466. }, [count]), "/", this.maxlength]);
  467. }
  468. },
  469. genMessage: function genMessage() {
  470. var h = this.$createElement;
  471. if (this.vanForm && this.vanForm.showErrorMessage === false) {
  472. return;
  473. }
  474. var message = this.errorMessage || this.validateMessage;
  475. if (message) {
  476. var errorMessageAlign = this.getProp('errorMessageAlign');
  477. return h("div", {
  478. "class": bem('error-message', errorMessageAlign)
  479. }, [message]);
  480. }
  481. },
  482. getProp: function getProp(key) {
  483. if (isDef(this[key])) {
  484. return this[key];
  485. }
  486. if (this.vanForm && isDef(this.vanForm[key])) {
  487. return this.vanForm[key];
  488. }
  489. },
  490. genLabel: function genLabel() {
  491. var h = this.$createElement;
  492. var colon = this.getProp('colon') ? ':' : '';
  493. if (this.slots('label')) {
  494. return [this.slots('label'), colon];
  495. }
  496. if (this.label) {
  497. return h("span", [this.label + colon]);
  498. }
  499. }
  500. },
  501. render: function render() {
  502. var _bem;
  503. var h = arguments[0];
  504. var slots = this.slots;
  505. var disabled = this.getProp('disabled');
  506. var labelAlign = this.getProp('labelAlign');
  507. var scopedSlots = {
  508. icon: this.genLeftIcon
  509. };
  510. var Label = this.genLabel();
  511. if (Label) {
  512. scopedSlots.title = function () {
  513. return Label;
  514. };
  515. }
  516. var extra = this.slots('extra');
  517. if (extra) {
  518. scopedSlots.extra = function () {
  519. return extra;
  520. };
  521. }
  522. return h(Cell, {
  523. "attrs": {
  524. "icon": this.leftIcon,
  525. "size": this.size,
  526. "center": this.center,
  527. "border": this.border,
  528. "isLink": this.isLink,
  529. "required": this.required,
  530. "clickable": this.clickable,
  531. "titleStyle": this.labelStyle,
  532. "valueClass": bem('value'),
  533. "titleClass": [bem('label', labelAlign), this.labelClass],
  534. "arrowDirection": this.arrowDirection
  535. },
  536. "scopedSlots": scopedSlots,
  537. "class": bem((_bem = {
  538. error: this.showError,
  539. disabled: disabled
  540. }, _bem["label-" + labelAlign] = labelAlign, _bem['min-height'] = this.type === 'textarea' && !this.autosize, _bem)),
  541. "on": {
  542. "click": this.onClick
  543. }
  544. }, [h("div", {
  545. "class": bem('body')
  546. }, [this.genInput(), this.showClear && h(Icon, {
  547. "attrs": {
  548. "name": "clear"
  549. },
  550. "class": bem('clear'),
  551. "on": {
  552. "touchstart": this.onClear
  553. }
  554. }), this.genRightIcon(), slots('button') && h("div", {
  555. "class": bem('button')
  556. }, [slots('button')])]), this.genWordLimit(), this.genMessage()]);
  557. }
  558. });