ContentEditable - team-yaza/mozi-client GitHub Wiki

MOZI์—์„œ Todo์˜ Title์ด๋‚˜ Description๊ฐ™์€ ์š”์†Œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ input์œผ๋กœ ํ•  ๊ฒƒ์ธ๊ฐ€ div ํƒœ๊ทธ์˜ contentEditable์ด๋ผ๋Š” ์†์„ฑ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ธ๊ฐ€ ๊ณ ๋ฏผ์ด ์žˆ์—ˆ๋‹ค.

contentEditable์€ HTML์š”์†Œ๋ฅผ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ์†์„ฑ์ด๋‹ค. HTML5์˜ ๋ชจ๋“  ์—˜๋ฆฌ๋จผํŠธ๋Š” contenteditable='true'๋กœ ์„ค์ •ํ•จ์œผ๋กœ์จ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ ๋‚ด๋ถ€์— ํ…์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

contentEditable์†์„ฑ์€ ์—ด๊ฑฐํ˜•(enum) ์†์„ฑ์ธ๋ฐ, true, false ์ด์™ธ์—๋„ inherit์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ(default)์€ ๋ถ€๋ชจ์š”์†Œ์—์„œ ํŽธ์ง‘๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ์ƒ์†๋ฐ›๋Š” inherit์ด๋‹ค.

HTMLElement.isContentEditable์™€ ๊ฐ™์ด ์ ‘๊ทผํ•˜์—ฌ HTMLElement์˜ contentEditable ์†์„ฑ์„ ๋ถˆ๋ฆฌ์–ธ(boolean) ๊ฐ’์œผ๋กœ ๋ฐ›์•„๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด div์— contentEditable ์†์„ฑ์„ true๋กœ ์„ค์ •ํ•˜๋ฉด div ์š”์†Œ๋„ input์ฒ˜๋Ÿผ ์ž…๋ ฅ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. (๋…ธ์…˜ ํŽ˜์ด์ง€ ๋‚ด๋ถ€ ํ•˜๋‚˜์˜ ๋ธ”๋ก) contentEditable์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ์›น ์—๋””ํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•จ์ด๋‹ค. contentEditable์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž์ฒด์ ์œผ๋กœ ํด๋ฆฝ๋ณด๋“œ, ๋“œ๋ž˜๊ทธ&๋“œ๋กญ, ์‹คํ–‰ ์ทจ์†Œ, ์„œ์‹๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ „๋ถ€ ์ œ๊ณตํ•ด์ค€๋‹ค.

๋˜ํ•œ ํŠน์ • ๋‚ด์šฉ์„ ํŽธ์ง‘๋ชจ๋“œ๋กœ ์ˆ˜์ •ํ•˜๊ธฐ ์‰ฝ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค. ํŽธ์ง‘ ๋ชจ๋“œ ๋ณ€๊ฒฝ์‹œ input์ด๋‚˜ textarea๋กœ ์ˆ˜์ •ํ•  ํ•„์š”๊ฐ€ ์—†์ด contentEditable ์†์„ฑ๋งŒ ์ถ”๊ฐ€ํ•ด์ค˜๋„ ํŽธ์ง‘ ๋ชจ๋“œ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ๋•Œ๋ฌธ์— div๋ฅผ input์œผ๋กœ ๋ฐ”๊ฟจ์„ ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์Šคํƒ€์ผ ๋ณ€๊ฒฝ์„ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์•„๋„ ๋˜์–ด ํŽธ๋ฆฌํ•˜๋‹ค.

contentEditable์€ React์—์„œ ์‚ฌ์šฉํ•  ๋•Œ ๊ฝค๋‚˜ ๋ฌธ์ œ๊ฐ€ ๋งŽ๋‹ค. input๊ณผ ๋™์ž‘ ๋ฐฉ์‹์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. div ํƒœ๊ทธ์— contentEditable ์†์„ฑ์„ ์ฃผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

image

์œ„ ์—๋Ÿฌ๋Š” suppressContentEditableWarning ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜๋‹ค.

