๋ฐ˜์‘ํ˜•

 

์•„๋ž˜์™€ ๊ฐ™์€ ๋ฌธ์ž์—ด์ด ์žˆ์„ ๋•Œ ๋งˆ์šฐ์Šค๋กœ ๋“œ๋ž˜๊ทธํ•ด์„œ ํ…์ŠคํŠธ๋ฅผ ์„ ํƒํ•  ๋•Œ๋งˆ๋‹ค <span> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ๋ชฉํ‘œ. 

<!-- ์ดˆ๊ธฐ ์ƒํƒœ -->
<p>Pharetra convallis hendrerit integer nec eleifend tellus luctus lorem dignissim</p>

<!-- convallis hendrerit integer ์˜์—ญ์„ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ -->
<p>Pharetra <span>convallis hendrerit integer</span> nec eleifend tellus luctus lorem dignissim</p>

<!-- hendrerit ์˜์—ญ์„ ๋“œ๋ž˜๊ทธํ–ˆ์„ ๋•Œ -->
<p>Pharetra <span>convallis <span>hendrerit</span> integer</span> nec eleifend tellus luctus lorem dignissim</p>

 

๋“œ๋ž˜๊ทธํ•œ ๋ฌธ์ž์—ด ๋žฉํ•‘


โถ ๋งˆ์šฐ์Šค ํด๋ฆญ/ํ…์ŠคํŠธ ๋“œ๋ž˜๊ทธ/ํด๋ฆญ ํ•ด์ œ → onMouseUp ์ด๋ฒคํŠธ ํ˜ธ์ถœ

 

โท ์„ ํƒํ•œ ์˜์—ญ์— ๋Œ€ํ•œ Selection ๊ฐ์ฒด ํš๋“

const selection = window.getSelection();

 

โธ ์ฒซ๋ฒˆ์งธ ์„ ํƒ ์˜์—ญ์— ๋Œ€ํ•œ Range ๊ฐ์ฒด ํš๋“

const range = selection?.getRangeAt(0);

 

โน ํ•ธ๋“ค๋Ÿฌ ์‹คํ–‰ ์กฐ๊ฑด ๊ฒ€์‚ฌ

a. range ๊ฐ์ฒด๊ฐ€ ์—†๊ฑฐ๋‚˜ ํ˜น์€ deleteMode ์—ฌ๋ถ€

if (!range || deleteMode) return;

 

b. ์„ ํƒํ•œ ์˜์—ญ์˜ .selected ํด๋ž˜์Šค ํฌํ•จ ์—ฌ๋ถ€ (์ค‘์ฒฉ ๋“œ๋ž˜๊ทธ ๋ฐฉ์ง€)

if (range.cloneContents().querySelector('.selected')) return;
// range.cloneContents ๋ฉ”์„œ๋“œ๋Š” Range์— ํฌํ•จ๋œ Node ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•œ ํ›„ DocumentFragment ๋ฐ˜ํ™˜
// ๋“œ๋ž˜๊ทธ ์˜์—ญ : convallis hendrerit integer nec (hendrerit๋Š” <span>์œผ๋กœ ๋žฉํ•‘๋˜์–ด ์žˆ์Œ)
// ๋ฐ˜ํ™˜๊ฐ’ : "convallis "<span class="selected">hendrerit</span>" integer"
  • range.cloneContents.querySelector๋Š” document.querySelector ์…€๋ ‰ํ„ฐ์™€ ์‚ฌ์šฉ๋ฒ• ๋™์ผ(์ฒซ ๋ฒˆ์งธ ์„ ํƒ๋œ ์—˜๋ฆฌ๋จผํŠธ ํ˜น์€ ์„ ํƒ๋œ ์—˜๋ฆฌ๋จผํŠธ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜)
  • range.start|endContainer : Range ์‹œ์ž‘|์ข…๋ฃŒ ๋…ธ๋“œ ๋ฐ˜ํ™˜
  • range.start|endContainer.parentNode : Range ์‹œ์ž‘|์ข…๋ฃŒ ๋…ธ๋“œ์˜ ๋ถ€๋ชจ ์š”์†Œ ๋ฐ˜ํ™˜

 

โบ ์„ ํƒํ•œ ํ…์ŠคํŠธ๋ฅผ ๋žฉํ•‘ ํ•  ์ƒˆ๋กœ์šด <span> ํƒœ๊ทธ ์ƒ์„ฑ

