仿微软的ip输入框

2,093 阅读4分钟

前言

最近在单位的一次常规需求当中,原型图上有一个ip的输入框,用于控制可访问资源的ip地址。需求没有给出很具体的ip输入框的具体的功能,只要求能用就可以了。由于没有对此功能的具体的描述,于是我参考了微软的IP输入框作为我实现的模板。【本文是基于vue2.x、 antd vue 1.7.8 版本】。

需求分析

ip-input.gif

上图是微软的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:虽然需求分析出来了,但是脑子里当时想的都仅仅是按下按键聚焦到下一个输入框的功能,就觉得应该都挺简单的】,等到具体代码实现的时候,我就突然懵了,我该怎么判断鼠标位置

question.jpg

我最开始的想法是通过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输入框,我们可以看到,当全部有值的时候,鼠标光标是依次跳动的(这边的关键点在于:当鼠标光标第一次到达末尾的时候,应当做停留,再次按下右方向键才会跳转到下一个输入框),而上面的代码中,并不能判断是否是第一次到达末尾,所以出现的问题就是当到达值的末尾时,就会直接跳转到下一个输入框中去,如下图所示【下图中我只按了两下右箭头→】:

ip-input-bug2.gif

所以为了解决这个问题,我为每个输入框引入了一个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();
     }
   },

看起来,已经实现了,对吗?

6fa7b65f66eab003aecfb454aec562b.jpg

那么为啥呢?

原来在实际测试的过程中,中文输入法并不会被阻止!也就是说,在中文输入法的情况下,我还是可以输入q等之类的按键。此时的我,心态发生了亿乃乃的变化。我很想忽略掉这个问题,并且把使用体验的问题甩给用户,简单的在ip输入框的右边加个提示符请使用英文输入法输入IP,这不是Bug,这是你不会用。但,这合理吗?这是一个前端工程师该有的作为吗?

ip-input-bug.gif 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 事件不一定会对元素值的每次更改触发。

所以我们只需要绑定compositionstartcompositionend的事件,每次都记录中文输入法输入的文字,最后再通过正则去掉因为中文输入法索输入的文字,就可以做到中文输入法下文字的控制与清除。

   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:删除输入的字符后,还会导致,鼠标光标不在原来的位置,我这边也做了处理,主要是记录之前的位置,在删除之后重新调整光标位置。这边的效果如下所示:】 ip-input-remove.gif

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-input-result.gif

总结

这个就是仿微软的ip输入框文章的全部内容,本以为是不太难的【错误的估计了实现的难度,调研时候脑子没转过弯来】,但现实是实现起来,碰到许许多多的状况。这些状况可能超出自己的知识储备,但通过不断的摸索学习,最终解决,这个对我而言的成就感是非常大的,并且确实学到了许多,比如compositionstart的事件、Github Pages的使用、npm组件的打包上传等。

如果有更好的一些实现方法或者组件的使用bug,欢迎提issue讨论,谢谢~


一些链接