简介
在线设计协作工具 MasterGo/即时设计/Pixso 都有在线编辑器,编辑器中最常用的就是数值输入和颜色输入组件,本章节主要介绍如何实现一个体验尚可的数值输入组件。
适用场景
可视化编辑器、Web设计工具、管理平台个性化输入表单
功能分析
- 实现手动输入数值
- 可以拖动增减数值
- 上下键增减数值
- 最大和最小值限制
- 可配置图标和单位显示
- 可使用v-model绑定状态
技术实现
样式与结构
整体使用Flex 左右布局
- 左边为图标,右边为数值和单位
- 这里图标为必须的,因为拖动改变数值需要有一个响应区域
- 单位单独一个元素不放在
Input的value中的好处是不用去格式化value
基本样式和结构
CSS
.prop-input-number {
display: flex;
&-left {
width: 30px;
height: 30px;
}
&-right {
width: 60px;
border: 1px solid #ccc;
display:flex;
&-input {
outline: 0;
border: 0;
// 没有单位时,是100%宽,有单位时,是计算出来的固定宽度
flex: 1;
}
&-unit {
flex:1;
}
}
}
HTML
<div class="prop-input-number">
<div class="prop-input-number__pointer" style={this.pointerStyle}></div>
<section
class="prop-input-number__left"
onMousedown={this.onMousedown}
>
{this.renderIcon()}
</section>
<section class="prop-input-number__right">
<input
class="prop-input-number__right-input"
value={this.value}
onInput={this.onInput}
onFocus={this.onFocus}
onKeydown={this.onKeydown}
type="text"
style={{
width: this.unit ? `${(this.value + '').length * 8}px` : '100%',
}}
/>
<div class="prop-input-number__right-unit">{this.unit}</div>
</section>
</div>
功能实现
鼠标样式锁定
当鼠标在图标上的时候,显示鼠标样式为 cursor:ew-resize; 表示可以左右拖动,但实际上会有一个问题,当鼠标离开图标区域后,左右拖动的鼠标样式就失效了,参考了其它的编辑器实现方案,大部分是先生成一个div,fixed定位,默认隐藏, 当在鼠标拖动时,这个div显示到鼠标下方,动态调整这个div的位置,让div始终保持在鼠标下面,并给这个div设置鼠标样式为 cursor:ew-resize;
gif图
实现
遮罩Div默认样式
transform: translateX(-150px);
width: 300px;
height: 24px;
position: fixed;
left: 0;
top: 13px;
z-index: 999;
display: none;
当鼠标按下时,设置 actived 为 true,显示遮罩div,并动态设置div的位置,位置可根据当前鼠标在屏幕上的位置动态计算出来
优化
这样可以实现鼠标在离开组件区域后,任然能保持鼠标样式,动态计算遮罩div样式也可以优化一下,比如:遮罩div默认全屏隐藏,当鼠标按下时,直接设置遮罩Div样式为block 显示就可以了。这样不用计算位置,减少性能消耗。
拖动改变数值
监听事件,计算数值,定义鼠标按下事件,监听 mousemove 和 mouseup 事件,计算水平移动的距离,在鼠标抬起的时候并移除两个事件的注册,这里暂只记录一下水平距离就可以了,下面可以根据当前水平距离来计算当前数值。
onMousedown(e) {
const startX = e.clientX;
this.acitved = true;
const move = (moveEvent) => {
moveEvent.stopPropagation();
moveEvent.preventDefault();
const currX = moveEvent.clientX;
const distance = currX - startX;
};
const up = () => {
this.acitved = false;
this.prevDistance = 0;
document.removeEventListener('mousemove', move, true);
document.removeEventListener('mouseup', up, true);
};
document.addEventListener('mousemove', move, true);
document.addEventListener('mouseup', up, true);
},
数值递增与递减的控制
鼠标移动时,判断运动方向,如果是向右移动,则将当前数值加1,如果是向左移动,则将当前数据减1(这里我最初的实现是直接 value + distance, 这样虽然可以实现,但是数据的增加的频率过快,无法做到匀速递增,且不方便控制数值大小)
这里要实现匀速递增和递减,我的方法是,判断上一次的distance 和当前的ditance比较,如果上一次的移动的距离比当前移动的距离小,则判断为向右,即 value+=1,反之,则 value-=1,这里要处理上一次和这次相等的情况,相等时,则不做处理
const distance = currX - startX;
if (this.prevDistance === distance) return;
const isAdd = this.prevDistance < distance;
this.prevDistance = distance;
this.handleDragChange(isAdd);
handleDragChange(isAdd) {
const dis = isAdd ? 1 : -1;
this.value = Math.floor(dis + this.value);
},
向上加向下减功能实现
这个很简单,监听 onKeydown 事件,判断键值是上还是下,进行加1或减1
onKeydown(e) {
const { keyCode } = e;
const { value } = this;
if (![38, 40].includes(keyCode)) return;
this.value = keyCode === 38 ? value + 1 : value - 1;
}
最小值和最大值限制
上面的 this.value 直接赋值改调用函数 calculateValue
// 计算数值
calculateValue(value) {
// 限制最小值
let limitValue = Math.max(value, this.min);
// 限制最大值
if (isNumber(this.max)) {
limitValue = Math.min(limitValue, this.max);
}
this.$emit('change', limitValue);
},
单位显示
单位显示主要考虑样式布局,如果没有单位,那么input 输入框是 width: 100%, 如果有单位,他们被 flex 容器包裹,input 的宽度需要根据字符数来动态计算出来,这样可以保证单位紧跟在数值后面,具体请参考 Pixso 的交互效果
改进和优化
- Light/Dark模式
- 允许小数数值(需要考虑浮点数溢出)
总结
一个小小的输入组件,在实现的过程中,交互样式布局这些其实都可以参考第三方网站,内部js的逻辑其实也能猜到一二,但在实现的过程中还是有很多小的细节要注意,同时注重用户体验的提升,从美观度、易用度各方面都要找到一个平衡,编码也是种艺术,尤其是前端的开发工作。