需求:
1、 鼠标选中文本后,为选中的文本添加背景色。并在选中的文本上增加标签。
2、 生成的文本标签有两种,不同类型的标签可以相互拖拽行成关系,并形成连线,连线后增加关系标签。
3、点击标签可以查看到当前点击的标签指向哪个文本内容,关系标签也是如此
实现工具:
1、JQ
2、svg
实现思路
前期:乍一看这个功能的实现其实还挺简单,但是在实现过程中,由于之前过分依赖vue框架,导致对原生js以及Jq的一些api有些不熟悉。
在实现过程中断断续续出现了很多问题。
1、如何为选中的文本添加背景色
这里有两种实现思路:
-
根据鼠标选中的坐标,再使用svg进行画图。
弊端:我前期是使用这种方法,但是这种方法根据鼠标点击的位置来获取x,y的坐标后再去画图的,这就意味一个问题。鼠标的点击位置是不统一的导致标签不对齐问题。(找不到解决思路)
-
根据选中的文本位置,为文本增加span标签,再给span标签加上样式。这种通过添加节点的方式,解决了我们的标签不对齐问题。
但这里面还有很多问题。后面呈现代码时分析。
2、如何画线
svg画线就是根据坐标进行画线的,所以我们要拿到两个标签的位置进行连线。
他有两种情况,第一种是同一行标签上连线。第二行是跨行连线。
这里没什么需要特别注意的。
代码实现
<div class="txtBox">
<div id="1" data-num="1">【性状】本品为肠溶衣片,除去肠溶衣后显类白色或淡黄色。</div>
</div>
// 选中文本
let text = ''; // 获取选中的文本
let range = ''; // 获取选中的文本对象
let beginTxt;
// 文本结束位置
let endTxt;
// 当前行数
let rowNum;
// 鼠标事件
$(".txtBox").mouseup(function(e){
text = ''
range = ''
// 获取屏幕的宽度
let clientWidth = document.body.clientWidth;
// 这个方法可以获取当前鼠标选中的文本
txt = window.getSelection();
// 获取到当行选中的文本开始索引和结束索引
beginTxt = txt.anchorOffset;
endTxt = txt.focusOffset;
var parentOffset = $(this).offset();
if(txt.type!=='None'){
// range 对象是我们当前选中的文本对象
// 有了这个对象,我们可以为选中的文本添加标签
range = txt.getRangeAt(0);
if (txt.toString().length >= 1) {
text = txt.toString();
}
}
})
这里只贴关键代码
选中了文本对象后,我是通过一个点击事件后才进行标签生成的。所以在前面
记录了range对象。选中文本后我们要生成的标签有两个。
// 随机生成一个id,因为要生成的标签有两个,后面我们有删除功能
// 根据同一个id来删除,当然我们生成标签是不能重复id,所以有一个标签
// 用class来记录
let id = Number(Math.random().toString().substr(3,length) + Date.now()).toString(36)
// 这里面有些自定义属性始根据我需求来的,关键的就class和style
var newNode = document.createElement("span");
newNode.setAttribute("class",`${id}`);
newNode.setAttribute("style",`background:${bgColor}`);
newNode.setAttribute("data-type",`${type}`);
newNode.setAttribute("data-bg",`${bgColor}`);
newNode.setAttribute("data-coverText",`${text}`);
newNode.setAttribute("data-rowNum",`${rowNum}`);
// 这个方法就可以为文本添加标签
range.surroundContents(newNode);
目前已经实现到这里了
但是这里有一个比较严重的问题。我们目前是选中文本之后使用js的getSelection内置方法才得到range对象的。如果我们之后要回显,是没有选中文本这一说的。我们应该怎么记录选中后的文本呢,而且准确的在那个文本里增加标签。
其实我一开始的思路是直接使用选中的文本和当前的行数的文本做配对。但是如果一行内有多个相同的文本,这种方法行不通
所以还是要精准的方式,获取到这一行里面选中文本的当前索引。然后用自定义属性保存当前文本的开始和结束位置
// 前面已经实现了。
beginTxt = txt.anchorOffset;
endTxt = txt.focusOffset;
但是考虑到之前的是纯文本,所以选中文本时肯定可以拿到索引。如果这个文本已经加过标签了。
我已经<span>加过标签了</span>了,已经不是纯文本。
那么getSelection就不会帮你算的这么准了。它变成这样计算了,也就是跳过了前面的标签。因为一行可以有很多个标签,所以做了一个循环计算当前的文本索引
// 随机生成一个id,因为要生成的标签有两个,后面我们有删除功能
// 根据同一个id来删除,当然我们生成标签是不能重复id,所以有一个标签
// 用class来记录
let id = Number(Math.random().toString().substr(3,length) + Date.now()).toString(36)
// 这里面有些自定义属性始根据我需求来的,关键的就class和style
var newNode = document.createElement("span");
// 原本应该在这里就添加文本的开始和结束索引
newNode.setAttribute("class",`${id}`);
newNode.setAttribute("style",`background:${bgColor}`);
newNode.setAttribute("data-type",`${type}`);
newNode.setAttribute("data-bg",`${bgColor}`);
newNode.setAttribute("data-coverText",`${text}`);
newNode.setAttribute("data-rowNum",`${rowNum}`);
// 标签还是要加,不过在判断后加
//range.surroundContents(newNode);
// 首先我的文本结构是做成这样的
//看下图,并且定义一个mousedown事件记录当前点击的是第几行
// 使用 rowNum 来记录
if(rowNum){
// 标签还是要加的,我们要改变的只是要获取开始和结束
// 目前的div结构可能是这样的
// <div id="1">我是<span>标签1</span>我<span>标签</span>我不是</div>
range.surroundContents(newNode);
// div的id要和行数一致
var divName=document.getElementById(rowNum);
// 记录当前的文本在哪个位置
let strLength = 0;
//判断在此之前有无添加过标签
if(divName.childNodes){
for(let i = 0; i<divName.childNodes.length; i++){
if(divName.childNodes[i].nodeType===3){
strLength += divName.childNodes[i].length;
}
if(divName.childNodes[i].nodeType===1&&divName.childNodes[i].class!==id){
strLength += divName.childNodes[i].dataset.covertext.length;
}
if(divName.childNodes[i].class===id){
beginTxt = strLength;
endTxt = strLength+text.length
break;
}
}
}
$(`.${id}`).attr('data-beginTxt', beginTxt);
$(`.${id}`).attr('data-endTxt', endTxt);
// 获取到标签的位置,新增第二个标签
let heigth = $(`.${id}`).offset().top+ $(".contentBox").scrollTop()+10;
let spanX = downX+$(".contentBox").scrollLeft();
let clientWidth = document.body.clientWidth;
let candrag = e.target.dataset.type==="ingredient"?true:false;
let width = 16*(e.target.innerHTML.length);
// 添加标签
let spanElement = `
<span
id='${id}'
data-covertext='${text}'
data-type='${type}'
class='labelCategory'
style='background:${bgColor};
width:${width}px'
draggable="${candrag}"
ondragstart="drag(event)"
ondragover="allowDrop(event)"
ondrop="drop(event)">
${ e.target.innerHTML }
</span>`;
$(`.${id}`).append(spanElement)
$(".txt_dialog").hide();
}
}
$(".txtBox").mousedown(function(e){
rowNum = e.target.dataset.num;
downX = e.pageX;
})
这样就能拿到文本的索引值了,当然我们这里不考虑换行。
画线
我觉得这里没什么好讲的,就是获取两个标签的索引和位置,判断高度,如果同一高度证明在同一行,否则跨行而已。代码仅供参考
<!--画图面板-->
<svg class="svgBox" width='100%' height='1000px' xmlns='http://www.w3.org/2000/svg'>
<g id='whiskers'></g>
</svg>
function draw(currentSpanId,dragDownSpan,height,height2,left,left2,text1,text2){
var mysvg = document.getElementById("whiskers");
var rectObj = document.createElementNS("http://www.w3.org/2000/svg","polyline");
if(rectObj){
// 用来记录线条,到时可以删除
let randomIndex = Math.round(Math.random()*100);
let lineClassName = Number(Math.random().toString().substr(3,length) + Date.now()).toString(36) +"b" + randomIndex
let lineClassName2 = lineClassName+'two'
// 如果跨行
if(height!==height2){
// 第一条
rectObj.setAttribute("points",`
${left} ${height-5},
${left+10} ${height-15},
${left+3000} ${height-15}`);
rectObj.setAttribute("style","stroke:black; fill:none");
mysvg.appendChild(rectObj);
// 第二条
var rectObj2 = document.createElementNS("http://www.w3.org/2000/svg","polyline");
rectObj2.setAttribute("points",`
0 ${height2-15},
${left2-10} ${height2-15},
${left2} ${height2-5}`);
rectObj2.setAttribute("style","stroke:black; fill:none");
rectObj.setAttribute("class",lineClassName);
rectObj2.setAttribute("class",lineClassName2);
mysvg.appendChild(rectObj2);
}else{
rectObj.setAttribute("points",`${left} ${height-5},${left+10} ${height-15}, ${left2-5} ${height2-15}, ${left2} ${height2-5}`);
rectObj.setAttribute("style","stroke:black; fill:none");
rectObj.setAttribute("class",lineClassName);
mysvg.appendChild(rectObj);
}
$('.txt_dialog').hide();
relationTag(currentSpanId,dragDownSpan,left,height,left2,height2,text1,text2,lineClassName,lineClassName2);
}
}
保存的数据结构
回显问题
1、标签的回显我觉得很麻烦。也是和之前的问题一样。一行可能有多个标注。
标注之后会有标签,加了标签之后就不是纯文本了,所以又要计算。
这里直接贴出代码。
function created () {
let str = '[{"class":"kf7oxv4ic58","name":"反复发","row":"0","type":"ingredient","relationId":"kf7oxy5vc98","beginTxt":"9","endTxt":"12","bg":"#ef5353"},{"class":"kf7oxy5vc98","name":"咳嗽","row":"1","type":"effect","beginTxt":"38","endTxt":"40","bg":"#f1ae45"},{"class":"kf7oy470c48","name":"病重","row":"1","type":"ingredient","relationId":"kf7oy6tmc40","beginTxt":"68","endTxt":"70","bg":"#ef5353"},{"class":"kf7oy6tmc40","name":"睡眠、胃纳稍差","row":"1","type":"effect","beginTxt":"99","endTxt":"106","bg":"#f1ae45"},{"class":"kf7oyf67c38","name":"耳: 外形","row":"13","type":"ingredient","relationId":"kf7oyh1kc81","beginTxt":"0","endTxt":"5","bg":"#ef5353"},{"class":"kf7oyh1kc81","name":"鼻窦压","row":"14","type":"effect","beginTxt":"25","endTxt":"28","bg":"#f1ae45"}]'
str = JSON.parse(str);
let lineArr = [];
let setLine = [];
// 这里是用来画线的,一个标签可以和多个标签有关系
for(let i = 0; i<str.length; i++){
if(str[i].relationId&&str[i].relationId.length>0){
setLine.push(str[i].class,str[i].relationId)
setLine = Array.from(new Set(setLine))
}
// 这里开始才是回显标注的文本的
var newNode = document.createElement("span");
newNode.setAttribute("class",`${str[i].class}`);
newNode.setAttribute("style",`background:${str[i].bg}`);
newNode.setAttribute("data-type",`${str[i].type}`);
newNode.setAttribute("data-bg",`${str[i].bg}`);
newNode.setAttribute("data-coverText",`${str[i].name}`);
newNode.setAttribute("data-beginTxt",`${str[i].beginTxt}`);
newNode.setAttribute("data-endTxt",`${str[i].endTxt}`);
newNode.setAttribute("data-rowNum",`${str[i].row}`);
newNode.setAttribute("data-relationId",`${str[i].relationId}`);
var divName=document.getElementById(str[i].row);
var content;
// 判断在此之前有无添加过标签
let strLength = 0;
// 判断是文本节点还是元素节点
if(divName.childNodes.length>1){
for(let j = 0; j<divName.childNodes.length; j++){
if(divName.childNodes[j].nodeType===3){
strLength += divName.childNodes[j].length;
}else if(divName.childNodes[j].nodeType===1){
strLength += divName.childNodes[j].dataset.covertext.length;
}
if(str[i].beginTxt<strLength&&divName.childNodes[j].className!==str[i].class){
let strToTalLenght = divName.childNodes[j].length;
let num1 = (strToTalLenght - (strLength - str[i].beginTxt));
let num2 = num1 + str[i].name.length;
if(num1>0&&num2>num1){
content = divName.childNodes[j];
let range = document.createRange();
range.setStart(content,num1);
range.setEnd(content,num2);
range.surroundContents(newNode);
createdTag(str[i])
}
}
}
}else{
content= divName.firstChild;
let range = document.createRange();
range.setStart(content,str[i].beginTxt);
range.setEnd(content,str[i].endTxt);
range.surroundContents(newNode);
createdTag(str[i])
}
}
// 画线
createdLine(setLine)
}