Установить позицию курсора на contentEditable <div>

142

Я пришел к окончательному кросс-браузерному решению, чтобы установить позицию курсора / каретки в последнюю известную позицию, когда contentEditable = 'on' <div> восстанавливает фокус. Кажется, по умолчанию функциональность редактируемого содержимого div заключается в перемещении курсора / курсора в начало текста в div при каждом нажатии на него, что нежелательно.

Я полагаю, что мне пришлось бы хранить в переменной текущую позицию курсора, когда они покидают фокус div, и затем переустанавливать это, когда они снова фокусируются внутри, но я не смог собрать или найти рабочее Пример кода пока нет.

Если у кого-то есть мысли, рабочие фрагменты кода или примеры, я буду рад их видеть.

У меня пока нет кода, но вот что у меня есть:

<script type="text/javascript">
// jQuery
$(document).ready(function() {
   $('#area').focus(function() { .. }  // focus I would imagine I need.
}
</script>
<div id="area" contentEditable="true"></div>

PS. Я пробовал этот ресурс, но, похоже, он не работает для <div>. Возможно, только для текстовой области ( как переместить курсор в конец объекта contenteditable )

GONeale
источник
Я не знал, contentEditableработал в браузерах не IE o_o
aditya
10
Да, это делает адитья.
Goneale
5
адитья, сафари 2+, Firefox 3+ я думаю.
век
Попробуйте установить tabindex = "0" на div. Это должно сделать его фокусируемым в большинстве браузеров.
Токимон

Ответы:

58

Это совместимо со стандартными браузерами, но, вероятно, не удастся в IE. Я предоставляю это как отправную точку. IE не поддерживает DOM Range.

var editable = document.getElementById('editable'),
    selection, range;

// Populates selection and range variables
var captureSelection = function(e) {
    // Don't capture selection outside editable region
    var isOrContainsAnchor = false,
        isOrContainsFocus = false,
        sel = window.getSelection(),
        parentAnchor = sel.anchorNode,
        parentFocus = sel.focusNode;

    while(parentAnchor && parentAnchor != document.documentElement) {
        if(parentAnchor == editable) {
            isOrContainsAnchor = true;
        }
        parentAnchor = parentAnchor.parentNode;
    }

    while(parentFocus && parentFocus != document.documentElement) {
        if(parentFocus == editable) {
            isOrContainsFocus = true;
        }
        parentFocus = parentFocus.parentNode;
    }

    if(!isOrContainsAnchor || !isOrContainsFocus) {
        return;
    }

    selection = window.getSelection();

    // Get range (standards)
    if(selection.getRangeAt !== undefined) {
        range = selection.getRangeAt(0);

    // Get range (Safari 2)
    } else if(
        document.createRange &&
        selection.anchorNode &&
        selection.anchorOffset &&
        selection.focusNode &&
        selection.focusOffset
    ) {
        range = document.createRange();
        range.setStart(selection.anchorNode, selection.anchorOffset);
        range.setEnd(selection.focusNode, selection.focusOffset);
    } else {
        // Failure here, not handled by the rest of the script.
        // Probably IE or some older browser
    }
};

// Recalculate selection while typing
editable.onkeyup = captureSelection;

// Recalculate selection after clicking/drag-selecting
editable.onmousedown = function(e) {
    editable.className = editable.className + ' selecting';
};
document.onmouseup = function(e) {
    if(editable.className.match(/\sselecting(\s|$)/)) {
        editable.className = editable.className.replace(/ selecting(\s|$)/, '');
        captureSelection();
    }
};

editable.onblur = function(e) {
    var cursorStart = document.createElement('span'),
        collapsed = !!range.collapsed;

    cursorStart.id = 'cursorStart';
    cursorStart.appendChild(document.createTextNode('—'));

    // Insert beginning cursor marker
    range.insertNode(cursorStart);

    // Insert end cursor marker if any text is selected
    if(!collapsed) {
        var cursorEnd = document.createElement('span');
        cursorEnd.id = 'cursorEnd';
        range.collapse();
        range.insertNode(cursorEnd);
    }
};

// Add callbacks to afterFocus to be called after cursor is replaced
// if you like, this would be useful for styling buttons and so on
var afterFocus = [];
editable.onfocus = function(e) {
    // Slight delay will avoid the initial selection
    // (at start or of contents depending on browser) being mistaken
    setTimeout(function() {
        var cursorStart = document.getElementById('cursorStart'),
            cursorEnd = document.getElementById('cursorEnd');

        // Don't do anything if user is creating a new selection
        if(editable.className.match(/\sselecting(\s|$)/)) {
            if(cursorStart) {
                cursorStart.parentNode.removeChild(cursorStart);
            }
            if(cursorEnd) {
                cursorEnd.parentNode.removeChild(cursorEnd);
            }
        } else if(cursorStart) {
            captureSelection();
            var range = document.createRange();

            if(cursorEnd) {
                range.setStartAfter(cursorStart);
                range.setEndBefore(cursorEnd);

                // Delete cursor markers
                cursorStart.parentNode.removeChild(cursorStart);
                cursorEnd.parentNode.removeChild(cursorEnd);

                // Select range
                selection.removeAllRanges();
                selection.addRange(range);
            } else {
                range.selectNode(cursorStart);

                // Select range
                selection.removeAllRanges();
                selection.addRange(range);

                // Delete cursor marker
                document.execCommand('delete', false, null);
            }
        }

        // Call callbacks here
        for(var i = 0; i < afterFocus.length; i++) {
            afterFocus[i]();
        }
        afterFocus = [];

        // Register selection again
        captureSelection();
    }, 10);
};
eyelidlessness
источник
Спасибо, я попробовал ваше решение, я немного торопился, но после его подключения он помещает только позицию "-" в последнюю точку фокусировки (которая, по-видимому, является маркером отладки?), И вот когда мы проигрываем фокус, кажется, он не восстанавливает курсор / каретку, когда я нажимаю назад (по крайней мере, не в Chrome, я попробую FF), он просто идет в конец div. Поэтому я приму решение Nico, потому что я знаю, что оно совместимо во всех браузерах и работает хорошо. Большое спасибо за ваши усилия, хотя.
GONeale
3
Знаете ли вы, что, забудьте мой последний ответ, после дальнейшего изучения как вашего, так и Нико, ваш не тот, о котором я просил в моем описании, а то, что я предпочитаю и понял бы, что мне нужно. Ваш правильно устанавливает положение курсора, в котором вы щелкаете при активизации фокуса обратно на <div>, как обычное текстовое поле. Восстановление фокуса до последней точки недостаточно для создания удобного поля ввода. Я награжу вас очками.
Goneale
9
Прекрасно работает! Вот jsfiddle вышеупомянутого решения: jsfiddle.net/s5xAr/3
vaughan
4
Спасибо за публикацию настоящего JavaScript, даже несмотря на то, что ОП не захотел использовать фреймворк.
Джон
cursorStart.appendChild(document.createTextNode('\u0002'));мы считаем разумной заменой. для - символ Спасибо за код
twobob
97

Это решение работает во всех основных браузерах:

saveSelection()прикрепляются к onmouseupи onkeyupсобытиям DIV и сохраняет выбор в переменном savedRange.

restoreSelection()присоединяется к onfocusсобытию div и повторно выбирает сохраненный в savedRange.

Это работает отлично, если только вы не хотите, чтобы выбор был восстановлен, когда пользователь щелкает div также (что немного неинтуитивно, поскольку обычно вы ожидаете, что курсор будет идти туда, куда вы щелкаете, но код включен для полноты)

Для достижения этой цели onclickи onmousedownмероприятия отменяются функции , cancelEvent()которая является кросс функция браузера , чтобы отменить событие. cancelEvent()Функция также запускает restoreSelection()функцию , так как событие щелчок отменило ДИВ не получает фокус , и поэтому ничего не выбрано вообще , если эта функция не будет работать.

Переменная isInFocusхранит, находится ли она в фокусе и изменена на «ложь» onblurи «истина» onfocus. Это позволяет отменять события щелчка, только если div не находится в фокусе (в противном случае вы вообще не сможете изменить выделение).

Если вы хотите к выбору быть изменения , когда DIV фокусировалось щелчком, а не восстановить выбор onclick(и только тогда , когда внимание уделяется элементу ими программным использованием document.getElementById("area").focus();или аналогичных затем просто удалить onclickи onmousedownсобытия. В onblurмероприятии и onDivBlur()и cancelEvent()функции также может быть безопасно удален в этих обстоятельствах.

Этот код должен работать, если он помещен непосредственно в тело html-страницы, если вы хотите быстро его протестировать:

<div id="area" style="width:300px;height:300px;" onblur="onDivBlur();" onmousedown="return cancelEvent(event);" onclick="return cancelEvent(event);" contentEditable="true" onmouseup="saveSelection();" onkeyup="saveSelection();" onfocus="restoreSelection();"></div>
<script type="text/javascript">
var savedRange,isInFocus;
function saveSelection()
{
    if(window.getSelection)//non IE Browsers
    {
        savedRange = window.getSelection().getRangeAt(0);
    }
    else if(document.selection)//IE
    { 
        savedRange = document.selection.createRange();  
    } 
}

function restoreSelection()
{
    isInFocus = true;
    document.getElementById("area").focus();
    if (savedRange != null) {
        if (window.getSelection)//non IE and there is already a selection
        {
            var s = window.getSelection();
            if (s.rangeCount > 0) 
                s.removeAllRanges();
            s.addRange(savedRange);
        }
        else if (document.createRange)//non IE and no selection
        {
            window.getSelection().addRange(savedRange);
        }
        else if (document.selection)//IE
        {
            savedRange.select();
        }
    }
}
//this part onwards is only needed if you want to restore selection onclick
var isInFocus = false;
function onDivBlur()
{
    isInFocus = false;
}

function cancelEvent(e)
{
    if (isInFocus == false && savedRange != null) {
        if (e && e.preventDefault) {
            //alert("FF");
            e.stopPropagation(); // DOM style (return false doesn't always work in FF)
            e.preventDefault();
        }
        else {
            window.event.cancelBubble = true;//IE stopPropagation
        }
        restoreSelection();
        return false; // false = IE style
    }
}
</script>
Нико Бернс
источник
1
Спасибо, это действительно работает! Протестировано в IE, Chrome и FF последней. Извините за супер отложенный ответ =)
GONeale
Не будет ли if (window.getSelection)...проверяться только поддержка браузером getSelection, нет ли выбора?
Сэнди Гиффорд
@ Санди Да, именно так. Эта часть кода решает, использовать ли стандартный getSelectionAPI или устаревший document.selectionAPI, используемый в более старых версиях IE. Более поздний getRangeAt (0)вызов вернется, nullесли нет выбора, который проверяется в функции восстановления.
Нико Бернс
@NicoBurns верно, но код во втором условном блоке ( else if (document.createRange)) - это то, на что я смотрю. Он будет вызываться только в том случае, если window.getSelectionего не существует, но используетwindow.getSelection
Сэнди Гиффорд
@NicoBurns, кроме того, я не думаю, что вы найдете браузер с, window.getSelectionно не document.createRange- значит, второй блок никогда не будет использован ...
Сэнди Гиффорд
19

Обновить

Я написал кросс-браузерную библиотеку диапазонов и выбора под названием Rangy, которая включает улучшенную версию кода, который я разместил ниже. Вы можете использовать модуль выбора и сохранения для этого конкретного вопроса, хотя я бы соблазнился использовать что-то вроде ответа @Nico Burns, если вы ничего не делаете с выборами в своем проекте и вам не нужна основная часть библиотека.

Предыдущий ответ

Вы можете использовать IERange ( http://code.google.com/p/ierange/ ), чтобы преобразовать TextRange в IE во что-то вроде диапазона DOM и использовать его вместе с чем-то вроде отправной точки отсутствия век. Лично я бы использовал только алгоритмы от IERange, которые выполняют преобразования Range <-> TextRange, а не используют все это. И объект выбора IE не имеет свойств focusNode и anchorNode, но вы должны иметь возможность просто использовать Range / TextRange, полученный из выбора.

Я мог бы что-то собрать, чтобы сделать это, отправлю сюда, если и когда я сделаю.

РЕДАКТИРОВАТЬ:

Я создал демо сценария, который делает это. Это работает во всем, что я пробовал до сих пор, за исключением ошибки в Opera 9, которую я еще не успел изучить. Браузеры, в которых он работает, - это IE 5.5, 6 и 7, Chrome 2, Firefox 2, 3 и 3.5 и Safari 4, все в Windows.

http://www.timdown.co.uk/code/selections/

Обратите внимание, что выбор может быть сделан в браузерах в обратном направлении, так что узел фокуса находится в начале выделения, а нажатие клавиши курсора вправо или влево переместит курсор в положение относительно начала выделения. Я не думаю, что это возможно повторить при восстановлении выделения, поэтому фокусный узел всегда находится в конце выделения.

Я напишу это полностью в какой-то момент в ближайшее время.

Тим Даун
источник
15

У меня была похожая ситуация, когда мне нужно было установить позицию курсора на КОНЕЦ contenteditable div. Я не хотел использовать полноценную библиотеку, такую ​​как Rangy, и многие решения были слишком тяжелыми.

В конце концов, я придумал эту простую функцию jQuery, чтобы установить позицию в каратах до конца contenteditable div:

$.fn.focusEnd = function() {
    $(this).focus();
    var tmp = $('<span />').appendTo($(this)),
        node = tmp.get(0),
        range = null,
        sel = null;

    if (document.selection) {
        range = document.body.createTextRange();
        range.moveToElementText(node);
        range.select();
    } else if (window.getSelection) {
        range = document.createRange();
        range.selectNode(node);
        sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
    tmp.remove();
    return this;
}

Теория проста: добавьте интервал в конец редактируемого, выберите его, а затем удалите интервал - оставив нам курсор в конце элемента div. Вы можете адаптировать это решение для вставки диапазона, где вы хотите, таким образом, поместив курсор в определенное место.

Использование простое:

$('#editable').focusEnd();

Это оно!

Зейн Клас
источник
3
Вам не нужно вставлять <span>, что случайно сломает встроенный в браузер стек отмены. См. Stackoverflow.com/a/4238971/96100
Тим Даун
6

Я взял ответ Нико Бернса и сделал его с помощью jQuery:

  • Универсальный: для каждого div contentEditable="true"
  • Более короткие

Вам понадобится jQuery 1.6 или выше:

savedRanges = new Object();
$('div[contenteditable="true"]').focus(function(){
    var s = window.getSelection();
    var t = $('div[contenteditable="true"]').index(this);
    if (typeof(savedRanges[t]) === "undefined"){
        savedRanges[t]= new Range();
    } else if(s.rangeCount > 0) {
        s.removeAllRanges();
        s.addRange(savedRanges[t]);
    }
}).bind("mouseup keyup",function(){
    var t = $('div[contenteditable="true"]').index(this);
    savedRanges[t] = window.getSelection().getRangeAt(0);
}).on("mousedown click",function(e){
    if(!$(this).is(":focus")){
        e.stopPropagation();
        e.preventDefault();
        $(this).focus();
    }
});

Gatsbimantico
источник
@salivan Я знаю, что уже поздно его обновлять, но я думаю, что теперь это работает. По сути, я добавил новое условие и изменил использование идентификатора элемента в индексе элемента, который должен существовать всегда :)
Gatsbimantico
4

После игры я изменил ответ без век выше и сделал его плагином jQuery, чтобы вы могли просто сделать один из них:

var html = "The quick brown fox";
$div.html(html);

// Select at the text "quick":
$div.setContentEditableSelection(4, 5);

// Select at the beginning of the contenteditable div:
$div.setContentEditableSelection(0);

// Select at the end of the contenteditable div:
$div.setContentEditableSelection(html.length);

Извините за длинный пост кода, но это может кому-то помочь:

$.fn.setContentEditableSelection = function(position, length) {
    if (typeof(length) == "undefined") {
        length = 0;
    }

    return this.each(function() {
        var $this = $(this);
        var editable = this;
        var selection;
        var range;

        var html = $this.html();
        html = html.substring(0, position) +
            '<a id="cursorStart"></a>' +
            html.substring(position, position + length) +
            '<a id="cursorEnd"></a>' +
            html.substring(position + length, html.length);
        console.log(html);
        $this.html(html);

        // Populates selection and range variables
        var captureSelection = function(e) {
            // Don't capture selection outside editable region
            var isOrContainsAnchor = false,
                isOrContainsFocus = false,
                sel = window.getSelection(),
                parentAnchor = sel.anchorNode,
                parentFocus = sel.focusNode;

            while (parentAnchor && parentAnchor != document.documentElement) {
                if (parentAnchor == editable) {
                    isOrContainsAnchor = true;
                }
                parentAnchor = parentAnchor.parentNode;
            }

            while (parentFocus && parentFocus != document.documentElement) {
                if (parentFocus == editable) {
                    isOrContainsFocus = true;
                }
                parentFocus = parentFocus.parentNode;
            }

            if (!isOrContainsAnchor || !isOrContainsFocus) {
                return;
            }

            selection = window.getSelection();

            // Get range (standards)
            if (selection.getRangeAt !== undefined) {
                range = selection.getRangeAt(0);

                // Get range (Safari 2)
            } else if (
                document.createRange &&
                selection.anchorNode &&
                selection.anchorOffset &&
                selection.focusNode &&
                selection.focusOffset
            ) {
                range = document.createRange();
                range.setStart(selection.anchorNode, selection.anchorOffset);
                range.setEnd(selection.focusNode, selection.focusOffset);
            } else {
                // Failure here, not handled by the rest of the script.
                // Probably IE or some older browser
            }
        };

        // Slight delay will avoid the initial selection
        // (at start or of contents depending on browser) being mistaken
        setTimeout(function() {
            var cursorStart = document.getElementById('cursorStart');
            var cursorEnd = document.getElementById('cursorEnd');

            // Don't do anything if user is creating a new selection
            if (editable.className.match(/\sselecting(\s|$)/)) {
                if (cursorStart) {
                    cursorStart.parentNode.removeChild(cursorStart);
                }
                if (cursorEnd) {
                    cursorEnd.parentNode.removeChild(cursorEnd);
                }
            } else if (cursorStart) {
                captureSelection();
                range = document.createRange();

                if (cursorEnd) {
                    range.setStartAfter(cursorStart);
                    range.setEndBefore(cursorEnd);

                    // Delete cursor markers
                    cursorStart.parentNode.removeChild(cursorStart);
                    cursorEnd.parentNode.removeChild(cursorEnd);

                    // Select range
                    selection.removeAllRanges();
                    selection.addRange(range);
                } else {
                    range.selectNode(cursorStart);

                    // Select range
                    selection.removeAllRanges();
                    selection.addRange(range);

                    // Delete cursor marker
                    document.execCommand('delete', false, null);
                }
            }

            // Register selection again
            captureSelection();
        }, 10);
    });
};
mkaj
источник
3

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

var el = document.getElementById('idOfYoursContentEditable');
var selection = window.getSelection();
var range = document.createRange();
selection.removeAllRanges();
range.selectNodeContents(el);
range.collapse(false);
selection.addRange(range);
el.focus();
zoonman
источник
Можно ли изменить этот код, чтобы позволить конечному пользователю все еще перемещать каретку в любую позицию, которую он хочет?
Заб
Да. Вы должны использовать методы setStart & setEnd для объекта диапазона. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman
0

В Firefox у вас может быть текст div в дочернем узле ( o_div.childNodes[0])

var range = document.createRange();

range.setStart(o_div.childNodes[0],last_caret_pos);
range.setEnd(o_div.childNodes[0],last_caret_pos);
range.collapse(false);

var sel = window.getSelection(); 
sel.removeAllRanges();
sel.addRange(range);
Йоав
источник