Field.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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", "click-input", "end-validate", "start-validate", "click-left-icon", "click-right-icon", "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("end-validate", {
  140. status: state.status
  141. });
  142. const validate = (rules = props.rules) => new Promise((resolve) => {
  143. resetValidation();
  144. if (rules) {
  145. emit("start-validate");
  146. runRules(rules).then(() => {
  147. if (state.status === "failed") {
  148. resolve({
  149. name: props.name,
  150. message: state.validateMessage
  151. });
  152. endValidate();
  153. } else {
  154. state.status = "passed";
  155. resolve();
  156. endValidate();
  157. }
  158. });
  159. } else {
  160. resolve();
  161. }
  162. });
  163. const validateWithTrigger = (trigger) => {
  164. if (form && props.rules) {
  165. const {
  166. validateTrigger
  167. } = form.props;
  168. const defaultTrigger = toArray(validateTrigger).includes(trigger);
  169. const rules = props.rules.filter((rule) => {
  170. if (rule.trigger) {
  171. return toArray(rule.trigger).includes(trigger);
  172. }
  173. return defaultTrigger;
  174. });
  175. if (rules.length) {
  176. validate(rules);
  177. }
  178. }
  179. };
  180. const limitValueLength = (value) => {
  181. const {
  182. maxlength
  183. } = props;
  184. if (isDef(maxlength) && getStringLength(value) > maxlength) {
  185. const modelValue = getModelValue();
  186. if (modelValue && getStringLength(modelValue) === +maxlength) {
  187. return modelValue;
  188. }
  189. return cutString(value, +maxlength);
  190. }
  191. return value;
  192. };
  193. const updateValue = (value, trigger = "onChange") => {
  194. value = limitValueLength(value);
  195. if (props.type === "number" || props.type === "digit") {
  196. const isNumber = props.type === "number";
  197. value = formatNumber(value, isNumber, isNumber);
  198. }
  199. if (props.formatter && trigger === props.formatTrigger) {
  200. value = props.formatter(value);
  201. }
  202. if (inputRef.value && inputRef.value.value !== value) {
  203. inputRef.value.value = value;
  204. }
  205. if (value !== props.modelValue) {
  206. emit("update:modelValue", value);
  207. }
  208. };
  209. const onInput = (event) => {
  210. if (!event.target.composing) {
  211. updateValue(event.target.value);
  212. }
  213. };
  214. const blur = () => {
  215. var _a;
  216. return (_a = inputRef.value) == null ? void 0 : _a.blur();
  217. };
  218. const focus = () => {
  219. var _a;
  220. return (_a = inputRef.value) == null ? void 0 : _a.focus();
  221. };
  222. const adjustTextareaSize = () => {
  223. const input = inputRef.value;
  224. if (props.type === "textarea" && props.autosize && input) {
  225. resizeTextarea(input, props.autosize);
  226. }
  227. };
  228. const onFocus = (event) => {
  229. state.focused = true;
  230. emit("focus", event);
  231. nextTick(adjustTextareaSize);
  232. if (getProp("readonly")) {
  233. blur();
  234. }
  235. };
  236. const onBlur = (event) => {
  237. if (getProp("readonly")) {
  238. return;
  239. }
  240. state.focused = false;
  241. updateValue(getModelValue(), "onBlur");
  242. emit("blur", event);
  243. validateWithTrigger("onBlur");
  244. nextTick(adjustTextareaSize);
  245. resetScroll();
  246. };
  247. const onClickInput = (event) => emit("click-input", event);
  248. const onClickLeftIcon = (event) => emit("click-left-icon", event);
  249. const onClickRightIcon = (event) => emit("click-right-icon", event);
  250. const onClear = (event) => {
  251. preventDefault(event);
  252. emit("update:modelValue", "");
  253. emit("clear", event);
  254. };
  255. const showError = computed(() => {
  256. if (typeof props.error === "boolean") {
  257. return props.error;
  258. }
  259. if (form && form.props.showError && state.status === "failed") {
  260. return true;
  261. }
  262. });
  263. const labelStyle = computed(() => {
  264. const labelWidth = getProp("labelWidth");
  265. if (labelWidth) {
  266. return {
  267. width: addUnit(labelWidth)
  268. };
  269. }
  270. });
  271. const onKeypress = (event) => {
  272. const ENTER_CODE = 13;
  273. if (event.keyCode === ENTER_CODE) {
  274. const submitOnEnter = form && form.props.submitOnEnter;
  275. if (!submitOnEnter && props.type !== "textarea") {
  276. preventDefault(event);
  277. }
  278. if (props.type === "search") {
  279. blur();
  280. }
  281. }
  282. emit("keypress", event);
  283. };
  284. const getInputId = () => props.id || `${id}-input`;
  285. const getValidationStatus = () => state.status;
  286. const renderInput = () => {
  287. const controlClass = bem("control", [getProp("inputAlign"), {
  288. error: showError.value,
  289. custom: !!slots.input,
  290. "min-height": props.type === "textarea" && !props.autosize
  291. }]);
  292. if (slots.input) {
  293. return _createVNode("div", {
  294. "class": controlClass,
  295. "onClick": onClickInput
  296. }, [slots.input()]);
  297. }
  298. const inputAttrs = {
  299. id: getInputId(),
  300. ref: inputRef,
  301. name: props.name,
  302. rows: props.rows !== void 0 ? +props.rows : void 0,
  303. class: controlClass,
  304. disabled: getProp("disabled"),
  305. readonly: getProp("readonly"),
  306. autofocus: props.autofocus,
  307. placeholder: props.placeholder,
  308. autocomplete: props.autocomplete,
  309. enterkeyhint: props.enterkeyhint,
  310. "aria-labelledby": props.label ? `${id}-label` : void 0,
  311. onBlur,
  312. onFocus,
  313. onInput,
  314. onClick: onClickInput,
  315. onChange: endComposing,
  316. onKeypress,
  317. onCompositionend: endComposing,
  318. onCompositionstart: startComposing
  319. };
  320. if (props.type === "textarea") {
  321. return _createVNode("textarea", inputAttrs, null);
  322. }
  323. return _createVNode("input", _mergeProps(mapInputType(props.type), inputAttrs), null);
  324. };
  325. const renderLeftIcon = () => {
  326. const leftIconSlot = slots["left-icon"];
  327. if (props.leftIcon || leftIconSlot) {
  328. return _createVNode("div", {
  329. "class": bem("left-icon"),
  330. "onClick": onClickLeftIcon
  331. }, [leftIconSlot ? leftIconSlot() : _createVNode(Icon, {
  332. "name": props.leftIcon,
  333. "classPrefix": props.iconPrefix
  334. }, null)]);
  335. }
  336. };
  337. const renderRightIcon = () => {
  338. const rightIconSlot = slots["right-icon"];
  339. if (props.rightIcon || rightIconSlot) {
  340. return _createVNode("div", {
  341. "class": bem("right-icon"),
  342. "onClick": onClickRightIcon
  343. }, [rightIconSlot ? rightIconSlot() : _createVNode(Icon, {
  344. "name": props.rightIcon,
  345. "classPrefix": props.iconPrefix
  346. }, null)]);
  347. }
  348. };
  349. const renderWordLimit = () => {
  350. if (props.showWordLimit && props.maxlength) {
  351. const count = getStringLength(getModelValue());
  352. return _createVNode("div", {
  353. "class": bem("word-limit")
  354. }, [_createVNode("span", {
  355. "class": bem("word-num")
  356. }, [count]), _createTextVNode("/"), props.maxlength]);
  357. }
  358. };
  359. const renderMessage = () => {
  360. if (form && form.props.showErrorMessage === false) {
  361. return;
  362. }
  363. const message = props.errorMessage || state.validateMessage;
  364. if (message) {
  365. const slot = slots["error-message"];
  366. const errorMessageAlign = getProp("errorMessageAlign");
  367. return _createVNode("div", {
  368. "class": bem("error-message", errorMessageAlign)
  369. }, [slot ? slot({
  370. message
  371. }) : message]);
  372. }
  373. };
  374. const renderLabel = () => {
  375. const colon = getProp("colon") ? ":" : "";
  376. if (slots.label) {
  377. return [slots.label(), colon];
  378. }
  379. if (props.label) {
  380. return _createVNode("label", {
  381. "id": `${id}-label`,
  382. "for": getInputId()
  383. }, [props.label + colon]);
  384. }
  385. };
  386. const renderFieldBody = () => [_createVNode("div", {
  387. "class": bem("body")
  388. }, [renderInput(), showClear.value && _createVNode(Icon, {
  389. "ref": clearIconRef,
  390. "name": props.clearIcon,
  391. "class": bem("clear")
  392. }, null), renderRightIcon(), slots.button && _createVNode("div", {
  393. "class": bem("button")
  394. }, [slots.button()])]), renderWordLimit(), renderMessage()];
  395. useExpose({
  396. blur,
  397. focus,
  398. validate,
  399. formValue,
  400. resetValidation,
  401. getValidationStatus
  402. });
  403. provide(CUSTOM_FIELD_INJECTION_KEY, {
  404. customValue,
  405. resetValidation,
  406. validateWithTrigger
  407. });
  408. watch(() => props.modelValue, () => {
  409. updateValue(getModelValue());
  410. resetValidation();
  411. validateWithTrigger("onChange");
  412. nextTick(adjustTextareaSize);
  413. });
  414. onMounted(() => {
  415. updateValue(getModelValue(), props.formatTrigger);
  416. nextTick(adjustTextareaSize);
  417. });
  418. useEventListener("touchstart", onClear, {
  419. target: computed(() => {
  420. var _a;
  421. return (_a = clearIconRef.value) == null ? void 0 : _a.$el;
  422. })
  423. });
  424. return () => {
  425. const disabled = getProp("disabled");
  426. const labelAlign = getProp("labelAlign");
  427. const Label = renderLabel();
  428. const LeftIcon = renderLeftIcon();
  429. return _createVNode(Cell, {
  430. "size": props.size,
  431. "icon": props.leftIcon,
  432. "class": bem({
  433. error: showError.value,
  434. disabled,
  435. [`label-${labelAlign}`]: labelAlign
  436. }),
  437. "center": props.center,
  438. "border": props.border,
  439. "isLink": props.isLink,
  440. "clickable": props.clickable,
  441. "titleStyle": labelStyle.value,
  442. "valueClass": bem("value"),
  443. "titleClass": [bem("label", [labelAlign, {
  444. required: props.required
  445. }]), props.labelClass],
  446. "arrowDirection": props.arrowDirection
  447. }, {
  448. icon: LeftIcon ? () => LeftIcon : null,
  449. title: Label ? () => Label : null,
  450. value: renderFieldBody,
  451. extra: slots.extra
  452. });
  453. };
  454. }
  455. });
  456. export {
  457. stdin_default as default,
  458. fieldSharedProps
  459. };