如何使用 JavaScript 获取光标下的单词?

IT技术 javascript browser cursor dom-events
2021-01-10 22:30:30

例如,如果我有

<p> some long text </p>

在我的 HTML 页面上,我怎么知道鼠标光标位于单词“文本”上方?

6个回答

除了其他两个答案之外,您可以使用 jQuery(或通常使用 javascript)将您的段落拆分为跨度。

这样,您就无需考虑在单词周围输出带有跨度的文本。让您的 javascript 为您完成。

例如

<p>Each word will be wrapped in a span.</p>
<p>A second paragraph here.</p>
Word: <span id="word"></span>

<script type="text/javascript">
    $(function() {
        // wrap words in spans
        $('p').each(function() {
            var $this = $(this);
            $this.html($this.text().replace(/\b(\w+)\b/g, "<span>$1</span>"));
        });

        // bind to each span
        $('p span').hover(
            function() { $('#word').text($(this).css('background-color','#ffff66').text()); },
            function() { $('#word').text(''); $(this).css('background-color',''); }
        );
    });
</script>

请注意,上面的代码虽然有效,但会删除段落标签内的任何 html。

jsFiddle 示例

@idude您应该能够$('p')$('p,h1,h2,h3')等等替换第一个选择器同样,要使悬停起作用,您需要将第二个选择器更改为$('p span,h1 span,h2 span,h3 span')
2021-03-12 22:30:30
@Chetan - 谢谢,我不太擅长正则表达式,所以我用简单的方法做到了:) 我已经更新了。
2021-03-13 22:30:30
我们将如何编辑它以识别 h1、h2、h3 等标签,而不仅仅是 p 标签?
2021-03-24 22:30:30
我想过,但这是一个尴尬的解决方案(我是 JavaScript 新手,所以我的方式比你的糟糕得多)。感谢您的澄清。@Chetan - 这是一个很好的解决方案。
2021-04-03 22:30:30
或者你可以只做$(this).text().replace(/\b(\w+)\b/g, "<span>$1</span>")而不是循环。这将正确处理所有空白字符。
2021-04-09 22:30:30

我的另一个答案仅适用于 Firefox。此答案适用于 Chrome。(也可能在 Firefox 中工作,我不知道。)

function getWordAtPoint(elem, x, y) {
  if(elem.nodeType == elem.TEXT_NODE) {
    var range = elem.ownerDocument.createRange();
    range.selectNodeContents(elem);
    var currentPos = 0;
    var endPos = range.endOffset;
    while(currentPos+1 < endPos) {
      range.setStart(elem, currentPos);
      range.setEnd(elem, currentPos+1);
      if(range.getBoundingClientRect().left <= x && range.getBoundingClientRect().right  >= x &&
         range.getBoundingClientRect().top  <= y && range.getBoundingClientRect().bottom >= y) {
        range.expand("word");
        var ret = range.toString();
        range.detach();
        return(ret);
      }
      currentPos += 1;
    }
  } else {
    for(var i = 0; i < elem.childNodes.length; i++) {
      var range = elem.childNodes[i].ownerDocument.createRange();
      range.selectNodeContents(elem.childNodes[i]);
      if(range.getBoundingClientRect().left <= x && range.getBoundingClientRect().right  >= x &&
         range.getBoundingClientRect().top  <= y && range.getBoundingClientRect().bottom >= y) {
        range.detach();
        return(getWordAtPoint(elem.childNodes[i], x, y));
      } else {
        range.detach();
      }
    }
  }
  return(null);
}    

在您的 mousemove 处理程序中,调用 getWordAtPoint(e.target, e.x, e.y);

代码在 iOS (6/7) 上运行良好,但在 Android 4.0.3 中 getBoundingClientRect 可能导致 null。所以添加: range.getBoundingClientRect() != null 作为第一个循环中的条件(在获取 left 属性之前)。
2021-03-21 22:30:30
文档指出“单词”的边界是一个空白字符。但是扩展似乎不适用于网址。有任何想法吗?
2021-03-26 22:30:30
此外,while 循环中条件中的 +1 也是不必要的。textNode 的最后一个字符开始于range.endOffset(并结束于range.endOffset + 1)。所以,除非条件实际上while(currentPos < endPos)是最后一个字符,否则永远不会被测试。
2021-03-28 22:30:30
这是一段不错的代码,但在混合使用 textNodes 和其他内联元素时会中断。有两种情况会出现这种情况。1. 带有换行符的文本节点会有一个无意义的边界框。2.高度大于textNode行的内联元素可以重置range的垂直位置。我认为应该可以通过从一开始就逐个检查 textNodes 并通过假设 texNodes 永远不会高于它们以前的任何兄弟姐妹来补偿垂直位置的随机重置来克服这些问题(但这可能并不总是正确的)。
2021-03-31 22:30:30
@Eyal 我发现你的代码在 chrome 中运行良好,而不是在 Firefox 中运行良好。但是当 range.expand 被注释时,它可以为 firefox 提供光标下的字符。有任何想法让它在 Firefox 中工作吗?
2021-04-10 22:30:30

