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>では機能しないようです。おそらくtextareaの場合のみ(カーソルをcontenteditableエンティティの最後に移動する方法


contentEditableIE以外のブラウザで動作することを知りませんでしたo_o
aditya

10
はい、それはadityaを行います。
GONeale 2009

5
aditya、Safari 2 +、Firefox 3+だと思います。
まぶたがない2009

divで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を試してみます)。divの最後に移動します。だから私はニコの解決策を受け入れます。なぜならそれはすべてのブラウザで互換性があり、うまく機能する傾向があるからです。あなたの努力に感謝します。
GONeale

3
何を知っていますか、私の最後の返答を忘れて、あなたとニコの両方をさらに調べた後、あなたは私が私の説明で求めたものではありませんが、私が好むものであり、私が必要だと気付いたでしょう。あなたは、通常のテキストボックスのように、フォーカスをアクティブにしたときにクリックした場所のカーソルの位置を<div>に正しく設定します。フォーカスを最後のポイントに戻すだけでは、ユーザーフレンドリーな入力フィールドを作成できません。ポイントを差し上げます。
GONeale

9
よく働く!ここで上記溶液のjsfiddleは次のとおりです。jsfiddle.net/s5xAr/3
ヴォーン

4
OPが完成し、フレームワークを使用したいと思っていても、実際のJavaScriptを投稿していただきありがとうございます。
John

cursorStart.appendChild(document.createTextNode('\u0002'));私たちが考える合理的な代替品です。の— char。コードをありがとう
twobob

97

このソリューションは、すべての主要なブラウザーで機能します。

saveSelection()はdivのonmouseupおよびonkeyupイベントにアタッチされ、選択を変数に保存しますsavedRange

restoreSelection()onfocusdiv のイベントにアタッチされ、に保存された選択を再選択しsavedRangeます。

これは、ユーザーがdivをクリックしたときに選択を復元したい場合を除いて、完全に機能します(通常、カーソルはクリックした場所に移動すると予想されますが、完全を期すためにコードが含まれているため、少し直観的ではありません)。

これを達成するためにonclickonmousedownイベントはイベントをキャンセルcancelEvent()するクロスブラウザ機能である関数によってキャンセルされます。このcancelEvent()関数は関数も実行しrestoreSelection()ます。これは、クリックイベントがキャンセルされると、divがフォーカスを受け取らず、この関数が実行されない限り、何も選択されないためです。

変数isInFocusは、フォーカスがあるかどうかを格納し、 "false" onblurおよび "true"に変更されますonfocus。これにより、divがフォーカスされていない場合にのみクリックイベントをキャンセルできます(それ以外の場合は、選択をまったく変更できません)。

divがクリックによってフォーカスされたときに選択を変更し、選択を復元しonclickない場合(およびプログラムを使用して要素にフォーカスが与えられている場合のみ)とイベントをdocument.getElementById("area").focus();削除します。イベントとと関数これらの状況でも安全に削除できます。onclickonmousedownonbluronDivBlur()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はい。コードのこの部分は、IEの古いバージョンで使用されgetSelectionている標準APIまたはレガシーdocument.selectionAPIのどちらを使用するかを決定しています。復元機能でチェックされる選択がない場合は、後のgetRangeAt (0)呼び出しで戻りnullます。
Nico Burns

@NicoBurnsは正しいですが、2番目の条件ブロック(else if (document.createRange))のコードは私が見ているものです。window.getSelection存在しない場合にのみ呼び出されますが、使用されますwindow.getSelection
Sandy Gifford '

@NicoBurnsはさらに、ブラウザwindow.getSelectionが見つかるとは思いませんがdocument.createRange、2番目のブロックは決して使用されません...
Sandy Gifford

19

更新

以下に投稿したコードの改良版を組み込んだRangyと呼ばれる、クロスブラウザーの範囲および選択ライブラリーを作成しました。この特定の質問には、選択の保存と復元モジュールを使用できますが、プロジェクトの選択で他に何もせず、大量のaを必要としない場合は、@ Nico Burnsの回答のようなものを使用したくなるでしょう図書館。

前の答え

IERange(http://code.google.com/p/ierange/)を使用して、IEのTextRangeをDOM範囲のようなものに変換し、まぶたのない状態の開始点のようなものと組み合わせて使用​​できます。個人的には、全体を使用するのではなく、Range <-> TextRange変換を実行するIERangeのアルゴリズムのみを使用します。IEのselectionオブジェクトには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のENDに設定する必要がありました。私はRangyのような本格的なライブラリを使いたくなかったし、多くのソリューションは非常に重すぎた。

最後に、カラットの位置をcontenteditable divの最後に設定するために、この単純な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;
}

理論は簡単です。編集可能要素の末尾にスパンを追加し、それを選択してから、スパンを削除します。divの最後にカーソルを置いたままにします。このソリューションを適用して、スパンを好きな場所に挿入し、カーソルを特定の場所に置くことができます。

使い方は簡単です:

$('#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();

このコードを変更して、エンドユーザーがキャレットを任意の位置に移動できるようにすることは可能ですか?
Zabs

はい。範囲オブジェクトでメソッドsetStartおよびsetEndを使用する必要があります。developer.mozilla.org/en-US/docs/Web/API/Range/setStart
zoonman 2018

0

Firefoxでは、子ノード(o_div.childNodes[0])に divのテキストがある場合があります

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);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.