Field.mjs 14 KB

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