背景
基于业务需求,产品提出输入交互,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);
if((/\n|\,|\,|\s/).test(strn[x - 1]) && (/\n|\,|\,|\s/).test(strn[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);
if((/\n|\,|\,|\s/).test(strn[x - 1]) && (/\n|\,|\,|\s/).test(strn[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="订单号 " />
功能不复杂, 主要在处理粘贴数据大于数据限制需要截取的时候, 当时思考的还算比较多。记录一下。