突出显示和编辑长字符串中的文本

Posted

技术标签:

【中文标题】突出显示和编辑长字符串中的文本【英文标题】:highlighting and editing text in long string 【发布时间】:2017-08-20 12:48:36 【问题描述】:

html/javascript/React/Redux Web 应用程序中,我有一个很长的自然语言字符串(大约 300kb)。它是正在播放的录音的抄本。

我需要

突出显示当前说出的单词, 识别点击的单词, 提取选定范围 并替换部分字符串(当用户提交对成绩单的更正时)。

当我用自己的<span> 包装每个单词时,一切都很容易。但是,这会使浏览器无法承受元素的数量,并且页面变得非常缓慢。

我可以想到两种方法来解决这个问题:

我可以将每个句子包装在 <span> 中,并且只包装当前播放句子的每个单词。

我可以保留没有 HTML 标记的文本,通过document.caretPositionFromPoint 处理点击,但我不知道如何突出显示一个单词。

我欢迎更多关于难度和速度之间平衡的想法和想法。

【问题讨论】:

【参考方案1】:

“识别被点击的单词”

新答案

我认为,我之前的答案中的代码实际上必须在每次点击事件时将巨大的文本字符串拆分成一个巨大的数组。之后,对数组进行线性搜索以定位匹配的字符串。

但是,这可以通过预先计算单词数组并使用二进制搜索而不是线性搜索来改善。 现在每个突出显示都将在 O(log n) 而不是 O(n) 中运行

见:http://jsfiddle.net/amoshydra/vq8y8h19/

// Build character to text map
var text = content.innerText;

var counter = 1;
textMap = text.split(' ').map((word) => 
  result = 
    word: word,
    start: counter,
    end: counter + word.length,
  
  counter += word.length + 1;
    return result;
);

content.addEventListener('click', function (e) 
    var selection = window.getSelection();
  var result = binarySearch(textMap, selection.focusOffset, compare_word);
  var textNode = e.target.childNodes[0];

  if (textNode) 
      var range = document.createRange();
    range.setStart(textNode, textMap[result].start);
    range.setEnd(textNode, textMap[result].end);
    var r = range.getClientRects()[0];
    console.log(r.top, r.left, textMap[result].word);

    // Update overlay
    var scrollOffset = e.offsetY - e.clientY; // To accomondate scrolling
    overlay.innerHTML = textMap[result].word;
    overlay.style.top = r.top + scrollOffset + 'px';
    overlay.style.left = r.left + 'px';
  
);

// Slightly modified binary search algorithm
function binarySearch(ar, el, compare_fn) 
    var m = 0;
    var n = ar.length - 1;
    while (m <= n) 
        var k = (n + m) >> 1;
        var cmp = compare_fn(el, ar[k]);
        if (cmp > 0) 
            m = k + 1;
         else if(cmp < 0) 
            n = k - 1;
         else 
            return k;
        
    
    return m - 1;


function compare_word(a, b) 
  return a - b.start;


原答案

我从answer from aaron 中提取了一个代码分支并实现了这个:

我们可以在单词的顶部放置一个覆盖层,而不是在段落上设置一个 span 标签。 并在移动到一个单词时调整叠加层的大小和位置。

片段

JavaScript

// Update overlay
overlayDom.innerHTML = word;
overlayDom.style.top = r.top + 'px';
overlayDom.style.left = r.left + 'px';

CSS

使用带有透明颜色文本的叠加层,这样我们就可以使叠加层与单词的宽度相同。

#overlay 
  background-color: yellow;
  opacity: 0.4;
  display: block;
  position: absolute;
  color: transparent;

下面的完整分叉 JavaScript 代码

var overlayDom = document.getElementById('overlay');

function findClickedWord(parentElt, x, y) 
    if (parentElt.nodeName !== '#text') 
        console.log('didn\'t click on text node');
        return null;
    
    var range = document.createRange();
    var words = parentElt.textContent.split(' ');
    var start = 0;
    var end = 0;
    for (var i = 0; i < words.length; i++) 
        var word = words[i];
        end = start+word.length;
        range.setStart(parentElt, start);
        range.setEnd(parentElt, end);
        // not getBoundingClientRect as word could wrap
        var rects = range.getClientRects();
        var clickedRect = isClickInRects(rects);
        if (clickedRect) 
            return [word, start, clickedRect];
        
        start = end + 1;
    

    function isClickInRects(rects) 
        for (var i = 0; i < rects.length; ++i) 
            var r = rects[i]
            if (r.left<x && r.right>x && r.top<y && r.bottom>y)             
                return r;
            
        
        return false;
    
    return null;

function onClick(e) 
    var elt = document.getElementById('info');

    // Get clicked status
    var clicked = findClickedWord(e.target.childNodes[0], e.clientX, e.clientY);

    // Update status bar
    elt.innerHTML = 'Nothing Clicked';
    if (clicked) 
        var word = clicked[0];
        var start = clicked[1];
        var r = clicked[2];
        elt.innerHTML = 'Clicked: ('+r.top+','+r.left+') word:'+word+' at offset '+start;

        // Update overlay
        overlayDom.innerHTML = word;
        overlayDom.style.top = r.top + 'px';
        overlayDom.style.left = r.left + 'px';
    


