Wie setze ich die Caret (Cursor) Position in contenteditable element (div)?

188

Ich habe dieses einfache HTML als Beispiel:

<div id="editable" contenteditable="true">
  text text text<br>
  text text text<br>
  text text text<br>
</div>
<button id="button">focus</button>

Ich möchte eine einfache Sache - wenn ich auf die Schaltfläche klicke, möchte ich Caret (Cursor) an einer bestimmten Stelle im bearbeitbaren Div platzieren. Bei der Suche über das Internet habe ich diese JS an den Klick auf eine Schaltfläche angehängt, aber sie funktioniert nicht (FF, Chrome):

var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);

Ist es möglich, die Caret-Position so manuell einzustellen?

Frodik
quelle

Antworten:

257

In den meisten Browsern benötigen Sie die Objekte Rangeund Selection. Sie geben jede der Auswahlgrenzen als Knoten und Versatz innerhalb dieses Knotens an. Um das Caret beispielsweise auf das fünfte Zeichen der zweiten Textzeile zu setzen, gehen Sie wie folgt vor:

var el = document.getElementById("editable");
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el.childNodes[2], 5);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);

IE <9 funktioniert ganz anders. Wenn Sie diese Browser unterstützen müssen, benötigen Sie einen anderen Code.

jsFiddle-Beispiel: http://jsfiddle.net/timdown/vXnCM/

Tim Down
quelle
2
Ihre Lösung funktioniert perfekt. Vielen Dank. Gibt es eine Chance, dass es im "Textkontext" funktioniert - das bedeutet, dass Position 5 der fünfte Buchstabe auf einem Bildschirm und nicht der fünfte Buchstabe in einem Code ist?
Frodik
3
@Frodik: Sie können die setSelectionRange()Funktion aus der Antwort verwenden, die ich hier geschrieben habe: stackoverflow.com/questions/6240139/… . Wie ich in der Antwort bemerkt habe, gibt es verschiedene Dinge, die nicht richtig / konsistent gehandhabt werden, aber es kann gut genug sein.
Tim Down
7
Wie wäre es, wenn Sie das Caret in einem Span-Tag wie folgt setzen: << div id = "editable" contenteditable = "true"> test1 <br> test2 <br> <span> </ span> </ div>
Med Akram Z
1
@MalcolmOcean: Barf, weil IE <9 kein hat document.createRange(oder window.getSelection, aber es wird nicht so weit kommen).
Tim Down
1
@undroid: Die jsfiddle funktioniert gut für mich in Firefox 38.0.5 auf Mac.
Tim Down
60

Die meisten Antworten, die Sie zur inhaltsbearbeitbaren Cursorpositionierung finden, sind ziemlich simpel, da sie nur Eingaben mit einfachem Vanille-Text ermöglichen. Sobald Sie HTML-Elemente im Container verwenden, wird der eingegebene Text in Knoten aufgeteilt und großzügig über eine Baumstruktur verteilt.

Um die Cursorposition zu setzen, habe ich diese Funktion, die alle untergeordneten Textknoten innerhalb des angegebenen Knotens umkreist und einen Bereich vom Anfang des Anfangsknotens bis zum Zeichen chars.count festlegt :

function createRange(node, chars, range) {
    if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
    }

    if (chars.count === 0) {
        range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.length < chars.count) {
                chars.count -= node.textContent.length;
            } else {
                range.setEnd(node, chars.count);
                chars.count = 0;
            }
        } else {
           for (var lp = 0; lp < node.childNodes.length; lp++) {
                range = createRange(node.childNodes[lp], chars, range);

                if (chars.count === 0) {
                    break;
                }
            }
        }
    } 

    return range;
};

Ich rufe dann die Routine mit dieser Funktion auf:

