Web、H5标签输入框

217 阅读1分钟

最近项目有个标签输入框的需求,大概和微信图片提取文字那样:

bd884724564273ecbd8ea850185d7cc.jpg

就是输入一行文字,按回车、失去焦点或者输入某个符号作为断句,形成一个标签。在任何标签后面获取焦点删除、编辑,实际这样子:

1680766630040.jpg

image.png

需求明确,开干!

标签输入框第一个想到就是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()
    }
  }
},