코드 블록의 선택 끝점은 텍스트 노드가 아닐 수 있다
Kaya Toast의 본문 노트는 선택한 글에 시작·끝 오프셋을 붙여 저장한다. 구문 강조된 코드에서는 같은 줄을 선택해도 노트를 만들지 못하는 경우가 있었다. 이전 계산은 텍스트 노드만 순회하며 선택 끝점과 같은 노드를 찾았는데, 브라우저의 Range는 요소와 그 자식 사이의 경계도 끝점으로 반환할 수 있다.
변경은 블록 시작부터 선택 끝점까지 새 Range를 만들고, 그 문자열 길이를 오프셋으로 사용했다. 텍스트 노드의 offset은 문자열 내부 위치이고 요소의 offset은 자식 노드 사이 위치다. 둘을 직접 같은 숫자로 해석하는 대신 DOM의 Range가 이 차이를 처리하도록 맡겼다.
const prefix = document.createRange();
prefix.setStart(block, 0);
prefix.setEnd(node, offset);
return prefix.toString().length; 위는 범위 확인과 예외 처리를 생략한 핵심 계산이다. 이 길이는 JavaScript 문자열의 UTF-16 단위다. 이모지 하나가 항상 오프셋 1이라는 뜻은 아니며, 복원할 때도 같은 단위로 텍스트 노드를 읽어야 한다.
여러 문단을 드래그해도 저장 범위는 한 블록이다
이전에는 양 끝점이 같은 블록에 있어야 노트 위치를 찾았다. 변경 후에는 Range의 시작을 포함한 블록을 고른다. 여기서 시작은 사용자가 마우스를 처음 누른 곳이 아니라 문서 순서로 정규화된 시작점이다. 뒤에서 앞으로 드래그해도 문서상 앞쪽 블록이 기준이 된다.
| 선택 범위 | 저장되는 위치와 인용 |
|---|---|
| 양 끝이 같은 블록 | 그 블록 안의 시작·끝 오프셋과 선택 문자열 |
| 끝이 다음 블록 | 시작 블록의 끝에서 자른 오프셋과 인용 문자열 |
| 본문 밖까지 걸친 선택 | 공통 조상이 본문 안에 없으면 노트 생성 거절 |
이 선택은 blockIdx / startOffset / endOffset / textSnippet이라는 기존 저장 형태를 유지한다. 여러 문단에 걸친 노트를 새로 표현할 필요가 없는 대신, 사용자가 드래그한 인용의 뒷부분은 저장되지 않는다. 함께 바뀐 메뉴 위치는 fixed 요소에 맞춰 mouseup의 clientX / clientY를 쓴다. 문서 좌표인 scrollY를 더하던 계산을 제거해 좌표계를 맞췄다.
오프셋으로 다시 그릴 수 있어도 본문 수정에 강한 것은 아니다
하이라이트 복원은 저장한 블록 번호와 오프셋으로 텍스트 노드를 다시 찾는다. 인라인 경계 때문에 surroundContents가 실패하면 내용을 추출해 mark에 넣는 경로도 있다. 다만 저장된 인용문을 검색해 새 위치로 옮기는 fallback은 구현되어 있지 않다. 블록 순서나 본문이 바뀌면 옛 위치가 같은 뜻을 유지한다고 보장할 수 없다.
기존 Storybook은 실제 선택 로직을 쓰지 않는 정적 표시 예제다. Range 상호작용 회귀 테스트도 없어, span 경계·역방향 드래그·블록 간 잘림·스크롤한 화면은 실제 브라우저에서 따로 검증할 범위로 남는다. 선택 감지는 mouseup/mousedown에 연결되어 있으므로 키보드와 터치 선택의 지원까지 이 변경으로 설명할 수는 없다. Kaya Toast에서 해결한 범위는 브라우저 선택 끝점을 기존의 한 블록 노트 형식으로 변환하는 부분이다.