function setCurrentCursorPosition(chars) {
    if (chars >= 0) {
        var selection = window.getSelection();

        range = createRange(document.getElementById("test").parentNode, { count: chars });

        if (range) {
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
};

Der range.collapse (false) setzt den Cursor auf das Ende des Bereichs. Ich habe es mit den neuesten Versionen von Chrome, IE, Mozilla und Opera getestet und alle funktionieren einwandfrei.

PS. Wenn jemand interessiert ist, erhalte ich die aktuelle Cursorposition mit diesem Code:

function isChildOf(node, parentId) {
    while (node !== null) {
        if (node.id === parentId) {
            return true;
        }
        node = node.parentNode;
    }

    return false;
};

function getCurrentCursorPosition(parentId) {
    var selection = window.getSelection(),
        charCount = -1,
        node;

    if (selection.focusNode) {
        if (isChildOf(selection.focusNode, parentId)) {
            node = selection.focusNode; 
            charCount = selection.focusOffset;

            while (node) {
                if (node.id === parentId) {
                    break;
                }

                if (node.previousSibling) {
                    node = node.previousSibling;
                    charCount += node.textContent.length;
                } else {
                     node = node.parentNode;
                     if (node === null) {
                         break
                     }
                }
           }
      }
   }

    return charCount;
};

Der Code macht das Gegenteil der Set-Funktion - er ruft das aktuelle window.getSelection (). FocusNode und focusOffset ab und zählt alle gefundenen Textzeichen rückwärts, bis er auf einen übergeordneten Knoten mit der ID containerId trifft. Die Funktion isChildOf prüft vor dem Ausführen, ob der angegebene Knoten tatsächlich ein untergeordnetes Element der angegebenen übergeordneten ID ist .

Der Code sollte ohne Änderung direkt funktionieren, aber ich habe ihn gerade aus einem von mir entwickelten jQuery-Plugin übernommen und ein paar davon gehackt - lassen Sie mich wissen, wenn etwas nicht funktioniert!

Liam
quelle
1
Könnten Sie bitte einen Überblick über diese Arbeit geben? Ich bin zu kämpfen, um herauszufinden , wie das funktioniert , wie ich bin mir nicht sicher , was node.idund parentIdbeziehen sich auf ohne Beispiel. Danke :)
Bendihossan
4
@Bendihossan - versuchen Sie es mit jsfiddle.net/nrx9yvw9/5 - aus irgendeinem Grund fügt der in diesem Beispiel bearbeitbare Div in Zeichen einige Zeichen und einen Wagenrücklauf am Anfang des Textes hinzu (es kann sogar sein, dass jsfiddle es selbst tut, wie es nicht tut auf meinem asp.net-Server nicht dasselbe tun).
Liam
@Bendihossan - Die HTML-Elemente innerhalb des inhaltsbearbeitbaren Div werden in eine Baumstruktur mit einem Knoten für jedes HTML-Element zerlegt. Die getCurrentCursorPosition ruft die aktuelle Auswahlposition ab und geht den Baum zurück, wobei gezählt wird, wie viele Klartextzeichen vorhanden sind. Node.id ist die HTML-Element-ID, während parentId auf die HTML-Element-ID verweist, auf die nicht mehr zurückgezählt werden soll
Liam
Das war wirklich großartig. Ich habe die Methoden für meine Verwendung in ein jQuery-Plugin verpackt, aber dann Ihren Kommentar erneut gelesen und Sie sagten, Sie hätten bereits einen. Haben Sie es irgendwo in einem Repo geteilt, auf das ich verweisen und das ich gutschreiben kann?
Dom Hastings
1
Es steht auf meiner Aufgabenliste, eine zu schreiben, die völlig unabhängig von meinem UI-Code ist - ich werde sie veröffentlichen, wenn ich eine Sekunde Zeit habe.
Liam
3

Wenn Sie jQuery nicht verwenden möchten, können Sie diesen Ansatz ausprobieren:

public setCaretPosition() {
    const editableDiv = document.getElementById('contenteditablediv');
    const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, '');
    const selection = window.getSelection();
    selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length);
}

editableDivWenn Sie ein bearbeitbares Element haben, vergessen Sie nicht, ein Element dafür festzulegen id. Dann müssen Sie Ihre innerHTMLaus dem Element holen und alle Bremsleitungen abschneiden. Und setzen Sie einfach den Zusammenbruch mit den nächsten Argumenten.

Volodymyr Khmil
quelle
3
  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }
Sagar M.
quelle
3

function set_mouse() {
  var as = document.getElementById("editable");
  el = as.childNodes[1].childNodes[0]; //goal is to get ('we') id to write (object Text) because it work only in object text
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el, 1);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);

  document.getElementById("we").innerHTML = el; // see out put of we id
}
<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd
  <p>dd</p>psss
  <p>dd</p>
  <p>dd</p>
  <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>

Es ist sehr schwer, Caret in die richtige Position zu bringen, wenn Sie ein fortgeschrittenes Element wie (p) (span) usw. haben. Das Ziel ist es, (Objekttext) zu erhalten:

<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p>
    <p>dd</p>
    <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>
<script>

    function set_mouse() {
        var as = document.getElementById("editable");
        el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text
        var range = document.createRange();
        var sel = window.getSelection();
        range.setStart(el, 1);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);

        document.getElementById("we").innerHTML = el;// see out put of we id
    }
</script>
Jalaluddin Rumi
quelle
1
Könnten Sie Ihre Antwort in ein ausführbares Code-Snippet umwandeln, um Ihre Antwort schnell testen zu können? Vielen Dank im Voraus.
Basj
1

Ich schreibe einen Syntax-Textmarker (und einen grundlegenden Code-Editor) und musste wissen, wie man ein Zeichen in einfachen Anführungszeichen automatisch eingibt und das Caret zurückschiebt (wie heutzutage viele Code-Editoren).