const selectedText = range.toString(); // ๋“œ๋ž˜๊ทธํ•œ ์˜์—ญ์˜ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
const span = document.createElement('span');
span.className = 'selected';
span.textContent = selectedText;

 

โป ๊ธฐ์กด ์„ ํƒํ•œ ํ…์ŠคํŠธ ๋…ธ๋“œ ์‚ญ์ œ ํ›„ ์ƒ์„ฑํ•œ <span> ํƒœ๊ทธ๋กœ ๋Œ€์ฒด

range.deleteContents();
range.insertNode(span);

 

๋žฉํ•‘ ํ•œ ํƒœ๊ทธ ์‚ญ์ œ


๐Ÿ’ก ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง์œผ๋กœ ์ธํ•ด ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ๋ถ€๋ชจ๋กœ ์ „ํŒŒ๋˜๋ฏ€๋กœ, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๋ถ€๋ชจ ์˜ค์†Œ์—๋งŒ ํ• ๋‹นํ•œ๋‹ค.

 

โถ ๋งˆ์šฐ์Šค ํด๋ฆญ → ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์‹คํ–‰

<!-- ์•„๋ž˜ ์š”์†Œ์—์„œ hello๋ฅผ ํด๋ฆญํ–ˆ๋‹ค๊ณ  ๊ฐ€์ • -->
<span class="selected">hello <span class="selected">world</span></span>

 

โท ํ•ธ๋“ค๋Ÿฌ ์‹คํ–‰ ์กฐ๊ฑด ๊ฒ€์‚ฌ

// deleteMode ์ƒํƒœ์ด๊ณ , ํด๋ฆญํ•œ ํƒ€๊ฒŸ ์š”์†Œ๋‚ด selected ํด๋ž˜์Šค๋ฅผ ํฌํ•จํ•  ๋•Œ๋งŒ ์‹คํ–‰
if (!deleteMode || !target.classList.contains('selected')) return;

 

โธ ํด๋ฆญํ•œ ์š”์†Œ์˜ innerHTML ๋ณต์‚ฌ

// ์ด ๊ณผ์ •์—์„œ ํด๋ฆญํ•œ ์š”์†Œ ๊ฐ€์žฅ ๋ฐ”๊นฅ์˜ ์‹œ์ž‘/์ข…๋ฃŒ ํƒœ๊ทธ๋Š” ํฌํ•จํ•˜์ง€ ์•Š์Œ
// ํด๋ฆญํ•œ ์š”์†Œ : <span>hello <span>world</span></span>
// innerHTML : hello <span>world>/span> 
const spanContent = target.innerHTML;

 

โน ์ž„์‹œ๋กœ ์‚ฌ์šฉํ•  ์š”์†Œ ์ƒ์„ฑ

const textNode = document.createElement('span');

 

โบ ๋ณต์‚ฌํ•œ ์ฝ˜ํ…์ธ ๋ฅผ ์ž„์‹œ ์ƒ์„ฑํ•œ ์š”์†Œ์˜ innerHTML์— ๋ณต์‚ฌ

textNode.innerHTML = spanContent;

 

โป ์ƒˆ๋กœ์šด DocumentFragment ์ƒ์„ฑ

const frag = document.createDocumentFragment();

 

๐Ÿ’ก DocumentFragment๋Š” ์ž„์‹œ ์ปจํ…Œ์ด๋„ˆ๋กœ DOM ๋…ธ๋“œ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค. ๋‹ค์ˆ˜ ๋…ธ๋“œ์— ๋Œ€ํ•œ CRUD ๋“ฑ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์œ ์šฉ. ์‹ค์ œ DOM ํŠธ๋ฆฌ์— ์†ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ reflow/repaint๋ฅผ ์œ ๋ฐœํ•˜์ง€ ์•Š๊ณ (ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ๊ณผ์ • ์ƒ๋žต) ์—ฌ๋Ÿฌ DOM ๋…ธ๋“œ๋ฅผ ํ•œ ๋ฒˆ์— ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

โผ textNode์˜ ๋ชจ๋“  ์ž์‹ ์š”์†Œ๋ฅผ frag๋กœ ์ด๋™

