import { ref, watch, provide, computed, nextTick, reactive, onMounted, defineComponent, createVNode as _createVNode, mergeProps as _mergeProps, createTextVNode as _createTextVNode } from "vue"; import { isDef, extend, addUnit, toArray, FORM_KEY, numericProp, unknownProp, resetScroll, formatNumber, preventDefault, makeStringProp, makeNumericProp, createNamespace, clamp } from "../utils/index.mjs"; import { cutString, runSyncRule, endComposing, mapInputType, isEmptyValue, startComposing, getRuleMessage, resizeTextarea, getStringLength, runRuleValidator } from "./utils.mjs"; import { cellSharedProps } from "../cell/Cell.mjs"; import { useParent, useEventListener, CUSTOM_FIELD_INJECTION_KEY } from "@vant/use"; import { useId } from "../composables/use-id.mjs"; import { useExpose } from "../composables/use-expose.mjs"; import { Icon } from "../icon/index.mjs"; import { Cell } from "../cell/index.mjs"; const [name, bem] = createNamespace("field"); const fieldSharedProps = { id: String, name: String, leftIcon: String, rightIcon: String, autofocus: Boolean, clearable: Boolean, maxlength: numericProp, max: Number, min: Number, formatter: Function, clearIcon: makeStringProp("clear"), modelValue: makeNumericProp(""), inputAlign: String, placeholder: String, autocomplete: String, autocapitalize: String, autocorrect: String, errorMessage: String, enterkeyhint: String, clearTrigger: makeStringProp("focus"), formatTrigger: makeStringProp("onChange"), spellcheck: { type: Boolean, default: null }, error: { type: Boolean, default: null }, disabled: { type: Boolean, default: null }, readonly: { type: Boolean, default: null }, inputmode: String }; const fieldProps = extend({}, cellSharedProps, fieldSharedProps, { rows: numericProp, type: makeStringProp("text"), rules: Array, autosize: [Boolean, Object], labelWidth: numericProp, labelClass: unknownProp, labelAlign: String, showWordLimit: Boolean, errorMessageAlign: String, colon: { type: Boolean, default: null } }); var stdin_default = defineComponent({ name, props: fieldProps, emits: ["blur", "focus", "clear", "keypress", "clickInput", "endValidate", "startValidate", "clickLeftIcon", "clickRightIcon", "update:modelValue"], setup(props, { emit, slots }) { const id = useId(); const state = reactive({ status: "unvalidated", focused: false, validateMessage: "" }); const inputRef = ref(); const clearIconRef = ref(); const customValue = ref(); const { parent: form } = useParent(FORM_KEY); const getModelValue = () => { var _a; return String((_a = props.modelValue) != null ? _a : ""); }; const getProp = (key) => { if (isDef(props[key])) { return props[key]; } if (form && isDef(form.props[key])) { return form.props[key]; } }; const showClear = computed(() => { const readonly = getProp("readonly"); if (props.clearable && !readonly) { const hasValue = getModelValue() !== ""; const trigger = props.clearTrigger === "always" || props.clearTrigger === "focus" && state.focused; return hasValue && trigger; } return false; }); const formValue = computed(() => { if (customValue.value && slots.input) { return customValue.value(); } return props.modelValue; }); const showRequiredMark = computed(() => { var _a; const required = getProp("required"); if (required === "auto") { return (_a = props.rules) == null ? void 0 : _a.some((rule) => rule.required); } return required; }); const runRules = (rules) => rules.reduce((promise, rule) => promise.then(() => { if (state.status === "failed") { return; } let { value } = formValue; if (rule.formatter) { value = rule.formatter(value, rule); } if (!runSyncRule(value, rule)) { state.status = "failed"; state.validateMessage = getRuleMessage(value, rule); return; } if (rule.validator) { if (isEmptyValue(value) && rule.validateEmpty === false) { return; } return runRuleValidator(value, rule).then((result) => { if (result && typeof result === "string") { state.status = "failed"; state.validateMessage = result; } else if (result === false) { state.status = "failed"; state.validateMessage = getRuleMessage(value, rule); } }); } }), Promise.resolve()); const resetValidation = () => { state.status = "unvalidated"; state.validateMessage = ""; }; const endValidate = () => emit("endValidate", { status: state.status, message: state.validateMessage }); const validate = (rules = props.rules) => new Promise((resolve) => { resetValidation(); if (rules) { emit("startValidate"); runRules(rules).then(() => { if (state.status === "failed") { resolve({ name: props.name, message: state.validateMessage }); endValidate(); } else { state.status = "passed"; resolve(); endValidate(); } }); } else { resolve(); } }); const validateWithTrigger = (trigger) => { if (form && props.rules) { const { validateTrigger } = form.props; const defaultTrigger = toArray(validateTrigger).includes(trigger); const rules = props.rules.filter((rule) => { if (rule.trigger) { return toArray(rule.trigger).includes(trigger); } return defaultTrigger; }); if (rules.length) { validate(rules); } } }; const limitValueLength = (value) => { var _a; const { maxlength } = props; if (isDef(maxlength) && getStringLength(value) > +maxlength) { const modelValue = getModelValue(); if (modelValue && getStringLength(modelValue) === +maxlength) { return modelValue; } const selectionEnd = (_a = inputRef.value) == null ? void 0 : _a.selectionEnd; if (state.focused && selectionEnd) { const valueArr = [...value]; const exceededLength = valueArr.length - +maxlength; valueArr.splice(selectionEnd - exceededLength, exceededLength); return valueArr.join(""); } return cutString(value, +maxlength); } return value; }; const updateValue = (value, trigger = "onChange") => { var _a, _b; const originalValue = value; value = limitValueLength(value); const limitDiffLen = getStringLength(originalValue) - getStringLength(value); if (props.type === "number" || props.type === "digit") { const isNumber = props.type === "number"; value = formatNumber(value, isNumber, isNumber); if (trigger === "onBlur" && value !== "" && (props.min !== void 0 || props.max !== void 0)) { const adjustedValue = clamp(+value, (_a = props.min) != null ? _a : -Infinity, (_b = props.max) != null ? _b : Infinity); value = adjustedValue.toString(); } } let formatterDiffLen = 0; if (props.formatter && trigger === props.formatTrigger) { const { formatter, maxlength } = props; value = formatter(value); if (isDef(maxlength) && getStringLength(value) > +maxlength) { value = cutString(value, +maxlength); } if (inputRef.value && state.focused) { const { selectionEnd } = inputRef.value; const bcoVal = cutString(originalValue, selectionEnd); formatterDiffLen = getStringLength(formatter(bcoVal)) - getStringLength(bcoVal); } } if (inputRef.value && inputRef.value.value !== value) { if (state.focused) { let { selectionStart, selectionEnd } = inputRef.value; inputRef.value.value = value; if (isDef(selectionStart) && isDef(selectionEnd)) { const valueLen = getStringLength(value); if (limitDiffLen) { selectionStart -= limitDiffLen; selectionEnd -= limitDiffLen; } else if (formatterDiffLen) { selectionStart += formatterDiffLen; selectionEnd += formatterDiffLen; } inputRef.value.setSelectionRange(Math.min(selectionStart, valueLen), Math.min(selectionEnd, valueLen)); } } else { inputRef.value.value = value; } } if (value !== props.modelValue) { emit("update:modelValue", value); } }; const onInput = (event) => { if (!event.target.composing) { updateValue(event.target.value); } }; const blur = () => { var _a; return (_a = inputRef.value) == null ? void 0 : _a.blur(); }; const focus = () => { var _a; return (_a = inputRef.value) == null ? void 0 : _a.focus(); }; const adjustTextareaSize = () => { const input = inputRef.value; if (props.type === "textarea" && props.autosize && input) { resizeTextarea(input, props.autosize); } }; const onFocus = (event) => { state.focused = true; emit("focus", event); nextTick(adjustTextareaSize); if (getProp("readonly")) { blur(); } }; const onBlur = (event) => { state.focused = false; updateValue(getModelValue(), "onBlur"); emit("blur", event); if (getProp("readonly")) { return; } validateWithTrigger("onBlur"); nextTick(adjustTextareaSize); resetScroll(); }; const onClickInput = (event) => emit("clickInput", event); const onClickLeftIcon = (event) => emit("clickLeftIcon", event); const onClickRightIcon = (event) => emit("clickRightIcon", event); const onClear = (event) => { preventDefault(event); emit("update:modelValue", ""); emit("clear", event); }; const showError = computed(() => { if (typeof props.error === "boolean") { return props.error; } if (form && form.props.showError && state.status === "failed") { return true; } }); const labelStyle = computed(() => { const labelWidth = getProp("labelWidth"); const labelAlign = getProp("labelAlign"); if (labelWidth && labelAlign !== "top") { return { width: addUnit(labelWidth) }; } }); const onKeypress = (event) => { const ENTER_CODE = 13; if (event.keyCode === ENTER_CODE) { const submitOnEnter = form && form.props.submitOnEnter; if (!submitOnEnter && props.type !== "textarea") { preventDefault(event); } if (props.type === "search") { blur(); } } emit("keypress", event); }; const getInputId = () => props.id || `${id}-input`; const getValidationStatus = () => state.status; const renderInput = () => { const controlClass = bem("control", [getProp("inputAlign"), { error: showError.value, custom: !!slots.input, "min-height": props.type === "textarea" && !props.autosize }]); if (slots.input) { return _createVNode("div", { "class": controlClass, "onClick": onClickInput }, [slots.input()]); } const inputAttrs = { id: getInputId(), ref: inputRef, name: props.name, rows: props.rows !== void 0 ? +props.rows : void 0, class: controlClass, disabled: getProp("disabled"), readonly: getProp("readonly"), autofocus: props.autofocus, placeholder: props.placeholder, autocomplete: props.autocomplete, autocapitalize: props.autocapitalize, autocorrect: props.autocorrect, enterkeyhint: props.enterkeyhint, spellcheck: props.spellcheck, "aria-labelledby": props.label ? `${id}-label` : void 0, "data-allow-mismatch": "attribute", onBlur, onFocus, onInput, onClick: onClickInput, onChange: endComposing, onKeypress, onCompositionend: endComposing, onCompositionstart: startComposing }; if (props.type === "textarea") { return _createVNode("textarea", _mergeProps(inputAttrs, { "inputmode": props.inputmode }), null); } return _createVNode("input", _mergeProps(mapInputType(props.type, props.inputmode), inputAttrs), null); }; const renderLeftIcon = () => { const leftIconSlot = slots["left-icon"]; if (props.leftIcon || leftIconSlot) { return _createVNode("div", { "class": bem("left-icon"), "onClick": onClickLeftIcon }, [leftIconSlot ? leftIconSlot() : _createVNode(Icon, { "name": props.leftIcon, "classPrefix": props.iconPrefix }, null)]); } }; const renderRightIcon = () => { const rightIconSlot = slots["right-icon"]; if (props.rightIcon || rightIconSlot) { return _createVNode("div", { "class": bem("right-icon"), "onClick": onClickRightIcon }, [rightIconSlot ? rightIconSlot() : _createVNode(Icon, { "name": props.rightIcon, "classPrefix": props.iconPrefix }, null)]); } }; const renderWordLimit = () => { if (props.showWordLimit && props.maxlength) { const count = getStringLength(getModelValue()); return _createVNode("div", { "class": bem("word-limit") }, [_createVNode("span", { "class": bem("word-num") }, [count]), _createTextVNode("/"), props.maxlength]); } }; const renderMessage = () => { if (form && form.props.showErrorMessage === false) { return; } const message = props.errorMessage || state.validateMessage; if (message) { const slot = slots["error-message"]; const errorMessageAlign = getProp("errorMessageAlign"); return _createVNode("div", { "class": bem("error-message", errorMessageAlign) }, [slot ? slot({ message }) : message]); } }; const renderLabel = () => { const labelWidth = getProp("labelWidth"); const labelAlign = getProp("labelAlign"); const colon = getProp("colon") ? ":" : ""; if (slots.label) { return [slots.label(), colon]; } if (props.label) { return _createVNode("label", { "id": `${id}-label`, "for": slots.input ? void 0 : getInputId(), "data-allow-mismatch": "attribute", "onClick": (event) => { preventDefault(event); focus(); }, "style": labelAlign === "top" && labelWidth ? { width: addUnit(labelWidth) } : void 0 }, [props.label + colon]); } }; const renderFieldBody = () => [_createVNode("div", { "class": bem("body") }, [renderInput(), showClear.value && _createVNode(Icon, { "ref": clearIconRef, "name": props.clearIcon, "class": bem("clear") }, null), renderRightIcon(), slots.button && _createVNode("div", { "class": bem("button") }, [slots.button()])]), renderWordLimit(), renderMessage()]; useExpose({ blur, focus, validate, formValue, resetValidation, getValidationStatus }); provide(CUSTOM_FIELD_INJECTION_KEY, { customValue, resetValidation, validateWithTrigger }); watch(() => props.modelValue, () => { updateValue(getModelValue()); resetValidation(); validateWithTrigger("onChange"); nextTick(adjustTextareaSize); }); onMounted(() => { updateValue(getModelValue(), props.formatTrigger); nextTick(adjustTextareaSize); }); useEventListener("touchstart", onClear, { target: computed(() => { var _a; return (_a = clearIconRef.value) == null ? void 0 : _a.$el; }) }); return () => { const disabled = getProp("disabled"); const labelAlign = getProp("labelAlign"); const LeftIcon = renderLeftIcon(); const renderTitle = () => { const Label = renderLabel(); if (labelAlign === "top") { return [LeftIcon, Label].filter(Boolean); } return Label || []; }; return _createVNode(Cell, { "size": props.size, "class": bem({ error: showError.value, disabled, [`label-${labelAlign}`]: labelAlign }), "center": props.center, "border": props.border, "isLink": props.isLink, "clickable": props.clickable, "titleStyle": labelStyle.value, "valueClass": bem("value"), "titleClass": [bem("label", [labelAlign, { required: showRequiredMark.value }]), props.labelClass], "arrowDirection": props.arrowDirection }, { icon: LeftIcon && labelAlign !== "top" ? () => LeftIcon : null, title: renderTitle, value: renderFieldBody, extra: slots.extra }); }; } }); export { stdin_default as default, fieldProps, fieldSharedProps };