본문 바로가기
Javascript

Template literals 구현 (1) - Selection과 Range

by memeseo 2023. 2. 7.

Template literals를 구현해 볼까 하고 시작한 일이 어언 2주 가까이 흘렀다. (말이 Template literals이지 멘션 기능과 흡사.) 시작할 때는 이렇게까지 시간이 오래 걸릴 줄 몰랐다. (당연함) 2주 동안 골머리를 싸맸기 때문에 꼭 아카이브로 남기고 싶었다.

 

내가 구현한 걸 간단히 설명하자면, 특정 구분자(#{)를 사용했을 때 div 태그 안에 span 태그를 추가하여 새로운 range를 만들고, 그 range안에 입력된 값에 대한 유효성 검사를 실시하고 오류가 있을 시 오류 메시지를 출력해 주는 기능이다. 위에 gif 이미지를 보면 알 수 있듯이 미리 정의해 둔 변수는 variable1과 variable2이다. variable1과 variable2가 아닐 경우 그리고 잘 못된 Template literals를 사용할 경우 오류 메시지를 출력한다. 

 

구현하면서 가장 힘들었던 부분은 캐럿 다루기였다. 이번 포스팅만 꼼꼼히 본다면 캐럿 다루기는 식은 죽 먹기일 것이다. (아마도.)

 

내가 구현한 방법은 크게 네 가지 개념을 알고 있어야 한다.

1. Selection API
2. Range API
3. Custom Element
4. Shadow DOM

 

순차적으로 포스팅 예정이며 마지막 포스팅 때는 전체적인 구현 흐름과 코드 공유 예정이다.

 


 

1. Selection

현재 선택된 텍스트 또는 캐럿의 위치에 관한 정보 제공선택된 텍스트와 캐럿의 위치를 조작할 수 있는 메서드들을 제공하는 Web API이다. 2023년 현재 거의 대부분 브라우저에서 지원하고 있다.

 

 사용 방법

<div contenteditable="true">하나둘셋넷</div>
const selection = window.getSelection(); //또는
const selection = document.getSelection();

 

window.getSelection()은 현재 선택된 범위에 대한 Selection 객체를 반환한다. Selection 프로퍼티는 아래와 같다.

 

 

Selection 객체 속성

 

anchorNode / focusNode

anchorNode와 focusNode는 각각 선택이 시작된 노트와 끝난 노드를 나타낸다. 위 캡처 이미지를 보면 '둘'에서 시작해 '셋'에서 끝난다. '둘'과 '셋'의 위치는 각각 2와 4이다.

 

❗ 두 node 모두 div가 아닌 '하나둘셋넷'이라는 textNode를 가리킨다.
자세한 내용은 Range API 설명할 때 참고.

 

anchorOffset / focusOffset

anchorOffset과 focusOffset은 각 선택이 시작된 시점과 끝난 시점을 나타낸다. '하나둘셋넷' 텍스트 노드를 예시로 봤을 때 '하'는 offset이 0이고, '넷'의 오른쪽이 offset이 4이다. 위 예제에서는 선택한 '둘'의 offset은 2, '셋'의 offset은 3이다.

 

❗ 선택은 앞에서 뒤로만 이루어지지 않는다. 마우스 드래그를 오른쪽에서 왼쪽으로 했을 경우 anchorOffset과 focusOffset은 반대가 된다.

 

isCollapsed

isCollapsed는 anchorOffset과 focusOffset이 동일한 지점에 있는지 여부를 나타낸다.

'하나둘셋넷'에서 셋과 넷 사이에 마우스를 클릭한 뒤 Selection 객체를 보면 isCollapsed가 true로 나온다.

 

rangeCount

selection 객체는 Range 객체를 포함하고 있다. Range 객체는 노드나 텍스트 노드의 일부분을 포함하는 문서의 조각(fragment)이며, selection은 여러 개의 Range 객체를 가질 수 있다. rangeCount는 Selection 객체가 갖고 있는 Range 객체의 개수를 나타낸다.

 

대부분의 브라우저에서는 Selection 객체당 1개의 Range 객체를 가지지만 FireFox는 Ctrl 키를 누르고 여러 개를 선택할 수 있는 기능(드래그를 여러 개 할 수 있다는 말.)이 있어 FireFox에서는 Selection객체가 여러 개의 Range 객체를 가질 수 있다.

 

Type

처음에 아무런 선택 이벤트가 발생하지 않은 상태에서는 'None'이다. isCollapsed가 true라면 'Caret'을, 그렇지 않으면 'Range'를 반환한다.

 

Selection 객체 메서드

 

getRangeAt(index)

const selection = window.getSelection();
const range = selection.getRangeAt(0);
console.log(range);

Range에 selection.prototype.getRangeAt을 통해 접근할 수 있다. 현재 크롬을 사용하고 있어서 getRangeAt(0)으로 하면 선택한 Range가 출력되지만, FireFox일 경우 모든 Range를 받아오고 싶다면 static 하게 0으로 입력하면 안 된다.

 

addRange(range)

현재 Selection 객체에 Range를 추가할 수 있다.

 

removeAllRange()

Selection 객체 안에 있는 모든 Range를 제거하여 아무것도 선택되지 않는 상태로 만든다.

 

이 외에도 더 많은 메서드들과 이벤트가 있지만, 내가 구현하는데 필요했던 메서드와 속성 개념만 설명했다. 더 자세하게 알고 싶다면 아래 링크 참고.

 

https://developer.mozilla.org/ko/docs/Web/API/Selection

 

Selection - Web API | MDN

A Selection object represents the range of text selected by the user or the current position of the caret. To obtain a Selection object for examination or manipulation, call window.getSelection().

developer.mozilla.org

 


2. Range 

Document의 일부분(fragment)으로, 인터페이스에서 제공하는 메서드를 이용해 document 내의 특정 부분을 지정하여 가져올 수 있다. 

 

사용 방법

const range = new Range();

 

Range 파헤치기

 

아래와 같은 HTML fragment가 있을 때,

<p id='p'>Example : <i>italic</i> and <b>bold</b></p>

DOM 구조는 아래와 같다. 이 DOM 구조를 이해하고 있어야 Caret 조작 시 수월하다. *

Node의 인덱스를 살펴보면 아래와 같다. 이걸 이용해 아래 이미지와 같이 특정 node들만 선택해 보자.

let range = new Range();

range.setStart(p, 0);
range.setEnd(p, 2);

let selection = document.getSelection();

selection.removeAllRanges();
selection.addRange(range);

range 메서드에 range와 선택하고자 하는 range의 인덱스를 입력하면 원하는 위치가 드래그가 된다.

위 코드 결과는 아래와 같다.

 

❗ 텍스트 노드와 요소 노드의 Caret 조작 방법은 다르다. *

startOffset MDN에 따르면 startOffset은 Range가 시작되는 startContainer의 위치를 나타낸다. 만약 startContainer가 Text, Comment 등과 같은 타입의 노드라면 해당 노드의 문자 수가 offset으로 지정되고 그 이외의 경우에는 reference Node의 child 노드 개수로 지정된다.

참고 : https://ko.javascript.info/selection-range

 

Range 객체 속성

 

collpased

Range의 시작점과 끝점이 동일한지 여부를 알 수 있다.

 

commonAncestorContainer

시작 컨테이너와 끝 컨테이너를 포함하고 있는 상위 노드이다.

 

endContainer

Range가 끝나는 지점의 끝 컨테이너 노드이다.

 

endOffset

endContainer 노드에서 끝 지점의 오프셋이다.

 

startContainer

Range가 시작하는 지점의 컨테이너 노드이다.

 

rangeStartOffset

startContainer 노드에서 시작 지점의 오프셋이다.

 

Range 객체 메서드

 

(1) 범위 시작 지점 조정

 

setStart(node, offset) 

선택한 node에서 시작 지점을 설정하는 메서드이다.

 

setStartBefore(node, offset)

선택한 node 이전에 위치한 노드를 시작지점으로 설정하는 메서드이다.

 

setStartAfter(node, offset)

선택한 node 이후에 위치한 노드를 시작지점으로 설정하는 메서드이다.

 

(2) 범위 종료 지점 조정

 

setEnd(node, offset)

선택한 node에서 종료지점 설정하는 메서드이다.

 

setEndBefore(node, offset)

선택한 node 이전에 위치한 노드를 종료지점으로 설정하는 메서드이다.

 

setEndAfter(node, offset)

선택한 node 이후에 위치한 노드를 종료지점으로 설정하는 메서드이다.

 

 

(3) 그 외 범위 지정 메서드

 

selectNode(node)

선택된 node를 전체 범위로 지정하는 메서드이다.

 

selectNodeContents(node)

선택된 node 전체의 콘텐츠를 범위로 지정하는 메서드이다.

 

cloneRange()

동일한 start/end 지점을 가지고 있는 range 객체를 복하는 메서드이다.

 

(4) 범위 내 콘텐츠 조작 메서드

 

deleteContents()

범위 내 콘텐츠를 제거하는 메서드이다.

 

cloneContents()

범위 내 콘텐츠 내용을 DocumentFragment객체로 반환하는 메서드이다.

 

insertNode(node)

node를 범위 시작 부분에 삽입하는 메서드이다.

 

더 많은 메서드는 아래 링크 참고.

https://developer.mozilla.org/en-US/docs/Web/API/Range/cloneContents

 

Range.cloneContents() - Web APIs | MDN

The Range.cloneContents() returns a DocumentFragment copying the objects of type Node included in the Range.

developer.mozilla.org