最近项目有个标签输入框的需求,大概和微信图片提取文字那样:
就是输入一行文字,按回车、失去焦点或者输入某个符号作为断句,形成一个标签。在任何标签后面获取焦点删除、编辑,实际这样子:
需求明确,开干!
标签输入框第一个想到就是contenteditable这个属性,这个属性可以轻松的使标签变成可编辑状态,只需要在html标签上加上:
<div contenteditable="true" class="input"></div>
但是还有一个问题,contenteditable="true"复制粘贴的时候不能过滤html标签;
通过百度,发现了 user-modify 这个css属性。read-write 和 read-write-plaintext-only 会让元素表现得像个文本域一样,可以focus以及输入内容。 区别是前者可以输入富文本,后者只可以输入纯文本,改用 user-modify: read-write-plaintext-only就可以让html标签轻松实现纯文本输入,不考虑兼容性问题哈,如果要考虑兼容性问题,可以直接用input标签。
页面布局
<div class="inp-tag-con" ref="tagConRef" @click.stop="tagConClick()">
<div class="item-tag" :class="{'item-last': index === tags.length - 1}" v-for="(item, index) in tags" :key="index" @click.stop="tagClick($event, index)">
<div class="tag" :style="{backgroundColor: item.color}">{{item.label}}</div>
<div class="tag-inp" @keyup="tagInpKeyUp($event, index)" @blur="tagInpBlur($event, index)"></div>
</div>
<div class="tag-inp" placeholder="请输入" ref="tagInpRef" v-show="!tags.length" @keyup="tagInpKeyUp($event, 0)" @blur="tagInpBlur($event, 0)"></div>
</div>
样式
.inp-tag-con {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 6px;
width: 100%;
padding: 8px 12px;
background-color: hsla(0, 0%, 100%, 0.6);
border: 1px solid rgba(44, 43, 71, 0.15);
border-radius: 4px;
font-size: 16px;
line-height: 24px;
color: #333;
overflow: auto;
cursor: text;
&:active {
outline: auto var(--main-color);
}
.item-tag {
display: flex;
align-items: flex-start;
gap: 4px;
position: relative;
z-index: 1;
&.item-last {
flex: auto;
}
}
.tag-inp {
flex: auto;
border: 0;
outline: 0;
padding: 4px 0;
line-height: 24px;
font-size: 16px;
-webkit-user-modify: read-write-plaintext-only;
min-width: 1px;
&:only-child:empty::before {
content: attr(placeholder);
color: #828282;
}
}
.tag {
display: flex;
align-items: center;
padding: 4px 8px;
font-size: 16px;
line-height: 24px;
background: var(--minor-color);
color: #fff;
cursor: default;
border-radius: 2px;
min-height: 32px;
}
}
JS
我这里是用vue实现的
tagConClick() {
if (!this.tags.length) {
this.$refs['tagInpRef'].focus(); //点击任意地方输入框都需要聚焦
} else {
const itemTags = this.$refs['tagConRef'].querySelectorAll('.item-tag')
const len = itemTags.length - 1
const lastItem = itemTags[len]
if (lastItem) {
lastItem.querySelector('.tag-inp').focus()
}
}
},
tagClick(e, index) {
console.log(e, index)
let target = e.target
const withClass = 'item-tag'
let currentTag = target
if (target.classList.contains('tag') && target.nextElementSibling) {
target.nextElementSibling.focus()
return
}
if (target.classList.contains('tag-inp')) {
target.focus()
return
}
if (target && !target.classList.contains(withClass) && currentTag.parentNode && currentTag.parentNode.classList.contains(withClass)) {
currentTag = currentTag.parentNode
}
if (currentTag) {
const tagInp = currentTag.querySelector('.tag-inp')
if (tagInp) {
tagInp.focus()
}
}
},
/***
* ev 当前监听
* that dom元素
* tagIndex 当前数组游标
* listenerType 监听类型 keyup和blur
*/
tagCreate(ev, that, tagIndex, listenerType) {
ev.preventDefault()
// 随机背景颜色
const colors = ['#42a842', '#7f71de', '#DDA8FF', '#FF808B', '#36CFD1']
const index = Math.floor(Math.random() * 5)
const txt = that.innerText.replace(/[\r\n]|(^\s*)|(\s*$)/g, "")
if (/^[\s!!??::,,。.;;]/.test(txt)) {
that.innerText = '';
return
}
if (txt) { // 输入框内容通过 innerText 获取
let txtArr = txt.split(/[!!??::,,。.;;\r\n]/).filter(word => word !== '') || []
const tagArr = []
txtArr.forEach(titem => {
tagArr.push({
label: titem,
color: colors[index]
})
})
that.innerText = '';
this.tags.splice(tagIndex + 1, 0, ...tagArr)
this.$nextTick(() => {
const next = this.tags[tagIndex + 1]
const itemTags = this.$refs['tagConRef'].querySelectorAll('.item-tag')
if (next && itemTags[tagIndex + 1] && listenerType !== 'blur') {
itemTags[tagIndex + 1].querySelector('.tag-inp').focus()
} else if (itemTags[tagIndex] && listenerType !== 'blur') {
itemTags[tagIndex].querySelector('.tag-inp').focus()
}
})
} else {
that.innerText = '';
}
},
tagInpKeyUp(ev, index) {
const that = ev.target
const signReg = /[!!??::,,。.;;]/g
const innerText = that.innerText
if (ev.key === 'Enter' || (innerText && signReg.test(innerText))) {
this.tagCreate(ev, that, index, 'keyup')
}
if (ev.key === 'Backspace' && !innerText) {
this.tags.splice(index, 1)
this.tagInpFocus(index)
}
console.log('keyup')
},
tagInpBlur(ev, index) {
console.log('blur')
const that = ev.target
this.tagCreate(ev, that, index, 'blur')
},
tagInpFocus(tagIndex) {
console.log('focus')
if (!this.tags.length) {
this.$nextTick(() => {
this.$refs['tagInpRef'].focus()
})
} else {
const prev = this.tags[tagIndex - 1]
const itemTags = this.$refs['tagConRef'].querySelectorAll('.item-tag')
if (prev && itemTags[tagIndex - 1]) {
itemTags[tagIndex - 1].querySelector('.tag-inp').focus()
} else if (itemTags[tagIndex]) {
itemTags[tagIndex].querySelector('.tag-inp').focus()
}
}
},