基于elementui input完成的输入控件

背景

基于业务需求,产品提出输入交互,element-ui本身不能支持。完整交互如下视频。

IMG

大致描述:输入框可输入多条数据,超过一条,则以数量的 +n的形式显示,用户可以在输入框中输入,也可以点击右边的编辑小手,在展开的textarea中输入数据。数据以回车,中文逗号,英文逗号分隔(后续传给服务端时需要组成数组的形式。)

开发基本思路:

由于我们项目系统使用的elementui框架,所以为了保持风格统一,使用el-input作为载体,上面的输入框使用默认 type='text',下方的输入框使用 type='textarea'。

其中缩写的小方块用div元素包装,通过绝对定位 使其置于input输入框之上。

下面是html结构

<div class="input-wrap" @click.stop>
   <el-input
      size="mini"
      id="tag-input"
      v-model="inputVal"
      :class="customClass"
      class="y-w-160"
      :placeholder="placeholder">
    </el-input>
    <i class="el-icon-edit" @click="showTextArea = !showTextArea"></i>
    <div class="tagcut" v-if="contentArr.length > 0">
      <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light f-tag" @click="showTextArea = true">
        <span class="el-select__tags-text">{{ contentArr[0] }}</span>
        <i class="y-bk-C0C4CC el-tag__close el-icon-close" @click="delContent"></i>
      </span>
      <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light" @click="showTextArea = true" v-if="contentArr.length > 1">
        <span class="el-select__tags-text">+ {{ contentArr.length - 1 }}</span>
      </span>
    </div>
    <transition name="text-area">
      <div class="textareabox" v-if="showTextArea">
        <el-input
          placeholder="请输入单号、最多20个,请用,或者换行隔开"
          class="t-setting"
          :rows="8"
          size="mini"
          resize="none"
          v-model="localText"
          type="textarea">
        </el-input>
      </div>
    </transition>
  </div>

有几个关键点这里提一下:

1. 当输入数据时,输入框中会生成tag(div包裹的内容),覆盖在input框之上,这个时候需要处理一下input框的光标位置,即padding-left的值,以免被tag框遮挡。

处理方式:监听textarea中的内容变化,当tag框有值时,获取tag框的宽度,动态设置input框的padding-left。代码如下

watch: {
    localText(val) {
      // 处理输入的单号
      this.contentArr = val.replace(/\n/g, ',').replace(/,/g, ',').split(',').filter(item => item.trim());
      if(this.contentArr.length > this.maxLength) {
        this.$message.error('输入不能超过' + this.maxLength + '条');
        // this.contentArr中有重复数据
        let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据的位置
        let maxLengthArr = this.contentArr.slice(0, this.maxLength);
        maxLengthArr.forEach((item, index) => {
          if(item === maxLengthArr[this.maxLength - 1]) {
            idx++;
          }
        })
        // 查找字符串中字符的位置做截取
        let indexOfNum = getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx - 1);
        this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
      }
      this.$nextTick(() => {
        let tagcutref = document.getElementsByClassName('tagcut');
        if(tagcutref[0]) {
          document.getElementById('tag-input').style.paddingLeft = tagcutref[0].offsetWidth + 2 + 'px';
        } else {
          document.getElementById('tag-input').style.paddingLeft = '10px';
        }
      })
      this.$emit('change', this.localText);
    }
}

2. 该输入框中的内容通过v-model双向绑定。关键代码

export default {
  props: {
      textareaContent: {
        type: String,
        required: true
      },
  },
  model: {
    prop: 'textareaContent',
    event: 'change', // 通过$emti('change'),修改textareaContent的值
  },
  watch: {
    inputVal(val) {
      // 监听 , ,
      if(val.indexOf(',') !== -1 || val.indexOf(',') !== -1) {
        let inputValArr = val.replace(/,/g, ',').split(',').filter(item => item.trim());
        inputValArr.forEach(item => {
          if(this.localText) {
            // 如果有值
            this.localText = this.localText + '\n' + item;
          } else {
            // 第一条数据不需要换行
            this.localText = this.localText + item;
          }
          this.$emit('change', this.localText); // 双向绑定输入框中的内容
        })
        this.inputVal = ''; // 输入框数据处理之后 置空
      }
    },
  }
}

3. 在input框通过粘贴输入内容,复制的内容如果有换行符(\n),则会被转换成空格,不符合我们规定的数据输入格式(换行,中文逗号,英文逗号)。所以这里的处理方式是将input框的粘贴功能禁掉,然后监听粘贴的内容,对粘贴的内容进行处理,直接转换成对应tag框的形式展示在input框中。在textarea框中,则展示粘贴内容的原文本。

