资讯详情

vue3结合element-plus实现标签手动标注效果

先上效果图

在这里插入图片描述 功能描述:默认首选标签是第一个时间【读者可以根据代码修改默认标签】,可以添加标签。 点击不同的标签切换标签。在文本部分,可以根据输入的文本进行输入txt,或者内置的html标记文件,标记选定的单词或段落反映在背景颜色和文本节点上title’属性】。您可以再次单击标记的内容取消标记。最终标记的结果将以对象数组的形式保存,读者可以自行操作标记的内容。标记结果如下:

Proxy { 
        0: { 
        …}, 1: { 
        …}, 2: { 
        …}, 3: { 
        …}, 4: { 
        …}, 5: { 
        …}, 6: { 
        …}, 7: { 
        …}, 8: { 
        …}, 9: { 
        …}} [[Handler]]: Object [[Target]]: Array(10) 0: { 
        name: '时间', comment: ‘公铁立交’} 1: { 
        name: '人物', comment: '省道S30'} 2: { 
        name: '颜色', comment: 上部结构} 3:{
     
       name: '颜色', comment: '下部结构'}
4: { 
        name: ' 部位', comment: '中心'}
5: { 
        name: ' 部位', comment: '桩基础'}
6: { 
        name: ' 部位', comment: '层,设置异型钢'}
7: { 
        name: ' 部位', comment: '过各项无损'}
8: { 
        name: ' 部位', comment: '土双柱'}
9: { 
        name: ' 部位', comment: '土空心'}
length: 10
[[Prototype]]: Array(0)
[[IsRevoked]]: false

实现原理

:监听鼠标在文本上动作,根据时间间隔来区分是选中了文本?还是只是点击了一下?

    useMouse() { 
        
      let last = new Date().getTime();
        //松开鼠标后,获得时间
      function mousedown() { 
        
        last = new Date().getTime();
      }
      //根据时间间隔判断是选中还是点击事件
      function getMouseEvent() { 
        
        const d = new Date().getTime() - last;
        return d > 200 ? "select" : "click";
      }
      //暴露出的接口
      return { 
        
        mousedown,
        getMouseEvent,
      };
    },

通过对鼠标活动时间的判断,我们可以区分点击和选中这两个动作,显然我们对于点击事件不会做任何处理,需要对选中动作做出一系列操作。

监听既然有了,肯定是选择监听对象了。根据vue生命周期的不同钩子函数所处不同的vue实例状态前提,我们将在vue实例完成对data和methods属性初始化,vDOM挂载到真正得DOM树上后的mounted()钩子函数中将我们的监听挂载到某个DOM节点上。关于vue生命周期的相关内容这里不多讲,可以参考博客vue生命周期的理解

