
VanillaJS로 editor기능을 구현하던 중 텍스트 커서 문제를 해결 했던 과정입니다.
innerHTML을 통해서 리렌더링하면 텍스트 커서가 사라져서 사용자는 text를 이어서 입력할 수 없는 문제가 있었다.
this.$target.addEventListener('input', (event)=>{
this.setState(newValue);
//setState에서 innerHTML을 통해서 리렌더링이 실행된다.
const $editor=document.querySelector('.editor');
//innerHTML로 리렌더링되고 event.target의 엘리먼트는 사라져서 target을 다시 선언해줘야한다.
$editor.focus();
})
리렌더링 후 커서가 계속 있었지만 이전에 있던 위치에 있는 것이 아니라 맨앞에 위치해 있었고 글을 쓰면 맨앞에서부터 써졌다. 이 때문에 사용자는 글을 이어서 쓸 수 없었다.
구글링을 통해서 텍스트 커서에 대한 정보를 selection 객체가 가지고 있다는 것을 알게 되었고 이것을 적용해보았다.
Selection 객체는 사용자가 선택한 텍스트의 범위 또는 캐럿(텍스트 커서)의 현재 위치를 나타냅니다. 검사 또는 조작을 위해 Selection 객체를 가져오려면 window.getSelection()을 호출합니다.
MDN Selection 객체
적용 방법은 렌더링 전에 Selection객체에서 현재 커서가 있는 node와 해당 node에서 어디에 위치 했는지 위치 정보를 받아서 렌더링 후 해당 엘리먼트에 커서가 위치 하도록 설정하는 것이다.
this.$target.addEventListener('input', (event)=>{
const selection = window.getSelection();
const{anchorNode, anchorOffset}=selection;
//텍스트 커서가 위치해 있는 node를 가지고 있는 anchorNode, anchorNode에서 텍스트 커서의 위치값이 있는 anchorOffset
anchorNode.setAttribute('id', "current_cursor");
//렌더링 후에도 해당 노드를 찾기 위해서 id를 설정해준다.
this.setState(newValue);
// setState 실행시 innerHTML로 리렌더링된다.
const $cursorPositionNode=document.querySelector('#current_cursor');
//리렌더링된 후 기존의 anchorNode는 사라지고 새로운 node가 있어서 이전에 설정해준 id로 찾는다.
selection.setPosition($cursorPositionNode, anchorOffset);
//커서를 위치시키는 setPosition 함수
$cursorPositionNode.removeAtrribute('id');
//커서가 있던 노드 표시를 위해서 설정했던 id를 제거해서 다음 사용시 문제가 없도록 한다.
})
이제 렌더링이 된 후에도 이전에 텍스트 커서가 어디에 있던 상관 없이 커서가 이전 위치를 유지하게 되었고, 이어서 텍스트를 입력하는데도 문제가 없어 졌다.
3.selection 객체와 Range 객체 사용
위에서 selection 객체만을 이용해서 해당 위치와 offset을 통해서 커서 위치를 설정했다. 하지만 마크다운 기능이 추가 되면서 렌더링 전에 설정되었던 offset과 다른 위치에 커서 설정이 필요했다. 그래서 selection 객체만 사용하는 것이 아니라 Range 객체도 이용하느 방식으로 커서를 설정했다.
this.$target.addEventListener('input', (event)=>{
const selection = window.getSelection();
const{anchorNode, anchorOffset}=selection;
//텍스트 커서가 위치해 있는 node를 가지고 있는 anchorNode, anchorNode에서 텍스트 커서의 위치값이 있는 anchorOffset
const range= selection.getRangeAt(0);
//현재 텍스트 커서에 대한 range 객체를 반환하는 함수이다.
const $mark=document.createElement('span');
// 커서 위치에 넣어 줄 span 태그이다.
$mark.setAttribute('id', 'current_cursor');
// 렌더링 이 후에도 찾을 수 있도록 id로 표시
range.insertNode($mark);
//헌재 커서 위치에 노드를 삽입해주는 함수
this.setState(newValue);
// setState 실행시 innerHTML로 리렌더링된다.
const $cursorPositionNode=document.querySelector('#current_cursor');
// 이전에 표시한 span태그를 찾아준다.
selection.setPostion($cursorPositionNode, 0);
//커서를 넣어준다. 해당 노드가 사라져도 커서는 유지된다.
$cursorPositionNode.remove();
//표시를 했던 node는 삭제를 해준다.
이제 마크다운으로 innerHTML 내용이 변경되더라도 그외 무관하게 커서를 설정할 수 있다. 그리고 this.setState 부분을 콜백을 넣어주는 방식으로 구현하면 모듈화를 할 수 있다 .