Field.mjs 16 KB

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