Преамбула:
Если у вас есть несколько интервалов и вложенный HTML, которые разделяют слова (или даже символы в словах), то все вышеприведенные решения будут иметь проблемы с возвратом полного и правильного слова.
Вот пример из вопроса о вознаграждении: Х</span>rт0съ
. Как правильно вернуть Хrт0съ
? Эти проблемы не были рассмотрены еще в 2010 году, поэтому сейчас я представлю два решения (2015).
Решение 1 - Снимите внутренние метки, оберните области вокруг каждого полного слова:
Одним из решений является удаление тегов span внутри абзацев, но сохранение их текста. Таким образом, разделенные слова и фразы объединяются в обычный текст. Каждое слово находится путем деления пробела (не просто пробела), и эти слова заключены в промежутки, к которым можно получить индивидуальный доступ.
В демоверсии вы можете выделить все слово и таким образом получить текст всего слова.
Код:
$(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 - Проверка каретки и обход DOM:
Вот более сложное решение. Это алгоритмическое решение с использованием обхода узла, которое точно фиксирует полное и правильное слово под курсором в текстовом узле.
Временное слово можно найти, проверив позицию каретки (используя caretPositionFromPoint
или caretRangeFromPoint
, кредиты за идею @chrisv). Это может быть, а может и не быть полным словом.
Затем он анализируется, чтобы увидеть, находится ли он на любом краю своего текстового узла (начало или конец). Если это так, предыдущий текстовый узел или следующий текстовый узел проверяется, чтобы увидеть, нужно ли его объединить, чтобы сделать этот фрагмент слова длиннее.
Пример:
Х</span>rт0съ
должен вернуть Хrт0съ
, а не Х
или rт0съ
.
Дерево DOM обходит, чтобы получить следующий небарьерный текстовый узел. Если два фрагмента слова разделены <p>
или каким-либо другим барьерным тегом, то они не являются смежными и, следовательно, не являются частью одного и того же слова.
Пример:
њб.)</p><p>Во
не должен возвращаться њб.)Во
В демоверсии левый плавающий div - это слово под курсором. Правый плавающий элемент div, если он виден, показывает, как образовалось слово на границе. Другие теги могут быть безопасно встроены в текст этого решения.
Код:
$(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>
( Примечание: Я позволил себе применить стили к тэгам span, которые присутствовали в вашем образце HTML, чтобы осветить границы текстовых узлов.)
(Работает в Chrome и IE до сих пор. Для IE метод из IERange должен был использоваться как прокладка для кросс-браузерной совместимости)