Выделить диапазон текста с помощью JavaScript

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

Пример:

<div>abcd<em>efg</em>hij</div>

highlight(2, 6) нужно выделить "cdef", не удаляя тег.

Я уже пытался использовать объект TextRange, но безуспешно.

Заранее спасибо!


person Vincent    schedule 05.06.2011    source источник
comment
Можете ли вы удалить теги во временной строке, а затем подстроку из этой строки?   -  person kevinji    schedule 05.06.2011
comment
Вы не можете просто игнорировать теги, иначе вы получите недействительный html: ab<x>cd<em>ef</x>f</em>. Вам нужно будет сделать что-то вроде ab<x>cd</x><em><x>ef</x>g</em>   -  person serg    schedule 05.06.2011
comment
Конечно, я не могу игнорировать теги, но было бы неплохо, если бы браузер каким-то образом решал эти проблемы за меня.   -  person Vincent    schedule 05.06.2011


Ответы (4)


Ниже приведена функция для выбора пары смещений символов внутри определенного элемента. Это наивная реализация: она не принимает во внимание любой текст, который может быть сделан невидимым (либо с помощью CSS, либо, например, находясь внутри элемента <script> или <style>) и может иметь несоответствия браузера (IE по сравнению со всем остальным) с разрывами строк, и не учитывает свернутые пробелы (например, 2 или более последовательных пробела, свернутые в один видимый пробел на странице). Тем не менее, он работает для вашего примера во всех основных браузерах.

Что касается другой части, выделения, я бы предложил использовать для этого document.execCommand(). Вы можете использовать мою функцию ниже, чтобы установить выбор, а затем вызвать document.execCommand(). Вам нужно сделать документ временно редактируемым в браузерах, отличных от IE, чтобы команда работала. См. мой ответ здесь для кода: getSelection & SurroundContents по нескольким тегам

Вот пример jsFiddle, показывающий все это, работающий во всех основных браузерах: http://jsfiddle.net/8mdX4/1211/

И код настройки выбора:

function getTextNodesIn(node) {
    var textNodes = [];
    if (node.nodeType == 3) {
        textNodes.push(node);
    } else {
        var children = node.childNodes;
        for (var i = 0, len = children.length; i < len; ++i) {
            textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
        }
    }
    return textNodes;
}

function setSelectionRange(el, start, end) {
    if (document.createRange && window.getSelection) {
        var range = document.createRange();
        range.selectNodeContents(el);
        var textNodes = getTextNodesIn(el);
        var foundStart = false;
        var charCount = 0, endCharCount;

        for (var i = 0, textNode; textNode = textNodes[i++]; ) {
            endCharCount = charCount + textNode.length;
            if (!foundStart && start >= charCount
                    && (start < endCharCount ||
                    (start == endCharCount && i <= textNodes.length))) {
                range.setStart(textNode, start - charCount);
                foundStart = true;
            }
            if (foundStart && end <= endCharCount) {
                range.setEnd(textNode, end - charCount);
                break;
            }
            charCount = endCharCount;
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (document.selection && document.body.createTextRange) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(el);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    }
}
person Tim Down    schedule 05.06.2011
comment
Блестящее решение @tim-down! Ваш код адаптирован для объединения вложенного текста в формате HTML. stackoverflow.com/questions/16226671 / - person Mike Wolfe; 27.04.2013
comment
Когда я запускаю этот код, тег span добавляется ко всему выделенному тексту. Могу ли я добавить класс и событие щелчка в этот тег span? - person Prateek; 13.09.2013
comment
@prateek: я полагаю, вы имеете в виду код подсветки? Поскольку промежутки добавляются браузером автоматически, найти и изменить их непросто. Вы можете использовать модуль применения класса моего Rangy. - person Tim Down; 13.09.2013
comment
@TimDown разве это невозможно при создании тегов span? Я могу добавить некоторые атрибуты и события, например, вы добавили цвет фона - person Prateek; 13.09.2013
comment
@prateek: промежутки, применяющие цвет фона, генерируются браузером в ответ на простой вызов document.execCommand(...). Вы можете искать диапазоны, проверяя вычисленное значение background-color для каждого диапазона в документе, как в stackoverflow.com/questions/8076341/ - person Tim Down; 13.09.2013
comment
@TimDown спасибо за ваше предложение, в моем случае он отлично работает, но когда иерархия тегов больше, он не проверяет эту скрипку. Я использую здесь ваш код javascript. jsfiddle.net/Bvd9d/47 - person Prateek; 13.09.2013
comment
В этом есть ошибка. Этот тест: (start == endCharCount && i ‹ textNodes.length) Должно быть: (start == endCharCount && i ‹= textNodes.length) Поскольку вы уже увеличили i в цикле (поэтому для последнего текстового узла это в этот момент будет равно textNodes.length). - person Brandon Paddock; 16.06.2015
comment
@BrandonPaddock: Хорошее место, спасибо. Обычно я избегаю использования этой формы цикла for, если только не знаю, что мне не нужно использовать счетчик циклов именно по этой причине. - person Tim Down; 16.06.2015
comment
@TimDown это отличное решение, и я использую эту функцию, она отлично работает для меня. я также выделяю текст с помощью функции document.execcommand. Но можете ли вы дать решение, как я могу снова удалить цвет фона или выделенный текст, который я добавил через document.execcommand? - person Gitesh Purbia; 17.04.2017
comment
@TimDown - большое спасибо за замечательный код. У меня есть требование выделить разные диапазоны в одном документе. Когда я использую selectAndHighlightRange() несколько раз. Он не показывает тот же желтый цвет на всех. Как я могу это исправить? - person Amod Gokhale; 10.06.2021

Вы можете посмотреть, как работает эта мощная утилита JavaScript, которая поддерживает выбор нескольких элементов DOM:

МАША (сокращение от Mark & ​​Share) позволяет вам отмечать интересные части содержимого веб-страницы и делиться ими

http://mashajs.com/index_eng.html

Он также доступен на GitHub https://github.com/SmartTeleMax/MaSha.

Работает даже в Mobile Safari и IE!

person Nico Pernice    schedule 06.07.2012

Следующее решение не работает для IE, для этого вам нужно применить объекты TextRange и т.д. Поскольку для этого используются выборки, в обычных случаях он не должен нарушать HTML, например:

<div>abcd<span>efg</span>hij</div>

С highlight(3,6);

выходы:

<div>abc<em>d<span>ef</span></em><span>g</span>hij</div>

Обратите внимание, как он переносит первый символ за пределами диапазона в em, а затем остальные символы в пределах span в новый. Где, как если бы он просто открывал его на символе 3 и заканчивал бы на символе 6, это дало бы недопустимую разметку, например:

<div>abc<em>d<span>ef</em>g</span>hij</div>

Код:

var r = document.createRange();
var s = window.getSelection()

r.selectNode($('div')[0]);
s.removeAllRanges();
s.addRange(r);

// not quite sure why firefox has problems with this
if ($.browser.webkit) {
    s.modify("move", "backward", "documentboundary");
}

function highlight(start,end){
    for(var st=0;st<start;st++){
        s.modify("move", "forward", "character");
    }

    for(var st=0;st<(end-start);st++){
        s.modify("extend", "forward", "character");
    }
}

highlight(2,6);

var ra = s.getRangeAt(0);
var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents()); 
ra.insertNode(newNode);

