群聊可@输入框

简介:群聊@他人的输入框实现。使用h5新增的contenteditable属性,使标签拥有可供输入的能力。对输入框的光标位置获取使用window.getSelection()实现,可查看文档。@弹框支持键盘上下按键对列表进行滚动选择,支持输入文字对列表进行数据筛选。按下enter确认选中后,依赖canvas将选择的名字进行绘画,生成一张base64的图片以供预览。
注:键盘上下操作滚动列表可参考另一篇文章,键盘上下选择列表项
请添加图片描述

html

<!-- 编辑区域 -->
  <div class="container">
    <div class="textBox" contenteditable></div>
    <div class="textBoxAssist"></div>
  </div>
  <!-- @弹框 -->
  <ul class="popUpBox">
    <li id="0" class="highlight">张无忌</li>
    <li id="1">张三</li>
    <li id="2">李四</li>
    <li id="3">王五</li>
    <li id="4">赵六</li>
    <li id="5">张三丰</li>
    <li id="6">老王</li>
    <li id="7">李小小</li>
    <li id="8">李小二</li>
    <li id="9">李小三</li>
  </ul>
  <div id="shade"></div>
  <canvas id="canvas"></canvas>

css

<style>
  * {
    margin: 0;
    padding: 0;
  }
  html, body {
    /* height: ; */
    font-size: 16px;
    font-family: arial;
  }
  /* 编辑区域 */
  .container {
    position: relative;
    height: 400px;
    /* background-color: red; */
  }
  .container .textBox, .container .textBoxAssist {
    width: 500px;
    height: 300px;
    background-color: skyblue;
    outline: none;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
    z-index: 1;
  }
  .container .textBox img, .container .textBoxAssist img {
    vertical-align: bottom;
  }
  .container .textBoxAssist {
    z-index: 0;
    word-break: break-all;
    /* margin-left: 100px; */
  }
  /* 弹框区域 */
  .popUpBox {
    width: 100px;
    height: 100px;
    overflow: auto;
    border: 1px solid #000;
    background-color: #fff;
    position: absolute;
    top: 0;
    left: 0;
    z-index: 9999;
    visibility: hidden;
  }
  .popUpBox .highlight {
    background-color: #ccc;
  }
  .popUpBox li {
    list-style: none;
    line-height: 25px;
  }
  .popUpBox li:hover {
    background-color: #ccc;
  }
  #shade {
    width: 100%;
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    background-color: #ccc;
    opacity: 0;
    z-index: 9998;
    display: none;
  }
  #canvas {
    border: 1px solid #ccc;
  }
</style>

js
注:art-template.js用于生成html的结构,可在官网进行代码导出,复制到自己文件夹下面

