contenteditable element (div)에서 캐럿 (커서) 위치를 설정하는 방법은 무엇입니까?


190

이 간단한 HTML을 예로 들어 보겠습니다.

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

간단한 것을 원합니다. 버튼을 클릭하면 캐럿 (커서)을 편집 가능한 div의 특정 위치에 배치하고 싶습니다. 웹 검색에서 버튼 클릭 에이 JS가 첨부되었지만 작동하지 않습니다 (FF, Chrome).

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

이와 같이 수동 캐럿 위치를 설정할 수 있습니까?

답변:


260

대부분의 브라우저에는 RangeSelection객체 가 필요 합니다. 각 선택 경계를 노드로 지정하고 해당 노드 내에서 오프셋을 지정합니다. 예를 들어, 캐럿을 두 번째 텍스트 행의 다섯 번째 문자로 설정하려면 다음을 수행하십시오.

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는 완전히 다르게 작동합니다. 이러한 브라우저를 지원해야하는 경우 다른 코드가 필요합니다.

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


2
귀하의 솔루션은 완벽하게 작동합니다. 고마워 "텍스트 컨텍스트"에서 작동 할 가능성이 있습니까? 즉, 위치 # 5는 코드의 다섯 번째 문자가 아니라 화면의 다섯 번째 문자가됩니다.
Frodik

3
@ Frodik : setSelectionRange()내가 쓴 답변 에서 함수를 사용할 수 있습니다 : stackoverflow.com/questions/6240139/… . 대답에서 언급했듯이 올바르게 / 일관 적으로 처리하지 못하지만 충분할 수있는 여러 가지가 있습니다.
Tim Down

7
<< div id = "editable"contenteditable = "true"> test1 <br> test2 <br> <span> </ span> </ div>
Med Akram Z

1
@ MalcolmOcean : IE <9에는 없기 때문에 Barf document.createRange(또는 window.getSelection, 그러나 그렇게 멀지는 않습니다).
Tim Down

1
@ undroid : jsfiddle은 Mac의 Firefox 38.0.5에서 잘 작동합니다.
Tim Down

62

내용 편집 가능한 커서 위치에서 찾을 수있는 대부분의 답변은 일반 바닐라 텍스트가있는 입력에만 적합하다는 점에서 상당히 단순합니다. 컨테이너 내에서 html 요소를 사용하면 입력 한 텍스트가 노드로 분할되고 트리 구조에 자유롭게 배포됩니다.

커서 위치를 설정하려면 제공된 노드 내의 모든 자식 텍스트 노드를 순환하고 초기 노드의 시작에서 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;
};

그런 다음이 함수를 사용하여 루틴을 호출하십시오.

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)는 커서를 범위의 끝으로 설정합니다. 최신 버전의 Chrome, IE, Mozilla 및 Opera에서 테스트했으며 모두 정상적으로 작동합니다.

추신. 누군가 관심이 있다면이 코드를 사용하여 현재 커서 위치를 얻습니다.

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

이 코드는 set 함수의 반대 기능을 수행합니다. 현재 window.getSelection (). focusNode 및 focusOffset을 가져오고 containerId가 id 인 상위 노드에 도달 할 때까지 발생한 모든 텍스트 문자를 거꾸로 계산합니다. isChildOf 함수는 실행하기 전에 해당 노드가 실제로 제공된 parentId 의 자식인지 확인합니다 .

이 코드는 변경없이 바로 작동해야하지만 난 그냥 몇 밖으로 해킹 그래서 개발 한 플러그인 JQuery와에서 촬영 한 이의를 - 아무것도 작동하지 않는 경우 알려주세요!


1
이 작업의 jsfiddle을 제공해 주시겠습니까? 나는 이것이 무엇인지 확실 하지 않고 예제가없는 것과 관련하여 이것이 어떻게 작동하는지 알아 내려고 고심 node.id하고 parentId있습니다. 감사합니다 :)
Bendihossan

4
@Bendihossan-이 jsfiddle.net/nrx9yvw9/5를 시도하십시오 -어떤 이유로이 예제의 내용 편집 가능 div는 텍스트 시작 부분에 일부 문자와 캐리지 리턴을 추가하는 것입니다. ; t 내 asp.net 서버에서 동일한 작업을 수행하십시오).
Liam

