Come impostare la posizione del cursore (cursore) nell'elemento contenteditable (div)?


191

Ho questo semplice HTML come esempio:

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

Voglio una cosa semplice: quando faccio clic sul pulsante, voglio posizionare il cursore (cursore) in un punto specifico nel div modificabile. Dalla ricerca sul Web, ho questo JS collegato al clic sul pulsante, ma non funziona (FF, Chrome):

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

È possibile impostare manualmente la posizione del cursore in questo modo?

Risposte:


261

Nella maggior parte dei browser, sono necessari gli oggetti Rangee Selection. È possibile specificare ciascuno dei limiti di selezione come nodo e offset all'interno di quel nodo. Ad esempio, per impostare il punto di inserimento sul quinto carattere della seconda riga di testo, esegui le seguenti operazioni:

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 funziona in modo completamente diverso. Se devi supportare questi browser, avrai bisogno di un codice diverso.

Esempio jsFiddle: http://jsfiddle.net/timdown/vXnCM/


2
La tua soluzione funziona perfettamente. Molte grazie. C'è una possibilità che possa essere fatto funzionare in "contesto testuale" - ciò significa che la posizione n. 5 sarebbe la quinta lettera su uno schermo e non la quinta lettera in un codice?
Frodik,

3
@Frodik: è possibile utilizzare la setSelectionRange()funzione dalla risposta che ho scritto qui: stackoverflow.com/questions/6240139/… . Come ho notato nella risposta, ci sono varie cose che non gestirà correttamente / coerentemente ma potrebbe essere abbastanza buono.
Tim Down,

7
che ne dici di impostare il cursore all'interno di un tag span come questo: << div id = "editable" contenteditable = "true"> test1 <br> test2 <br> <span> </span> </div>
Med Akram Z

1
@MalcolmOcean: Barf, perché IE <9 non ha document.createRange(o window.getSelection, ma non andrà così lontano).
Tim Down,

1
@undroid: jsfiddle funziona bene per me in Firefox 38.0.5 su Mac.
Tim Down

62

La maggior parte delle risposte che trovi sul posizionamento del cursore contenteditable sono abbastanza semplicistiche in quanto soddisfano solo input con testo semplice alla vaniglia. Dopo aver utilizzato gli elementi HTML nel contenitore, il testo inserito viene suddiviso in nodi e distribuito liberamente su una struttura ad albero.

Per impostare la posizione del cursore ho questa funzione che avvolge tutti i nodi di testo figlio all'interno del nodo fornito e imposta un intervallo dall'inizio del nodo iniziale al carattere chars.count :

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

Quindi chiamo la routine con questa funzione:

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

Range.collapse (false) imposta il cursore alla fine dell'intervallo. L'ho provato con le ultime versioni di Chrome, IE, Mozilla e Opera e funzionano tutti bene.

PS. Se qualcuno è interessato ottengo la posizione corrente del cursore usando questo codice:

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

Il codice fa l'opposto della funzione set - ottiene l'attuale window.getSelection (). FocusNode e focusOffset e conta all'indietro tutti i caratteri di testo incontrati fino a quando non colpisce un nodo padre con id di containerId. La funzione isChildOf verifica solo prima di eseguire che il nodo fornito sia in realtà un figlio del parentId fornito .

Il codice dovrebbe funzionare dritto senza cambiamento, ma ho appena preso da un plugin jQuery che ho sviluppato in modo da aver violato un paio di questo di - fatemi sapere se qualcosa non funziona!


1
Potresti fornire un jsfiddle di questo lavoro per favore? Sto lottando per capire come funziona, dato che non sono sicuro di cosa node.ide di cui mi parentIdriferisco senza un esempio. Grazie :)
Bendihossan,

4
@Bendihossan - prova questo jsfiddle.net/nrx9yvw9/5 - per qualche motivo il div modificabile del contenuto in questo esempio sta aggiungendo alcuni caratteri e un ritorno a capo all'inizio del testo (potrebbe anche essere jsfiddle stesso a farlo come non ; t faccio lo stesso sul mio server asp.net).
Liam,

@Bendihossan: gli elementi html all'interno del div contenteditable vengono suddivisi in una struttura ad albero con un nodo per ogni elemento html. GetCurrentCursorPosition ottiene la posizione di selezione corrente e risale l'albero contando il numero di caratteri di testo normale presenti. Node.id è l'id dell'elemento html, mentre parentId si riferisce all'id dell'elemento html su cui dovrebbe smettere di contare
Liam

1
È sulla mia lista delle cose da fare per scriverne uno completamente separato dal mio codice UI - lo posterò quando avrò un secondo.
Liam,

1
Per poter testare rapidamente le diverse soluzioni, è possibile modificare la risposta in frammenti di codice eseguibili? Grazie in anticipo.
Basj,

3

Se non vuoi usare jQuery puoi provare questo approccio:

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

