在javascript中创建重叠的文本跨度

Creating overlapping text spans in javascript

本文关键字:文本 重叠 javascript 创建      更新时间:2023-09-26

参见

我得到了一系列"跨度",需要在这些索引的HTML中添加div,以便"突出显示"该范围的文本。目前,我正在添加spanStarting spanEnding周围的文本,我想突出显示。之后,我将spanStarting/spanEnding替换为。

span看起来像这样:

{
"begin": 145,
"end": 155
}

我有这个工作只要跨度从不重叠,现在我需要处理重叠跨度。例如,重叠的跨度看起来像这样:

{
"begin": 4,
"end": 18
},{
"begin": 4,
"end": 41
}

当有重叠的span时添加spanStarting/spanEnding会扭曲索引,并且无法找到要高亮显示的正确文本。你们可以看到目前为止我演奏的小提琴。由于有重叠的跨度,我的代码无法找到正确的索引来放置代码。

我代码:

String.prototype.replaceBetween = function(start, end, what) {
  start = this.substring(0, start);
  end = this.substring(end, this.length);
  return start + what + end;
};
function createHighlights(subElements, snippet, raw) {
  var currentHighlight = subElements;
  currentHighlight.spanStart = currentHighlight.begin;
  currentHighlight.spanStop = currentHighlight.end;
  var currentWord = raw.substring(currentHighlight.spanStart, currentHighlight.spanStop);
  currentHighlight.spanStart = snippet.text.indexOf(currentWord);
  currentHighlight.spanStop = currentHighlight.spanStart + currentWord.length;
  snippet.text = snippet.text.replaceBetween(currentHighlight.spanStart, currentHighlight.spanStop, 'spanStarting' + currentWord + 'spanEnding');
}
var element = {
    "text": "The blood pressure was initially elevated on the patient's outpatient medications, so his hypertension medicines were adjusted by increasing his lisinopril to 20 mg qd."
  },
  rawText = element.text.slice(),
  spans = [{
    "begin": 145,
    "end": 155
  }, {
    "begin": 4,
    "end": 18
  }, {
    "begin": 4,
    "end": 18
  }, {
    "begin": 90,
    "end": 102
  }, {
    "begin": 4,
    "end": 41
  }];
spans.forEach(function(currentHighlight) {
  if (element.text.indexOf('<span') === -1) {
    createHighlights(currentHighlight, element, rawText)
  };
})
element.text = element.text.replace(/spanStarting/g, '<span class="highlight">');
element.text = element.text.replace(/spanEnding/g, '</span>');
document.getElementById('text').innerHTML = element.text;

显示分层高光的关键之一是首先"平坦"你的高光范围集合。这与在二维平面上执行三维物体的正交投影没有什么不同。本例中的三维对象是高亮图层的集合。关键是将这些层投影到单个层上,同时持久化重叠的数据。如果您可以将所有图层平铺成单个图层,那么将更容易向用户显示它们,并且当您尝试在范围周围添加标记时,您将不会遇到多层相互竞争的问题。它还将消除当用户试图与具有多层堆叠的部分交互时可能出现的冲突(例如,使用工具提示)。

我建议创建一个函数,使所有的高光范围变平。相交范围可以在数学上简化为范围的平面数组,并且可以包含关于每个平坦部分的合并信息(例如该部分中产生较暗高亮的交叉点的数量,以及来自该合并范围的多个交叉点的属性集合,例如该范围中的工具提示集合)。

例如,如果您有三个范围

{begin:4,end:18,tooltip:'section 1'}
{begin:4,end:41,tooltip:'section 2'}
{begin:10,end:51,tooltip:'section 3'}

它们将合并到以下结构中:

{begin:4,end:10,tooltip:['section 1','section 2'],count:2},
{begin:10,end:18,tooltip:['section 1','section 2','section 3'],count:3},
{begin:18,end:41,tooltip:['section 2','section 3'],count:2}
{begin:41,end:51,tooltip:['section 3'],count:1}

这本质上是采用分层范围并使其扁平化,同时合并交叉点的数量和属性集合。你会注意到,在新的结构中,一切都是连续的。没有重叠的范围(这将消除你在UI中遇到的冲突)。您还会注意到,原结构中的所有数据也出现在新结构中。

此外,在扁平化图层之后,您需要对新集合做一些"dom特定"的事情。我建议"膨胀"您的范围集合,以便未突出显示的文本部分将放在特殊的范围对象中(没有突出显示的范围)。这将使执行最后一步变得更容易:将这些范围呈现给DOM。您还可以在这里仔细检查您的范围,以确保索引正确排列为从零开始的索引字符串。

这是一个flatten函数,它将把重叠的范围集合平放为一个"平放"的范围序列,并合并任何额外的数据(例如,工具提示)。

