Определить, какое слово было нажато в тексте

Я создаю JS-скрипт, который в какой-то момент может на данной странице позволить пользователю щелкнуть любое слово и сохранить его в переменной.

У меня есть одно решение, которое довольно уродливо и включает в себя синтаксический анализ класса с использованием jQuery: я сначала анализирую весь HTML, разбиваю все на каждом пространстве " "и повторно добавить все, завернутые в <span class="word">word</span>, а затем я добавляю событие с jQ для обнаружения кликов по такому классу, и используя $ (this).innerHTML, я получаю слово, на которое нажали.

Это медленно и безобразно во многих отношениях, и я надеялся, что кто-то знает другой способ добиться этого.

PS: я мог бы рассмотреть возможность его использования в качестве расширения для браузера, поэтому, если это невозможно с простым JS, и если вы знаете API-интерфейс браузера, который позволил бы это, не стесняйтесь упомянуть об этом!

Возможным вариантом было бы заставить пользователя выделять слово вместо того, чтобы щелкать по нему, но я бы очень хотел, чтобы можно было достичь того же с помощью всего лишь щелчка мышью!

16 ответов

Решение

Вот решение, которое будет работать, не добавляя тонны промежутков к документу (работает на Webkit и Mozilla и IE9 +):

http://jsfiddle.net/Vap7C/15/

<p class="clickable">some words</p>

$(".clickable").click(function(e) {
    s = window.getSelection();
    var range = s.getRangeAt(0);
    var node = s.anchorNode;
    while (range.toString().indexOf(' ') != 0) {
        range.setStart(node, (range.startOffset - 1));
    }
    range.setStart(node, range.startOffset + 1);
    do {
        range.setEnd(node, range.endOffset + 1);

    } while (range.toString().indexOf(' ') == -1 && range.toString().trim() != '' && range.endOffset < node.length);
    var str = range.toString().trim();
    alert(str);
});​

в IE8 возникают проблемы из-за getSelection. Эта ссылка ( есть ли кросс-браузерное решение для getSelection ()?) Может помочь с этими проблемами. Я не проверял на Опере.

Я использовал http://jsfiddle.net/Vap7C/1/ из аналогичного вопроса в качестве отправной точки. Он использовал функцию Selection.modify:

s.modify('extend','forward','word');
s.modify('extend','backward','word');

К сожалению, они не всегда получают полное слово. В качестве обходного пути я получил диапазон для выделения и добавил два цикла, чтобы найти границы слов. Первый продолжает добавлять символы в слово, пока оно не достигнет пробела. второй цикл идет до конца слова, пока не достигнет пробела.

Это также захватит любую пунктуацию в конце слова, поэтому убедитесь, что вы обрежете это, если вам нужно.

Вот улучшения для принятого ответа:

$(".clickable").click(function (e) {
    var selection = window.getSelection();
    if (!selection || selection.rangeCount < 1) return true;
    var range = selection.getRangeAt(0);
    var node = selection.anchorNode;
    var word_regexp = /^\w*$/;

    // Extend the range backward until it matches word beginning
    while ((range.startOffset > 0) && range.toString().match(word_regexp)) {
      range.setStart(node, (range.startOffset - 1));
    }
    // Restore the valid word match after overshooting
    if (!range.toString().match(word_regexp)) {
      range.setStart(node, range.startOffset + 1);
    }

    // Extend the range forward until it matches word ending
    while ((range.endOffset < node.length) && range.toString().match(word_regexp)) {
      range.setEnd(node, range.endOffset + 1);
    }
    // Restore the valid word match after overshooting
    if (!range.toString().match(word_regexp)) {
      range.setEnd(node, range.endOffset - 1);
    }

    var word = range.toString();
});​

Насколько я знаю, добавление span для каждого слова это единственный способ сделать это.

Вы могли бы рассмотреть использование Lettering.js, который обрабатывает разбиение для вас. Хотя это на самом деле не повлияет на производительность, если ваш "код разделения" неэффективен.

Тогда вместо привязки .click() каждому span было бы более эффективно связать один .click() к контейнеру span и проверить event.target чтобы увидеть, какой span была нажата.

И еще один взгляд на ответ @stevendaniel:

