input-number.vue
<template> <div @dragstart.prevent :class="[ 'el-input-number', inputNumberSize ? 'el-input-number--' + inputNumberSize : '', { 'is-disabled': inputNumberDisabled }, { 'is-without-controls': !controls }, { 'is-controls-right': controlsAtRight } ]"> <span class="el-input-number__decrease" role="button" v-if="controls" v-repeat-click="decrease" :class="{'is-disabled': minDisabled}" @keydown.enter="decrease"> <i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i> </span> <span class="el-input-number__increase" role="button" v-if="controls" v-repeat-click="increase" :class="{'is-disabled': maxDisabled}" @keydown.enter="increase"> <i :class="`el-icon-${controlsAtRight ? 'arrow-up' : 'plus'}`"></i> </span> <el-input ref="input" :value="displayValue" :placeholder="placeholder" :disabled="inputNumberDisabled" :size="inputNumberSize" :max="max" :min="min" :name="name" :label="label" @keydown.up.native.prevent="increase" @keydown.down.native.prevent="decrease" @blur="handleBlur" @focus="handleFocus" @input="handleInput" @change="handleInputChange"> </el-input> </div> </template> <script> import ElInput from 'element-ui/packages/input'; import Focus from 'element-ui/src/mixins/focus'; import RepeatClick from 'element-ui/src/directives/repeat-click'; export default { name: 'ElInputNumber', mixins: [Focus('input')], inject: { elForm: { default: '' }, elFormItem: { default: '' } }, directives: { repeatClick: RepeatClick }, components: { ElInput }, props: { // 计数器步长 step: { type: Number, default: 1 }, // 是否只能输入 step 的倍数 stepStrictly: { type: Boolean, default: false }, // 设置计数器允许的最大值 max: { type: Number, default: Infinity }, // 设置计数器允许的最小值 min: { type: Number, default: -Infinity }, // value / v-model 绑定值 value: {}, // 是否禁用计数器 disabled: Boolean, // 计数器尺寸 string large, small size: String, // 是否使用控制按钮 controls: { type: Boolean, default: true }, // 控制按钮位置 string right controlsPosition: { type: String, default: '' }, // 原生属性 name: String, // 输入框关联的label文字 label: String, // 输入框默认 placeholder placeholder: String, // 数值精度 precision: { type: Number, validator(val) { return val >= 0 && val === parseInt(val, 10); } } }, data() { return { currentValue: 0, userInput: null }; }, watch: { // vaue值变化 value: { // 立即触发 immediate: true, // 自定义函数 handler(value) { let newVal = value === undefined ? value : Number(value); if (newVal !== undefined) { if (isNaN(newVal)) { return; } // 是否只能输入 step 的倍数 if (this.stepStrictly) { const stepPrecision = this.getPrecision(this.step); const precisionFactor = Math.pow(10, stepPrecision); newVal = Math.round(newVal / this.step) * precisionFactor * this.step / precisionFactor; } if (this.precision !== undefined) { newVal = this.toPrecision(newVal, this.precision); } } if (newVal >= this.max) newVal = this.max; if (newVal <= this.min) newVal = this.min; this.currentValue = newVal; this.userInput = null; this.$emit('input', newVal); } } }, computed: { // 不能小于最小数 minDisabled() { return this._decrease(this.value, this.step) < this.min; }, // 不能大于最大数 maxDisabled() { return this._increase(this.value, this.step) > this.max; }, numPrecision() { const { value, step, getPrecision, precision } = this; const stepPrecision = getPrecision(step); if (precision !== undefined) { if (stepPrecision > precision) { console.warn('[Element Warn][InputNumber]precision should not be less than the decimal places of step'); } return precision; } else { return Math.max(getPrecision(value), stepPrecision); } }, // 控制条在右侧 controlsAtRight() { return this.controls && this.controlsPosition === 'right'; }, _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, inputNumberSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; }, inputNumberDisabled() { return this.disabled || (this.elForm || {}).disabled; }, displayValue() { if (this.userInput !== null) { return this.userInput; } let currentValue = this.currentValue; if (typeof currentValue === 'number') { // 如果只能输入 step 的倍数 if (this.stepStrictly) { // 小数点后面几位小数 const stepPrecision = this.getPrecision(this.step); const precisionFactor = Math.pow(10, stepPrecision); currentValue = Math.round(currentValue / this.step) * precisionFactor * this.step / precisionFactor; } if (this.precision !== undefined) { currentValue = currentValue.toFixed(this.precision); } } return currentValue; } }, methods: { // 截取为传入的位数 toPrecision(num, precision) { if (precision === undefined) precision = this.numPrecision; return parseFloat(Number(num).toFixed(precision)); }, // 获取小数点后面还有几位 getPrecision(value) { if (value === undefined) return 0; const valueString = value.toString(); const dotPosition = valueString.indexOf('.'); let precision = 0; if (dotPosition !== -1) { precision = valueString.length - dotPosition - 1; } return precision; }, _increase(val, step) { if (typeof val !== 'number' && val !== undefined) return this.currentValue; const precisionFactor = Math.pow(10, this.numPrecision); // Solve the accuracy problem of JS decimal calculation by converting the value to integer. return this.toPrecision((precisionFactor * val + precisionFactor * step) / precisionFactor); }, // 递减函数 _decrease(val, step) { if (typeof val !== 'number' && val !== undefined) return this.currentValue; // eg: 10的0.1次方 const precisionFactor = Math.pow(10, this.numPrecision); return this.toPrecision((precisionFactor * val - precisionFactor * step) / precisionFactor); }, increase() { if (this.inputNumberDisabled || this.maxDisabled) return; const value = this.value || 0; const newVal = this._increase(value, this.step); this.setCurrentValue(newVal); }, decrease() { if (this.inputNumberDisabled || this.minDisabled) return; const value = this.value || 0; const newVal = this._decrease(value, this.step); this.setCurrentValue(newVal); }, handleBlur(event) { this.$emit('blur', event); }, handleFocus(event) { this.$emit('focus', event); }, setCurrentValue(newVal) { const oldVal = this.currentValue; if (typeof newVal === 'number' && this.precision !== undefined) { newVal = this.toPrecision(newVal, this.precision); } if (newVal >= this.max) newVal = this.max; if (newVal <= this.min) newVal = this.min; if (oldVal === newVal) return; this.userInput = null; this.$emit('input', newVal); this.$emit('change', newVal, oldVal); this.currentValue = newVal; }, handleInput(value) { this.userInput = value; }, handleInputChange(value) { const newVal = value === '' ? undefined : Number(value); if (!isNaN(newVal) || value === '') { this.setCurrentValue(newVal); } this.userInput = null; }, select() { this.$refs.input.select(); } }, mounted() { let innerInput = this.$refs.input.$refs.input; innerInput.setAttribute('role', 'spinbutton'); innerInput.setAttribute('aria-valuemax', this.max); innerInput.setAttribute('aria-valuemin', this.min); innerInput.setAttribute('aria-valuenow', this.currentValue); innerInput.setAttribute('aria-disabled', this.inputNumberDisabled); }, updated() { if (!this.$refs || !this.$refs.input) return; const innerInput = this.$refs.input.$refs.input; innerInput.setAttribute('aria-valuenow', this.currentValue); } }; </script>repeat-click.js
import { once, on } from 'element-ui/src/utils/dom'; export default { bind (el, binding, vnode) { let interval = null; let startTime; /** * context是一个 Component 类型的数据结构,这个Component是flow定义的结构,具体可看vue源码中的flow内的内容, * Component就是组件,所以这个context就是该vnode所在的组件上下文,再来看 binding.expression, 官网说这就是 * v - repeat - click="decrease" 中的decrease方法,这个方法写在组件的methods内, * 那么 context[binding.expression] 就是 context['decrease'] 因此就拿到了组件内的decrease方法, * 类似于在组件中使用 this.decrease 一样,然后最后的 apply() 就很奇怪了,apply的用法是参数的第一个表示要执行的目标对象,如果为null或者undefined则表示在window上调用该方法,这里没有参数, 那就是undefined,所以是在window上执行 * */ const handler = () => vnode.context[binding.expression].apply(); const clear = () => { if (Date.now() - startTime < 100) { handler(); } clearInterval(interval); interval = null; }; on(el, 'mousedown', (e) => { /* 这个方法就是给元素绑定事件,if-else处理了兼容性的情况, attachEvent 是ie的方法, addEventListener 是其他主流浏览器的方法。 on 的第三个参数就是事件处理函数, on 中第一句 if (e.button !== 0) return 的 e.button 是按下了鼠标的哪个键 Element源码分析系列7-InputNumber(数字输入框) 不等于0则是说明按下的不是左键,因为一般只处理左键的点击事件,注意 onclick 只响应鼠标左键的按下,而 onm ousedown 则响应3个键的按下,所以这里要区分。 on 最后一句 interval = setInterval(handler, 100) 设置了定时器定时执行handler方法从而每隔0.1s触发一次数字增加或减少事件, 然后我们思考,按下去鼠标时给dom元素添加了事件:定时执行handler,那么在鼠标抬起时肯定要销毁这个定时器,否则将会无限触发handler方法, 造成数字一直增加或减少,因此 once(document, 'mouseup', clear) 这句话就是在鼠标抬起时销毁定时器,先看clear方法 里面就是clearInterval销毁定时器,前面的if逻辑很关键,在按下鼠标时记录一个时间,抬起鼠标时检测当前时间 - 按下时的时间 < 100毫秒,如果是则触发一次点击,如果不写这个if,则无法实现单击操作,因为如果不写,由于interval = setInterval(handler, 100), 在按下后100毫秒后才会触发一次点击,则在100毫秒内抬起鼠标时interval已经被clear了。最后注意下 once(document, 'mouseup', clear) , once 是只触发一次的高阶函数 */ if (e.button !== 0) return; startTime = Date.now(); once(document, 'mouseup', clear); clearInterval(interval); interval = setInterval(handler, 100); }); } };