function flattenRanges(ranges) {
  var points = [];
  var flattened = [];
  for (var i in ranges) {
    if (ranges[i].end < ranges[i].begin) { //RE-ORDER THIS ITEM (BEGIN/END)
      var tmp = ranges[i].end; //RE-ORDER BY SWAPPING
      ranges[i].end = ranges[i].begin;
      ranges[i].begin = tmp;
    }
    points.push(ranges[i].begin);
    points.push(ranges[i].end);
  }
  //MAKE SURE OUR LIST OF POINTS IS IN ORDER
  points.sort(function(a, b){return a-b});
  //FIND THE INTERSECTING SPANS FOR EACH PAIR OF POINTS (IF ANY)
  //ALSO MERGE THE ATTRIBUTES OF EACH INTERSECTING SPAN, AND INCREASE THE COUNT FOR EACH INTERSECTION
  for (var i in points) {
    if (i==0 || points[i]==points[i-1]) continue;
    var includedRanges = ranges.filter(function(x){
      return (Math.max(x.begin,points[i-1]) < Math.min(x.end,points[i]));
    });
    if (includedRanges.length > 0) {
      var flattenedRange = {
        begin:points[i-1],
        end:points[i],
        count:0
      }
      for (var j in includedRanges) {
        var includedRange = includedRanges[j];
        for (var prop in includedRange) {
          if (prop != 'begin' && prop != 'end') {
            if (!flattenedRange[prop]) flattenedRange[prop] = [];
            flattenedRange[prop].push(includedRange[prop]);
          }
        }
        flattenedRange.count++;
      }
      flattened.push(flattenedRange);
    }
  }
  return flattened;
}

下一步将是inflate的平坦范围。这个函数将用一个空范围结构填充没有高亮显示的部分。它还清除了可能导致DOM中重叠的任何索引。

function inflateRanges(ranges, length=0) {
  var inflated = [];
  var lastIndex;
  for (var i in ranges) {
    if (i==0) {
      //IF THERE IS EMPTY TEXT IN THE BEGINNING, CREATE AN EMOTY RANGE
      if (ranges[i].begin > 0){
        inflated.push({
          begin:0,
          end:ranges[i].begin-1,
          count:0
        });
      }
      inflated.push(ranges[i]);
    } else {
      if (ranges[i].begin == ranges[i-1].end) {
        ranges[i-1].end--;
      }
      if (ranges[i].begin - ranges[i-1].end > 1) {
        inflated.push({
          begin:ranges[i-1].end+1,
          end:ranges[i].begin-1,
          count:0
        });
      }
      inflated.push(ranges[i]);
    }
    lastIndex = ranges[i].end;
  }
  //FOR SIMPLICITY, ADD ANY REMAINING TEXT AS AN EMPTY RANGE
  if (lastIndex+1 < length-1) {
    inflated.push({
      begin:lastIndex+1,
      end:length-1,
      count:0
    })
  }
  return inflated;
}

最后,我建议将这些范围内的实际文本添加到每个相应的范围中。这不是绝对必要的,但它使测试更容易。

function fillRanges(ranges, text) {
  for (var i in ranges) {
    ranges[i].text = text.slice(ranges[i].begin,ranges[i].end+1);
  }
  return ranges;
}

我还创建了一个工作示例,使用您问题中的确切数据。我确实添加了一个名为tooltip的附加属性来说明如何使用合并属性。此外,我还添加了一些简单的tooltip CSS,以展示如何将工具提示与分层范围一起使用的实际示例。

下面是工作示例:https://jsfiddle.net/mspinks/shfpxp82/
var element = {
    "text": "The blood pressure was initially elevated on the patient's outpatient medications, so his hypertension medicines were adjusted by increasing his lisinopril to 20 mg qd."
  },
  rawText = element.text.slice(),
  spans = [{
    "begin": 145,
    "end": 155
  }, {
    "begin": 4,
    "end": 18
  }, {
    "begin": 4,
    "end": 18
  }, {
    "begin": 90,
    "end": 102
  }, {
    "begin": 4,
    "end": 41
  }];
var flags = new Array();
for (i = 0; i < spans.length; i++) { 
    for (j = spans[i].begin; j <= spans[i].end; j++) { 
    flags[j] = true;
    }
}
isSpan = false
var starts = new Array();
var ends = new Array();
for (i = 0; i < flags.length; i++) { 
    if (flags[i]) {
    if (!isSpan) {
      starts.push(i);
    }
    isSpan = true;
  }
  else {
    if (isSpan) {
      ends.push(i);
    }
    isSpan = false;
  }
}
if (flags.length > 0 && ends.length < starts.lenght) {
    ends.push(flags.length - 1);
}
var newSpans = new Array();
for (i = 0; i < starts.length; i++) { 
    newSpans.push({"begin": starts[i], "end": ends[i]});
}
newSpans.forEach(function(currentHighlight) {
  if (element.text.indexOf('<span') === -1) {
    createHighlights(currentHighlight, element, rawText)
  };
})
element.text = element.text.replace(/spanStarting/g, '<span class="highlight">');
element.text = element.text.replace(/spanEnding/g, '</span>');
document.getElementById('text').innerHTML = element.text;