$('.clickable').click(function(){
   var sel=window.getSelection();
   var str=sel.anchorNode.nodeValue,len=str.length, a=b=sel.anchorOffset;
   while(str[a]!=' '&&a--){}; if (str[a]==' ') a++; // start of word
   while(str[b]!=' '&&b++<len){};                   // end of word+1
   console.log(str.substring(a,b));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<p class="clickable">The objective can also be achieved by simply analysing the
string you get from <code>sel=window.getSelection()</code>. Two simple searches for
the next blank before and after the word, pointed to by the current position
(<code>sel.anchorOffset</code>) and the work is done:</p>

<p>This second paragraph is <em>not</em> clickable. I tested this on Chrome and Internet explorer (IE11)</p>

Единственный кросс-браузерный (IE < 8) способ, который я знаю, это span элементы. Это ужасно, но не так медленно.

Этот пример взят прямо из документации по функции jQuery .css(), но с огромным блоком текста для предварительной обработки:

http://jsfiddle.net/kMvYy/

Вот еще один способ сделать это (данный здесь: jquery перехватывает значение слова) в том же блоке текста, который не требует переноса в span, http://jsfiddle.net/Vap7C/1

-EDIT- Как насчет этого? оно использует getSelection() привязан к mouseup

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
$(document).ready(function(){
    words = [];
    $("#myId").bind("mouseup",function(){
        word = window.getSelection().toString();
        if(word != ''){
            if( confirm("Add *"+word+"* to array?") ){words.push(word);}
        }
    });
    //just to see what we've got
    $('button').click(function(){alert(words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>

Я не могу придумать способ разделения, вот что я сделаю, маленький плагин, который будет разделен на spans и при нажатии он добавит свой контент в array для дальнейшего использования:

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
//plugin, take it to another file
(function( $ ){
$.fn.splitWords = function(ary) {
    this.html('<span>'+this.html().split(' ').join('</span> <span>')+'</span>');
    this.children('span').click(function(){
        $(this).css("background-color","#C0DEED");
        ary.push($(this).html());
    });
};
})( jQuery );
//plugin, take it to another file

$(document).ready(function(){
    var clicked_words = [];
    $('#myId').splitWords(clicked_words);
    //just to see what we've stored
    $('button').click(function(){alert(clicked_words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>

Здесь совершенно другой метод. Я не уверен в его практичности, но он может дать вам несколько разных идей. Вот что я подумаю, если у вас есть контейнерный тег с положением относительно него и текстом. Затем вы можете поместить интервал вокруг каждой записи слова с его смещением Высота, Ширина, Слева и Верх, а затем удалить интервал. Сохраните их в массив, а затем, когда в области будет щелчок, выполните поиск, чтобы выяснить, какое слово было наиболее близким к щелчку. Это, очевидно, будет интенсивным в начале. Так что это будет лучше всего работать в ситуации, когда человек будет тратить некоторое время на просмотр статьи. Преимущество в том, что вам не нужно беспокоиться о сотнях дополнительных элементов, но в лучшем случае это преимущество может быть незначительным.

Заметьте, я думаю, вы могли бы удалить элемент контейнера из DOM, чтобы ускорить процесс и все же получить расстояния смещения, но я не уверен.

Это продолжение моего комментария к ответу stevendaniels (выше):

В первом разделе кода выше range.setStart(node, (range.startOffset - 1)); падает при запуске первого слова в "узле", потому что он пытается установить диапазон в отрицательное значение. Я попытался добавить логику, чтобы предотвратить это, но затем последующий range.setStart(node, range.startOffset + 1); возвращает все, кроме первой буквы первого слова. Кроме того, когда слова разделены новой строкой, последнее слово в предыдущей строке возвращается в дополнение к нажатому слову. Итак, это требует некоторой работы.

Вот мой код, чтобы сделать код расширения диапазона в этом ответе надежно:

while (range.startOffset !== 0) {                   // start of node
    range.setStart(node, range.startOffset - 1)     // back up 1 char
    if (range.toString().search(/\s/) === 0) {      // space character
        range.setStart(node, range.startOffset + 1);// move forward 1 char
        break;
    }
}

while (range.endOffset < node.length) {         // end of node
    range.setEnd(node, range.endOffset + 1)     // forward 1 char
    if (range.toString().search(/\s/) !== -1) { // space character
        range.setEnd(node, range.endOffset - 1);// back 1 char
        break;
    }
}

Как и в случае с , это решение использует window.getSelectionдля определения позиции курсора в тексте. Он использует регулярное выражение для надежного поиска границы слова и не ограничивает начальный и конечный узлы одним и тем же узлом.

В этом коде есть следующие улучшения по сравнению с принятым ответом :

  • Работает в начале текста.
  • Позволяет выбирать между несколькими узлами.
  • Не изменяет диапазон выбора.
  • Позволяет пользователю переопределить диапазон с помощью настраиваемого выбора.
  • Обнаруживает слова, даже если они окружены пробелами (например, "\t\n")
  • Использует только ванильный JavaScript.
  • Никаких предупреждений!

Поскольку он использует стрелочные функции , этот код не работает в IE; но это легко изменить. Кроме того, поскольку он позволяет пользователю выбирать между узлами, он может возвращать текст, который обычно не виден пользователю, например, содержимое тега сценария, который существует в пределах выбора пользователя. (Трижды щелкните последний абзац, чтобы продемонстрировать этот недостаток.)

Вы должны решить, какие типы узлов следует видеть пользователю, и отфильтровать ненужные, что, как мне казалось, выходит за рамки вопроса.

Для полноты остальных ответов я добавлю пояснения к основным используемым методам:

  • window.getSelection(): это основной метод. Он используется для получения информации о выделении, которое вы сделали в тексте (нажатием кнопки мыши, перетаскиванием и затем отпусканием, а не простым щелчком). Он возвращаетобъект Selection, основными свойствами которого являются anchorOffset и focusOffset, которые представляют собой позицию первого и последнего выбранных символов соответственно. В случае, если это не имеет полного смысла, это описание привязки и фокуса, которые предлагает веб-сайт MDN, на который я ссылался ранее:

    Якорь, где пользователь начал отбор и фокус, где пользователь заканчивает выбор

    • toString(): этот метод возвращает выделенный текст.

    • anchorOffset: начальный индекс выделения в тексте узла, на котором вы сделали выбор.
      Если у вас есть этот html:

      <div>aaaa<span>bbbb cccc dddd</span>eeee/div>
      

      и вы выбираете 'cccc', затем anchorOffset == 5, потому что внутри узла выбор начинается с 5-го символа элемента html.

    • focusOffset: окончательный указатель выбора в тексте узла, на котором вы сделали выбор.
      Следуя предыдущему примеру, focusOffset == 9.

    • getRangeAt(): возвращаетобъект Range. Он получает индекс как параметр, потому что (я подозреваю, мне действительно нужно подтверждение этого) в некоторых браузерах, таких как Firefox, вы можете выбрать сразу несколько независимых текстов.

      • startOffset: это свойство Range аналогично anchorOffset.
      • endOffset: Как и ожидалось, это аналог focusOffset.
      • toString: аналогично методу toString() объекта Selection.

Помимо других решений, есть еще один метод, которого никто, кажется, не заметил: Document.caretRangeFromPoint()

Метод caretRangeFromPoint() интерфейса Document возвращает объект Range для фрагмента документа с указанными координатами.

Если вы перейдете по этой ссылке, вы увидите, как на самом деле документация предоставляет пример, который очень похож на то, что запрашивал OP. В этом примере не получается конкретное слово, на которое щелкнул пользователь, а вместо этого добавляется<br> сразу после символа, по которому щелкнул пользователь.

function insertBreakAtPoint(e) {
  let range;
  let textNode;
  let offset;

  if (document.caretPositionFromPoint) {
    range = document.caretPositionFromPoint(e.clientX, e.clientY);
    textNode = range.offsetNode;
    offset = range.offset;    
  } else if (document.caretRangeFromPoint) {
    range = document.caretRangeFromPoint(e.clientX, e.clientY);
    textNode = range.startContainer;
    offset = range.startOffset;
  }
  // Only split TEXT_NODEs
  if (textNode && textNode.nodeType == 3) {
    let replacement = textNode.splitText(offset);
    let br = document.createElement('br');
    textNode.parentNode.insertBefore(br, replacement);
  }
}

let paragraphs = document.getElementsByTagName("p");
for (let i = 0; i < paragraphs.length; i++) {
  paragraphs[i].addEventListener('click', insertBreakAtPoint, false);
}
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>

Это просто вопрос, чтобы получить слово, получив весь текст после предыдущего и перед следующими пустыми символами.

Выбранное решение иногда не работает с русскими текстами (показывает ошибку). Для русского и английского текстов я бы предложил следующее решение:

function returnClickedWord(){
    let selection = window.getSelection(),
        text = selection.anchorNode.data,
        index = selection.anchorOffset,
        symbol = "a";
    while(/[a-zA-z0-9а-яА-Я]/.test(symbol)&&symbol!==undefined){
        symbol = text[index--];
    }
    index += 2;
    let word = "";
    symbol = "a";
    while(/[a-zA-z0-9а-яА-Я]/.test(symbol) && index<text.length){
        symbol = text[index++];
    word += symbol;
    }
    alert(word);
}
document.addEventListener("click", returnClickedWord);

Вот альтернатива принятому ответу, работающая с кириллицей. Я не понимаю, зачем нужна проверка границ слов, но по умолчанию для меня выбор по какой-то причине свернут.

      let selection = window.getSelection();
if (!selection || selection.rangeCount < 1) return
let node = selection.anchorNode
let range = selection.getRangeAt(0)

let text = selection.anchorNode.textContent

let startIndex, endIndex
startIndex = endIndex = selection.anchorOffset
const expected = /[A-ZА-Я]*/i

function testSlice() {
  let slice = text.slice(startIndex, endIndex)
  return slice == slice.match(expected)[0]
}

while(startIndex > 0 && testSlice()) {
  startIndex -= 1
}
startIndex += 1

while(endIndex < text.length && testSlice()){
  endIndex += 1
}
endIndex -= 1

range.setStart(node, startIndex)
range.setEnd(node, endIndex)

let word = range.toString()
return word

анонимный пользователь предложил это редактирование: улучшенное решение, которое всегда получает нужное слово, проще и работает в IE 4+.

http://jsfiddle.net/Vap7C/80/

      document.body.addEventListener('click',(function() {
 // Gets clicked on word (or selected text if text is selected)
 var t = '';
 if (window.getSelection && (sel = window.getSelection()).modify) {
    // Webkit, Gecko
    var s = window.getSelection();
    if (s.isCollapsed) {
        s.modify('move', 'forward', 'character');
        s.modify('move', 'backward', 'word');
        s.modify('extend', 'forward', 'word');
        t = s.toString();
        s.modify('move', 'forward', 'character'); //clear selection
    }
    else {
        t = s.toString();
    }
  } else if ((sel = document.selection) && sel.type != "Control") {
    // IE 4+
    var textRange = sel.createRange();
    if (!textRange.text) {
        textRange.expand("word");
    }
    // Remove trailing spaces
    while (/\s$/.test(textRange.text)) {
        textRange.moveEnd("character", -1);
    }
    t = textRange.text;
 }
 alert(t);
});

Что выглядит как немного более простое решение.

document.addEventListener('selectionchange', () => {
  const selection = window.getSelection();
  const matchingRE = new RegExp(`^.{0,${selection.focusOffset}}\\s+(\\w+)`);
  const clickedWord = (matchingRE.exec(selectiaon.focusNode.textContent) || ['']).pop();
});

Я тестирую

Вот альтернатива, которая не подразумевает визуального изменения выбора диапазона.

      /**
 * Find a string from a selection
 */
export function findStrFromSelection(s: Selection) {
  const range = s.getRangeAt(0);
  const node = s.anchorNode;
  const content = node.textContent;

  let startOffset = range.startOffset;
  let endOffset = range.endOffset;
  // Find starting point
  // We move the cursor back until we find a space a line break or the start of the node
  do {
    startOffset--;
  } while (startOffset > 0 && content[startOffset - 1] != " " && content[startOffset - 1] != '\n');

  // Find ending point
  // We move the cursor forward until we find a space a line break or the end of the node
  do {
    endOffset++;
  } while (content[endOffset] != " " && content[endOffset] != '\n' && endOffset < content.length);
  
  return content.substring(startOffset, endOffset);
}
Другие вопросы по тегам