Создание свернутого диапазона из позиции пикселя в FF/Webkit
Используя JavaScript, я хотел бы создать свернутый диапазон из позиции пикселя, чтобы вставить новые узлы в поток документа после диапазона, определенного этой позицией.
Это можно сделать с помощью объекта TextRange в Internet Exporer ( метод moveToPoint(x, y)).
Как я могу сделать это в FireFox и Webkit?
Я могу получить элемент контейнера из позиции с помощью document.elementFromPoint (x, y). Но когда положение оказывается внутри текстового узла, как я могу получить больше информации о смещении текста, необходимого для построения диапазона?
5 ответов
Вот моя реализация caretRangeFromPoint
для старых браузеров:
if (!document.caretRangeFromPoint) {
document.caretRangeFromPoint = function(x, y) {
var log = "";
function inRect(x, y, rect) {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}
function inObject(x, y, object) {
var rects = object.getClientRects();
for (var i = rects.length; i--;)
if (inRect(x, y, rects[i]))
return true;
return false;
}
function getTextNodes(node, x, y) {
if (!inObject(x, y, node))
return [];
var result = [];
node = node.firstChild;
while (node) {
if (node.nodeType == 3)
result.push(node);
if (node.nodeType == 1)
result = result.concat(getTextNodes(node, x, y));
node = node.nextSibling;
}
return result;
}
var element = document.elementFromPoint(x, y);
var nodes = getTextNodes(element, x, y);
if (!nodes.length)
return null;
var node = nodes[0];
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, 1);
for (var i = nodes.length; i--;) {
var node = nodes[i],
text = node.nodeValue;
range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, text.length);
if (!inObject(x, y, range))
continue;
for (var j = text.length; j--;) {
if (text.charCodeAt(j) <= 32)
continue;
range = document.createRange();
range.setStart(node, j);
range.setEnd(node, j + 1);
if (inObject(x, y, range)) {
range.setEnd(node, j);
return range;
}
}
}
return range;
};
}
Вот результат моего исследования для получения позиции символа внутри текстового узла из позиции пикселя:
- Стандартизированный способ: получить диапазон из позиции с помощью document.caretRangeFromPoint(x, y). См. Спецификацию на W3c. Это именно то, что я искал. Проблема в том, что Chrome является единственным веб-браузером, который реализует этот метод на момент написания статьи (июль 2010 г.)
- MS IE путь с проприетарным textRange.moveToPoint (x, y).
- Путь Firefox: если позиция пикселя (x, y) извлекается из события мыши, тогда Firefox добавит два полезных свойства к объекту события: rangParent и rangeOffset
- Для Safari & Opera (и на самом деле единственный кросс-браузерный метод) - воссоздать содержащие блоки для текстовых узлов, а затем использовать положение пикселя внутри содержащего поля, чтобы вывести положение символа. Для этого вы должны:
- Оберните все текстовые узлы в элементы (информация об измерениях доступна только для элементов, но не для текстовых узлов)
- Вызовите span.getClientRects(), чтобы получить содержащие поля для каждого textNode (заключенного в ). Если текстовый узел занимает несколько строк, вы получите несколько полей.
- Найдите поле, в котором содержится ваша (x, y) позиция пикселя, и выведите положение символа с помощью простого "правила трех", основанного на общей ширине пикселя и длине текста.
Под MSIE вы написали:
var range = document.selection.createRange();
range.moveToPoint(x, y);
Для других браузеров идея состоит в том, чтобы определить элемент HTML в позиции x/y и создать для него выделение из одного символа. На основе range.getBoundingClientRect()
, вы можете определить, если один символ выбора, если до или после позиции х / у. Затем мы можем выбрать следующий символ, пока позиция выбора не изменится на позицию x/y. Я написал следующую реализацию для Firefox, Safari и Chrome:
var nodeInfo = getSelectionNodeInfo(x, y);
var range = document.createRange();
range.setStart(nodeInfo.node, nodeInfo.offsetInsideNode);
range.setEnd(nodeInfo.node, nodeInfo.offsetInsideNode);
/**
Emulates MSIE function range.moveToPoint(x,y) b
returning the selection node info corresponding
to the given x/y location.
@param x the point X coordinate
@param y the point Y coordinate
@return the node and offset in characters as
{node,offsetInsideNode} (e.g. can be passed to range.setStart)
*/
function getSelectionNodeInfo(x, y) {
var startRange = document.createRange();
window.getSelection().removeAllRanges();
window.getSelection().addRange(startRange);
// Implementation note: range.setStart offset is
// counted in number of child elements if any or
// in characters if there is no childs. Since we
// want to compute in number of chars, we need to
// get the node which has no child.
var elem = document.elementFromPoint(x, y);
var startNode = (elem.childNodes.length>0?elem.childNodes[0]:elem);
var startCharIndexCharacter = -1;
do {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter+1);
var rangeRect = startRange.getBoundingClientRect();
} while (rangeRect.left<x && startCharIndexCharacter<startNode.length-1);
return {node:startNode, offsetInsideNode:startCharIndexCharacter};
}
Эти два фрагмента кода были протестированы в:
- MSIE 7, MSIE 9
- Firefox 5, Firefox 10
- Chrome 9
- Safari 5
Следующие ситуации не были проверены:
- проблемы с коэффициентом масштабирования
- HTML-элементы с более чем одной текстовой строкой
С тех пор, как этот вопрос был опубликован, ситуация изменилась, и большинство ответов было опубликовано: все основные браузеры теперь имеют по крайней мере один из методов, которые делают это относительно простым:
- Основанный на стандартах подход из CSSOM View spec:
document.caretPositionFromPoint()
- Собственная версия WebKit того же:
document.caretRangeFromPoint()
- IE проприетарный
TextRange
объект, который имеетmoveToPoint()
метод, который принимает пиксельные координаты. Тем не менее, кажется, чтоmoveToPoint()
может быть глючит (см. здесь и здесь, например); Мне просто повезло, что это сработало во всех документах, в которых я его использовал.
Обратите внимание, что в IE до версии 11 включительно созданный объект является TextRange
а не DOM Range
, В версиях IE, которые поддерживают Range
нет простого способа конвертировать между ними, хотя, если вы хотите возиться с выделением, вы можете сделать что-то вроде следующего, предполагая, что у вас есть TextRange, хранящийся в переменной с именем textRange
:
textRange.select();
var range = window.getSelection().getRangeAt(0);
Вот пример кода. Он работает в IE 5+, Edge, Safari и Chrome примерно с 2010 года, Firefox >= 20 и Opera >= 15.
Демонстрационная версия: http://jsfiddle.net/timdown/rhgyw2dg/
Код:
function createCollapsedRangeFromPoint(x, y) {
var doc = document;
var position, range = null;
if (typeof doc.caretPositionFromPoint != "undefined") {
position = doc.caretPositionFromPoint(x, y);
range = doc.createRange();
range.setStart(position.offsetNode, position.offset);
range.collapse(true);
} else if (typeof doc.caretRangeFromPoint != "undefined") {
range = doc.caretRangeFromPoint(x, y);
} else if (typeof doc.body.createTextRange != "undefined") {
range = doc.body.createTextRange();
range.moveToPoint(x, y);
}
return range;
}
Расширение ответа Жюльена выше. Это справляется с несколькими строками. Нужно немного подправить, но, похоже, работает. Он находит количество строк, получая высоту начального и конечного выделения, а также высоту выделения из одной буквы, деления двух и округления. Возможно, есть ситуации, когда это не сработает, но для большинства целей...
function getLineCount(node, range) {
if ((node) && (range)) {
range.setStart(node, 0);
range.setEnd(node, 1);
var r = range.getBoundingClientRect();
var h1 = r.bottom - r.top;
range.setEnd(node, node.length);
r = range.getBoundingClientRect();
return Math.round((r.bottom - r.top) / h1);
}
};
Вот измененная версия приведенного выше кода, использующая приведенную выше процедуру подсчета строк. Он также немного лучше справляется с выделениями внутри узла, но справа от самого текста. Ничто из этого не оптимизировано, но мы здесь находимся во времени пользователя, так что миллисекунды, вероятно, не слишком важны.
function getSelectionNodeInfo(x, y) {
var startRange = document.createRange();
window.getSelection().removeAllRanges();
window.getSelection().addRange(startRange);
// Implementation note: range.setStart offset is
// counted in number of child elements if any or
// in characters if there is no childs. Since we
// want to compute in number of chars, we need to
// get the node which has no child.
var elem = document.elementFromPoint(x, y);
console.log("ElementFromPoint: " + $(elem).attr('class'));
var startNode = (elem.childNodes.length>0?elem.childNodes[0]:elem);
var lines = getLineCount(startNode, startRange);
console.log("Lines: " + lines);
var startCharIndexCharacter = 0;
startRange.setStart(startNode, 0);
startRange.setEnd(startNode, 1);
var letterCount = startNode.length;
var rangeRect = startRange.getBoundingClientRect();
var rangeWidth = 0
if (lines>1) {
while ((rangeRect.bottom < y) && (startCharIndexCharacter < (letterCount-1))) {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter + 1);
rangeRect = startRange.getBoundingClientRect();
rangeWidth = rangeRect.right - rangeRect.left
}
}
while (rangeRect.left < (x-(rangeWidth/2)) && (startCharIndexCharacter < (letterCount))) {
startCharIndexCharacter++;
startRange.setStart(startNode, startCharIndexCharacter);
startRange.setEnd(startNode, startCharIndexCharacter + ((startCharIndexCharacter<letterCount) ? 1 : 0));
rangeRect = startRange.getBoundingClientRect();
rangeWidth = rangeRect.right - rangeRect.left
}
return {node:startNode, offsetInsideNode:startCharIndexCharacter};
}