<!-- @paste.native.capture.prevent禁用粘贴功能 -->
    <el-input
      size="mini"
      @blur="handleEnter"
      id="tag-input"
      v-model="inputVal"
      @paste.native.capture.prevent="handlePaste"
      :class="customClass"
      class="y-w-160"
      :placeholder="placeholder">
    </el-input>

       处理粘贴内容的时候有个比较麻烦的问题:如果粘贴的内容超过输入限制(这里默认是20条),则需要截取,截取之后需要保证复制的文本的格式(可能有中文逗号,英文逗号,换行符)。截取方法:找到第20条数据的下标位置。注意数据可能会有重复。

// contentArr是数据格式化之后的数组,maxLength是最大可输入的数据长度,默认20。
if(this.contentArr.length > this.maxLength) {
        this.$message.error('输入不能超过' + this.maxLength + '条');
        // this.contentArr中可能有重复数据
        let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据在this.contentArr数组中是第几条数据
        let maxLengthArr = this.contentArr.slice(0, this.maxLength);
        maxLengthArr.forEach((item, index) => {
          if(item === maxLengthArr[this.maxLength - 1]) {
            idx++; // 如果有重复,标记是第几条重复数据,便于查找下标。
          }
        })
        // 查找字符串中字符的位置做截取
        let indexOfNum = getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx);
        this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
      }


function getIndexofNum(str, cha, num) {
  let strn = ' ' + str + ' ';
  let x = -1;
  let i = 0;
  while(i < num) {
    x = strn.indexOf(cha, x + 1);
    i++;
  }
  return x;
}

下面是完整代码:

<template>
  <div class="input-wrap" @click.stop>
    <el-input
      size="mini"
      @blur="handleEnter"
      id="tag-input"
      @keyup.enter.native="handleEnter"
      v-model="inputVal"
      @paste.native.capture.prevent="handlePaste"
      :class="customClass"
      class="y-w-160"
      :placeholder="placeholder">
    </el-input>
    <i class="el-icon-edit" @click="showTextArea = !showTextArea"></i>
    <div class="tagcut" v-if="contentArr.length > 0">
      <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light f-tag" @click="showTextArea = true">
        <span class="el-select__tags-text">{{ contentArr[0] }}</span>
        <i class="y-bk-C0C4CC el-tag__close el-icon-close" @click="delContent"></i>
      </span>
      <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light" @click="showTextArea = true" v-if="contentArr.length > 1">
        <span class="el-select__tags-text">+ {{ contentArr.length - 1 }}</span>
      </span>
    </div>
    <transition name="text-area">
      <div class="textareabox" v-if="showTextArea">
        <el-input
          placeholder="请输入单号、最多20个,请用,或者换行隔开"
          class="t-setting"
          :rows="8"
          size="mini"
          resize="none"
          v-model="localText"
          type="textarea">
        </el-input>
      </div>
    </transition>
  </div>