editableDivelemento modificabile, non dimenticare di impostarne uno id. Quindi è necessario estrarre innerHTMLl'elemento dall'elemento e tagliare tutte le linee dei freni. E basta impostare il collasso con i prossimi argomenti.


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

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>

È molto difficile impostare il cursore nella posizione corretta quando si hanno elementi avanzati come (p) (span) ecc. L'obiettivo è quello di ottenere (testo dell'oggetto):

<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>

1
Per poter testare rapidamente la tua risposta, potresti modificare la tua risposta in uno snippet di codice eseguibile? Grazie in anticipo.
Basj,

1

Sto scrivendo un evidenziatore di sintassi (e un editor di codice di base) e avevo bisogno di sapere come digitare automaticamente un singolo carattere di virgoletta e spostare indietro il cursore (come molti editor di codice al giorno d'oggi).

Ecco un frammento della mia soluzione, grazie al grande aiuto di questo thread, dei documenti MDN e di molte console moz che guardano ...

//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

Questo è in un elemento div contenteditable

Lascio questo qui come ringraziamento, rendendosi conto che esiste già una risposta accettata.


1

L'ho fatto per il mio semplice editor di testo.

Differenze rispetto ad altri metodi:

  • Alte prestazioni
  • Funziona con tutti gli spazi

uso

// 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)

selection.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('')
}

1

Ho refactored la risposta di Liam. L'ho messo in una classe con metodi statici, ho fatto in modo che le sue funzioni ricevessero un elemento anziché un #id, e alcune altre piccole modifiche.

Questo codice è particolarmente utile per fissare il cursore in una casella di testo ricca con cui potresti creare <div contenteditable="true">. Ero bloccato su questo per diversi giorni prima di arrivare al codice qui sotto.

modifica: la sua risposta e questa risposta hanno un bug che coinvolge colpire invio. Poiché enter non conta come carattere, la posizione del cursore viene incasinata dopo aver premuto invio. Se riesco a correggere il codice, aggiornerò la mia risposta.

edit2: risparmiati un sacco di mal di testa e assicurati che lo <div contenteditable=true>sia display: inline-block. Questo risolve alcuni bug relativi alla messa in Chrome <div>invece che <br>quando premi Invio.

Come usare

let richText = document.getElementById('rich-text');
let offset = Cursor.getCurrentCursorPosition(richText);
// do stuff to the innerHTML, such as adding/removing <span> tags
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus();

Codice

// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
    static getCurrentCursorPosition(parentElement) {
        var selection = window.getSelection(),
            charCount = -1,
            node;
        
        if (selection.focusNode) {
            if (Cursor._isChildOf(selection.focusNode, parentElement)) {
                node = selection.focusNode; 
                charCount = selection.focusOffset;
                
                while (node) {
                    if (node === parentElement) {
                        break;
                    }

                    if (node.previousSibling) {
                        node = node.previousSibling;
                        charCount += node.textContent.length;
                    } else {
                        node = node.parentNode;
                        if (node === null) {
                            break;
                        }
                    }
                }
            }
        }
        
        return charCount;
    }
    
    static setCurrentCursorPosition(chars, element) {
        if (chars >= 0) {
            var selection = window.getSelection();
            
            let range = Cursor._createRange(element, { count: chars });

            if (range) {
                range.collapse(false);
                selection.removeAllRanges();
                selection.addRange(range);
            }
        }
    }
    
    static _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 = Cursor._createRange(node.childNodes[lp], chars, range);

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

        return range;
    }
    
    static _isChildOf(node, parentElement) {
        while (node !== null) {
            if (node === parentElement) {
                return true;
            }
            node = node.parentNode;
        }

        return false;
    }
}

0

Penso che non sia semplice impostare il cursore su una posizione nell'elemento contenteditable. Ho scritto il mio codice per questo. Ignora l'albero dei nodi calcolando il numero di caratteri rimasti e imposta il punto di inserimento nell'elemento necessario. Non ho testato molto questo codice.

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

Ho anche scritto un codice per ottenere la posizione del punto di inserimento corrente (non testato):

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

È inoltre necessario essere consapevoli di range.startOffset e range.endOffset contengono offset dei caratteri per i nodi di testo (nodeType === 3) e offset dei nodi figlio per i nodi di elementi (nodeType === 1). range.startContainer e range.endContainer possono fare riferimento a qualsiasi nodo elemento di qualsiasi livello nella struttura (ovviamente possono anche fare riferimento a nodi di testo).


0

Basato sulla risposta di Tim Down, ma controlla l'ultima riga di testo "buona" conosciuta. Posiziona il cursore proprio alla fine.

Inoltre, ho anche potuto controllare ricorsivamente / iterativamente l'ultimo figlio di ogni ultimo figlio consecutivo per trovare l'ultimo nodo di testo "buono" assoluto nel DOM.

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>

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.