// ๋ฐ˜๋ณตํ•  ๋•Œ๋งˆ๋‹ค textNode์˜ ์ฒซ๋ฒˆ์งธ ์ž์‹์„ frag ์ž์‹์œผ๋กœ ๋ถ™์ž„
while (textNode.firstChild) frag.appendChild(textNode.firstChild);
// textNode ์ฒ˜์Œ ์ƒํƒœ ์˜ˆ์‹œ : convallis <span>hendrerit</span> integer
// childNode๋กœ ํ‘œํ˜„ํ–ˆ์„ ๋•Œ : NodeList [text, span, text]
// 1ํšŒ ์ˆœํšŒ ํ›„ textNode : <span>hendrerit</span> integer
// 2ํšŒ ์ˆœํšŒ ํ›„ textNode :  integer
// 3ํšŒ ์ˆœํšŒ ํ›„ textNode : ๋น„์–ด์žˆ์Œ

 

hasChildNodes๋กœ ์ž์‹ ๋…ธ๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์žˆ๋‹ค๋ฉด append๋กœ ๋ฐ”๋กœ ๋ถ™์ด๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค.

if (textNode.hasChildNodes()) frag.append(...textNode.childNodes)

 

โฝ ํด๋ฆญํ•œ ์š”์†Œ๋ฅผ(๋žฉํ•‘์„ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด ํด๋ฆญํ•œ ๋Œ€์ƒ) frag๋กœ ๋Œ€์ฒด

target.replaceWith(frag);

 

๋…ธ๋“œ ์ค‘์ฒฉ ๊นŠ์ด ๊ณ„์‚ฐ


์ค‘์ฒฉ ๊ธฐ์ค€

์—ฌ๋Ÿฌ ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ(๋“œ๋ž˜๊ทธํ•œ ๋ฌธ์ž์—ด์„ <span> ํƒœ๊ทธ๋กœ ๋žฉํ•‘) ์ค‘์ฒฉ๋œ ๊ตฌ์กฐ๊ฐ€ ๋์„ ๋•Œ ๊ฐ ํƒœ๊ทธ์˜ ๊นŠ์ด.

Pharetra
<span class="selected"> <!-- ์ค‘์ฒฉ ๋ ˆ๋ฒจ 1 -->
  convallis
  <span class="selected"> <!-- ์ค‘์ฒฉ ๋ ˆ๋ฒจ 2 -->
    hendrerit
  </span>
  integer
</span>
nec eleifend tellus luctus lorem dignissim

 

์ค‘์ฒฉ ๋ ˆ๋ฒจ ๊ณ„์‚ฐ

์ธ์ž๋กœ ์ „๋‹ฌ๋ฐ›์€ Node์˜ ๋ถ€๋ชจ ์š”์†Œ๊ฐ€ selected ํด๋ž˜์Šค๋ฅผ ํฌํ•จํ•  ๋•Œ๋งˆ๋‹ค ์ค‘์ฒฉ ๋ ˆ๋ฒจ + 1์”ฉ ์ฆ๊ฐ€

const calculateNestingLevelFromNode = (node: AnchorNode) => {
  let nestingLevel = 1;
  let parentNode = node;

  while (parentNode && parentNode instanceof HTMLElement) {
    if (parentNode.classList.contains('selected')) nestingLevel++;
    parentNode = parentNode.parentNode;
  }

  return nestingLevel;
};

 

selected ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  ํƒœ๊ทธ์˜ ์ค‘์ฒฉ ๋ ˆ๋ฒจ ๊ฒ€์‚ฌ

const calculateNestingLevel = (ref: React.RefObject<HTMLParagraphElement>) => {
  const spans = ref.current?.querySelectorAll('.selected');
  if (!spans) return;
  spans.forEach((spanElement) => {
    const span = spanElement as HTMLElement;
    const nestingLevel = calculateNestingLevelFromNode(span.parentNode);
    span.dataset.nestingLevel = nestingLevel.toString();
  });
};

 

์ฝ”๋“œ ์‹คํ–‰ ๊ฒฐ๊ณผ

Pharetra
<span class="selected" data-nesting-level="1">
  convallis
  <span class="selected" data-nesting-level="2">
    hendrerit
  </span>
  integer
</span>
nec eleifend tellus luctus lorem dignissim

 

๋ Œ๋”๋ง


DOMPurify ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด XSS ๊ณต๊ฒฉ์— ์‚ฌ์šฉ๋˜๋Š” ์Šคํฌ๋ฆฝํŠธ/ํƒœ๊ทธ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