@Bendihossan-contenteditable div 내의 html 요소는 각 html 요소마다 하나의 노드를 가진 트리 구조로 나뉩니다. getCurrentCursorPosition은 현재 선택 위치를 가져 와서 일반 텍스트 문자 수를 세는 트리로 돌아갑니다. Node.id는 html 요소 ID이고 parentId는 다시 계산을 중단해야하는 html 요소 ID를 나타냅니다.
Liam

1
내 UI 코드와 완전히 별 개인 것을 작성하는 것은 할 일 목록에 있습니다. 초가 있으면 게시하겠습니다.
Liam

1
다른 솔루션을 빠르게 테스트하기 위해 실행 가능한 코드 스 니펫으로 답변을 편집 할 수 있습니까? 미리 감사드립니다.
Basj

3

jQuery를 사용하지 않으려면 다음 방법을 시도하십시오.

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

editableDiv편집 가능한 요소를 설정하는 것을 잊지 마십시오 id. 그런 다음 innerHTML요소에서 벗어나 모든 브레이크 라인을 절단해야합니다. 그리고 다음 인수로 축소를 설정하십시오.


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>

(p) (스팬) 등과 같은 고급 요소가있을 때 캐럿을 올바른 위치에 놓기가 매우 어렵습니다. 목표는 (객체 텍스트)를 얻는 것입니다.

<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
답변을 신속하게 테스트하려면 답변을 실행 가능한 코드 스 니펫으로 편집 할 수 있습니까? 미리 감사드립니다.
Basj

1

구문 강조 표시기 (및 기본 코드 편집기)를 작성하고 있으며 작은 따옴표 문자를 자동 입력하고 캐럿을 다시 이동시키는 방법을 알아야했습니다 (요즘 많은 코드 편집기와 마찬가지로).

이 스레드, MDN 문서 및 많은 moz 콘솔 시청의 도움 덕분에 내 솔루션의 스 니펫이 있습니다.

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

이것은 contenteditable div 요소에 있습니다

나는 여기에 이미 인정 된 답변이 있음을 깨닫고 고마워합니다.


1

나는 이것을 간단한 텍스트 편집기로 만들었습니다.

다른 방법과의 차이점 :

  • 고성능
  • 모든 공간에서 작동

용법

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

@Liam의 답변을 리팩토링했습니다. 정적 메소드를 사용하여 클래스에 넣고 함수를 #id 대신 요소를 받도록하고 다른 작은 조정을했습니다.

이 코드는 커서를 사용할 수있는 서식있는 텍스트 상자에 커서를 고정하는 데 특히 좋습니다 <div contenteditable="true">. 아래 코드에 도달하기 전에 며칠 동안 붙어있었습니다.

편집 : 그의 대답 과이 대답은 엔터를 치면 관련된 버그가 있습니다. enter는 문자로 계산되지 않으므로 enter를 누르면 커서 위치가 엉망이됩니다. 코드를 수정할 수 있으면 답변을 업데이트하겠습니다.

EDIT2 : 저장 자신 두통을 많이하고 있는지 당신의 확인 <div contenteditable=true>IS를 display: inline-block. Enter 키를 누르지 <div>않고 Chrome 퍼팅과 관련된 일부 버그가 수정되었습니다 <br>.

사용하는 방법

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

암호

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

contenteditable 요소에서 캐럿을 특정 위치로 설정하는 것은 간단하지 않다고 생각합니다. 나는 이것을 위해 내 자신의 코드를 썼다. 남아있는 문자 수를 계산하는 노드 트리를 무시하고 필요한 요소에 캐럿을 설정합니다. 나는이 코드를 많이 테스트하지 않았다.

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

또한 현재 캐럿 위치를 얻는 코드를 작성했습니다 (테스트하지 않았습니다).

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

또한 range.startOffset 및 range.endOffset에는 텍스트 노드에 대한 문자 오프셋 (nodeType === 3)과 요소 노드에 대한 하위 노드 오프셋 (nodeType === 1)이 포함되어 있어야합니다. range.startContainer 및 range.endContainer는 트리에서 모든 레벨의 요소 노드를 참조 할 수 있습니다 (물론 텍스트 노드도 참조 할 수 있음).


0

Tim Down의 답변을 기반으로하지만 마지막으로 알려진 "양호한"텍스트 행을 확인합니다. 커서를 맨 끝에 놓습니다.

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.