"Selection.toString이 텍스트 줘. Range가 rectangle, 둘러싼 context, anchor와 focus node, selection을 programmatic으로 grow / shrink 할 능력 줘. Lesson 3가 ClipDeck이 highlight / screenshot / 결국 둘러싼-문단 capture에 필요한 deep cut."
Selection vs Range
두 object, 관련 있지만 distinct:
Selection— user가 현재 document에서 highlight 한 것. document 당 정확히 하나의Selection,window.getSelection()통해 access. 현대 browser에서 0 또는 1 개 range 가짐 (multi-range는 Firefox-only feature 였음).Range— DOM 트리의 두 지점 사이 구간이야. user의 선택과는 상관없이 존재해. 원하는 만큼 만들어서 강조하거나, 크기를 재거나, 그 자리로 스크롤하는 데 쓸 수 있어.
user가 고른 영역은 window.getSelection().getRangeAt(0)으로 잡아. 그 Range 하나에서 ClipDeck이 clip에 붙여 둘 정보를 전부 뽑아낼 수 있어.
Range field
range.startContainer/range.endContainer— selection이 시작 / 끝나는 DOM node. 종종 Text node, 가끔 Element node.range.startOffset/range.endOffset— Text node 안 문자 offset, 또는 Element node 안 child index.range.commonAncestorContainer— 두 endpoint 다 포함하는 가장 낮은 DOM node. 둘러싼 문단이나 article 찾기에 유용.range.collapsed— start === end 일 때 true (길이 0 selection, cursor blink).range.toString()— 선택 영역을 plain text로 돌려줘. 결과는selection.toString()과 같아.range.cloneContents()— 선택 node 들이 deep-clone 된DocumentFragment반환. Clip에 formatting (link / bold / code span) 보존에 사용.range.getBoundingClientRect()— selection의 visible 위치 union rect. Screenshot과 floating UI 배치용.range.getClientRects()— rect 배열, 줄 당 하나. Selection이 여러 줄 걸칠 때 유용.
둘러싼 문단으로 grow
Default user selection은 highlight 한 거 — 종종 mid-sentence 나 그냥 phrase. ClipDeck이 'expand to paragraph' 를 tweak으로 제공 가능:
function expandToParagraph(range) {
const ancestor = range.commonAncestorContainer;
const para = (ancestor.nodeType === 1 ? ancestor : ancestor.parentElement)
.closest('p, li, blockquote, h1, h2, h3, h4, h5, h6, div');
if (!para) return range;
const newRange = document.createRange();
newRange.selectNodeContents(para);
return newRange;
}
'highlight 한 문장만 아닌 전체 문단 원함' 에 유용. Preview-and-confirm UI (Lesson 6)에서 제공.
Clip의 formatting 보존
ClipDeck v1이 plain text 저장. Formatting 선택적으로 보존 원하면:
const fragment = range.cloneContents();
const tmp = document.createElement('div');
tmp.appendChild(fragment);
const htmlContent = tmp.innerHTML; // 저장 전 DOMPurify 통해 sanitize
toString으로 뽑은 text와, 있으면 html까지 같이 저장해 둬. side panel은 html이 있으면 그걸 그리고, 클립보드로는 언제나 text가 나가.
Multi-line selection bound
User가 텍스트 여러 줄 highlight 하면, getBoundingClientRect()가 다 감싸는 큰 rectangle 하나 반환 — 영역 screenshot에 유용하지만 line-by-line UI 엔 부정확. getClientRects()가 줄 당 rect 하나 반환, 정확한 highlight overlay 그릴 때 원하는 거:
const rects = Array.from(range.getClientRects());
rects.forEach((r) => drawHighlightOverlay(r));
Programmatic selection
Selection을 programmatic으로 설정 / 복원 (예: user가 '이 clip이 어디서 왔는지 보여 줘' click 후):
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(rangeIRestored);
여기에 range.startContainer.scrollIntoView({ behavior: 'smooth', block: 'center' })를 얹으면, user를 그 clip이 원래 있던 자리로 데려가서 다시 강조해 줄 수 있어. ClipDeck v2에서 붙일 만한 기능이지.
window.getSelection()을 불러도 열린 shadow root 안쪽 선택은 안 보여. 거긴 자기만의 Selection을 따로 갖고 있어서 그 shadow root의 getSelection()으로 꺼내야 하거든. 닫힌 shadow root는 완전 opaque. ClipDeck v1 엔 제한이 충분히 드물어 defer. v2가 web component 의존 site 위해 명시적으로 shadow root walk 할 수도.