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


142

Мені після остаточного рішення для крос-браузера встановити позицію курсора / каретки на останню відому позицію, коли contentEditable = 'on' <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>. Можливо, лише для textarea ( Як перемістити курсор до кінця вмістового об'єкта )


Я не знаю , contentEditableпрацював в НЕ IE браузерах о_О
Адітья

10
Так, це робить aditya.
GONeale

5
aditya, Safari 2+, Firefox 3+ я думаю.
безвічність

Спробуйте встановити tabindex = "0" на діві. Це повинно зробити його орієнтованим у більшості браузерів.
Токімон

Відповіді:


58

Це сумісно з браузерами на основі стандартів, але, ймовірно, не вдасться в IE. Я надаю це як вихідний пункт. IE не підтримує діапазон DOM.

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);
};

Дякую, я спробував ваше рішення, я трохи поспішав, але після того, як його роз'єднати, він розміщує лише позицію "-" в останній точці фокусування (яка, здається, маркер налагодження?), І ось тоді ми втрачаємо фокус, здається, не відновлює курсор / карету, коли я натискаю назад (принаймні, не в Chrome, я спробую FF), це просто переходить до кінця розділу. Тож я прийму рішення Ніко, оскільки знаю, що воно сумісне у всіх браузерах, і прагне добре працювати. Дуже дякую за ваші зусилля.
GONeale

3
Ви знаєте що, забудьте мою останню відповідь, після подальшого вивчення і вашої, і Ніко, ваша - це не те, про що я просив у своєму описі, але це те, що я віддаю перевагу і зрозумів би, що мені потрібно. Ваш коректно встановлює положення курсору, де ви клацаєте, активуючи фокус назад до <div>, як звичайне текстове поле. Відновлення фокусу до останньої точки недостатньо для створення зручного поля для введення. Я присуджую вам бали.
GONeale

9
Чудово працює! Ось jsfiddle вищезазначеного рішення: jsfiddle.net/s5xAr/3
vaughan

4
Дякуємо, що опублікували справжній JavaScript, навіть незважаючи на те, що ОП вийшов з ладу та хотів використовувати рамку.
Джон

cursorStart.appendChild(document.createTextNode('\u0002'));ми вважаємо розумною заміною. для - char. Дякую за код
twobob

97

Це рішення працює у всіх основних браузерах:

saveSelection()додається до onmouseupі onkeyupподій діва та зберігає вибір до змінної savedRange.

restoreSelection()додається до onfocusподії div і перебирає збережений у ньому вибір savedRange.

Це прекрасно працює, якщо ви не хочете, щоб вибір було відновлено, коли користувач натискає на div div (що трохи неінвазивно, оскільки зазвичай ви очікуєте, що курсор піде туди, куди ви натискаєте, але код включений для повноти)

Щоб досягти цього, onclickі onmousedownподії скасовуються функцією, cancelEvent()яка є функцією крос-браузера для скасування події. cancelEvent()Функція також запускає restoreSelection()функцію , так як подія клацання скасував ДІВ не отримує фокус , і тому нічого не вибрано взагалі , якщо ця функція не працюватиме.

Змінна isInFocusзберігає, чи знаходиться вона у фокусі, чи змінюється на "хибна" onblurта "справжня" onfocus. Це дозволяє скасувати події клацання лише в тому випадку, якщо 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, чи не є вибір чи ні?
Сенді Гіффорд,

@Sandy Так. Ця частина коду вирішує, чи використовувати стандартний getSelectionapi чи застарілий document.selectionapi, що використовується у старих версіях IE. Пізніший getRangeAt (0)виклик повернеться, nullякщо немає вибору, що перевіряється у функції відновлення.
Ніко Бернс

@NicoBurns правильно, але код у другому умовному блоці ( else if (document.createRange)) - це те, на що я дивлюся. Він буде називатися лише у тому випадку, якщо window.getSelectionйого не існує, але він використовуєwindow.getSelection
Sandy Gifford

@NicoBurns, крім того, я не думаю, що ви знайдете браузер, window.getSelectionале ні document.createRange- значить, другий блок ніколи не буде використаний ...
Сенді Гіффорд,

19

Оновлення

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

Попередня відповідь

Ви можете використовувати IERange ( http://code.google.com/p/ierange/ ) для перетворення TextRange IE в щось на зразок діапазону DOM і використовувати його в поєднанні з чимось на кшталт початкової точки безвікості. Особисто я використовував би лише алгоритми IERange, які роблять перетворення діапазону <-> TextRange, а не використовують цілу справу. І об'єкт відбору IE не має властивостей focusNode та anchorNode, але ви повинні мати можливість просто використовувати діапазон / 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

У мене була пов’язана ситуація, коли мені спеціально потрібно було встановити позицію курсора в КІНЦЕ дива, що міститься на диску. Я не хотів використовувати повноцінну бібліотеку, як Rangy, і багато рішень були надто важкими.

Врешті-решт я придумав цю просту функцію jQuery, щоб встановити положення карата в кінці змістовного поділу:

$.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;
}

Теорія проста: додайте проміжок до кінця редагованого, виберіть його, а потім видаліть проміжок - залишаючи нас курсором у кінці розділу. Ви можете адаптувати це рішення, щоб вставити проміжок куди завгодно, тим самим поставивши курсор на певне місце.

Використання просте:

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

Це воно!


3
Вам не потрібно вставляти те <span>, що випадково порушить вбудований стек скасування в браузері. Дивіться stackoverflow.com/a/4238971/96100
Tim Down

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();
    }
});


@salivan Я знаю, що пізно її оновити, але я думаю, що це працює зараз. В основному я додав нову умову і змінив з використання id елемента на індекс елемента, який повинен існувати завжди :)
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);
    });
};

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();

чи можна змінити цей код, щоб дозволити кінцевому користувачеві все-таки перемістити карету в будь-яку позицію, яку він хоче?
Забс

Так. Ви повинні використовувати методи setStart & setEnd на об'єкті діапазону. developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman

0

У Firefox у вас може бути текст діва в дочірньому вузлі ( 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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.