<script src="./art-template.js"></script>
  <script id="temp" type="text/html">
    {{each}}
      <li id="{{$index}}" class="{{$index===0 ? 'highlight': ''}}">{{$value}}</li>
    {{/each}}
  </script>
  <script>
    // 编辑dom
    let textBox = document.querySelector('.textBox')
    let textBoxSel = {} // 编辑dom中的选区对象
    let textBoxRange = {} // 输入@时的选区对象
    let newRange = {} // @后其他文本的选区对象
    // 获取光标位置的辅助dom
    let textBoxAssist = document.querySelector('.textBoxAssist')
    // 弹框domx
    let popUpBox = document.querySelector('.popUpBox')
    let liArr = ['张三', '李四', '王五', '赵六', '张三丰', '老王', '李小小', '李小二', '李小三']
    let filterArr = []
    let popUpBoxLis = [];
    let highlightIndex = 0  // 高亮索引
    let isVisity = false
    let searchStr = ''
    // 遮罩
    let shade = document.getElementById('shade')
    // 输入文字
    textBox.onkeydown = function(e) {
      // console.log(e)
      // 13: enter
      if(isVisity && e.keyCode === 13) {
        e.preventDefault()
        createImg()
      } else if(e.shiftKey && e.code === 'Digit2') {
        // Digit2: @按键
        // 需要一个定时器延时一下,等待输入框值文本变化了才执行
        setTimeout(() => {
          initPopUpBox()
        }, 100);
      } else if(isVisity && e.keyCode === 38) {
        // 38: 键盘上
        e.preventDefault()
        if (highlightIndex > 0) {
          highlightIndex--;
          // 当前列的位置 < 滚动出去的距离,说明当前列在可视区域上面,上滚动
          if(popUpBoxLis[highlightIndex].offsetTop < popUpBox.scrollTop) {
              popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop
          } else if(popUpBoxLis[highlightIndex].offsetTop > popUpBox.scrollTop + 75) {
            popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop - 75
          }
        }
        // 重新渲染@弹框背景色
        updateLisBgc()
      } else if(isVisity && e.keyCode === 40) {
        // 40: 键盘下
        e.preventDefault()
        if (highlightIndex < popUpBoxLis.length - 1) {
          highlightIndex++;
          // 当前列的位置 > 上滚动出去的距离 + 可视区域的大小,表示当前列在可视区域下面,下滚动
          if(popUpBoxLis[highlightIndex].offsetTop > popUpBox.scrollTop + 75) {
              popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop - 75
          } else if(popUpBoxLis[highlightIndex].offsetTop < popUpBox.scrollTop) {
              popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop
          }
        }
        // 重新渲染@弹框背景色
        updateLisBgc()
      } else if(isVisity && [37, 39].includes(e.keyCode)) {
        // 键盘左右
        // 1. 隐藏弹框
        popUpBox.style.cssText += `visibility: hidden`
        isVisity = false
        // 2. 遮罩
        isShowShade()
      } else if(isVisity) {
        // 弹框出现,开始获取查询字符串
        setTimeout(() => {
          newRange = window.getSelection().getRangeAt(0)
          if(textBoxRange.startContainer.data?.includes('@')) {
            // 得到@后面输入的文本
            searchStr = textBoxRange.startContainer.data?.slice(textBoxRange.startOffset, newRange.startOffset)
            getSelectPosition()
            // console.log(searchStr)
            filterLiTag()
            if(filterArr.length === 0) {
              popUpBox.style.cssText += `visibility: hidden`
            } else {
              popUpBox.style.cssText += `visibility: block`
            }
          } else {
            // 1. 隐藏弹框
            popUpBox.style.cssText += `visibility: hidden`
            isVisity = false
            // 2. 遮罩
            isShowShade()
          }
        }, 100);
      }
    }
    // 过滤li标签
    function filterLiTag() {
      filterArr = liArr.filter(item => {
        return item.includes(searchStr)
      })
      let resHtml = template('temp', filterArr)
      // console.log(resHtml)
      popUpBox.innerHTML = resHtml
      popUpBoxLis = document.querySelectorAll(".popUpBox li")
      highlightIndex = 0
    }
    // 手动选择@弹框中的项
    popUpBox.onclick = function(e) {
      highlightIndex = e.target.id
      createImg()
    }
    // 初始化弹框
    function initPopUpBox() {
      // 1. Selection对象用于获取用户文本选区(选区被压缩至一点时,也就是光标位置)
      textBoxSel = window.getSelection()
      // 2. Range: 当前选区对象, 部分浏览器按住ctrl可以选择不连续的文本区域
      textBoxRange = textBoxSel.getRangeAt(0)
      newRange = textBoxSel.getRangeAt(0)
      // 3. 调整@弹框在光标的位置显示
      getSelectPosition()
      // 4. 显示@弹框
      isVisity = true
      highlightIndex = 0
      popUpBox.scrollTop = 0
      searchStr = ''
      // 5. 渲染li标签
      filterLiTag()
      // 6. li标签背景色
      updateLisBgc()
      // 7. 遮罩
      isShowShade()
    }
    // 调整@弹框在光标的位置显示
    function getSelectPosition() {
      let textBoxRange = window.getSelection().getRangeAt(0)
      // console.log(textBoxRange)
      // 1. 选中当前光标前面的所有内容
      textBoxRange.setStart(textBox, 0)
      // 2. 获取选中的节点的文档片段
      let cloneContents = textBoxRange.cloneContents()
      // 3. 片段中增加辅助的span标签
      let span = document.createElement('span')
      span.className = 'spanAssist'
      // console.log(cloneContents.lastElementChild)
      if(cloneContents.lastElementChild && cloneContents.lastElementChild.tagName !== 'IMG') {
        cloneContents.lastElementChild.appendChild(span)
      }else {
        cloneContents.appendChild(span)
      }
      // 4. 片段添加到辅助dom中
      textBoxAssist.innerHTML = ''
      textBoxAssist.appendChild(cloneContents)
      // 5. 将第一步选中的选区恢复
      // false: 选区折叠到结束位置
      textBoxRange.collapse(false)

      // 6. 获取辅助dom中的辅助span标签在浏览器中的位置
      let spanAssist = textBoxAssist.querySelector('.spanAssist')
      let { left: screenLeft, top: screenTop } = spanAssist.getBoundingClientRect()
      popUpBox.style.cssText += `;left: ${screenLeft}px; top: ${screenTop > popUpBox.offsetHeight ? screenTop - popUpBox.offsetHeight : screenTop + spanAssist.offsetHeight}px; visibility: visible`
      
    }
    // 更新li标签的背景颜色
    function updateLisBgc() {
      popUpBoxLis.forEach((item, index) => {
        if (index === highlightIndex) {
          popUpBoxLis[index].className = "highlight"
        } else {
          popUpBoxLis[index].className = ""
        }
      })
    }
    // 创建图片标签
    function createImg() {
      let name = popUpBoxLis[highlightIndex].innerText
      let { url, width, height } = createNameImg('@' + name)
      let img = document.createElement('img')
      img.src = url
      img.style.width = width + 'px'
      img.style.height = height + 'px'
      // let span = document.createElement('span')
      // span.appendChild(img)
      // 插入图片
      myInnerHtml(img)
    }
    // 插入图片
    function myInnerHtml(html) {
      // console.log(newRange)
      // 1. 将当前光标所在位置的前一个内容进行拖蓝,也就是选中输入的"@"
      newRange.setStart(newRange.startContainer, newRange.startOffset - searchStr.length - 1);
      // 2. 删除选中的"@"符号 + 文本
      newRange.deleteContents();
      // 3. 插入新的元素节点
      newRange.insertNode(html);
      // 4. 将光标移动到新节点的后面,手动点击li标签会导致Range选区失去焦点,所以该对象的属性无效
      // textBoxRange.collapse(false)
      // 4. 重新设置光标位置
      // textBoxSel.collapse(html, 1);
      textBoxSel.collapse(newRange.endContainer, newRange.endOffset);

      // 5. 隐藏弹框
      popUpBox.style.cssText += `visibility: hidden`
      isVisity = false

      // 6. 遮罩
      isShowShade()
    }
    // 遮罩
    shade.onclick = function() {
      popUpBox.style.cssText += `visibility: hidden`
      isVisity = false
      isShowShade()
    }
    function isShowShade() {
      shade.style.display = isVisity ? 'block' : 'none'
    }

    // 依赖canvas将名字转为图片
    let canvas = document.getElementById('canvas')
    let ctx = canvas.getContext('2d')
    let fontSize = Number.parseInt(window.getComputedStyle(document.documentElement).fontSize)
    function createNameImg(name) {
      // createNameImg('张三张三张三')
      let width = name.length * fontSize + 10
      let height = fontSize + 5
      canvas.width = width
      canvas.height = height
      ctx.font = `${fontSize}px arial`
      ctx.fillText(name, 5, fontSize) // 参数: text, x, y
      return {
        url: canvas.toDataURL(), // base64的图片格式
        width,
        height: height
      }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值