Создание свернутого диапазона из позиции пикселя в 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 (и на самом деле единственный кросс-браузерный метод) - воссоздать содержащие блоки для текстовых узлов, а затем использовать положение пикселя внутри содержащего поля, чтобы вывести положение символа. Для этого вы должны:
    1. Оберните все текстовые узлы в элементы (информация об измерениях доступна только для элементов, но не для текстовых узлов)
    2. Вызовите span.getClientRects(), чтобы получить содержащие поля для каждого textNode (заключенного в ). Если текстовый узел занимает несколько строк, вы получите несколько полей.
    3. Найдите поле, в котором содержится ваша (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};
}
Другие вопросы по тегам