Hier ist ein Ausschnitt meiner Lösung, dank viel Hilfe von diesem Thread, den MDN-Dokumenten und viel Moz Console Watching.

//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event

Dies ist in einem inhaltsbearbeitbaren div-Element

Ich lasse dies hier als Dank, da mir klar wird, dass es bereits eine akzeptierte Antwort gibt.

Jonathan Crowder
quelle
1

Ich habe das für meinen einfachen Texteditor gemacht.

Unterschiede zu anderen Methoden:

  • Hochleistung
  • Funktioniert mit allen Räumen

Verwendung

// get current selection
const [start, end] = getSelectionOffset(container)

// change container html
container.innerHTML = newHtml

// restore selection
setSelectionOffset(container, start, end)

// use this instead innerText for get text with keep all spaces
const innerText = getInnerText(container)
const textBeforeCaret = innerText.substring(0, start)
const textAfterCaret = innerText.substring(start)

Auswahl.ts

/** return true if node found */
function searchNode(
    container: Node,
    startNode: Node,
    predicate: (node: Node) => boolean,
    excludeSibling?: boolean,
): boolean {
    if (predicate(startNode as Text)) {
        return true
    }

    for (let i = 0, len = startNode.childNodes.length; i < len; i++) {
        if (searchNode(startNode, startNode.childNodes[i], predicate, true)) {
            return true
        }
    }

    if (!excludeSibling) {
        let parentNode = startNode
        while (parentNode && parentNode !== container) {
            let nextSibling = parentNode.nextSibling
            while (nextSibling) {
                if (searchNode(container, nextSibling, predicate, true)) {
                    return true
                }
                nextSibling = nextSibling.nextSibling
            }
            parentNode = parentNode.parentNode
        }
    }

    return false
}

function createRange(container: Node, start: number, end: number): Range {
    let startNode
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            const dataLength = (node as Text).data.length
            if (start <= dataLength) {
                startNode = node
                return true
            }
            start -= dataLength
            end -= dataLength
            return false
        }
    })

    let endNode
    if (startNode) {
        searchNode(container, startNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                const dataLength = (node as Text).data.length
                if (end <= dataLength) {
                    endNode = node
                    return true
                }
                end -= dataLength
                return false
            }
        })
    }

    const range = document.createRange()
    if (startNode) {
        if (start < startNode.data.length) {
            range.setStart(startNode, start)
        } else {
            range.setStartAfter(startNode)
        }
    } else {
        if (start === 0) {
            range.setStart(container, 0)
        } else {
            range.setStartAfter(container)
        }
    }

    if (endNode) {
        if (end < endNode.data.length) {
            range.setEnd(endNode, end)
        } else {
            range.setEndAfter(endNode)
        }
    } else {
        if (end === 0) {
            range.setEnd(container, 0)
        } else {
            range.setEndAfter(container)
        }
    }

    return range
}

export function setSelectionOffset(node: Node, start: number, end: number) {
    const range = createRange(node, start, end)
    const selection = window.getSelection()
    selection.removeAllRanges()
    selection.addRange(range)
}

function hasChild(container: Node, node: Node): boolean {
    while (node) {
        if (node === container) {
            return true
        }
        node = node.parentNode
    }

    return false
}

function getAbsoluteOffset(container: Node, offset: number) {
    if (container.nodeType === Node.TEXT_NODE) {
        return offset
    }

    let absoluteOffset = 0
    for (let i = 0, len = Math.min(container.childNodes.length, offset); i < len; i++) {
        const childNode = container.childNodes[i]
        searchNode(childNode, childNode, node => {
            if (node.nodeType === Node.TEXT_NODE) {
                absoluteOffset += (node as Text).data.length
            }
            return false
        })
    }

    return absoluteOffset
}

