Field.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import { ref, watch, provide, computed, nextTick, reactive, onMounted, defineComponent, createVNode as _createVNode, mergeProps as _mergeProps, createTextVNode as _createTextVNode } from "vue";
  2. import { isDef, extend, addUnit, toArray, FORM_KEY, numericProp, unknownProp, resetScroll, formatNumber, preventDefault, makeStringProp, makeNumericProp, createNamespace, clamp } from "../utils/index.mjs";
  3. import { cutString, runSyncRule, endComposing, mapInputType, isEmptyValue, startComposing, getRuleMessage, resizeTextarea, getStringLength, runRuleValidator } from "./utils.mjs";
  4. import { cellSharedProps } from "../cell/Cell.mjs";
  5. import { useParent, useEventListener, CUSTOM_FIELD_INJECTION_KEY } from "@vant/use";
  6. import { useId } from "../composables/use-id.mjs";
  7. import { useExpose } from "../composables/use-expose.mjs";
  8. import { Icon } from "../icon/index.mjs";
  9. import { Cell } from "../cell/index.mjs";
  10. const [name, bem] = createNamespace("field");
  11. const fieldSharedProps = {
  12. id: String,
  13. name: String,
  14. leftIcon: String,
  15. rightIcon: String,
  16. autofocus: Boolean,
  17. clearable: Boolean,
  18. maxlength: numericProp,
  19. max: Number,
  20. min: Number,
  21. formatter: Function,
  22. clearIcon: makeStringProp("clear"),
  23. modelValue: makeNumericProp(""),
  24. inputAlign: String,
  25. placeholder: String,
  26. autocomplete: String,
  27. autocapitalize: String,
  28. autocorrect: String,
  29. errorMessage: String,
  30. enterkeyhint: String,
  31. clearTrigger: makeStringProp("focus"),
  32. formatTrigger: makeStringProp("onChange"),
  33. spellcheck: {
  34. type: Boolean,
  35. default: null
  36. },
  37. error: {
  38. type: Boolean,
  39. default: null
  40. },
  41. disabled: {
  42. type: Boolean,
  43. default: null
  44. },
  45. readonly: {
  46. type: Boolean,
  47. default: null
  48. },
  49. inputmode: String
  50. };
  51. const fieldProps = extend({}, cellSharedProps, fieldSharedProps, {
  52. rows: numericProp,
  53. type: makeStringProp("text"),
  54. rules: Array,
  55. autosize: [Boolean, Object],
  56. labelWidth: numericProp,
  57. labelClass: unknownProp,
  58. labelAlign: String,
  59. showWordLimit: Boolean,
  60. errorMessageAlign: String,
  61. colon: {
  62. type: Boolean,
  63. default: null
  64. }
  65. });
  66. var stdin_default = defineComponent({
  67. name,
  68. props: fieldProps,
  69. emits: ["blur", "focus", "clear", "keypress", "clickInput", "endValidate", "startValidate", "clickLeftIcon", "clickRightIcon", "update:modelValue"],
  70. setup(props, {
  71. emit,
  72. slots
  73. }) {
  74. const id = useId();
  75. const state = reactive({
  76. status: "unvalidated",
  77. focused: false,
  78. validateMessage: ""
  79. });
  80. const inputRef = ref();
  81. const clearIconRef = ref();
  82. const customValue = ref();
  83. const {
  84. parent: form
  85. } = useParent(FORM_KEY);
  86. const getModelValue = () => {
  87. var _a;
  88. return String((_a = props.modelValue) != null ? _a : "");
  89. };
  90. const getProp = (key) => {
  91. if (isDef(props[key])) {
  92. return props[key];
  93. }
  94. if (form && isDef(form.props[key])) {
  95. return form.props[key];
  96. }
  97. };
  98. const showClear = computed(() => {
  99. const readonly = getProp("readonly");
  100. if (props.clearable && !readonly) {
  101. const hasValue = getModelValue() !== "";
  102. const trigger = props.clearTrigger === "always" || props.clearTrigger === "focus" && state.focused;
  103. return hasValue && trigger;
  104. }
  105. return false;
  106. });
  107. const formValue = computed(() => {
  108. if (customValue.value && slots.input) {
  109. return customValue.value();
  110. }
  111. return props.modelValue;
  112. });
  113. const showRequiredMark = computed(() => {
  114. var _a;
  115. const required = getProp("required");
  116. if (required === "auto") {
  117. return (_a = props.rules) == null ? void 0 : _a.some((rule) => rule.required);
  118. }
  119. return required;
  120. });
  121. const runRules = (rules) => rules.reduce((promise, rule) => promise.then(() => {
  122. if (state.status === "failed") {
  123. return;
  124. }
  125. let {
  126. value
  127. } = formValue;
  128. if (rule.formatter) {
  129. value = rule.formatter(value, rule);
  130. }
  131. if (!runSyncRule(value, rule)) {
  132. state.status = "failed";
  133. state.validateMessage = getRuleMessage(value, rule);
  134. return;
  135. }
  136. if (rule.validator) {
  137. if (isEmptyValue(value) && rule.validateEmpty === false) {
  138. return;
  139. }
  140. return runRuleValidator(value, rule).then((result) => {
  141. if (result && typeof result === "string") {
  142. state.status = "failed";
  143. state.validateMessage = result;
  144. } else if (result === false) {
  145. state.status = "failed";
  146. state.validateMessage = getRuleMessage(value, rule);
  147. }
  148. });
  149. }
  150. }), Promise.resolve());
  151. const resetValidation = () => {
  152. state.status = "unvalidated";
  153. state.validateMessage = "";
  154. };
  155. const endValidate = () => emit("endValidate", {
  156. status: state.status,
  157. message: state.validateMessage
  158. });
  159. const validate = (rules = props.rules) => new Promise((resolve) => {
  160. resetValidation();
  161. if (rules) {
  162. emit("startValidate");
  163. runRules(rules).then(() => {
  164. if (state.status === "failed") {
  165. resolve({
  166. name: props.name,
  167. message: state.validateMessage
  168. });
  169. endValidate();
  170. } else {
  171. state.status = "passed";
  172. resolve();
  173. endValidate();
  174. }
  175. });
  176. } else {
  177. resolve();
  178. }
  179. });
  180. const validateWithTrigger = (trigger) => {
  181. if (form && props.rules) {
  182. const {
  183. validateTrigger
  184. } = form.props;
  185. const defaultTrigger = toArray(validateTrigger).includes(trigger);
  186. const rules = props.rules.filter((rule) => {
  187. if (rule.trigger) {
  188. return toArray(rule.trigger).includes(trigger);
  189. }
  190. return defaultTrigger;
  191. });
  192. if (rules.length) {
  193. validate(rules);
  194. }
  195. }
  196. };
  197. const limitValueLength = (value) => {
  198. var _a;
  199. const {
  200. maxlength
  201. } = props;
  202. if (isDef(maxlength) && getStringLength(value) > +maxlength) {
  203. const modelValue = getModelValue();
  204. if (modelValue && getStringLength(modelValue) === +maxlength) {
  205. return modelValue;
  206. }
  207. const selectionEnd = (_a = inputRef.value) == null ? void 0 : _a.selectionEnd;
  208. if (state.focused && selectionEnd) {
  209. const valueArr = [...value];
  210. const exceededLength = valueArr.length - +maxlength;
  211. valueArr.splice(selectionEnd - exceededLength, exceededLength);
  212. return valueArr.join("");
  213. }
  214. return cutString(value, +maxlength);
  215. }
  216. return value;
  217. };
  218. const updateValue = (value, trigger = "onChange") => {
  219. var _a, _b;
  220. const originalValue = value;
  221. value = limitValueLength(value);
  222. const limitDiffLen = getStringLength(originalValue) - getStringLength(value);
  223. if (props.type === "number" || props.type === "digit") {
  224. const isNumber = props.type === "number";
  225. value = formatNumber(value, isNumber, isNumber);
  226. if (trigger === "onBlur" && value !== "" && (props.min !== void 0 || props.max !== void 0)) {
  227. const adjustedValue = clamp(+value, (_a = props.min) != null ? _a : -Infinity, (_b = props.max) != null ? _b : Infinity);
  228. value = adjustedValue.toString();
  229. }
  230. }
  231. let formatterDiffLen = 0;
  232. if (props.formatter && trigger === props.formatTrigger) {
  233. const {
  234. formatter,
  235. maxlength
  236. } = props;
  237. value = formatter(value);
  238. if (isDef(maxlength) && getStringLength(value) > +maxlength) {
  239. value = cutString(value, +maxlength);
  240. }
  241. if (inputRef.value && state.focused) {
  242. const {
  243. selectionEnd
  244. } = inputRef.value;
  245. const bcoVal = cutString(originalValue, selectionEnd);
  246. formatterDiffLen = getStringLength(formatter(bcoVal)) - getStringLength(bcoVal);
  247. }
  248. }
  249. if (inputRef.value && inputRef.value.value !== value) {
  250. if (state.focused) {
  251. let {
  252. selectionStart,
  253. selectionEnd
  254. } = inputRef.value;
  255. inputRef.value.value = value;
  256. if (isDef(selectionStart) && isDef(selectionEnd)) {
  257. const valueLen = getStringLength(value);
  258. if (limitDiffLen) {
  259. selectionStart -= limitDiffLen;
  260. selectionEnd -= limitDiffLen;
  261. } else if (formatterDiffLen) {
  262. selectionStart += formatterDiffLen;
  263. selectionEnd += formatterDiffLen;
  264. }
  265. inputRef.value.setSelectionRange(Math.min(selectionStart, valueLen), Math.min(selectionEnd, valueLen));
  266. }
  267. } else {
  268. inputRef.value.value = value;
  269. }
  270. }
  271. if (value !== props.modelValue) {
  272. emit("update:modelValue", value);
  273. }
  274. };
  275. const onInput = (event) => {
  276. if (!event.target.composing) {
  277. updateValue(event.target.value);
  278. }
  279. };
  280. const blur = () => {
  281. var _a;
  282. return (_a = inputRef.value) == null ? void 0 : _a.blur();
  283. };
  284. const focus = () => {
  285. var _a;
  286. return (_a = inputRef.value) == null ? void 0 : _a.focus();
  287. };
  288. const adjustTextareaSize = () => {
  289. const input = inputRef.value;
  290. if (props.type === "textarea" && props.autosize && input) {
  291. resizeTextarea(input, props.autosize);
  292. }
  293. };
  294. const onFocus = (event) => {
  295. state.focused = true;
  296. emit("focus", event);
  297. nextTick(adjustTextareaSize);
  298. if (getProp("readonly")) {
  299. blur();
  300. }
  301. };
  302. const onBlur = (event) => {
  303. state.focused = false;
  304. updateValue(getModelValue(), "onBlur");
  305. emit("blur", event);
  306. if (getProp("readonly")) {
  307. return;
  308. }
  309. validateWithTrigger("onBlur");
  310. nextTick(adjustTextareaSize);
  311. resetScroll();
  312. };
  313. const onClickInput = (event) => emit("clickInput", event);
  314. const onClickLeftIcon = (event) => emit("clickLeftIcon", event);
  315. const onClickRightIcon = (event) => emit("clickRightIcon", event);
  316. const onClear = (event) => {
  317. preventDefault(event);
  318. emit("update:modelValue", "");
  319. emit("clear", event);
  320. };
  321. const showError = computed(() => {
  322. if (typeof props.error === "boolean") {
  323. return props.error;
  324. }
  325. if (form && form.props.showError && state.status === "failed") {
  326. return true;
  327. }
  328. });
  329. const labelStyle = computed(() => {
  330. const labelWidth = getProp("labelWidth");
  331. const labelAlign = getProp("labelAlign");
  332. if (labelWidth && labelAlign !== "top") {
  333. return {
  334. width: addUnit(labelWidth)
  335. };
  336. }
  337. });
  338. const onKeypress = (event) => {
  339. const ENTER_CODE = 13;
  340. if (event.keyCode === ENTER_CODE) {
  341. const submitOnEnter = form && form.props.submitOnEnter;
  342. if (!submitOnEnter && props.type !== "textarea") {
  343. preventDefault(event);
  344. }
  345. if (props.type === "search") {
  346. blur();
  347. }
  348. }
  349. emit("keypress", event);
  350. };
  351. const getInputId = () => props.id || `${id}-input`;
  352. const getValidationStatus = () => state.status;
  353. const renderInput = () => {
  354. const controlClass = bem("control", [getProp("inputAlign"), {
  355. error: showError.value,
  356. custom: !!slots.input,
  357. "min-height": props.type === "textarea" && !props.autosize
  358. }]);
  359. if (slots.input) {
  360. return _createVNode("div", {
  361. "class": controlClass,
  362. "onClick": onClickInput
  363. }, [slots.input()]);
  364. }
  365. const inputAttrs = {
  366. id: getInputId(),
  367. ref: inputRef,
  368. name: props.name,
  369. rows: props.rows !== void 0 ? +props.rows : void 0,
  370. class: controlClass,
  371. disabled: getProp("disabled"),
  372. readonly: getProp("readonly"),
  373. autofocus: props.autofocus,
  374. placeholder: props.placeholder,
  375. autocomplete: props.autocomplete,
  376. autocapitalize: props.autocapitalize,
  377. autocorrect: props.autocorrect,
  378. enterkeyhint: props.enterkeyhint,
  379. spellcheck: props.spellcheck,
  380. "aria-labelledby": props.label ? `${id}-label` : void 0,
  381. "data-allow-mismatch": "attribute",
  382. onBlur,
  383. onFocus,
  384. onInput,
  385. onClick: onClickInput,
  386. onChange: endComposing,
  387. onKeypress,
  388. onCompositionend: endComposing,
  389. onCompositionstart: startComposing
  390. };
  391. if (props.type === "textarea") {
  392. return _createVNode("textarea", _mergeProps(inputAttrs, {
  393. "inputmode": props.inputmode
  394. }), null);
  395. }
  396. return _createVNode("input", _mergeProps(mapInputType(props.type, props.inputmode), inputAttrs), null);
  397. };
  398. const renderLeftIcon = () => {
  399. const leftIconSlot = slots["left-icon"];
  400. if (props.leftIcon || leftIconSlot) {
  401. return _createVNode("div", {
  402. "class": bem("left-icon"),
  403. "onClick": onClickLeftIcon
  404. }, [leftIconSlot ? leftIconSlot() : _createVNode(Icon, {
  405. "name": props.leftIcon,
  406. "classPrefix": props.iconPrefix
  407. }, null)]);
  408. }
  409. };
  410. const renderRightIcon = () => {
  411. const rightIconSlot = slots["right-icon"];
  412. if (props.rightIcon || rightIconSlot) {
  413. return _createVNode("div", {
  414. "class": bem("right-icon"),
  415. "onClick": onClickRightIcon
  416. }, [rightIconSlot ? rightIconSlot() : _createVNode(Icon, {
  417. "name": props.rightIcon,
  418. "classPrefix": props.iconPrefix
  419. }, null)]);
  420. }
  421. };
  422. const renderWordLimit = () => {
  423. if (props.showWordLimit && props.maxlength) {
  424. const count = getStringLength(getModelValue());
  425. return _createVNode("div", {
  426. "class": bem("word-limit")
  427. }, [_createVNode("span", {
  428. "class": bem("word-num")
  429. }, [count]), _createTextVNode("/"), props.maxlength]);
  430. }
  431. };
  432. const renderMessage = () => {
  433. if (form && form.props.showErrorMessage === false) {
  434. return;
  435. }
  436. const message = props.errorMessage || state.validateMessage;
  437. if (message) {
  438. const slot = slots["error-message"];
  439. const errorMessageAlign = getProp("errorMessageAlign");
  440. return _createVNode("div", {
  441. "class": bem("error-message", errorMessageAlign)
  442. }, [slot ? slot({
  443. message
  444. }) : message]);
  445. }
  446. };
  447. const renderLabel = () => {
  448. const labelWidth = getProp("labelWidth");
  449. const labelAlign = getProp("labelAlign");
  450. const colon = getProp("colon") ? ":" : "";
  451. if (slots.label) {
  452. return [slots.label(), colon];
  453. }
  454. if (props.label) {
  455. return _createVNode("label", {
  456. "id": `${id}-label`,
  457. "for": slots.input ? void 0 : getInputId(),
  458. "data-allow-mismatch": "attribute",
  459. "onClick": (event) => {
  460. preventDefault(event);
  461. focus();
  462. },
  463. "style": labelAlign === "top" && labelWidth ? {
  464. width: addUnit(labelWidth)
  465. } : void 0
  466. }, [props.label + colon]);
  467. }
  468. };
  469. const renderFieldBody = () => [_createVNode("div", {
  470. "class": bem("body")
  471. }, [renderInput(), showClear.value && _createVNode(Icon, {
  472. "ref": clearIconRef,
  473. "name": props.clearIcon,
  474. "class": bem("clear")
  475. }, null), renderRightIcon(), slots.button && _createVNode("div", {
  476. "class": bem("button")
  477. }, [slots.button()])]), renderWordLimit(), renderMessage()];
  478. useExpose({
  479. blur,
  480. focus,
  481. validate,
  482. formValue,
  483. resetValidation,
  484. getValidationStatus
  485. });
  486. provide(CUSTOM_FIELD_INJECTION_KEY, {
  487. customValue,
  488. resetValidation,
  489. validateWithTrigger
  490. });
  491. watch(() => props.modelValue, () => {
  492. updateValue(getModelValue());
  493. resetValidation();
  494. validateWithTrigger("onChange");
  495. nextTick(adjustTextareaSize);
  496. });
  497. onMounted(() => {
  498. updateValue(getModelValue(), props.formatTrigger);
  499. nextTick(adjustTextareaSize);
  500. });
  501. useEventListener("touchstart", onClear, {
  502. target: computed(() => {
  503. var _a;
  504. return (_a = clearIconRef.value) == null ? void 0 : _a.$el;
  505. })
  506. });
  507. return () => {
  508. const disabled = getProp("disabled");
  509. const labelAlign = getProp("labelAlign");
  510. const LeftIcon = renderLeftIcon();
  511. const renderTitle = () => {
  512. const Label = renderLabel();
  513. if (labelAlign === "top") {
  514. return [LeftIcon, Label].filter(Boolean);
  515. }
  516. return Label || [];
  517. };
  518. return _createVNode(Cell, {
  519. "size": props.size,
  520. "class": bem({
  521. error: showError.value,
  522. disabled,
  523. [`label-${labelAlign}`]: labelAlign
  524. }),
  525. "center": props.center,
  526. "border": props.border,
  527. "isLink": props.isLink,
  528. "clickable": props.clickable,
  529. "titleStyle": labelStyle.value,
  530. "valueClass": bem("value"),
  531. "titleClass": [bem("label", [labelAlign, {
  532. required: showRequiredMark.value
  533. }]), props.labelClass],
  534. "arrowDirection": props.arrowDirection
  535. }, {
  536. icon: LeftIcon && labelAlign !== "top" ? () => LeftIcon : null,
  537. title: renderTitle,
  538. value: renderFieldBody,
  539. extra: slots.extra
  540. });
  541. };
  542. }
  543. });
  544. export {
  545. stdin_default as default,
  546. fieldProps,
  547. fieldSharedProps
  548. };