"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 tree 의 두 지점 사이 span. User selection 과 독립; 원하는 만큼 만들고 highlight / scroll / 측정 / 스크롤에 사용 가능.
User 의 selection 이 window.getSelection().getRangeAt(0). 그 Range 에서 ClipDeck 의 clip metadata 에 중요한 모든 것 read 가능.
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() — selection.toString() 과 같은 plain text.
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 으로 제공 가능:
'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
text (toString 에서) 와 optional html field 둘 다 저장. Side panel 이 html 있으면 렌더; clipboard 는 항상 text 받음.
Multi-line selection bound
User 가 텍스트 여러 줄 highlight 하면, getBoundingClientRect() 가 다 감싸는 큰 rectangle 하나 반환 — 영역 screenshot 에 유용하지만 line-by-line UI 엔 부정확. getClientRects() 가 줄 당 rect 하나 반환, 정확한 highlight overlay 그릴 때 원하는 거:
range.startContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }) 와 결합, user 를 clip 의 원래 위치로 teleport 하고 다시 highlight 가능. ClipDeck v2 feature.
Selection 이 user intent 줘. Range 가 geometry, 구조, 그것에 operate 할 능력 줘. 함께 단순 텍스트 grab 부터 full screenshot-with-highlight feature 까지 모든 것 cover.
Shadow DOM 예외. Isolated content script 의 window.getSelection() 이 open shadow root 안 selection 안 봄 — 그것들이 shadow root 의 getSelection() 통해 access 가능한 자체 Selection object 가짐. Closed shadow root 는 완전 opaque. ClipDeck v1 엔 제한이 충분히 드물어 defer; v2 가 web component 의존 site 위해 명시적으로 shadow root walk 할 수도.
Code
content.js — selection 도구가 text / html / rect 반환·javascript
// content.js — rect + optional HTML 가진 full selection capture
CD_TOOLS.selection = async (args = {}) => {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return { ok: false, reason: "no-selection" };
let range = sel.getRangeAt(0);
if (range.collapsed) return { ok: false, reason: "empty" };
if (args.expandToParagraph) {
range = expandToParagraph(range);
}
const rect = range.getBoundingClientRect();
const lineRects = Array.from(range.getClientRects()).map((r) => ({
top: r.top, left: r.left, width: r.width, height: r.height,
}));
const fragment = range.cloneContents();
const tmp = document.createElement("div");
tmp.appendChild(fragment);
const html = tmp.innerHTML; // caller 가 저장 전 sanitize
return {
ok: true,
selection: {
text: range.toString(),
html,
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
lineRects,
ancestorTag: (range.commonAncestorContainer.nodeType === 1
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement
)?.tagName?.toLowerCase() ?? null,
},
};
};
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;
}
content.js — clip provenance 위한 restore + scroll-into-view skeleton·javascript
// content.js — 저장된 selection 복원 + scroll-to
// (range 의 serializable 표현 ship 하고 rehydrate.)
function restoreSelection(serialized) {
// serialized = { startContainerPath, startOffset, endContainerPath, endOffset }
const start = nodeFromPath(serialized.startContainerPath);
const end = nodeFromPath(serialized.endContainerPath);
if (!start || !end) return false;
const range = document.createRange();
range.setStart(start, serialized.startOffset);
range.setEnd(end, serialized.endOffset);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
start.parentElement?.scrollIntoView({ behavior: "smooth", block: "center" });
return true;
}
// 'nodeFromPath' 가 indexed step list 로 DOM walk — serialize 방식에 따라
// 구현 다름. 흔한 접근: document.body 에서 child index 배열. Heavily-dynamic
// SPA 엔 brittle; 안정적 page 에선 잘 동작.
첫 번째 code block (CD_TOOLS.selection + expandToParagraph) 을 clipdeck/content.js 에 추가. Reload. Wikipedia article 에서 문단 가운데 문장 highlight. Content script DevTools console 에서 chrome.runtime.sendMessage({type:'tool', name:'selection', args:{}}) 타이핑하고 응답 inspect — text, html, rect, lineRects populate 된 거 보임. args:{expandToParagraph:true} 로 재시도 — text 가 이제 전체 문단 cover 해야 함. One-line vs multi-line highlight 의 lineRects count 비교; multi-line 이 더 많은 rect 반환, Lesson 4 의 screenshot crop 에 유용.
Hint
cloneContents() 가 빈 fragment 반환하면, selection 이 비었거나 range 가 collapse 됨; sel.rangeCount 와 range.collapsed 먼저 확인. expandToParagraph 가 같은 range 반환하면, commonAncestorContainer 에 <p> ancestor 없음 — page 의 실제 <p> element 안에서 선택 시도. 두 번째 block 의 serialize/restore 코드는 예시; production 코드는 적당한 DOM 변경 살아남는 nodeFromPath 구현 필요 — XPath 가진 document.evaluate 가 robust option 하나.
Progress
Progress is local-only — sign in to sync across devices.