export function getSelectionOffset(container: Node): [number, number] {
    let start = 0
    let end = 0

    const selection = window.getSelection()
    for (let i = 0, len = selection.rangeCount; i < len; i++) {
        const range = selection.getRangeAt(i)
        if (range.intersectsNode(container)) {
            const startNode = range.startContainer
            searchNode(container, container, node => {
                if (startNode === node) {
                    start += getAbsoluteOffset(node, range.startOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                start += dataLength
                end += dataLength

                return false
            })

            const endNode = range.endContainer
            searchNode(container, startNode, node => {
                if (endNode === node) {
                    end += getAbsoluteOffset(node, range.endOffset)
                    return true
                }

                const dataLength = node.nodeType === Node.TEXT_NODE
                    ? (node as Text).data.length
                    : 0

                end += dataLength

                return false
            })

            break
        }
    }

    return [start, end]
}

export function getInnerText(container: Node) {
    const buffer = []
    searchNode(container, container, node => {
        if (node.nodeType === Node.TEXT_NODE) {
            buffer.push((node as Text).data)
        }
        return false
    })
    return buffer.join('')
}
Nikolay Makhonin
quelle
0

Ich denke, es ist nicht einfach, Caret auf eine Position in einem inhaltsbearbeitbaren Element zu setzen. Ich habe meinen eigenen Code dafür geschrieben. Es umgeht den Knotenbaum, der berechnet, wie viele Zeichen noch übrig sind, und setzt Caret in das benötigte Element. Ich habe diesen Code nicht viel getestet.

//Set offset in current contenteditable field (for start by default or for with forEnd=true)
function setCurSelectionOffset(offset, forEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return;

    const firstRange = sel.getRangeAt(0);

    if (offset > 0) {
        bypassChildNodes(document.activeElement, offset);
    }else{
        if (forEnd)
            firstRange.setEnd(document.activeElement, 0);
        else
            firstRange.setStart(document.activeElement, 0);
    }



    //Bypass in depth
    function bypassChildNodes(el, leftOffset) {
        const childNodes = el.childNodes;

        for (let i = 0; i < childNodes.length && leftOffset; i++) {
            const childNode = childNodes[i];

            if (childNode.nodeType === 3) {
                const curLen = childNode.textContent.length;

                if (curLen >= leftOffset) {
                    if (forEnd)
                        firstRange.setEnd(childNode, leftOffset);
                    else
                        firstRange.setStart(childNode, leftOffset);
                    return 0;
                }else{
                    leftOffset -= curLen;
                }
            }else
            if (childNode.nodeType === 1) {
                leftOffset = bypassChildNodes(childNode, leftOffset);
            }
        }

        return leftOffset;
    }
}

Ich habe auch Code geschrieben, um die aktuelle Caret-Position zu erhalten (nicht getestet):

//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true)
function getCurSelectionOffset(calcEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return 0;

    const firstRange     = sel.getRangeAt(0),
          startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer,
          startOffset    = calcEnd ? firstRange.endOffset    : firstRange.startOffset;
    let needStop = false;

    return bypassChildNodes(document.activeElement);



    //Bypass in depth
    function bypassChildNodes(el) {
        const childNodes = el.childNodes;
        let ans = 0;

        if (el === startContainer) {
            if (startContainer.nodeType === 3) {
                ans = startOffset;
            }else
            if (startContainer.nodeType === 1) {
                for (let i = 0; i < startOffset; i++) {
                    const childNode = childNodes[i];

                    ans += childNode.nodeType === 3 ? childNode.textContent.length :
                           childNode.nodeType === 1 ? childNode.innerText.length :
                           0;
                }
            }

            needStop = true;
        }else{
            for (let i = 0; i < childNodes.length && !needStop; i++) {
                const childNode = childNodes[i];
                ans += bypassChildNodes(childNode);
            }
        }

        return ans;
    }
}

Sie müssen sich auch darüber im Klaren sein, dass range.startOffset und range.endOffset einen Zeichenversatz für Textknoten (nodeType === 3) und einen untergeordneten Knotenversatz für Elementknoten (nodeType === 1) enthalten. range.startContainer und range.endContainer können sich auf jeden Elementknoten einer beliebigen Ebene im Baum beziehen (natürlich können sie sich auch auf Textknoten beziehen).

vitaliydev
quelle
0

Basierend auf der Antwort von Tim Down, aber es wird nach der letzten bekannten "guten" Textzeile gesucht. Der Cursor befindet sich ganz am Ende.

Außerdem könnte ich das letzte Kind jedes aufeinanderfolgenden letzten Kindes rekursiv / iterativ überprüfen, um den absolut letzten "guten" Textknoten im DOM zu finden.

function onClickHandler() {
  setCaret(document.getElementById("editable"));
}

function setCaret(el) {
  let range = document.createRange(),
      sel = window.getSelection(),
      lastKnownIndex = -1;
  for (let i = 0; i < el.childNodes.length; i++) {
    if (isTextNodeAndContentNoEmpty(el.childNodes[i])) {
      lastKnownIndex = i;
    }
  }
  if (lastKnownIndex === -1) {
    throw new Error('Could not find valid text content');
  }
  let row = el.childNodes[lastKnownIndex],
      col = row.textContent.length;
  range.setStart(row, col);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
  el.focus();
}

function isTextNodeAndContentNoEmpty(node) {
  return node.nodeType == Node.TEXT_NODE && node.textContent.trim().length > 0
}
<div id="editable" contenteditable="true">
  text text text<br>text text text<br>text text text<br>
</div>
<button id="button" onclick="onClickHandler()">focus</button>

Mr. Polywhirl
quelle