dangerouslySetInnerHTML ์†์„ฑ์„ ์ด์šฉํ•ด innerHTML ๋ฌธ์ž์—ด์„ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋งํ•˜๋Š” ์˜ˆ์‹œ. ํƒœ๊ทธ ์ถ”๊ฐ€/์‚ญ์ œ ์‹œ ์‹คํ–‰๋˜๋Š” updateStack ํ•ธ๋“ค๋Ÿฌ ๋‚ด๋ถ€์—์„œ DOMPurity.sanitize ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ์ถ”๊ฐ€์ ์ธ sanitize ์ž‘์—…์€ ํ•˜์ง€ ์•Š์•˜๋‹ค. 

const [stackIndex, setStackIndex] = useState(0);
const [historyStack, setHistoryStack] = useState(DEFAULT_TEXT);

return (
  // ...
  <p dangerouslySetInnerHTML={{ __html: historyStack[stackIndex] }} />
);

 

ํƒœ๊ทธ ์ถ”๊ฐ€ ์‹œ historyStack ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ณ , ํ˜„์žฌ stackIndex์— ํ•ด๋‹นํ•˜๋Š” historyStack ๋ Œ๋”๋ง

// ํƒœ๊ทธ ์ถ”๊ฐ€ ์ „ historyStack[stackIndex]
'Pharetra convallis hendrerit integer ...'

// ํƒœ๊ทธ ์ถ”๊ฐ€ ํ›„ historyStack[stackIndex]
'Pharetra <span class="selected">convallis hendrerit integer</span> ...'

 

ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ


โถ ์ƒํƒœ ์ •์˜ (historyStack ์ƒํƒœ ๋ณ€ํ™” โ–ผ)

// ์ดˆ๊ธฐ ์ƒํƒœ
[
  "Pharetra convallis hendrerit integer ..."
]

// ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€/์‚ญ์ œํ–ˆ์„ ๋•Œ ์ƒํƒœ
[
  "Pharetra convallis hendrerit integer ...",
  "Pharetra <span class=\"selected\" data-nesting-level=\"1\"> ..."
]

 

  • historyStack : ํ…์ŠคํŠธ ๋žฉํ•‘ ์ด๋ ฅ(๋ถ€๋ชจ ์š”์†Œ์ธ <p> ํƒœ๊ทธ์˜ innerHTML ๋ฐ˜ํ™˜๊ฐ’)
  • stackIndex : historyStack์˜ ํ˜„์žฌ ์ธ๋ฑ์Šค

 

โท ํƒœ๊ทธ ์ถ”๊ฐ€ / ์‚ญ์ œ ์‹œ

import DOMPurify from 'dompurify';

// ...

const updateStack = () => {
  setHistoryStack((prevStack) => {
    if (!paragraphRef.current) return prevStack;

    // DOMPurify ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ XSS ๊ณต๊ฒฉ์— ์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ ๊ฒ€์‚ฌ/์ œ๊ฑฐ
    const cleanedHTML = DOMPurify.sanitize(paragraphRef.current.innerHTML);
    const newStack = [...prevStack, paragraphRef.current.innerHTML];

    if (newStack.length > 20) newStack.shift(); // ์ตœ๋Œ€ 20๊ฐœ๊นŒ์ง€๋งŒ ์ €์žฅ
    setStackIndex(newStack.length - 1); // ์‚ญ์ œ ํ˜น์€ ์ถ”๊ฐ€์‹œ ์ƒˆ๋กœ์šด ์Šคํƒ์˜ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋กœ ์ด๋™
    return newStack;
  });
};

 

  • ํƒœ๊ทธ ์ถ”๊ฐ€/์‚ญ์ œ ์‹œ ํ˜„์žฌ HTML ๋ฌธ์ž์—ด์„(๋ถ€๋ชจ ์š”์†Œ์˜ innerHTML) historyStack ์ƒํƒœ์— ์ถ”๊ฐ€
  • stackIndex ์ƒํƒœ๋ฅผ ํ˜„์žฌ historyStack์˜ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ

 

โธ undo / redo ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ

  • undo : stackIndex - 1 (stackIndex๊ฐ€ 0์ด๋ฉด ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑ)
  • redo : stackIndex + 1 (stackIndex์™€ historyStack ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค ๊ฐ™์œผ๋ฉด ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑ)
  1.  

 

์ „์ฒด ์ฝ”๋“œ


๋”๋ณด๊ธฐ

React + TypeScript โ–ผ

import React, { MouseEvent, useRef, useState } from 'react';
import classnames from 'classnames';
import DOMPurify from 'dompurify';

const initialText =
  'Pharetra convallis hendrerit integer nec eleifend tellus luctus lorem dignissim';
const spanClasses =
  'selected text-blue-500 before:content-["["] after:content-["]"]';

