vant/packages/field/index.js

321 lines
7.2 KiB
JavaScript

import Icon from '../icon';
import Cell from '../cell';
import { cellProps } from '../cell/shared';
import { preventDefault } from '../utils/dom/event';
import { getRootScrollTop } from '../utils/dom/scroll';
import { createNamespace, isObj, isDef, suffixPx } from '../utils';
import { isIOS } from '../utils/validate/system';
const [createComponent, bem] = createNamespace('field');
export default createComponent({
inheritAttrs: false,
props: {
...cellProps,
error: Boolean,
leftIcon: String,
rightIcon: String,
readonly: Boolean,
clearable: Boolean,
labelWidth: [String, Number],
labelClass: null,
labelAlign: String,
inputAlign: String,
autosize: [Boolean, Object],
errorMessage: String,
errorMessageAlign: String,
type: {
type: String,
default: 'text'
}
},
data() {
return {
focused: false
};
},
watch: {
value() {
this.$nextTick(this.adjustSize);
}
},
mounted() {
this.format();
this.$nextTick(this.adjustSize);
},
computed: {
showClear() {
return (
this.clearable &&
this.focused &&
this.value !== '' &&
isDef(this.value) &&
!this.readonly
);
},
listeners() {
const listeners = {
...this.$listeners,
input: this.onInput,
keypress: this.onKeypress,
focus: this.onFocus,
blur: this.onBlur
};
delete listeners.click;
return listeners;
},
labelStyle() {
const { labelWidth } = this;
if (labelWidth) {
return { width: suffixPx(labelWidth) };
}
}
},
methods: {
focus() {
if (this.$refs.input) {
this.$refs.input.focus();
}
},
blur() {
if (this.$refs.input) {
this.$refs.input.blur();
}
},
// native maxlength not work when type = number
format(target = this.$refs.input) {
if (!target) {
return;
}
let { value } = target;
const { maxlength } = this.$attrs;
if (this.type === 'number' && isDef(maxlength) && value.length > maxlength) {
value = value.slice(0, maxlength);
target.value = value;
}
return value;
},
onInput(event) {
// not update v-model when composing
if (event.target.composing) {
return;
}
this.$emit('input', this.format(event.target));
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
// hack for safari
/* istanbul ignore if */
if (this.readonly) {
this.blur();
}
},
onBlur(event) {
this.focused = false;
this.$emit('blur', event);
// Hack for iOS12 page scroll
// https://developers.weixin.qq.com/community/develop/doc/00044ae90742f8c82fb78fcae56800
/* istanbul ignore next */
if (isIOS()) {
window.scrollTo(0, getRootScrollTop());
}
},
onClick(event) {
this.$emit('click', event);
},
onClickLeftIcon() {
this.$emit('click-left-icon');
},
onClickRightIcon() {
this.$emit('click-right-icon');
},
onClear(event) {
preventDefault(event);
this.$emit('input', '');
this.$emit('clear');
},
onKeypress(event) {
if (this.type === 'number') {
const { keyCode } = event;
const allowPoint = String(this.value).indexOf('.') === -1;
const isValidKey =
(keyCode >= 48 && keyCode <= 57) ||
(keyCode === 46 && allowPoint) ||
keyCode === 45;
if (!isValidKey) {
preventDefault(event);
}
}
// trigger blur after click keyboard search button
/* istanbul ignore next */
if (this.type === 'search' && event.keyCode === 13) {
this.blur();
}
this.$emit('keypress', event);
},
adjustSize() {
const { input } = this.$refs;
if (!(this.type === 'textarea' && this.autosize) || !input) {
return;
}
input.style.height = 'auto';
let height = input.scrollHeight;
if (isObj(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';
}
},
renderInput() {
const inputSlot = this.slots('input');
if (inputSlot) {
return (
<div class={bem('control', this.inputAlign)}>
{inputSlot}
</div>
);
}
const inputProps = {
ref: 'input',
class: bem('control', this.inputAlign),
domProps: {
value: this.value
},
attrs: {
...this.$attrs,
readonly: this.readonly
},
on: this.listeners,
// add model directive to skip IME composition
directives: [
{
name: 'model',
value: this.value
}
]
};
if (this.type === 'textarea') {
return <textarea {...inputProps} />;
}
return <input type={this.type} {...inputProps} />;
},
renderLeftIcon() {
const showLeftIcon = this.slots('left-icon') || this.leftIcon;
if (showLeftIcon) {
return (
<div class={bem('left-icon')} onClick={this.onClickLeftIcon}>
{this.slots('left-icon') || <Icon name={this.leftIcon} />}
</div>
);
}
},
renderRightIcon() {
const { slots } = this;
const showRightIcon = slots('right-icon') || this.rightIcon;
if (showRightIcon) {
return (
<div class={bem('right-icon')} onClick={this.onClickRightIcon}>
{slots('right-icon') || <Icon name={this.rightIcon} />}
</div>
);
}
}
},
render(h) {
const { slots, labelAlign } = this;
const scopedSlots = {
icon: this.renderLeftIcon
};
if (slots('label')) {
scopedSlots.title = () => slots('label');
}
return (
<Cell
icon={this.leftIcon}
size={this.size}
title={this.label}
center={this.center}
border={this.border}
isLink={this.isLink}
required={this.required}
clickable={this.clickable}
titleStyle={this.labelStyle}
titleClass={[bem('label', labelAlign), this.labelClass]}
class={bem({
error: this.error,
disabled: this.$attrs.disabled,
[`label-${labelAlign}`]: labelAlign,
'min-height': this.type === 'textarea' && !this.autosize
})}
scopedSlots={scopedSlots}
onClick={this.onClick}
>
<div class={bem('body')}>
{this.renderInput()}
{this.showClear && (
<Icon name="clear" class={bem('clear')} onTouchstart={this.onClear} />
)}
{this.renderRightIcon()}
{slots('button') && <div class={bem('button')}>{slots('button')}</div>}
</div>
{this.errorMessage && (
<div class={bem('error-message', this.errorMessageAlign)}>
{this.errorMessage}
</div>
)}
</Cell>
);
}
});