div contenteditable이용한 editor 생성 시 커서 위치 문제 해결

yugyeongKim·2023년 11월 1일
1

Javascript

목록 보기
6/7

수정 위치에 표식용 태그를 삽입 후 해당 태그 위치를 커서 위치로 정하는 방식을 택했다.

1. span태그 넣을 위치 찾기

const getPosition = (parentElement, offset) => {
  let currentNode = parentElement;
  const indexStack = [];
  let count = 0;
  
  while (currentNode === $content) {
    let text = currentNode.outerHTML;
    count += 1;
    indexStack.push(getIndex(currentNode));

    currentNode = currentNode.parentElement;
  }
  
  for (let i = 0; i < indexStack.length-1; i++) {
    currentNode = currentNode.children[indexStack[i]];
  }
  
  return currentNode;
}

const getIndex = (element) => {
  let count = 0;
  while ((element = element.previousSibling) != null) {
    count += 1;
  }
  return count;
};

2. 표식한 위치에 삽입

$content.addEventListener("compositionend", (e) => {
  const selection = window.getSelection();
  const node = selection.focusNode;
  const offset = selection.focusOffset;

  const elementPosition = getPosition(node.parentElement, offset);
  const $empty = document.createElement('span');
  $empty.setAttribute('class', 'empty');
 
 if(!elementPosition.querySelector('.empty')) {
    elementPosition.appendChild($empty)
  }
  ...
}

3. setState후 $empty를 range로 할당


$content.addEventListener("compositionend", (e) => {
  let selection = window.getSelection();
  const node = selection.focusNode;
  let offset = selection.focusOffset;
  
  // 첫 입력시 div가 없어서 div를 씌어주기 위한 판별
  const isDiv = e.target.innerHTML.includes("<div>");
  
  // span을 삽입할 node를 찾는다.
  const elementPosition = getPosition(node.parentElement, offset);
  const $empty = document.createElement("span");
  $empty.setAttribute("class", "empty");
  
  // 변환이 되면서 줄어든 문자열을 반영
  const text = transformText(node.data);
  
  if (text !== node.data) {
    offset = deleteText(node.data).length;
    if (offset === text.length) {
      offset = text.length;
    }
  } else {
    if (offset === node.data.length) {
      offset = node.data.length;
    }
  }
  
  if (!elementPosition.querySelector(".empty")) {
    elementPosition.appendChild($empty);
  }
  
  this.setState({
    ...this.state,
    content: isDiv ? e.target.innerHTML : `<div>${e.target.innerHTML}</div>`,
  });
  
  // 원래 있던 range를 모두 제거
  selection.removeAllRanges();
  // 새로운 range생성
  const range = document.createRange();
  const temp = document.querySelector(".empty");
  // range의 처음부터 offset까지 범위 설정
  range.setStart(temp.previousSibling, 0);
  range.setEnd(temp.previousSibling, offset);
  range.collapse(false);
  // 위치 표시해주는 span태그를 range에 더해준다.
  selection.addRange(range);
  // 위치를 찾기 위해 임시로 삽입한 span태그 제거
  temp.remove();
  
  onEditing(this.state);
});

3.1 정규식

const transformTag = (text) => {
  let h1Pattern = /<div>#\s+(.*?)<\/div>/g;
  let h2Pattern = /<div>##\s+(.*?)<\/div>/g;
  let h3Pattern = /<div>###\s+(.*?)<\/div>/g;
  let h4Pattern = /<div>####\s+(.*?)<\/div>/g;
  let boldPattern = />(.*?)\*\*(.*?)\*\*(.*?)</g
  let italicPattern = />(.*?)_(.*?)_(.*?)</g;
  let strikePattern = />(.*?)~~(.*?)~~(.*?)</g;
  return text
    .replace(h1Pattern, "<div><h1>$1</h1></div>")
    .replace(h2Pattern, "<div><h2>$1</h2></div>")
    .replace(h3Pattern, "<div><h3>$1</h3></div>")
    .replace(h4Pattern, "<div><h4>$1</h4></div>")
    .replace(boldPattern, ">$1<b>$2</b>$3<")
    .replace(italicPattern, ">$1<i>$2</i>$3<")
    .replace(strikePattern, ">$1<s>$2</s>$3<")
    .replace(/&nbsp;/g, " ")
    .replace(/\n/g, "<br>")
    .replace(/<h1><br><\/h1>/g, "<br>")
    .replace(/<h2><br><\/h2>/g, "<br>")
    .replace(/<h3><br><\/h3>/g, "<br>")
    .replace(/<h4><br><\/h4>/g, "<br>")
    .replace(/<i><br><\/i>/g, "<br>")
    .replace(/<b><br><\/b>/g, "<br>")
    .replace(/<s><br><\/s>/g, "<br>");
};

const transformText = (text) => {
  return text
    .replace("#### ", "")
    .replace("### ", "")
    .replace("## ", "")
    .replace("# ", "")
    .replace(/\*\*(.*?)\*\*/g, "$1")
    .replace(/_(.*?)_/g, "$1")
    .replace(/~~(.*?)~~/g, "$1");
};

const deleteText = (text) => {
  if (text.indexOf("#") === 0) {
    text = text
      .replace("#### ", "")
      .replace("### ", "")
      .replace("## ", "")
      .replace("# ", "");
  }

  return text
    .replace(/(.*?)\*\*(.*?)\*\*/g, "")
    .replace(/(.*?)_(.*?)_/g, "")
    .replace(/(.*?)~~(.*?)~~/g, "");
};

0개의 댓글