前言
最近在单位的一次常规需求当中,原型图上有一个ip的输入框,用于控制可访问资源的ip地址。需求没有给出很具体的ip输入框的具体的功能,只要求能用就可以了。由于没有对此功能的具体的描述,于是我参考了微软的IP输入框作为我实现的模板。【本文是基于vue2.x、 antd vue 1.7.8 版本】。
需求分析
上图是微软的ip输入框,经过一段时间的鼓捣,总结的功能需求如下(总体会按照下方列出的需求点来实现):
- 输入框最多支持输入三位数字,且超过255的数字在失焦的时候会被转换为255
- 输入框无法输入出数字、英文标点之外的字符【ps:这边最后在实现的时候针对中文输入法处理死了好多脑细胞-.-】
- 当输入框内的数字是3位的时候自动聚焦到下一个输入框
- 按下箭头左键以及右键鼠标光标依次按顺序跳动【ps:这个在实现的时候也改了好多Bug】
- 当鼠标光标位于输入的末尾时,按下句号键可跳转到下一个输入框
- 可以按删除键删除数字
- may be more...
需求分析完毕,那就开始写代码吧!
代码分析实现
1.初始化
首先我们要做的是打地基,初始化一些必要的数据以及样式,通过下面的代码我们会得到一个基本功能的ip输入框【我初始化了4个input输入框,本文中会用第n个输入框代指这些小的input输入框】
<template>
<ul class="fan-ip-addr">
<li v-for="(item, index) in ip" :key="index" class="fan-ip-item">
<a-input
size="small"
v-model.number="item.value"
class="fan-ip-input"
></a-input>
<span class="fan-ip-dot" v-if="index < 3"></span>
</li>
</ul>
</template>
<script>
import { Input } from "ant-design-vue";
export default {
name: "fanIpinputs",
data() {
return {
ip: [{ value: 0 }, { value: 0 }, { value: 0 }, { value: 0 }],
};
},
components: {
aInput: Input,
},
};
</script>
<style lang="less" scoped>
.fan-ip-addr {
display: inline-flex;
list-style: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0px 10px;
justify-content: space-around;
width: 190px;
height: 32px;
& .fan-ip-item {
line-height: 32px;
& .fan-ip-dot {
display: inline-block;
width: 2px;
height: 2px;
background: #9b8d8d;
border-radius: 50%;
box-shadow: 0 0 0 1px #fff;
}
& .fan-ip-input {
border: none;
width: 40px;
position: relative;
padding: 3px 8px;
&:focus {
box-shadow: none;
}
}
}
}
</style>
2.自定义v-model
通过对vue官网对于自定义组件的v-model的阅读,我们知道可以通过组件的model
选项来配置对应的prop以及emit事件。这里还有一个问题,就是我定义的prop传入的数据是一个String(0.0.0.0)形式的,而在组件中为了绑定4个input的model,在组件内我维护的是一个数组。所以我们在emit抛给父组件的时候,要把数组转换为字符串再传递给父组件,而从父组件传进来的字符串同样也需要处理成数组形式才行。所以我加了watch,通过监听prop来处理传进来的值。
所以首先我们给a-input
组件绑定change
事件,在值发生变化的时候,处理成需要的ip格式,并且通过result
的emit事件抛出去:
changeIp(e, index) {
const resultIp = this.ip.map((ip) => ip.value).join(".");
this.$emit("result", resultIp);
}
其次,针对传进来的prop的值,我们通过watch
来处理:
watch: {
value: {
immediate: true,
handler: function (newIp, oldIp) {
this.ip = [];
newIp.split(".").forEach((ele) => {
this.ip.push({ value: ele });
});
},
},
},
经过这样子处理之后,我们可以通过<fan-ip-input v-model="ip"></fan-ip-input>
来使用此组件。
3.鼠标光标
当我实现了上述功能以及输入框最多支持输入三位数字,且超过255的数字在失焦的时候会被转换为255
需求之后,我开始着手实现按箭头键光标依次跳动的相关功能。最开始我在需求分析的时候,没有仔细在意到鼠标光标依次跳动这个关键点【ps:虽然需求分析出来了,但是脑子里当时想的都仅仅是按下按键聚焦到下一个输入框的功能,就觉得应该都挺简单的】,等到具体代码实现的时候,我就突然懵了,我该怎么判断鼠标位置?
我最开始的想法是通过event.screenX
的值来判断鼠标位置,然后根据输入框的一些位置信息来判断鼠标的位置,以达到鼠标光标依次跳动的功能。但稍加思考,就想给自己一个大嘴巴子,这么离谱的想法你也想得出来。于是开始向百度求助,果然,功夫不负有心人,度娘告诉我可以通过selectionStart
以及selectionEnd
来获取鼠标光标的位置以及设置鼠标光标的位置。
哇塞,激动的心,颤抖的手,代码都呼之欲出了!!!
pressKey(e, index, item) {
switch (e.code) {
case "ArrowRight":
if (item.value.toString().length === e.currentTarget.selectionStart) {
this.$refs.ipInput[index === 3 ? 0 : index + 1].focus();
break;
case "ArrowLeft":
if (e.currentTarget.selectionStart === 0) {
this.$refs.ipInput[index === 0 ? 3 : index - 1].focus();
}
break;
default:
break;
}
},
咋一看,貌似没啥问题,我判断鼠标的位置在末尾或者首位的时候,再做聚焦下一个输入框的功能。但是,实际上,这么写有一个问题并没有被解决。让我们把视线拉回到微软的ip输入框,我们可以看到,当全部有值的时候,鼠标光标是依次跳动的(这边的关键点在于:当鼠标光标第一次到达末尾的时候,应当做停留,再次按下右方向键才会跳转到下一个输入框),而上面的代码中,并不能判断是否是第一次到达末尾,所以出现的问题就是当到达值的末尾时,就会直接跳转到下一个输入框中去,如下图所示【下图中我只按了两下右箭头→】:
所以为了解决这个问题,我为每个输入框引入了一个flag的值,该flag记录了鼠标光标是否是第一次到达最后一位(或者第一位),以决定是留在当前的输入框还是下一个输入框。
data() {
return {
firstFlag: [
{ start: true, end: true },
{ start: true, end: true },
{ start: true, end: true },
{ start: true, end: true },
],
};
},
在失焦、聚焦、按键等事件触发的时候,正确的改变此flag的值,就可以做到鼠标光标依次跳动。【虽然这边短短一句话带过,事实上,我这边花了很多的时间来完善的(〒︿〒)】
4.控制输入框的输入
这个看起来是不是很简单?是的,没有错。我们只需要绑定一个按键按下的事件,并且给不需要的按键直接preventDefault()
就行了。
keydown(e, index) {
const allowKey = [
"Backspace",
// "Period",
"ArrowRight",
"ArrowLeft",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
];
if (!allowKey.includes(e.key)) {
e.preventDefault();
}
},
看起来,已经实现了,对吗?
那么为啥呢?
原来在实际测试的过程中,中文输入法并不会被阻止!也就是说,在中文输入法的情况下,我还是可以输入q
等之类的按键。此时的我,心态发生了亿乃乃的变化。我很想忽略掉这个问题,并且把使用体验的问题甩给用户,简单的在ip输入框的右边加个提示符请使用英文输入法输入IP
,这不是Bug,这是你不会用。但,这合理吗?这是一个前端工程师该有的作为吗?
ps:这里稍微解释一下这个bug,虽然我给v-model绑定了.number的修饰符以及限制最多3位,但是在以下情况下会出现可以输入,中文输入法为前提,首先输入框内的值没有到达3位,此时按下q键,再按下Enter键,q就被输入在输入框内了,这是我不想看到的情况。或者说,我觉得这就是一个Bug,因为输入框内出现了不希望出现的字符,当然其实这边最好的是中文输入法也直接阻止掉,我尝试过给compositionstart直接
return false
,但很遗憾的是不起作用,如果有大佬直到怎么直接阻止掉,请指教。所以我退而求其次,将输入的字符去掉。
经过区区半个月时间的思想斗争,我决定去直面这个问题,不再逃避,尽力做到最好!
通过对资料的搜索整理,我在input输入中文时,如何过滤掉拼音 - 掘金 (juejin.cn)该文找到了中文输入法触发的事件:
keydown:按下一个键触发事件;
keypress:按下通常会产生字符值的键。此事件高度依赖设备,废弃;
keyup:释放一个键触发事件;
compositionstart:当用户使用拼音输入法开始输入汉字时,这个事件就会被触发;
compositionupdate:事件触发于字符被输入到一段文字的时候;
compositionend:当文本段落的组成完成或取消时, compositionend 事件将被触发;
input:元素的
value
被修改时,会触发input
事件change:当用户提交对元素值的更改时。与
input
事件不同,change 事件不一定会对元素值的每次更改触发。
所以我们只需要绑定compositionstart、compositionend的事件,每次都记录中文输入法输入的文字,最后再通过正则去掉因为中文输入法索输入的文字,就可以做到中文输入法下文字的控制与清除。
compositionstart(e, index) {
console.log("compositionstart", e);
},
compositionend(e, index) {
console.log("compositionend", e);
this.shouldRemoveText = e.data;
},
changeIp(e, index) {
if (this.shouldRemoveText) {
const { value } = e.currentTarget;
const iindex = value.indexOf(this.shouldRemoveText);
if (iindex >= 0) {
this.ip[index].value = value.replace(
new RegExp(this.shouldRemoveText, "g"),
""
);
this.shouldLockKeyupEvent = false;
this.shouldRemoveText = "";
} else {
console.error(`we didn't match the text[${this.shouldRemoveText}] in ${index} value😅`);
}
}
},
【ps:删除输入的字符后,还会导致,鼠标光标不在原来的位置,我这边也做了处理,主要是记录之前的位置,在删除之后重新调整光标位置。这边的效果如下所示:】
5.其它
经过上述的代码,我们已经实现了,一个ip输入框的基本功能(version0.0.1版本)了。具体的代码可以在Volta0719/ip-input: Imitation of Microsoft IP input box此仓库查看。此外,我将此组件上传到了npm上【fan-ip-input - npm (npmjs.com)】,也可以直接通过npm install fan-ip-input
安装使用。当然,你也可以[在线体验此组件地址](fan-ip-input (volta0719.github.io))。
6.最终效果
总结
这个就是仿微软的ip输入框文章的全部内容,本以为是不太难的【错误的估计了实现的难度,调研时候脑子没转过弯来】,但现实是实现起来,碰到许许多多的状况。这些状况可能超出自己的知识储备,但通过不断的摸索学习,最终解决,这个对我而言的成就感是非常大的,并且确实学到了许多,比如compositionstart
的事件、Github Pages
的使用、npm组件的打包上传等。
如果有更好的一些实现方法或者组件的使用bug,欢迎提issue讨论,谢谢~