基于textarea开发艾特组件核心思路分享

2,167 阅读3分钟

组件效果图片展示

image.png

image.png

2023年02月02日11:26:05 效果图补充:👇 更新了被@的呈现方式. 仅以文字颜色做区别. 思路也仅在css上做手脚, 不细说

image.png

1. 监听输入, 判定输入的字符
    const el = textareaElement
    // 渲染层文字, 需要和文本层文字保存同步
    let renderLayerText = textareaElement.value
    const text = el.value.slice(el.selectionStart, el.selectionEnd)
    // 获取@符号的输入位置
    const atIndex = text.lastIndexOf(at)
    // @符号之后文字
    const chunk = text.slice(atIndex + 1, text.length)
    // 假想用户
    const userList = ['张三', '李四']
    // 匹配到的用户
    const matchingUserist = userList.filter(item => {
        const name = item
        return name.toLowerCase().indexOf(chunk.toLowerCase()) > -1
    }
    
    if (matchingUserist.length > 0) {
      用 matchingUserist 打开候选人面板
    } else if (index > -1 && chunk.length === 0) {
      如果只输入了一个@符号, 则直接展示选人面板
    } else {
      关闭选人面板
    }
      
2. 选人面板定位
    // 首先来说, 肯定是希望光标输入结尾在什么地方我们的选人面板就是展示在什么地方
    const div = document.createElement('div')
    div.textContent = textareaElement.value.substring(0, atIndex)
    div.style.whiteSpace = 'pre-wrap' // `重点`
    div.style.position = 'absolute'
    div.style.overflow = 'hidden'
    // 这样我们就能得到和 textareaElement输入内容一致的内容层
    ...
    
    // 这个时候获取光标结束的位置
    const span = document.createElement('span')
    span.textContent = textareaElement.value.substring(atIndex) || '.' // 设置光标之后的内容 或者 内容为一个点
    div.appendChild(span)
    
    // IE9 以下使用的是 Element.currentStyle
    const style = window.getComputedStyle(textareaElement)
    
    // 获取上/左的边距就成功了, 就可以用来去定位面板位置了
    const top = span.offsetTop + parseInt(style['borderTopWidth']),
    const left = span.offsetLeft + parseInt(style['borderLeftWidth']),    
3. 所选人员姓名插入
    // 要插入张三到 textarea 文字内
    const userName = '张三' +  空格 // 名称后面补一个空格后续使用
    
    const start = textareaElement.selectionStart
    const end = textareaElement.selectionEnd
    
    // 有人来插队了.. 两边的让一让 [○・`Д´・ ○]
    textareaElement.value = ta.value.slice(0, start) + text + ta.value.slice(end)
    
    // 让开始&结尾光标回到他们应该待的新位置
    const newEnd = start + text.length
    
    textareaElement.selectionStart = newEnd
    textareaElement.selectionEnd = newEnd    
4. 根据渲染层文字划分段输出渲染,划分成数组渲染,没有变动过的内容, 视图就不会刷新啦
      // 渲染层文字
      const text = renderLayerText
      const textContentSegmentationList = []
      let tempContent = ''
      const addToArray = () => {
        tempArray.push(tempContent)
        tempContent = ''
      }

      for (let index = 0, len = text.length; index < len; index++) {
        const char = text[index]
        
        // 遇到单个字符为艾特符号或者空格的时候划分
        if (char === @ && tempContent) {
          addToArray()
        }
        if (char === 空格 && tempContent) {
          addToArray()
        }

        tempContent += char
      }

      tempContent && addToArray()

      return textContentSegmentationList
      
      // 例举 @张三 你叫上@李四 我们中午一起干饭🍚
      // 划分成: ['@张三', ' 你叫上', '@李四', ' 我们中午一起干饭🍚']
5. Vue 文本层 & 渲染层
    <div>
        <textarea
        ref="textareaElement"
    />
    
    <div
      ref="highlighterElement"
      class="highlighter-box"
    >
      <span
        v-for="(item, index) in textContentSegmentationList"
        :key="index"
        :class="[{ 'highlight': isName(item) }]"
      >
      <!-- 如果当前 item 👆🏻 是当前用户列表里面的人员姓名咱们就给它打上样式标记 -->
      <!-- 需要处理用户姓名的重复问题, 思路是人员姓名重复的时候加上编号, 就不给出代码了 -->
      <!-- _(:з」∠)_ -->
        {{ item }}
      </span>
      <!-- 解决光标单独换行的时候无法撑够高度的问题 -->
      <br />      
    </div>        
    </div>
    div {
        position: relative;
    }

    textarea {
        z-index: 1;  
        position: relative;
        background: transparent;
        word-break: unset !important;
        width: 100%;
        // 文本层文字颜色透明
        color: transparent;
        // 给光标搞个颜色
        caret-color: #535151;
    }
    
    // 定位到文本层下方
    .highlighter-box {
        position: absolute;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        // 渲染层藏在文本层下面
        z-index: 0;
        white-space: pre-wrap !important;
        border-color: transparent !important;
        color: transparent;
        overflow-y: auto;
        overflow-x: hidden;

        span {
          word-wrap: break-word;
          // 显示颜色
          color: #606266;
        }
    }
    
    // 刚刚被咱们打上标记📌的人员姓名颜色给个单独的背景色, 对没错就是上面👆的张三
    .highlight {
        border-radius: 2px;
        color: #fff;
        background: #e6f7ff;
    }    
6. 处理文本层和背景层同步滚动
    // 简写文本层滚动同步给渲染层 _(:з」∠)_
    textareaElement.scrollTop = highlighterElement.scrollTop
7. 监听退格的时候,删除联动人的姓名
    const el = textareaElement
    // 假设 text 现在等于 '@张三 ' 👈🏻有个空格
    const text = el.value.slice(0, el.selectionEnd)
    // 获取@符号位置和后续文字
    const atIndex = text.lastIndexOf(at)
    const chunk = text.slice(atIndex + 1, text.length)
    
    if (atIndex > -1) {
        const has = userList.some(item => {
            const name = item
            return chunk === name + 空格
        })
        
        if (has) {
          // el.value = ''
          el.value = el.value.slice(0, atIndex) + el.value.slice(el.selectionEnd)
          el.selectionStart = index + 1
          el.selectionEnd = index + 1
          // 这个地方再回到步骤 1. 
        }        
    }
8. 处理中文输入法输的问题
    // 因为输入中文的时候,候选项里面的字是未能实时同步给渲染层的文字, 所以需要将输入法候选项文字单独处理给渲染层文字
    const handleCompositionupdate = (event: CompositionEvent) => {
        const { selectionStart, selectionEnd } = textareaElement
        const beforeString = renderLayerText.substr(0, selectionStart)
        const afterString = renderLayerText.substr(selectionEnd)

        renderLayerText = `${beforeString}${event.data}${afterString}`
    }    
9. 获取本次艾特的人员信息
    // 获取渲染层下的所有后代
    const highlightList = Array.from(highlighterElement.children)
    // 转换层 输入的内容 + @uid 落库
    const textContentAndIdString = highlightList.map(item => {
        const content = spanElement.innerHTML
        
        // 如果是艾特的用户
        if (item.className === 'highlight') {
            const name = content.substr(1)
            // '@张三 ' '@李四 '
            // 这个时候就可以通过唯一的姓名去映射别的用户信息了, 例如uid之类的
            return uid
        }

        return content
    })
    // 返显的时候就将uid换成真实的姓名就可以啦

10. 结尾

    本次分享只介绍了组件中的核心思路, 代码仅讲解, 非真实代码, 希望本次分享的东西对你能有所帮助

image.png