document.addEventListener('click', onClick);

查看分叉演示:https://jsfiddle.net/amoshydra/pntzdpff/

此实现使用createRange API

【讨论】:

哇! range.getClientRects 方法是我实现这一点所缺少的链中的链接。我正在考虑通过定位覆盖突出显示,但不知道如何获取文本节点子字符串的坐标。谢谢先生。【参考方案2】:

我不认为&lt;span&gt; 元素的数量一旦定位就无法忍受。您可能只需要通过避免布局更改来最小化reflow。

小实验: 约 3kb 的文本通过 background-color 突出显示

// Create ~3kb of text:
let text = document.getElementById("text");
for (let i = 0; i < 100000; ++i) 
  let word = document.createElement("span");
  word.id = "word_" + i;
  word.textContent = "bla ";
  text.appendChild(word);

document.body.appendChild(text);

// Highlight text:
let i = 0;
let word;
setInterval(function() 
  if (word) word.style.backgroundColor = "transparent";
  word = document.getElementById("word_" + i);
  word.style.backgroundColor = "red";
  i++;
, 100)
&lt;div id="text"&gt;&lt;/div&gt;

初始布局完成后,我可以在 FF/Ubuntu/4 年以上的旧笔记本电脑上顺利渲染。

现在,如果您在哪里更改 font-weight 而不是 background-color,由于不断的布局更改会触发回流,上述内容将变得难以忍受。

【讨论】:

【参考方案3】:

这是一个简单的编辑器,可以轻松处理非常大的字符串。我尝试使用最小 DOM 来提高性能。

可以

识别点击的单词 突出显示当前单击的单词,或拖动选择 提取选定范围 替换部分字符串(当用户提交对成绩单的更正时)。

看到这个jsFiddle

var editor = document.getElementById("editor");

var highlighter = document.createElement("span");
highlighter.className = "rename";

var replaceBox = document.createElement("input");
replaceBox.className = "replace";
replaceBox.onclick = function() 
  event.stopPropagation();
;
editor.parentElement.appendChild(replaceBox);

editor.onclick = function() 
  var sel = window.getSelection();
  if (sel.anchorNode.parentElement === highlighter) 
    clearSelection();
    return;
  
  var range = sel.getRangeAt(0);
  if (range.collapsed) 
    var idx = sel.anchorNode.nodeValue.lastIndexOf(" ", range.startOffset);
    range.setStart(sel.anchorNode, idx + 1);
    var idx = sel.anchorNode.nodeValue.indexOf(" ", range.endOffset);
    if (idx == -1) 
      idx = sel.anchorNode.nodeValue.length;
    
    range.setEnd(sel.anchorNode, idx);
  
  clearSelection();
  range.surroundContents(highlighter);
  range.detach();
  showReplaceBox();
  event.stopPropagation();
;

document.onclick = function()
  clearSelection();
;

function clearSelection() 
  if (!!highlighter.parentNode) 
    replaceBox.style.display = "none";
    highlighter.parentNode.insertBefore(document.createTextNode(replaceBox.value), highlighter.nextSibling);
    highlighter.parentNode.removeChild(highlighter);
  
  editor.normalize(); // comment this line in case of any performance issue after an  edit


function showReplaceBox() 
  if (!!highlighter.parentNode) 
    replaceBox.style.display = "block";
    replaceBox.style.top = (highlighter.offsetTop + highlighter.offsetHeight) + "px";
    replaceBox.style.left = highlighter.offsetLeft + "px";
    replaceBox.value = highlighter.textContent;
    replaceBox.focus();
    replaceBox.selectionStart = 0;
    replaceBox.selectionEnd = replaceBox.value.length;
  
.rename 
  background: yellow;


.replace 
  position: absolute;
  display: none;
<div id="editor">
Your very large text goes here...
</div>

【讨论】:

【参考方案4】:

我会首先通过一些烦人的逻辑找到点击的词(尝试寻找here) 然后,您只需按照上面的建议用样式跨度包装确切的单词来突出显示该单词:)

【讨论】:

【参考方案5】:

嗯,我不太确定你是如何识别单词的。您可能需要第三方软件。要突出显示一个单词,您可以使用 CSS 和 span,如您所说。

CSS

span 
background-color: #B6B6B4;

要添加“跨度”标签,您可以使用查找和替换。喜欢this one。

查找:所有空格

替换:&lt;span&gt;

【讨论】:

以上是关于突出显示和编辑长字符串中的文本的主要内容,如果未能解决你的问题,请参考以下文章

UITextView带有可点击链接但没有文字突出显示

如何避免以特定符号开头的行中字符串的文本突出显示[java]

带有可点击链接但没有文本突出显示的 UITextView

如何在文本突出显示期间保留语法突出显示

Qt QML 如何格式化(突出显示)文本

如何使用 PDFKit 突出显示 pdf 中的选定文本?