ContentEditable이란

강원지·2023년 7월 9일
1

옾터디

목록 보기
3/4

contentEditable이란

어떤 HTML 요소든 내부에 텍스트를 작성할 수 있게 만들어주는 속성입니다. 이 속성을 이용하면 아무 태그나 input이나 textarea처럼 사용할 수 있습니다.

아래는 style 태그를 이용한 예시입니다. style 태그 내에 텍스트를 작성할 수 있도록 contentEditable = true와 block 처리를 해주었습니다.

    <style
      contenteditable="true"
      style="display: block; white-space: pre; border: 1px solid black"
    >
      html {
        background: brown;
      }
    </style>

style 태그 안에 css를 입력해주면 스타일들이 즉각적으로 반영되는 것을 볼 수 있습니다.

input, textarea와 뭐가 다른데?

input과 textarea의 입력 내용 저장 방식

이 경우에는 에디터로 사용하기 위해서는 사용자로부터 입력을 받은 뒤, 해당 value를 가져와 태그를 조합해 그 결과를 렌더링합니다. 즉, 태그 내부에 텍스트 노드를 생성하고 입력 내용을 저장합니다.

contenteditable의 입력 내용 저장 방식

contenteditable의 경우, text를 입력 받으면 contenteditable 내부에 요소 노드들이 생성됩니다. 그에 따라 노드들이 출력됩니다. value는 존재하지 않습니다.

결론적으로 input이나 textarea는 텍스트 노드만을 저장할 수 있기 때문에 태그를 이용한 효과를 주지 못합니다. 일반 텍스트만을 사용하는 경우에는 input과 textarea로 충분하지만, 코드를 작성하거나 <b>태그, <u> 태그 등을 통해 글자에 효과를 주기 위해서는 요소 노드들을 저장할 수 있는 contenteditable을 사용해야 합니다.

그래서 사용 방법은?

간단합니다.

onChange나 keyUp 이벤트가 아니라 input 이벤트가 일어날 때 글자가 입력된다는 점만 유의하면 됩니다.

<div contenteditable="true" style="border: 1px solid black"></div>
<script>
	document.querySelector("div").addEventListener("input", (e) => {});
</script>

노션 프로젝트 적용

this.render = () => {
	target.appendChild(editorElement);
	
  editorElement.innerHTML = `
      <input type='text' name='title' class='post-title'/>
      <div name='content' contentEditable='true'style='width:600px;height:400px; border:1px solid black; padding:8px;'></div>
  `;

	const { title, content } = this.state;

  editorElement.querySelector('[name=content]').innerHTML = richContent;

	editorElement.querySelector('[name=content]').addEventListener('input', e => {
        const nextState = {
          ...this.state,
          content: e.target.innerHTML,
        };

        delayedUpdate(nextState); // 내부에 setState 존재
      });
  };

문제

입력 즉시 마크업이 적용되지 않고 setState가 되어야(=delay 이후에) 적용됩니다.

해결 방법 고안

1. input 이벤트 핸들러 내부에 innerHTML에 richContent 주입하는 코드를 넣는다.

    
editorElement.querySelector('[name=content]').addEventListener('input', e => {
	const richContent = getRichContent(e.target.innerHTML);
    
    const nextState = {
        ...this.state,
        content: richContent,
    };
    
    editorElement.querySelector('[name=content]').innerHTML = richContent;
    
        delayedUpdate(nextState);
    });
};
⇒ 커서가 맨 앞으로 이동하는 문제가 발생합니다.

2. 커서(캐럿)를 맨 뒤로 유지하는 코드를 추가해본다.

window.getSelection()?.collapse(e.target, 1);

window.getSelection() 메서드를 통해 커서의 정보 Selection을 가져올 수 있습니다. .collapse() 메서드는 선택 영역을 축소하는 메서드입니다. 이를 통해 Selection 객체의 현재 선택 범위 e.target 객체를 축소한 뒤, offset인 1의 위치를 가리키게 합니다.

여러 코드를 추가해보았으나 적용이 되지 않았고, 커서 위치 조정에 성공했다 하더라도 한글을 입력할 때 자음 모음이 ㅂㅜㄴㄹㅣ되는 문제가 발생했다는 글을 보아 다른 방법을 찾아보았습니다.

시작 커서와 끝 커서의 위치 동일 여부 판단

window.getSelection()?.isCollapsed // 

window.getSelection()를 활용한 부가기능
추가로, window.getSelection()을 이용하면 글자를 드래그로 range를 선택하여 효과를 줄 수 있습니다. 더욱 rich해지겠네요.

        // Range 설정
        const newRange = document.createRange();
        newRange.setStart(startContainer, startOffset);
        newRange.setEnd(endContainer, endOffset);
        // Selection에 전달
        const selection = document.getSelection();
        selection.removeAllRanges();
        selection.addRange(newRange);

3. className으로 스타일 적용

contentEditable을 이용해 에디터를 구현하신 동기 분께 방법을 여쭤보니 태그를 직접 주입하지 않고 style로 헤딩을 처리하셨다고 합니다. 위의 방법들은 setState가 필요하기 때문에 딜레이없이 가능한 방법에 대한 고민이 주를 이뤄서 다른 방법에 대한 고려를 못했습니다. 알려주신 방법 대로 적용해보려고 합니다.

스페이스가 입력되면 indexOf(’#’)===0을 검사해서 style 적용

⇒ 딜레이 없이 가능

참고

기술 블로그 위한 contentEditable WYSIWYG 에디터 제작기
Wesbos님의 ContentEditable 시연 영상
ContentEditable에서 커서(Caret) 활용하기(1) - 자바스크립트

1개의 댓글

comment-user-thumbnail
2023년 7월 9일

ContentEditable 사용할때 참고할 블로그로 삼겠습니다

답글 달기