这里的DOM节点在本文中为正文下所对应的div标签并设置id为text,也是方便通过id获取DOM节点。

    var that = this;
    const ele = document.getElementById("text");
    const { 
         mousedown, getMouseEvent } = this.useMouse();
    /* 中间部分有省略,最终代码请向下看*/
     function mouseup() { 
        
      if (getMouseEvent() !== "select") { 
        
        return;
      }
      else{ 
        
      
      }
    ele.addEventListener("mousedown", mousedown);
    ele.addEventListener("mouseup", mouseup);

**再下一步:**我们需要对选中文本做一系列操作。首先应该是能够获得选中文本的信息。这里需要知道的是: Range 接口表示一个包含节点与文本节点的一部分的文档片段。可以用 Document 对象的 Document.createRange 方法创建 Range,也可以用 Selection 对象的 getRangeAt 方法获取 Range。我们采用 const e = window.getSelection();获得鼠标选中对象。Range中包含了四个属性endContainer,startContainer, startOffset, endOffset。我们通过 const{endContainer,startContainer, startOffset, endOffset } =e.getRangeAt(0);解析出来这四个属性。 他们代表的含义分别是

  1. Range.startContainer是只读属性,
  2. Range.endContainer 是一个只读属性。它会
  3. Range.startOffset 是一个只读属性,用于返回一个表示 Range 在 startContainer 中的起始位置的数字。如果 。对于其他的节点类型, startOffset 返回 startContainer 到边界点的子节点数量。
  4. Range.endOffset 返回代表 Range 结束位置在 Range.endContainer 中的偏移值的数字。**如果 endContainer 的 Node 类型为 Text, Comment,或 CDATASection,偏移值是 endContainer 节点开头到 Range 末尾的总字符个数。**对其他类型的 Node , endOffset 指 endContainer 开头到 Range 末尾的总 Node 个数。

为了便于理解这四个属性值,请看下面图例 可以对比得知,我们标注的内容【文本节点】,startContainer和endContainer 为其父结点,startOffset 为从父结点开始到range对象开始的字符偏移,endOffset 为从父结点开始到range对象结束的偏移,

上一步我们获得了选中对象range相对于父结点的位置。那么我们怎么在整个文本节点中获得代操作的标注对象呢?我们首先通过遍历根结点id="text"下的子节点,

    cutrrentNodes() { 
        
      function getNodes(ele, count = { 
          value: 0 }) { 
        
        const list = [];
        const items = ele.childNodes;
        for (let index in items) { 
        
          const _item = items[index];
          if (_item.nodeName === "#text") { 
        
            const item = _item;
            const value = item.nodeValue || "";
            const len = value.length;
            list.push({ 
        
              item,
              index: +index,
              len,
              value,
            });
            count.value += len;
          } else { 
        
            list.push(...getNodes(_item, count));
          }
        }
        return list;
      }
      return { 
        
        getNodes,
      };
    },

通过findIndex()搜索,我们可以得到包含标签结点的父结点在根结点的孩子结点的数组中的索引。需要注意的是getNodes的返回值是**每一次操作之前的孩子结点数组。**这是由于我们标签修改节点生效的操作在查询之前导致的,并不是错误。

          const nodes = getNodes(ele);
          const start = nodes.findIndex((x) => x.item === startContainer);
          const end = nodes.findIndex((x) => x.item === endContainer);

注:-1表示没有找到,其他值表示对应所处数组的下标值。

          if (start > -1 && end > -1) { 
        
           //startContainer和endContainer相同的情况即没有重叠的情况下 比较理想化的情况,不存在语义标签重叠和多语义元素
            if (start === end) { 
        
              const { 
         item, value } = nodes[start];
              const left = getText(value.slice(0, startOffset));
              const center = getSpan(value.slice(startOffset, endOffset));
              const right = getText(value.slice(endOffset));
              item.parentNode?.insertBefore(left, item);
              item.parentNode?.insertBefore(center, item);
              item.parentNode?.insertBefore(right, item);
              item.parentNode?.removeChild(item);
            } else { 
        
            //不相同的情况处理:
              for (let i = start; i <= end; i++) { 
        
                const { 
         item, value } = nodes[i];
                if (i === start) { 
        
                  const left = getText(value.slice(0, startOffset));
                  const right = getSpan(value.slice(startOffset));
                  item.parentNode?.insertBefore(left, item);
                  item.parentNode?.insertBefore(right, item);
                  item.parentNode?.removeChild(item);
                } else if (i === end) { 
        
                  const left = getSpan(value.slice(0, endOffset));
                  const right = getText(value.slice(endOffset));
                  item.parentNode?.insertBefore(left, item);
                  item.parentNode?.insertBefore(right, item);
                  item.parentNode?.removeChild(item);
                } else { 
        
                  item.parentNode?.replaceChild(getSpan(value), item);
                }
              }
            }
          }

比较重要的函数是getSpan()和getText()

    function getText(text) { 
        
      return document.createTextNode(text);
    }

getText(text)方法的作用是创建并返回一个内容为text(参数)的文本节点。 我们这么做的原因可以参照下图来理解:选中某一文本片段后,【文本内容是根据之前的偏移量进行截取的】我们将该节点分割成三个文本重新装回DOM树,左右两侧不添加特殊标签,选中的文本我们给节点外加span标签方便添加样式。

             //以这一段为例将节点分成三份
              const left = getText(value.slice(0, startOffset));
              const center = getSpan(value.slice(startOffset, endOffset));
              const right = getText(value.slice(endOffset));

    function getSpan(text) { 
        
      const span = document.createElement("span");
      //span.classList.add("mytest");
      //设置节点背景颜色
      span.style.backgroundColor = that.dynamicTags[that.selectedIndex].color;
      //设置节点的title属性,并赋予对应标签名
      span.setAttribute("title", that.dynamicTags[that.selectedIndex].name);
      let tempText = Object();
      tempText.name = that.dynamicTags[that.selectedIndex].name;
      tempText.comment = text;
      that.addTagText(tempText);
      span.addEventListener("click", function ($event) { 
        
        let tempText = Object();
        tempText.name = that.dynamicTags[that.selectedIndex].name;
        tempText.comment = this.innerText;
        that.deleteTagText(tempText);
        console.log(that.showSelectedText());
        var temp = document.createTextNode(this.innerText);
        var sour = $event.currentTarget;
        sour.parentNode.replaceChild(temp, sour);
        ele.normalize();
      });
      span.innerText = text.replace(/\n/g, "");
      return span;
    }

getSpan(text)方法中参数是我们选中的标签文本内容,我们需要再增加的操作包括,对该文本节点附带上我们所选的标签的背景颜色,并绑定title属性为标签名。

data中存放的标签类型和当前选中标签索引: dynamicTags: [ { name: “时间”, color: “red”, }, { name: “人物”, color: “yellow”, }, { name: “天气”, color: “blue”, }, ], selectedIndex: 0,

利用上述两个数据变量,可以实现对选中文本改为span标签下的文本节点,并添加上样式属性。后面就是通过对新的文本节点绑定监听再次点击事件,用于删除节点样式回复到未操作前的状态。在监听事件中我们简单(深拷贝)复制一遍节点信息【内容和节点标签名】。然后在我们选中的所有标签容器中找到对应项并删除,内部通过replaceChild()替换掉即可。最后也是官方提供的方法,多个文本节点在渲染上是看不出的,但是你通过F12可以查看源码为分割的片段,他们在DOM树上是兄弟关系。还是结合下图来理解,虽然你返还了选中内容,在外表上没有区别,但是他们依然是分割成3个片段而不是一个整体。为解决这个问题,我们使用了 ele.normalize();

注意:replaceChild()是通过当前选中的节点的父结点来执行的! normalize(),其作用是处理文档树中的文本节点。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。

最后就是抽出我们选中的文本进行操作:

    //选中好的文本添加到数组中
    addTagText(tempText) { 
        
      this.selectedText.push(tempText);
    },

    deleteTagText(tempText) { 
        
      // 先找到
      const deleteIndex = this.selectedText.findIndex((item) => { 
        
        // 不写return返回的是-1,谜
        return item.comment === tempText.comment;
      });
      console.log(deleteIndex); // 2
      this.selectedText.splice(deleteIndex, 1);
    },
    showSelectedText() { 
        
      return this.selectedText;
    },

整体代码(html通过iframe本地引用)

代码略微修改,样式太难看了,就小动了一下。关于iframe的使用需要注意的是路径问题,如果是本地存储则静态html文件应该在public目录下,如果是网络资源加载请确保具有读权限和在线预览。

    <!-- 文本框 -->
    <div id="text" >
     <iframe src="/html/test2.html" height="500px" width="100%"></iframe>
    </div>

<template>
  <div class="card">
    <!-- 标签值单选框-->
    <div class="container" >
      <el-tag
        :key="tag"
        v-for="(tag, index) in dynamicTags"
        closable
        :disable-transitions="false"
        @close="handleClose(tag)"
        @click="selectTag(tag, index)"
        :color="tag.color"
      >
        { 
        { 
         tag.name }}
      </el-tag>
      <el-input
        class="input-new-tag"
        v-if="inputVisible"
        v-model="inputValue"
        ref="saveTagInput"
        size="small"
        @keyup.enter="handleInputConfirm"
        @blur="handleInputConfirm"
      >
      </el-input>
      <el-button v-else class="button-new-tag" size="small" @click="showInput"
        >+ New Tag</el-button
      >
    </div>
    <el-divider content-position="center">正文</el-divider>
    <!-- 文本框 -->
    <div id="text" >
     <iframe src="/html/test2.html" height="500px" width="100%"></iframe>
    </div>

  </div>
</template>


<script>
export default { 
        
  // 数据源
  data() { 
        
    return { 
        
      color1: "#409EFF",
      dynamicTags: [
        { 
        
          name: "时间",
          color: "red",
        },
        { 
        
          name: "人物",
          color: "yellow",
        },
        { 
        
          name: "天气",
          color: "blue",
        },
      ],
      selectedIndex: 0,
      inputVisible: false,
      inputValue: "",
      selectedText: [],
    };
  },
  //生命周期函数---
  mounted() { 
        
    var that = this;
    const ele = document.getElementById("text");
    const { 
         mousedown, getMouseEvent } = that.useMouse();
    const { 
         getNodes } = that.cutrrentNodes();
    var index = that.getSelectTag();
    function getSpan(text) { 
        
      const span = document.createElement("span");
     // span.classList.add("mytest");
      span.style.backgroundColor = that.dynamicTags[that.selectedIndex].color;
      span.setAttribute("title", that.dynamicTags[that.selectedIndex].name);
      let tempText = Object();
      tempText.name = that.dynamicTags[that.selectedIndex
        标签: 51对射光电传感器pz4m7光颉电阻5kp90a直插二极管荧光法溶解氧传感器oos611zs5脚20a继电器hhg1s固体继电器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台