“识别被点击的单词”
新答案
我认为,我之前的答案中的代码实际上必须在每次点击事件时将巨大的文本字符串拆分成一个巨大的数组。之后,对数组进行线性搜索以定位匹配的字符串。
但是,这可以通过预先计算单词数组并使用二进制搜索而不是线性搜索来改善。
现在每个突出显示都将在 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