</template>
<script>
export default {
  props: {
    placeholder: {
      type: String,
      default: '运单号'
    },
    textareaContent: {
      type: String,
      required: true
    },
    // 最大可输入的单号数量
    maxLength: {
      type: Number,
      default: 20
    },
    customClass: {
      type: String
    }
  },
  model: {
    prop: 'textareaContent',
    event: 'change'
  },
  data() {
    return {
      contentArr: [],
      tagdetail: '',
      inputVal: '',
      showTextArea: false,
      localText: this.textareaContent
    }
  },
  methods: {
    getIndexofNum(str, cha, num) {
      let strn = ' ' + str + ' ';
      let x = -1;
      let i = 0;
      while(i < num) {
        x = strn.indexOf(cha, x + 1);
        i++;
      }
      return x;
    },
    delContent() {
      this.localText = this.textareaContent.slice(this.contentArr[0].length + 1);
      this.$emit('change', this.localText);
    },
    handleEnter() {
      // 输入框没有值 则不操作
      if(!this.inputVal.trim()) return;
      if(this.localText) {
        // 如果有值
        this.localText = this.localText + '\n' + this.inputVal;
      } else {
        // 第一条数据不需要换行
        this.localText = this.localText + this.inputVal;
      }
      this.$emit('change', this.localText);
      this.inputVal = '';
    },
    handlePaste(e) {
      if(e.clipboardData.getData('Text')) {
        if(this.localText) {
          // 如果有值
          this.localText = this.localText + '\n' + e.clipboardData.getData('Text');
        } else {
          // 第一条数据不需要换行
          this.localText = e.clipboardData.getData('Text');
        }
      }
    }
  },
  watch: {
    textareaContent(val) {
      // 这里设置,确保在父级页面重置textareaContent为空,localText的值可以响应
      this.localText = val;
    },
    localText(val) {
      // 处理输入的单号
      this.contentArr = val.replace(/\n/g, ',').replace(/,/g, ',').split(',').filter(item => item.trim());
      if(this.contentArr.length > this.maxLength) {
        this.$message.error('输入不能超过' + this.maxLength + '条');
        // this.contentArr中可能有重复数据
        let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据在this.contentArr数组中是第几条数据
        let maxLengthArr = this.contentArr.slice(0, this.maxLength);
        maxLengthArr.forEach((item, index) => {
          if(item === maxLengthArr[this.maxLength - 1]) {
            idx++; // 如果有重复,标记是第几条重复数据,便于查找下标。
          }
        })
        // 查找字符串中字符的位置做截取
        let indexOfNum = this.getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx);
        this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
      }
      this.$nextTick(() => {
        let tagcutref = document.getElementsByClassName('tagcut');
        if(tagcutref[0]) {
          document.getElementById('tag-input').style.paddingLeft = tagcutref[0].offsetWidth + 2 + 'px';
        } else {
          document.getElementById('tag-input').style.paddingLeft = '10px';
        }
      })
      this.$emit('change', this.localText);
    },
    inputVal(val) {
      // 监听 , ,
      if(val.indexOf(',') !== -1 || val.indexOf(',') !== -1) {
        let inputValArr = val.replace(/,/g, ',').split(',').filter(item => item.trim());
        inputValArr.forEach(item => {
          if(this.localText) {
            // 如果有值
            this.localText = this.localText + '\n' + item;
          } else {
            // 第一条数据不需要换行
            this.localText = this.localText + item;
          }
          this.$emit('change', this.localText);
        })
        this.inputVal = ''; // 输入框数据处理之后 置空
      }
    },
    showTextArea(val) {
      if(val) {
        document.body.addEventListener('click', () => {
          this.showTextArea = false;
        })
      } else {
        document.body.removeEventListener('click', () => {});
      }
    },
  }
}
</script>
<style lang="scss" scoped>
.input-wrap {
  display: inline-block;
  position: relative;
  .tagcut {
    position: absolute;
    left: 2px;
    bottom: 1px;
  }
  .textareabox {
    position: absolute;
    z-index: 1;
    .t-setting {
      width: 160px;
    }
    /deep/ .el-textarea__inner {
      border: 0;
      box-shadow: 0px 0px 5px 0px rgba(173,173,173,0.5);
      padding: 5px 10px;
      &::-webkit-scrollbar {
        width: 4px;
      }
      &::-webkit-scrollbar-thumb {
        border-radius: 2px;
        -webkit-box-shadow: inset 0 0 2px rgba(144,147,153,0.3);
        background-color:rgba(144,147,153,0.3);
      }
    }
  }
}
.text-area-enter-active{
  transition: opacity .5s;
}
.text-area-enter{
    opacity: 0;
}
.text-area-leave-active{
  transition: opacity .5s;
}
.text-area-leave-to{
  opacity: 0;
}
.el-icon-edit {
  position: absolute;
  right: 5px;
  color: #4c84ff;
  top: calc(50% - 7px);
  cursor: pointer;
}
.f-tag {
  .el-select__tags-text {
    max-width: 55px;
    display: inline-block;
    vertical-align: middle;
  }
}
.y-w-160 {
  width: 160px;
}
.y-c-p {
  cursor: pointer;
}
.y-bk-C0C4CC {
  background-color: #c0c4cc;
}
</style>

 README.md

# 运单号/订单号输入组件 -- 使用方法 -- 参数&事件说明
## @param v-model 双向绑定 输入内容
* 说明:输入框中的内容
* 是否必传: 是
* 值类型:String
## @param placeholder
* 说明:输入框的placeholder
* 是否必传:否
* 默认值:运单号
* 值类型:String
## @params customClass
* 说明:自定义的className
* 是否必传:否
* 默认值:'''
* 值类型:String
## @example
```html
<waybill-input
  customClass="y-w-300"
  v-model="waybillOrder"
  placeholder="订单号 " />

功能不复杂, 主要在处理粘贴数据大于数据限制需要截取的时候, 当时思考的还算比较多。记录一下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值