const calculateNestingLevelFromNode = (node?: Selection['anchorNode']) => {
  let nestingLevel = 1;
  let parentNode = node;

  while (parentNode && parentNode instanceof HTMLElement) {
    if (parentNode.classList.contains('selected')) nestingLevel++;
    parentNode = parentNode.parentNode;
  }

  return nestingLevel;
};

const calculateNestingLevel = (ref: React.RefObject<HTMLParagraphElement>) => {
  const spans = ref.current?.querySelectorAll('.selected');
  if (!spans) return;
  spans.forEach((spanElement) => {
    const span = spanElement as HTMLElement;
    const nestingLevel = calculateNestingLevelFromNode(span.parentNode);
    span.dataset.nestingLevel = nestingLevel.toString();
  });
};

export default function Fico() {
  const [deleteMode, setDeleteMode] = useState(false);
  const [stackIndex, setStackIndex] = useState(0);
  const [historyStack, setHistoryStack] = useState([initialText]);
  const paragraphRef = useRef<HTMLParagraphElement>(null);

  const updateStack = () => {
    setHistoryStack((prevStack) => {
      if (!paragraphRef.current) return prevStack;

      const cleanedHTML = DOMPurify.sanitize(paragraphRef.current.innerHTML);
      const newStack = [...prevStack, cleanedHTML];

      if (newStack.length > 20) newStack.shift(); // ์ตœ๋Œ€ 20๊ฐœ๊นŒ์ง€๋งŒ ์ €์žฅ
      setStackIndex(newStack.length - 1); // ์‚ญ์ œ ํ˜น์€ ์ถ”๊ฐ€์‹œ ์ƒˆ๋กœ์šด ์Šคํƒ์˜ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋กœ ์ด๋™
      return newStack;
    });
  };

  const addNewTag = () => {
    const selection = window.getSelection();
    const range = selection?.getRangeAt(0);
    if (!range || deleteMode) return;
    if (range.cloneContents().querySelector('.selected')) return;

    const selectedText = range.toString();
    if (selectedText.trim().length < 1) return;

    const span = document.createElement('span');
    span.className = spanClasses;
    span.textContent = selectedText;

    const nestingLevel = calculateNestingLevelFromNode(
      selection?.anchorNode?.parentNode,
    );
    span.dataset.nestingLevel = nestingLevel.toString();

    range.deleteContents();
    range.insertNode(span);

    updateStack();
  };

  const onUndoRedo = (type: 'undo' | 'redo') => {
    setStackIndex((prevIdx) => {
      return type === 'undo' ? prevIdx - 1 : prevIdx + 1;
    });
  };

  const deleteTag = (event: MouseEvent) => {
    const target = event.target as HTMLElement;
    if (!deleteMode || !target.classList.contains('selected')) return;

    const spanContent = target.innerHTML;
    const textNode = document.createElement('span');
    textNode.innerHTML = spanContent;
    const frag = document.createDocumentFragment();
    while (textNode.firstChild) frag.appendChild(textNode.firstChild);
    target.replaceWith(frag);

    calculateNestingLevel(paragraphRef);
    updateStack();
  };

  const onToggleDeleteMode = () => setDeleteMode((prev) => !prev);

  return (
    <section className={classnames({ 'cursor-eraser': deleteMode })}>
      <div className="mb-4 flex gap-2 items-center">
        <button onClick={() => onUndoRedo('undo')} disabled={stackIndex === 0}>
          UNDO
        </button>
        <button
          onClick={() => onUndoRedo('redo')}
          disabled={stackIndex === historyStack.length - 1}
        >
          REDO
        </button>
        <button onClick={onToggleDeleteMode}>DELETE MODE</button>
        <span>{deleteMode ? 'ON' : 'OFF'}</span>
      </div>
      <p
        onClick={deleteTag}
        ref={paragraphRef}
        className="text-2xl"
        onMouseUp={addNewTag}
        dangerouslySetInnerHTML={{ __html: historyStack[stackIndex] }}
      />
    </section>
  );
}

 

Selection API โšก๏ธ


(์œ„) 1๊ฐœ ํ…์ŠคํŠธ ๋…ธ๋“œ๋งŒ ์žˆ์„ ๋•Œ (์•„๋ž˜) 2๊ฐœ ์ด์ƒ์˜ ํ…์ŠคํŠธ ๋…ธ๋“œ๊ฐ€ ์žˆ์„ ๋•Œ