前言:

如果您有多个跨度和嵌套的 HTML 来分​​隔单词(甚至单词中的字符),那么上述所有解决方案都将无法返回完整且正确的单词。

下面是从赏金问题的例子:Х</span>rт0съ如何正确退货Хrт0съ这些问题在 2010 年没有得到解决,所以我现在将提出两个解决方案(2015 年)。


解决方案 1 - 剥离内部标签,环绕每个完整单词:

一种解决方案是去除段落内的跨度标签,但保留其文本。拆分的单词和短语因此重新连接在一起作为常规文本。每个单词都是通过空格分割(不仅仅是空格)找到的,这些单词被包装在可以单独访问的跨度中。

在演示中,您可以突出显示整个单词,从而获取整个单词的文本。


图 0

代码:

$(function() {
  // Get the HTML in #hoverText - just a wrapper for convenience
  var $hoverText = $("#hoverText");

  // Replace all spans inside paragraphs with their text
  $("p span", $hoverText).each(function() {
    var $this = $(this);
    var text = $this.text(); // get span content
    $this.replaceWith(text); // replace all span with just content
  });

  // Wrap words in spans AND preserve the whitespace
  $("p", $hoverText).each(function() {
    var $this = $(this);
    var newText = $this.text().replace(/([\s])([^\s]+)/g, "$1<span>$2</span>");
    newText = newText.replace(/^([^\s]+)/g, "<span>$1</span>");
    $this.empty().append(newText);
  });

  // Demo - bind hover to each span
  $('#hoverText span').hover(
    function() { $(this).css('background-color', '#ffff66'); },
    function() { $(this).css('background-color', ''); }
  );
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="hoverText">
  <p><span class="kinovar"><span id="selection_index3337" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со 
стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span>
  </p>
</div>

方案一全文演示


解决方案 2 - Caret 检查和 DOM 遍历:

这是一个更复杂的解决方案。它是一种使用节点遍历的算法解决方案,可准确捕获文本节点中光标下的完整正确单词。

通过检查插入符号位置(使用caretPositionFromPointcaretRangeFromPoint,将想法归功于@chrisv)找到了一个临时词这可能是也可能不是完整的词。

然后对其进行分析以查看它是否位于其文本节点的任一边缘(开始或结束)。如果是,则检查前一个文本节点或后一个文本节点以查看是否应将其连接起来以使该单词片段更长。

例子:

Х</span>rт0съ必须返回Хrт0съ,而Х不是rт0съ

遍历 DOM 树以获取下一个无障碍文本节点。如果两个单词片段被一个<p>或某个其他屏障标记分隔,则它们不相邻,因此不是同一个单词的一部分。

例子:

њб.)</p><p>Во 不应该回来 њб.)Во


在demo中,左边浮动的div就是光标下的单词。右侧浮动 div(如果可见)显示边界上的单词是如何形成的。其他标签可以安全地与此解决方案中的文本内联。

图1

代码:

$(function() {
  // Get the HTML in #hoverText - just a wrapper for convenience
  var $hoverText = $("#hoverText");

  // Get the full word the cursor is over regardless of span breaks
  function getFullWord(event) {
     var i, begin, end, range, textNode, offset;
    
    // Internet Explorer
    if (document.body.createTextRange) {
       try {
         range = document.body.createTextRange();
         range.moveToPoint(event.clientX, event.clientY);
         range.select();
         range = getTextRangeBoundaryPosition(range, true);
      
         textNode = range.node;
         offset = range.offset;
       } catch(e) {
         return ""; // Sigh, IE
       }
    }
    
    // Firefox, Safari
    // REF: https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint
    else if (document.caretPositionFromPoint) {
      range = document.caretPositionFromPoint(event.clientX, event.clientY);
      textNode = range.offsetNode;
      offset = range.offset;

      // Chrome
      // REF: https://developer.mozilla.org/en-US/docs/Web/API/document/caretRangeFromPoint
    } else if (document.caretRangeFromPoint) {
      range = document.caretRangeFromPoint(event.clientX, event.clientY);
      textNode = range.startContainer;
      offset = range.startOffset;
    }

    // Only act on text nodes
    if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
      return "";
    }

    var data = textNode.textContent;

    // Sometimes the offset can be at the 'length' of the data.
    // It might be a bug with this 'experimental' feature
    // Compensate for this below
    if (offset >= data.length) {
      offset = data.length - 1;
    }

    // Ignore the cursor on spaces - these aren't words
    if (isW(data[offset])) {
      return "";
    }

    // Scan behind the current character until whitespace is found, or beginning
    i = begin = end = offset;
    while (i > 0 && !isW(data[i - 1])) {
      i--;
    }
    begin = i;

    // Scan ahead of the current character until whitespace is found, or end
    i = offset;
    while (i < data.length - 1 && !isW(data[i + 1])) {
      i++;
    }
    end = i;

    // This is our temporary word
    var word = data.substring(begin, end + 1);

    // Demo only
    showBridge(null, null, null);

    // If at a node boundary, cross over and see what 
    // the next word is and check if this should be added to our temp word
    if (end === data.length - 1 || begin === 0) {

      var nextNode = getNextNode(textNode);
      var prevNode = getPrevNode(textNode);

      // Get the next node text
      if (end == data.length - 1 && nextNode) {
        var nextText = nextNode.textContent;

        // Demo only
        showBridge(word, nextText, null);

        // Add the letters from the next text block until a whitespace, or end
        i = 0;
        while (i < nextText.length && !isW(nextText[i])) {
          word += nextText[i++];
        }

      } else if (begin === 0 && prevNode) {
        // Get the previous node text
        var prevText = prevNode.textContent;

        // Demo only
        showBridge(word, null, prevText);

        // Add the letters from the next text block until a whitespace, or end
        i = prevText.length - 1;
        while (i >= 0 && !isW(prevText[i])) {
          word = prevText[i--] + word;
        }
      }
    }
    return word;
  }

  // Return the word the cursor is over
  $hoverText.mousemove(function(e) {
    var word = getFullWord(e);
    if (word !== "") {
      $("#result").text(word);
    }
  });
});

// Helper functions

// Whitespace checker
function isW(s) {
  return /[ \f\n\r\t\v\u00A0\u2028\u2029]/.test(s);
}

// Barrier nodes are BR, DIV, P, PRE, TD, TR, ... 
function isBarrierNode(node) {
  return node ? /^(BR|DIV|P|PRE|TD|TR|TABLE)$/i.test(node.nodeName) : true;
}

// Try to find the next adjacent node
function getNextNode(node) {
  var n = null;
  // Does this node have a sibling?
  if (node.nextSibling) {
    n = node.nextSibling;

    // Doe this node's container have a sibling?
  } else if (node.parentNode && node.parentNode.nextSibling) {
    n = node.parentNode.nextSibling;
  }
  return isBarrierNode(n) ? null : n;
}

// Try to find the prev adjacent node
function getPrevNode(node) {
  var n = null;

  // Does this node have a sibling?
  if (node.previousSibling) {
    n = node.previousSibling;

    // Doe this node's container have a sibling?
  } else if (node.parentNode && node.parentNode.previousSibling) {
    n = node.parentNode.previousSibling;
  }
  return isBarrierNode(n) ? null : n;
}

// REF: http://stackoverflow.com/questions/3127369/how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getChildIndex(node) {
  var i = 0;
  while( (node = node.previousSibling) ) {
    i++;
  }
  return i;
}

// All this code just to make this work with IE, OTL
// REF: http://stackoverflow.com/questions/3127369/how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getTextRangeBoundaryPosition(textRange, isStart) {
  var workingRange = textRange.duplicate();
  workingRange.collapse(isStart);
  var containerElement = workingRange.parentElement();
  var workingNode = document.createElement("span");
  var comparison, workingComparisonType = isStart ?
    "StartToStart" : "StartToEnd";

  var boundaryPosition, boundaryNode;

  // Move the working range through the container's children, starting at
  // the end and working backwards, until the working range reaches or goes
  // past the boundary we're interested in
  do {
    containerElement.insertBefore(workingNode, workingNode.previousSibling);
    workingRange.moveToElementText(workingNode);
  } while ( (comparison = workingRange.compareEndPoints(
    workingComparisonType, textRange)) > 0 && workingNode.previousSibling);

  // We've now reached or gone past the boundary of the text range we're
  // interested in so have identified the node we want
  boundaryNode = workingNode.nextSibling;
  if (comparison == -1 && boundaryNode) {
    // This must be a data node (text, comment, cdata) since we've overshot.
    // The working range is collapsed at the start of the node containing
    // the text range's boundary, so we move the end of the working range
    // to the boundary point and measure the length of its text to get
    // the boundary's offset within the node
    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);

    boundaryPosition = {
      node: boundaryNode,
      offset: workingRange.text.length
    };
  } else {
    // We've hit the boundary exactly, so this must be an element
    boundaryPosition = {
      node: containerElement,
      offset: getChildIndex(workingNode)
    };
  }

  // Clean up
  workingNode.parentNode.removeChild(workingNode);

  return boundaryPosition;
}

