在javascript中创建重叠的文本范围

时间:2016-10-18 20:06:56

标签: javascript

See Fiddle

我获得了一系列"跨度"并且需要在这些索引的HTML中添加div,以便突出显示"那段文字。目前我在我想要突出显示的文本周围添加spanStarting spanEnding。稍后,我将spanStarting / spanEnding替换为。

跨度如下:

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

只要跨度不重叠,我就可以工作了,现在我需要处理重叠跨度。例如,重叠跨度看起来像这样:

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

当存在重叠跨度时添加spanStarting / spanEnding会使索引失真,从而无法找到要突出显示的正确文本。 你可以在这个fiddle中看到我到目前为止的内容。由于存在重叠的跨度,我的代码无法找到正确的索引来放置代码。

我的代码:

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;

2 个答案:

答案 0 :(得分:4)

显示分层高光的关键之一是首先“展平”高光范围的集合。这与执行三维物体在二维平面上的正交投影不同。在这种情况下,三维对象是高光层的集合。关键是将这些图层投影到单个图层上,同时保留重叠的数据。如果您可以将所有图层展平为单个图层,则可以更轻松地将其显示给用户,并且当您尝试添加周围的标记时,您将不会遇到多个图层之间相互竞争的问题。范围。它还将消除当用户尝试与多个层叠在一起的部分进行交互时可能出现的冲突(例如,使用工具提示)。

我建议创建一个能够展开所有高光范围的功能。相交范围可以在数学上简化为平坦的范围数组,并且可以包括关于每个展平部分的合并信息(例如,该部分中的交叉点数量可以生成更深的突出显示,以及来自多个交叉点的属性集合span,例如该范围内的工具提示集合。)

例如,如果您有三个范围

{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/

答案 1 :(得分:0)

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;