// Utils import { resetScroll } from '../utils/dom/reset-scroll'; import { formatNumber } from '../utils/format/number'; import { preventDefault } from '../utils/dom/event'; import { getRootScrollTop, setRootScrollTop } from '../utils/dom/scroll'; import { isDef, addUnit, isObject, isPromise, isFunction, createNamespace, } from '../utils'; // Components import Icon from '../icon'; import Cell from '../cell'; import { cellProps } from '../cell/shared'; const [createComponent, bem] = createNamespace('field'); export default createComponent({ inheritAttrs: false, provide() { return { vanField: this, }; }, inject: { vanForm: { default: null, }, }, props: { ...cellProps, name: String, rules: Array, disabled: { type: Boolean, default: null, }, readonly: { type: Boolean, default: null, }, 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, value: { type: [Number, String], default: '', }, type: { type: String, default: 'text', }, error: { type: Boolean, default: null, }, colon: { type: Boolean, default: null, }, clearTrigger: { type: String, default: 'focus', }, formatTrigger: { type: String, default: 'onChange', }, }, data() { return { focused: false, validateFailed: false, validateMessage: '', }; }, watch: { value() { this.updateValue(this.value); this.resetValidation(); this.validateWithTrigger('onChange'); this.$nextTick(this.adjustSize); }, }, mounted() { this.updateValue(this.value, this.formatTrigger); this.$nextTick(this.adjustSize); if (this.vanForm) { this.vanForm.addField(this); } }, beforeDestroy() { if (this.vanForm) { this.vanForm.removeField(this); } }, computed: { showClear() { const readonly = this.getProp('readonly'); if (this.clearable && !readonly) { const hasValue = isDef(this.value) && this.value !== ''; const trigger = this.clearTrigger === 'always' || (this.clearTrigger === 'focus' && this.focused); return hasValue && trigger; } }, showError() { if (this.error !== null) { return this.error; } if (this.vanForm && this.vanForm.showError && this.validateFailed) { return true; } }, listeners() { return { ...this.$listeners, blur: this.onBlur, focus: this.onFocus, input: this.onInput, click: this.onClickInput, keypress: this.onKeypress, }; }, labelStyle() { const labelWidth = this.getProp('labelWidth'); if (labelWidth) { return { width: addUnit(labelWidth) }; } }, formValue() { if (this.children && (this.$scopedSlots.input || this.$slots.input)) { return this.children.value; } return this.value; }, }, methods: { // @exposed-api focus() { if (this.$refs.input) { this.$refs.input.focus(); } }, // @exposed-api blur() { if (this.$refs.input) { this.$refs.input.blur(); } }, runValidator(value, rule) { return new Promise((resolve) => { const returnVal = rule.validator(value, rule); if (isPromise(returnVal)) { return returnVal.then(resolve); } resolve(returnVal); }); }, isEmptyValue(value) { if (Array.isArray(value)) { return !value.length; } if (value === 0) { return false; } return !value; }, runSyncRule(value, rule) { if (rule.required && this.isEmptyValue(value)) { return false; } if (rule.pattern && !rule.pattern.test(value)) { return false; } return true; }, getRuleMessage(value, rule) { const { message } = rule; if (isFunction(message)) { return message(value, rule); } return message; }, runRules(rules) { return rules.reduce( (promise, rule) => promise.then(() => { if (this.validateFailed) { return; } let value = this.formValue; if (rule.formatter) { value = rule.formatter(value, rule); } if (!this.runSyncRule(value, rule)) { this.validateFailed = true; this.validateMessage = this.getRuleMessage(value, rule); return; } if (rule.validator) { return this.runValidator(value, rule).then((result) => { if (result === false) { this.validateFailed = true; this.validateMessage = this.getRuleMessage(value, rule); } }); } }), Promise.resolve() ); }, validate(rules = this.rules) { return new Promise((resolve) => { if (!rules) { resolve(); } this.resetValidation(); this.runRules(rules).then(() => { if (this.validateFailed) { resolve({ name: this.name, message: this.validateMessage, }); } else { resolve(); } }); }); }, validateWithTrigger(trigger) { if (this.vanForm && this.rules) { const defaultTrigger = this.vanForm.validateTrigger === trigger; const rules = this.rules.filter((rule) => { if (rule.trigger) { return rule.trigger === trigger; } return defaultTrigger; }); if (rules.length) { this.validate(rules); } } }, resetValidation() { if (this.validateFailed) { this.validateFailed = false; this.validateMessage = ''; } }, updateValue(value, trigger = 'onChange') { value = isDef(value) ? String(value) : ''; // native maxlength have incorrect line-break counting // see: https://github.com/vant-ui/vant/issues/5033 const { maxlength } = this; if (isDef(maxlength) && value.length > maxlength) { if (this.value && this.value.length === +maxlength) { ({ value } = this); } else { value = value.slice(0, maxlength); } } if (this.type === 'number' || this.type === 'digit') { const isNumber = this.type === 'number'; value = formatNumber(value, isNumber, isNumber); } if (this.formatter && trigger === this.formatTrigger) { value = this.formatter(value); } const { input } = this.$refs; if (input && value !== input.value) { input.value = value; } if (value !== this.value) { this.$emit('input', value); } }, onInput(event) { // not update v-model when composing if (event.target.composing) { return; } this.updateValue(event.target.value); }, onFocus(event) { this.focused = true; this.$emit('focus', event); // https://github.com/vant-ui/vant/issues/9715 this.$nextTick(this.adjustSize); // readonly not work in legacy mobile safari /* istanbul ignore if */ if (this.getProp('readonly')) { this.blur(); } }, onBlur(event) { if (this.getProp('readonly')) { return; } this.focused = false; this.updateValue(this.value, 'onBlur'); this.$emit('blur', event); this.validateWithTrigger('onBlur'); this.$nextTick(this.adjustSize); resetScroll(); }, onClick(event) { this.$emit('click', event); }, onClickInput(event) { this.$emit('click-input', event); }, onClickLeftIcon(event) { this.$emit('click-left-icon', event); }, onClickRightIcon(event) { this.$emit('click-right-icon', event); }, onClear(event) { preventDefault(event); this.$emit('input', ''); this.$emit('clear', event); }, onKeypress(event) { const ENTER_CODE = 13; if (event.keyCode === ENTER_CODE) { const submitOnEnter = this.getProp('submitOnEnter'); if (!submitOnEnter && this.type !== 'textarea') { preventDefault(event); } // trigger blur after click keyboard search button if (this.type === 'search') { this.blur(); } } this.$emit('keypress', event); }, adjustSize() { const { input } = this.$refs; if (!(this.type === 'textarea' && this.autosize) || !input) { return; } const scrollTop = getRootScrollTop(); input.style.height = 'auto'; let height = input.scrollHeight; if (isObject(this.autosize)) { const { maxHeight, minHeight } = this.autosize; if (maxHeight) { height = Math.min(height, maxHeight); } if (minHeight) { height = Math.max(height, minHeight); } } if (height) { input.style.height = height + 'px'; // https://github.com/vant-ui/vant/issues/9178 setRootScrollTop(scrollTop); } }, genInput() { const { type } = this; const disabled = this.getProp('disabled'); const readonly = this.getProp('readonly'); const inputSlot = this.slots('input'); const inputAlign = this.getProp('inputAlign'); if (inputSlot) { return (
{inputSlot}
); } const inputProps = { ref: 'input', class: bem('control', inputAlign), domProps: { value: this.value, }, attrs: { ...this.$attrs, name: this.name, disabled, readonly, placeholder: this.placeholder, }, on: this.listeners, // add model directive to skip IME composition directives: [ { name: 'model', value: this.value, }, ], }; if (type === 'textarea') { return