// DEMO-ONLY code - this shows how the word is recombined across boundaries
function showBridge(word, nextText, prevText) {
  if (nextText) {
    $("#bridge").html("<span class=\"word\">" + word + "</span>  |  " + nextText.substring(0, 20) + "...").show();
  } else if (prevText) {
    $("#bridge").html("..." + prevText.substring(prevText.length - 20, prevText.length) + "  |  <span class=\"word\">" + word + "</span>").show();
  } else {
    $("#bridge").hide();
  }
}
.kinovar { color:red; font-size:20px;}.slavic { color: blue;}#result {top:10px;left:10px;}#bridge { top:10px; right:80px;}.floater { position: fixed; background-color:white; border:2px solid black; padding:4px;}.word { color:blue;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <div id="bridge" class="floater"></div> <div id="result" class="floater"></div> <div id="hoverText"><p><span class="kinovar"><span id="selection_index3337" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span></p><div class="slavic"> <input value="Works around other tags!"><p><span id="selection_index3737" class="selection_index"></span>(л. рo7з њб.)</p><p><span class="kinovar"><span id="selection_index3738" class="selection_index"></span>Во вт0рникъ вeчера</span> </p><p><span class="kinovar"><span id="selection_index3739" class="selection_index"></span>tдaніе прaздника пaсхи.</span></p><p><span class="kinovar"><span id="selection_index3740" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.<input value="Works around inline tags too"></span></p><p><span class="kinovar"><span id="selection_index3741" class="selection_index"></span>На ГDи воззвaхъ: поeмъ стіхи6ры самоглaсны, слэпaгw, на ѕ7. Глaсъ в7:</span></p></div>

注意:我冒昧地将样式应用于示例 HTML 中的 span 标签,以阐明文本节点边框的位置。)

方案二全文演示

(目前在 Chrome 和 IE 中工作。对于 IE,必须使用来自IERange的方法作为跨浏览器兼容性的垫片)

@user1122069 我已经发布了第二个解决方案,一个更好的解决方案,它使用 DOM 遍历并且也适用于 IE。它速度很快,旨在为未来的 HTML 提供强大的支持。我喜欢这两种解决方案,但这个解决方案没有按照您的要求使用 span 标签包装。
2021-03-14 22:30:30
为什么你选择这样一个字母来得到这么好的答案......解决这个问题让我很头疼。
2021-03-14 22:30:30
谢谢。到目前为止完美运行。我已将这些函数封装为一个对象,以使其更好地与我的应用程序配合使用。jsfiddle.net/ohaf4ytL/1我认为这对其他人也非常有用。
2021-03-20 22:30:30
在这种斯拉夫语编码中,{ 表示重音,所以我只是将一个单词视为空格中的所有内容,甚至是真正的标点符号(因为我会自己删除它们)。答案在技术上不符合赏金,但如果它最能解决问题,我会选择。
2021-03-23 22:30:30
@codemonkey 谢谢。最初的赏金问题在该字母表中,需要处理的语料库要大得多。我不得不承认,这是一个疯狂而棘手的悬赏问题。
2021-04-02 22:30:30

据我所知,你不能。

我唯一能想到的是将每个单词放在它们自己的元素中,然后将鼠标悬停事件应用于这些元素。

<p><span>Some</span> <span>long</span> <span>text</span></p>

<script>
$(document).ready(function () {
  $('p span').bind('mouseenter', function () {
    alert($(this).html() + " is what you're currently hovering over!");
  });
});
</script>
下面是 jsfiddle 上面代码的演示:jsfiddle.net/5bT4B
2021-03-15 22:30:30

这是一个在大多数情况下适用于 Chrome 的简单解决方案:

function getWordAtPoint(x, y) {
  var range = document.caretRangeFromPoint(x, y);

  if (range.startContainer.nodeType === Node.TEXT_NODE) {
    range.expand('word');
    return range.toString().trim();
  }

  return null;
}

我将过滤标点符号和正确处理带连字符的单词作为练习留给读者:)。

x/y 坐标需要是 event.clientX 而不是 event.pageX。如果使用 pageX,则在滚动页面且鼠标位于初始视口坐标之外时,caretRangeFromPoint() 将返回 null。
2021-03-14 22:30:30
@chemamolins 这正是促使我想出这个食谱的原因:)。
2021-03-19 22:30:30
正是我需要的 Chrome 扩展程序。
2021-03-28 22:30:30
其它你可能感兴趣的问题