import { ref, watch, provide, computed, nextTick, reactive, onMounted, } from 'vue'; // Utils import { isDef, trigger, addUnit, isObject, isPromise, isFunction, resetScroll, formatNumber, preventDefault, createNamespace, } from '../utils'; import { runSyncRule } from './utils'; // Composition import { useParent } from '@vant/use'; import { useExpose } from '../composables/use-expose'; import { FORM_KEY, FIELD_KEY } from '../composables/use-link-field'; // Components import Icon from '../icon'; import Cell, { cellProps } from '../cell'; const [createComponent, bem] = createNamespace('field'); export default createComponent({ props: { ...cellProps, rows: [Number, String], name: String, rules: Array, disabled: Boolean, readonly: Boolean, autosize: [Boolean, Object], leftIcon: String, rightIcon: String, clearable: Boolean, formatter: Function, maxlength: [Number, String], labelWidth: [Number, String], labelClass: null, labelAlign: String, inputAlign: String, placeholder: String, errorMessage: String, errorMessageAlign: String, showWordLimit: Boolean, type: { type: String, default: 'text', }, error: { type: Boolean, default: null, }, colon: { type: Boolean, default: null, }, modelValue: { type: [String, Number], default: '', }, clearTrigger: { type: String, default: 'focus', }, formatTrigger: { type: String, default: 'onChange', }, }, emits: [ 'blur', 'focus', 'clear', 'keypress', 'click-input', 'click-left-icon', 'click-right-icon', 'update:modelValue', ], setup(props, { emit, slots }) { const state = reactive({ focused: false, validateFailed: false, validateMessage: '', }); const inputRef = ref(); const childFieldValue = ref(); const showClear = computed(() => { if (props.clearable && !props.readonly) { const hasValue = isDef(props.modelValue) && props.modelValue !== ''; const trigger = props.clearTrigger === 'always' || (props.clearTrigger === 'focus' && state.focused); return hasValue && trigger; } }); const formValue = computed(() => { if (childFieldValue.value && slots.input) { return childFieldValue.value(); } return props.modelValue; }); const runValidator = (value, rule) => new Promise((resolve) => { const returnVal = rule.validator(value, rule); if (isPromise(returnVal)) { return returnVal.then(resolve); } resolve(returnVal); }); const getRuleMessage = (value, rule) => { const { message } = rule; if (isFunction(message)) { return message(value, rule); } return message; }; const runRules = (rules) => rules.reduce( (promise, rule) => promise.then(() => { if (state.validateFailed) { return; } let { value } = formValue; if (rule.formatter) { value = rule.formatter(value, rule); } if (!runSyncRule(value, rule)) { state.validateFailed = true; state.validateMessage = getRuleMessage(value, rule); return; } if (rule.validator) { return runValidator(value, rule).then((result) => { if (result === false) { state.validateFailed = true; state.validateMessage = getRuleMessage(value, rule); } }); } }), Promise.resolve() ); const resetValidation = () => { if (state.validateFailed) { state.validateFailed = false; state.validateMessage = ''; } }; const validate = (rules = props.rules) => new Promise((resolve) => { if (!rules) { resolve(); } resetValidation(); runRules(rules).then(() => { if (state.validateFailed) { resolve({ name: props.name, message: state.validateMessage, }); } else { resolve(); } }); }); const { parent: form } = useParent(FORM_KEY); const validateWithTrigger = (trigger) => { if (form && props.rules) { const defaultTrigger = form.props.validateTrigger === trigger; const rules = props.rules.filter((rule) => { if (rule.trigger) { return rule.trigger === trigger; } return defaultTrigger; }); validate(rules); } }; const updateValue = (value, trigger = 'onChange') => { value = isDef(value) ? String(value) : ''; // native maxlength have incorrect line-break counting // see: https://github.com/youzan/vant/issues/5033 const { maxlength, modelValue } = props; if (isDef(maxlength) && value.length > maxlength) { if (modelValue && modelValue.length === +maxlength) { value = modelValue; } else { value = value.slice(0, maxlength); } } if (props.type === 'number' || props.type === 'digit') { const isNumber = props.type === 'number'; value = formatNumber(value, isNumber, isNumber); } if (props.formatter && trigger === props.formatTrigger) { value = props.formatter(value); } if (inputRef.value && value !== inputRef.value.value) { inputRef.value.value = value; } if (value !== props.modelValue) { emit('update:modelValue', value); } }; const onInput = (event) => { // skip update value when composing if (!event.target.composing) { updateValue(event.target.value); } }; const focus = () => { if (inputRef.value) { inputRef.value.focus(); } }; const blur = () => { if (inputRef.value) { inputRef.value.blur(); } }; const onFocus = (event) => { state.focused = true; emit('focus', event); // readonly not work in lagacy mobile safari if (props.readonly) { blur(); } }; const onBlur = (event) => { state.focused = false; updateValue(props.modelValue, 'onBlur'); emit('blur', event); validateWithTrigger('onBlur'); resetScroll(); }; const onClickInput = (event) => { emit('click-input', event); }; const onClickLeftIcon = (event) => { emit('click-left-icon', event); }; const onClickRightIcon = (event) => { emit('click-right-icon', 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.validateFailed) { return true; } }); const getProp = (key) => { if (isDef(props[key])) { return props[key]; } if (form && isDef(form.props[key])) { return form.props[key]; } }; const labelStyle = computed(() => { const labelWidth = getProp('labelWidth'); if (labelWidth) { return { width: addUnit(labelWidth) }; } }); const onKeypress = (event) => { const ENTER_CODE = 13; if (event.keyCode === ENTER_CODE) { const submitOnEnter = getProp('submitOnEnter'); if (!submitOnEnter && props.type !== 'textarea') { preventDefault(event); } // trigger blur after click keyboard search button if (props.type === 'search') { blur(); } } emit('keypress', event); }; const onCompositionStart = (event) => { event.target.composing = true; }; const onCompositionEnd = (event) => { const { target } = event; if (target.composing) { target.composing = false; trigger(target, 'input'); } }; const adjustSize = () => { const input = inputRef.value; if (!(props.type === 'textarea' && props.autosize) || !input) { return; } input.style.height = 'auto'; let height = input.scrollHeight; if (isObject(props.autosize)) { const { maxHeight, minHeight } = props.autosize; if (maxHeight) { height = Math.min(height, maxHeight); } if (minHeight) { height = Math.max(height, minHeight); } } if (height) { input.style.height = height + 'px'; } }; const renderInput = () => { const inputAlign = getProp('inputAlign'); if (slots.input) { return (