div contenteditable이용한 editor 생성시 오류 해결 과정

yugyeongKim·2023년 11월 1일
0

Javascript

목록 보기
7/7

💥 커서의 위치 문제

맨앞으로 커서가 튄다.

해결책: 맨뒤로 커서를 조정하자!

맨 마지막에 커서 위치시키기

그냥 맨 마지막에 커서를 위치 시키면 중간에 글을 수정할 수 없다. 그냥 무조건 마지막에 추가만 되는 에디터가 된다.

해결책: 커서의 위치를 기억해서 하면 어떨까?

위치를 기억 후 저장

그냥 텍스트를 계속 하거나 하면 괜찮은데, 마크업?그 뭐시냐 암튼 그런걸 사용하려면 innerHTML이 변경되고 positon의 위치가 달라지기에 뚱딴지 같은 곳에 커서가 위치되게 된다. 혹은 커서가 그냥 풀린다.

커서가 풀리는 이유:
range가 document로 잡힌다.

내용입력중
innerHTML: 내용

내용입력중갹

position: 9
slicedText:용입력

이렇게 일단 대충 positon이 해당 태그 안에 있고, 해당 태그의 마지막을 range로 하는 것. 하지만 positon이 정확한 위치에 있지 않아서 요상하게 튄다. positon을 제대로 구하는 것이 첫번째 과제인 것 같다.

내용 <div>내용입력중갹ㄱ</div><div>제대"로"</div>

position: 13
slicedText: 갹ㄱ<
innerHTML:

그리고 내가 긁어온 position찾는 함수는 나처럼 태그안의 내용이 태그가 아닌 그냥 text일때만 인것 같아서 position도 따로 구했다.

아 아무리 생각해도 시맨틱 태그로 변경되고 그 변경된걸 적용하고 어쩌구 저꺼구하는 게 너무 복잡해서 정확한 position을 찾는 것이 버그가 많이 생길 것 같다.

해결책: 태그를 변경되는 위치에 삽입했다가 그 태그의 위치로 커서를 둘까?

해결책: 수정 위치에 표식용 태그 삽입 후 해당 태그위치를 커서 위치로 정하기

  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;
  1. 위치에 삽입
$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)
  }
  ...
}
  1. setState후 $empty를 range로 할당

addRange안되는 문제


commonAncestorContainer의 text을 클릭하면 이렇게 현재 페이지에서 찾을 수 없다고 뜬다.

추가되고 난다음에 새롭게 document.querySelecor로 요소를 선택해서 해주면 된다.

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

$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);
  // 문자열의 마지막이 아니라 중간일 경우 offset으로 할당
  offset = text === node.data ? offset : text.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();
  const range = document.createRange();
  const temp = document.querySelector(".empty");
  range.setStart(temp.previousSibling, 0);
  range.setEnd(temp.previousSibling, offset);
  range.collapse(false);
  selection.addRange(range);
  // 위치를 찾기 위해 임시로 삽입한 span태그 제거
  temp.remove();
  // onEditing(this.state);
});

const getPosition = (parentElement) => {
  let currentNode = parentElement;
  const indexStack = [];
  
  while (currentNode === $content) {
    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;
};

transformText가 좀 말썽이다.

정규식 변경

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");
};
$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);
  // 문자열의 마지막이 아니라 중간일 경우 offset으로 할당
  
  // 변환이 됐다면? -> 1
  // 변환이 안됐고, 중간
  // 변환됐고 끝
  if(text !== node.data) {
    offset = 1;
    if(offset === text.length) {
      offset = text.length;
    }
  } else {
    if(offset === node.data.length) {
      offset = node.data.length;
    } 
  }
  ...
  
  selection.removeAllRanges();
  const range = document.createRange();
  const temp = document.querySelector(".empty");
  range.setStart(temp.previousSibling, 0);
  range.setEnd(temp.previousSibling, offset);
  range.collapse(false);
  selection.addRange(range);

잘된다. 하지만

  1. 띄어쓰기 뒤에 안함글자
  2. 띄어쓰기 뒤에 함 글자
    2번의 경우 offset이 2가되어야 하는데 1로 고정되어 커서가 잘못 지정된다.

ㅇㅇ안 / ㅇㅇ띄안

해결책: 정규식을 이용해 특수기호가 끝나는 부분까지를 다 날리고 남은 글자수를 offset으로 한다.

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

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

$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
    offset = deleteText(node.data).length;
    // 맨마지막에 됐다면 text.length를 offset(아마?)
    if (offset === text.length) {
      offset = text.length;
    }
  } else {
  	// 변환안됐고 마지막? 걍 다 이거인듯
    if (offset === node.data.length) {
      offset = node.data.length;
    }
  }
  ...
}

해결됐다.

0개의 댓글