div ํƒœ๊ทธ์— contentEditable ์†์„ฑ์„ ์ฃผ๋ฉด ๋จผ์ € ์ž…๋ ฅ์‹œ change event๊ฐ€ ์•„๋‹ˆ๋ผ input event๊ฐ€ ๋™์ž‘ํ•œ๋‹ค. ๋˜ํ•œ input์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— value ๊ฐ’์ด ์—†๋‹ค. ์ด ๋•Œ๋ฌธ์— ์ œ์–ด ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ๋ฆฌ์•กํŠธ๊ฐ€ DOM์„ ์ œ์–ดํ•  ์ˆ˜ ์—†๋‹ค.

๋”ฐ๋ผ์„œ ๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ ๋ฐฉ์‹์œผ๋กœ contentEditable์„ ๊ด€๋ฆฌํ•ด์ค˜์•ผํ•œ๋‹ค.

export const useContentEditable = (initialContent: string) => {
  const $contentEditable = useRef<HTMLDivElement>(null);
  const [content, _setContent] = useState(initialContent);
 
  const onInput = (event: ChangeEvent<HTMLDivElement>) => {
    _setContent(event.target.innerText);
  };
 
  const setContent = (newContent: string) => {
    if ($contentEditable.current) {
      $contentEditable.current.innerText = newContent;
      _setContent(newContent);
    }
  };
 
  useEffect(() => {
    setContent(initialContent);
  }, []);
 
  return { content, setContent, onInput, $contentEditable };
};

useContentEditable์€ DOM์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ref์™€ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ useState๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ์ƒํƒœ์—๋Š” contentEditable์˜ innerText๊ฐ€ ์ €์žฅ๋œ๋‹ค.

๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ƒํƒœ ๊ฐ’์„ ์ €์žฅํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๊ณ  ์ƒ๊ฐ๋  ์ˆ˜ ์žˆ์ง€๋งŒ ์ž…๋ ฅ๊ฐ’์„ ์ฆ‰๊ฐ์ ์œผ๋กœ validationํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ํ–ˆ๋‹ค.

contentEitable์€ ์•„๋‹ˆ์ง€๋งŒ element.focus() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํฌ์ปค์Šค๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํฌ์ปค์Šค๋ฅผ ์ฃผ๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ, contentEditable์— ์ž‘์„ฑ๋œ ๋‚ด์šฉ์ด ์žˆ์–ด๋„ ํฌ์ปค์Šค๊ฐ€ ๋งจ ์•ž์ชฝ์— ์œ„์น˜ํ•œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ input์— ํฌ์ปค์Šค๋ฅผ ์ฃผ๋ฉด ์ž‘์„ฑ๋œ ๋‚ด์šฉ์˜ ๊ฐ€์žฅ ๋’ค๋กœ ๊ฐ€๋Š” ๊ฒƒ๊ณผ๋Š” ๋Œ€์กฐ์ ์ด๋‹ค. ๋”ฐ๋ผ์„œ ๋ณ„๋„์˜ foucs ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

export const focusContentEditableTextToEnd = (element: HTMLElement) => {
  if (element.innerText.length === 0) {
    element.focus();
 
    return;
  }
 
  const selection = window.getSelection();
  const newRange = document.createRange();
  newRange.selectNodeContents(element);
  newRange.collapse(false);
  selection?.removeAllRanges();
  selection?.addRange(newRange);
};

๋งŒ์•ฝ contentEditable์˜ innerText์˜ ๊ธธ์ด๊ฐ€ 0์ด๋ผ๋ฉด ์ž‘์„ฑ๋œ ๋‚ด์šฉ์ด ์—†๋‹ค๋Š” ๊ฒƒ์ด๋ฏ€๋กœ focus๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์ž‘์„ฑ๋œ ๋‚ด์šฉ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ํฌ์ปค์Šค๋ฅผ ๊ฐ€์žฅ ๋’ค์ชฝ์— ๋ถ™์ด๊ธฐ ์œ„ํ•ด ํ˜„์žฌ Caret(์ปค์„œ)์˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•ด์ค˜์•ผํ•œ๋‹ค. caret์˜ ์œ„์น˜๋ฅผ ์ฐพ๊ณ  contentEditable์˜ range(๋“œ๋ž˜๊ทธ ๋œ ์˜์—ญ)๋ฅผ ๋งจ ๋์œผ๋กœ ์ด๋™์‹œํ‚จ๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ธฐ์กด range๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด range๋ฅผ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ contentEditable ๋‚ด์šฉ ๋์— ์ปค์„œ๋ฅผ ์œ„์น˜ ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.