1๊ฐœ ํ…์ŠคํŠธ ๋…ธ๋“œ๋งŒ ์žˆ์„ ๋• ์–ด๋””๋ฅผ ๋“œ๋ž˜๊ทธํ•˜๋“  anchorNode, focusNode๋Š” ๋™์ผํ•˜๋‹ค. ๋งŒ์•ฝ ๋“œ๋ž˜๊ทธํ•œ ์˜์—ญ์˜ ์‹œ์ž‘๊ณผ ๋ ์‚ฌ์ด์— ์—ฌ๋Ÿฌ ํ…์ŠคํŠธ ๋…ธ๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด anchorNode, focusNode๋Š” ๋‹ค๋ฅธ ํ…์ŠคํŠธ ๋…ธ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ , ์ธ๋ฑ์Šค๋Š” ๊ฐ๊ฐ์˜ ํ…์ŠคํŠธ ๋…ธ๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค.

const selection = window.getSelection();
// ...

 

๋ฉ”์„œ๋“œ

  • selection.toString() : ๋“œ๋ž˜๊ทธํ•œ ํ…์ŠคํŠธ ๋ฐ˜ํ™˜
  • selection.getRangeAt(index) : index(0๋ถ€ํ„ฐ ์‹œ์ž‘)์— ํ•ด๋‹นํ•˜๋Š” Range ๊ฐ์ฒด ๋ฐ˜ํ™˜
  • selection.addRange|removeRange(range) : ์„ ํƒ ์˜์—ญ์— range ์ถ”๊ฐ€/์‚ญ์ œ
  • selection.removeAllRanges() : ๋ชจ๋“  ์„ ํƒ ์˜์—ญ ์ œ๊ฑฐ. selection.empty() ๋ฉ”์„œ๋“œ์™€ ๋™์ผ

 

ํ”„๋กœํผํ‹ฐ

  • selection.anchorNode : ์„ ํƒ์„ ์‹œ์ž‘ํ•œ ํ…์ŠคํŠธ ๋…ธ๋“œ
  • selection.anchorOffset : anchorNode์—์„œ ์„ ํƒ์„ ์‹œ์ž‘ํ•œ ์œ„์น˜์˜ ์ธ๋ฑ์Šค
  • selection.focusNode : ์„ ํƒ์„ ์ข…๋ฃŒํ•œ ํ…์ŠคํŠธ ๋…ธ๋“œ
  • selection.focusOffset : focusNode์—์„œ ์„ ํƒ์„ ์ข…๋ฃŒํ•œ ์œ„์น˜์˜ ์ธ๋ฑ์Šค
  • selection.rangeCount : ์„ ํƒํ•œ ๋ฒ”์œ„์˜ ๊ฐœ์ˆ˜ (๋“œ๋ž˜๊ทธํ•ด์„œ ์—ฌ๋Ÿฌ ๊ฐœ์ฒด๋ฅผ ์„ ํƒํ–ˆ๋‹ค๋ฉด 1 ์ด์ƒ)
  • selection.isCollapsed : anchor/focus๊ฐ€ ๊ฐ™์€ ์ง€์ ์— ์žˆ๋Š”์ง€ ์—ฌ๋ถ€
    • ์„ ํƒํ•œ ํ…์ŠคํŠธ๊ฐ€ ์—†๊ณ  ์ปค์„œ(Caret)๋งŒ ์กด์žฌํ•˜๋ฉด true ๋ฐ˜ํ™˜
    • ํ…์ŠคํŠธ๋ฅผ ์„ ํƒํ•˜๋ฉด false ๋ฐ˜ํ™˜
  • selection.type :
    • 'Range' : ํ…์ŠคํŠธ ๋ฒ”์œ„๋ฅผ ์„ ํƒํ•œ ์ƒํƒœ (anchor/focus๊ฐ€ ๋‹ค๋ฅธ ์ง€์ ์— ์žˆ์Œ)
    • 'Caret' : ์ปค์„œ๋งŒ ์กด์žฌํ•˜๋Š” ์ƒํƒœ(anchor/focus๊ฐ€ ๊ฐ™์€ ์ง€์ ์— ์žˆ์Œ, isCollapsed === true)
    • 'None' : ์„ ํƒํ•œ ํ…์ŠคํŠธ/์ปค์„œ๊ฐ€ ์—†๋Š” ์ƒํƒœ(๋กœ๋”ฉ ์ดˆ๊ธฐ ํ˜น์€ removeAllRange ํ˜ธ์ถœ ํ›„)

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•