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

时间:2017-03-27 17:21:07

标签: javascript html performance optimization

在HTML / JavaScript / React / Redux Web应用程序中,我有一个长字符串(大约300kb)的自然语言。这是正在播放的录音的成绩单。

我需要

  • 突出显示当前发出的字词,
  • 识别单击的单词,
  • 提取所选范围
  • 并替换部分字符串(当用户提交对记录的更正时)。

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

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

  • 我可以用<span>包装每个句子,只包装当前播放的句子中的每个单词。

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

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

5 个答案:

答案 0 :(得分:2)

&#34;识别单击&#34;

的单词

新答案

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

然而,这可以通过预先计算单词数组并使用二进制搜索而不是线性搜索来改进。 现在,每个突出显示都将在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

答案 1 :(得分:2)

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

小型实验:通过background-color突出显示~3kb的文字

&#13;
&#13;
// 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)
&#13;
<div id="text"></div>
&#13;
&#13;
&#13;

初始布局完成后,这样可以在FF / Ubuntu / 4 +岁的笔记本电脑中顺利呈现。

现在,如果您要更改font-weight而不是background-color,由于不断布局更改会触发重排,上述情况会变得无法忍受。

答案 2 :(得分:2)

这是一个简单的编辑器,可以轻松处理非常大的字符串。我尝试使用最小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>

答案 3 :(得分:0)

我会先通过一些恼人的逻辑找到点击的单词(试试看here) 然后,您可以简单地通过如上所述使用样式范围包装确切的单词来突出显示该单词:)

答案 4 :(得分:0)

好吧,我不确定你怎么能识别单词。您可能需要第三方软件。要突出显示单词,您可以按照说法使用CSS和span。

<强> CSS

span {
background-color: #B6B6B4;
}

添加&#39; span&#39;标签,你可以使用查找和替换的东西。像this one一样。

查找:所有空格

替换:<span>