Пример: http://jsfiddle.net/niklasvh/4NDb9/

изменить Похоже, по крайней мере, у моего FF4 были проблемы с

s.modify("move", "backward", "documentboundary");

но в то же время вроде и без него работает, поэтому просто поменял на

if ($.browser.webkit) {
        s.modify("move", "backward", "documentboundary");
}

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

Код:

var r = document.createRange();
var s = window.getSelection()

var pos = 0;

function dig(el){
    $(el).contents().each(function(i,e){
        if (e.nodeType==1){
            // not a textnode
         dig(e);   
        }else{
            if (pos<start){
               if (pos+e.length>=start){
                range.setStart(e, start-pos);
               }
            }

            if (pos<end){
               if (pos+e.length>=end){
                range.setEnd(e, end-pos);
               }
            }            

            pos = pos+e.length;
        }
    });  
}
var start,end, range;

function highlight(element,st,en){
    range = document.createRange();
    start = st;
    end = en;
    dig(element);
    s.addRange(range);

}
highlight($('div'),3,6);

var ra = s.getRangeAt(0);

var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents()); 
ra.insertNode(newNode);

пример: http://jsfiddle.net/niklasvh/4NDb9/

person Niklas    schedule 05.06.2011
comment
Firefox реализует Selection.modify() только начиная с Firefox 4.0 и не поддерживает все настройки детализации, которые поддерживает WebKit. В частности, он не поддерживает documentboundary. См. developer.mozilla.org/en/DOM/selection/modify. - person Tim Down; 05.06.2011
comment
Кажется, это работает, только если на странице больше ничего нет. См., например: jsfiddle.net/4NDb9/89. Хотя Hello world находится за пределами div, он выделяется вместо текста внутри div. - person Vincent; 05.06.2011
comment
@Tim Down Очень хороший момент. Я переписал большую его часть, чтобы теперь избавиться от метода Modify(). - person Niklas; 05.06.2011

На основе идей плагина jQuery.highlight.

    private highlightRange(selector: JQuery, start: number, end: number): void {
        let cur = 0;
        let replacements: { node: Text; pos: number; len: number }[] = [];

        let dig = function (node: Node): void {
            if (node.nodeType === 3) {
                let nodeLen = (node as Text).data.length;
                let next = cur + nodeLen;
                if (next > start && cur < end) {
                    let pos = cur >= start ? cur : start;
                    let len = (next < end ? next : end) - pos;
                    if (len > 0) {
                        if (!(pos === cur && len === nodeLen && node.parentNode &&
                            node.parentNode.childNodes && node.parentNode.childNodes.length === 1 &&
                            (node.parentNode as Element).tagName === 'SPAN' && (node.parentNode as Element).className === 'highlight1')) {

                            replacements.push({
                                node: node as Text,
                                pos: pos - cur,
                                len: len,
                            });
                        }
                    }
                }
                cur = next;
            }
            else if (node.nodeType === 1) {
                let childNodes = node.childNodes;
                if (childNodes && childNodes.length) {
                    for (let i = 0; i < childNodes.length; i++) {
                        dig(childNodes[i]);
                        if (cur >= end) {
                            break;
                        }
                    }
                }
            }
        };

        selector.each(function (index, element): void {
            dig(element);
        });

        for (let i = 0; i < replacements.length; i++) {
            let replacement = replacements[i];
            let highlight = document.createElement('span');
            highlight.className = 'highlight1';
            let wordNode = replacement.node.splitText(replacement.pos);
            wordNode.splitText(replacement.len);
            let wordClone = wordNode.cloneNode(true);
            highlight.appendChild(wordClone);
            wordNode.parentNode.replaceChild(highlight, wordNode);
        }
    }
person Bill    schedule 19.03.2018