[Javascript] Selection에 대해 알아보자
https://ko.javascript.info/selection-range 의 Selection 파트를 번역했다
일부 내용이 포함되지 않을 수 있다
Selection
Range는 선택 범위를 관리하기 위한 generic 객체이다.
Range를 생성해도 range만으로는 실제로 선택을 할 수없다.
실제 선택을 하기위해서는 Selection 객체를 이용해야 한다.
window.getSelection( ) 이나 document.getSelection( )을 통해 Selection 객체를 얻을 수 있다.
Selection API specification 에 따르면
Selection은 0개 이상의 range를 가질 수도 있다고 하지만 Firefox에서만 여러개의 선택 범위를 가질 수 있다.
다른 브라우저에서는 최대 1개의 range만 가질 수 있다.
Selection의 속성들
range와 비슷하게 selection도 시작(anchor), 끝(focus)를 가진다
주요 속성들은
- anchorNode – selection이 시작하는 노드
- anchorOffset – anchorNode의 오프셋(시작하는 인덱스)
- focusNode – selection이 끝나는 노드
- focusOffset – focusNode의 오프셋(끝나는 인덱스)
- isCollapsed – range가 비어있거나 존재하지 않을 때, true가 됨
- rangeCount – range의 개수, 앞서 말했듯 Firefox 외에는 최대 1개
=> Selection의 끝이 시작 지점보다 앞에 있을 수도 있다
마우스로 화면을 드래그 한다고 했을 때,
드래그는 왼쪽 -> 오른쪽, 오른쪽 -> 왼쪽 두 방향 다 가능하다.

selection의 시작 지점이 끝 지점보다 앞에 있을 때 이 selection은 forward 방향을 가진다.

반대의 경우, selection은 backward 방향을 가진다. 이 경우 focus는 anchor보다 앞에 있다.
이는 Range와 다른 지점인데 range는 언제나 forward 방향이다. 시작 지점이 끝 지점보다 뒤에 있을 수 없다.
Selection 이벤트
selection을 추적하기 위한 이벤트들이 있다
- elem.onselectstart – selection이 elem에서 시작할 때 (예를 들어, 유저가 버튼을 누른 상태로 마우스를 움직이기 시작할 때)
- default action을 막도록 해서 selection이 시작하지 않도록 할 수 있음
- document.onselectionchange – selection이 바뀔 때 동작
- 주의 : 이 핸들러는 document 키워드를 이용해서만 설정될 수 있음
Selection tracking 예시
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
document.onselectionchange = function() {
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
from.value = `${anchorNode && anchorNode.data}:${anchorOffset}`;
to.value = `${focusNode && focusNode.data}:${focusOffset}`;
};
</script>
Selection getting 예시
Selection 전체를 가져오는법
- 텍스트로 가져오기 : document.getSelection( ).toString( )
- DOM 노드로 가져오기 : range를 불러온 후 cloneContents( ) 메서드 이용
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
cloned.innerHTML = astext.innerHTML = "";
// range를 통해 DOM 노드를 가져오는 코드
for (let i = 0; i < selection.rangeCount; i++) {
cloned.append(selection.getRangeAt(i).cloneContents());
}
// 텍스트를 가져오는 코드
astext.innerHTML += selection;
};
</script>
Selection 메서드들
range를 더하거나 지우는 메서드
- getRangeAt(i) –0 부터 시작해 i번째 range를 가져옴. firefox를 제외한 모든 브라우저에서 0만 사용
- addRange(range) – selection에 range 추가. selection이 이미 관련된 range를 가지고 있으면 Firefox를 제외한 모든 브라우저는 요청 무시
- removeRange(range) – range를 selection에서 제외
- removeAllRanges( ) – 모든 range를 삭제
- empty( ) – removeAllRanges 의 별칭
추가로 selection의 Range 객체 없이 range를 다이렉트로 조작하는 간편한 메서드들도 있다
- collapse(node, offset) – 선택된 range를 주어진 노드의 주어진 오프셋에서 시작해서 끝나는 range로 대체한다.
- setPosition(node, offset) – collapse 의 별칭.
- collapseToStart( ) – selection을 시작 지점 방향으로 비운다
- collapseToEnd( ) – selection을 끝 지점 방향으로 비운다
- extend(node, offset) – focus(끝나는 지점)를 주어진 node 의 주어진 offset 으로 옮긴다
- setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) – selection의 range의 시작을 주어진 anchorNode/anchorOffset 로, range의 끝 지점을 주어진 focusNode/focusOffset 로 대체한다. 그 사이의 모든 컨텐츠가 선택된다.
- selectAllChildren(node) – node의 모든 children을 선택한다
- deleteFromDocument( ) – 선택된 content를 document에서 지운다
- containsNode(node, allowPartialContainment = false) – selection이 node를 포함하고 있는지 체크 (두번째 인자가 true이면 부분적인 선택이어도 true가 된다)
다수의 작업이 굳이 Range 객체를 이용할 필요없이 Selection 메소드를 이용 가능하다.
예를 들어, <p>의 모든 컨텐츠를 선택하기 위해서 아래와 같이 사용할 수 있다
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
// <p>의 0번째부터 마지막 child까지 선택한다
document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>
같은 작업을 range를 이용해서 작성하면 아래와 같다
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too
document.getSelection().removeAllRanges(); // clear existing selection if any
document.getSelection().addRange(range);
</script>
선택하기 위해서 존재하는 Selection을 먼저 삭제해야 한다
Selection이 이미 존재하면, removeAllRanges()를 통해 먼저 비우고 range를 추가해야한다. 예외의 경우는 setBaseAndExtent 메소드를 사용해서 존재하는 selection을 대체하는 것이다
Form 컨트롤에서의 Selection
input이나 textarea와 같은 from 요소는 selection이나 range 없이 특별한 api를 제공한다 (special API for selection)
input의 value는 html이 아닌 순수 텍스트이기 때문에 객체가 따로 필요 없이 훨씬 단순하게 작동한다
속성
- input.selectionStart – selection이 시작하는 포지션 (쓰기가능)
- input.selectionEnd – selection이 끝나는 포지션 (쓰기가능)
- input.selectionDirection – selection의 방향. “forward”, “backward” 그리고 “none”이 있다(마우스 더블클릭 등의 경우가 이에 해당한다)
이벤트
- input.onselect – 선택된 내용이 있는 경우 동작한다
메소드
- input.select( ) – 텍스트 컨트롤을 전부 선택한다 (input 대신 textarea도 가능하다)
- input.setSelectionRange(start, end, [direction]) – selection을 start부터 end 포지션까지 span으로 변경한다. 방향 인자가 주어지면 방향대로 실행한다
- input.setRangeText(replacement, [start], [end], [selectionMode]) – range의 텍스트를 새로운 텍스트로 변경한다. 옵션 사항인 start 과 end가 주어지면, range의 시작과 끝을 설정한다. 아니면 selection이 사용된다.
마지막 인자 selectionMode는 텍스트가 변경되고 나서의 selection을 결정한다. 가능한 value들은 다음과 같다
- "select" – 새롭게 추가된 텍스트가 선택된다.
- "start" – selection range가 추가된 텍스트 바로 앞에서 비워진다. (커서가 추가된 텍스트 바로 앞에 위치)
- "end" – selection range가 추가된 텍스트 바로 뒤에서 비워진다. (커서는 바로 다음에 위치)
- "preserve" – selection을 보존한다. 디폴트 값이다.
예제: Selection 추적
아래 코드는 selection 추적을 위해 onselect를 사용한다.
**<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>
<script>
area.onselect = function() {
from.value = area.selectionStart;
to.value = area.selectionEnd;
};
</script>**
주의:
- onselect는 사용자가 뭔가를 선택하는 경우 실행되지만, selection이 지워졌을 때는 실행되지 않음
- spec 에 따르면, document.onselectionchange 이벤트는 from 컨트롤 안의 selection들에 대해서는 동작해서는 안된다. document의 selection과 ranges와 관계되지 않았기 때문이다.
예제: 커서 이동
우리는 selection을 설정하는 selectionStart와 selectionEnd를 변경할 수 있다.
중요한 엣지 케이스는 selectionStart와 selectionEnd가 각각 동일할 때이다.
그렇게 되면 그것이 바로 커서 위치가 된다. 즉 아무것도 선택되지 않은 경우 선택 항목이 커서 위치에서 사라진다.
따라서 selectionStart와 selectionEnd를 같게 설정함으로써 커서를 이동할 수 있다.
예시:
<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>
<script>
area.onfocus = () => {
// focus 액션이 실행되면 바로 실행하기 위해 setTimeout은 0이다.
setTimeout(() => {
// start와 end가 같으면, 커서는 바로 그 자리에 위치
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
예제: selection 수정
selection의 컨텐츠를 수정하기 위해서는 input.setRangeText( ) 메서드를 사용할 수 있다.
selectionStart/End 를 읽고 selection을 바탕으로 해당 값의 하위 문자열을 변경할 수도 있지만 setRangeText가 더 강력하고 편리하다.
예제를 보자
<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>
<script>
button.onclick = () => {
if (input.selectionStart == input.selectionEnd) {
return; // 선택된 것이 없는 경우
}
let selected = input.value.slice(input.selectionStart, input.selectionEnd);
input.setRangeText(`*${selected}*`);
};
</script>
input 안의 선택된 부분의 value 시작과 끝에 *를 붙인 텍스트로 변경한다
아래와 같이 start와 end 인자를 설정할 수도 있는데
해당하는 인덱스의 값의 텍스트를 변경한다.
<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>
<script>
button.onclick = () => {
let pos = input.value.indexOf("THIS");
if (pos >= 0) {
input.setRangeText("*THIS*", pos, pos + 4, "select");
input.focus(); // focus to make selection visible
}
};
</script>
예제: 커서 위치에 삽입
아무것도 선택되지 않은 경우나 setRangeText로 동일한 시작지점과 끝지점을 설정한 경우, 새로운 텍스트가 삽입되고 원래 내용은 제거되지 않는다.
setRangeText를 사용해서 커서 위치에 내용을 삽입할 수 있다.
<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>
<script>
button.onclick = () => {
input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
input.focus();
};
</script>
HELLO가 커서 위치에 삽입되게 된다.
선택 불가능하게 하기
세 가지 방법이 있다.
- CSS 속성인 user-select: none 을 이용한다
<style>
#elem {
user-select: none;
}
</style>
<div>Selectable <div id="elem">Unselectable</div> Selectable</div>
elem 에서는 선택이 시작되지 못하도록 한다.
하지만 사용자는 다른 요소에서 선택하기 시작해 elem을 포함시킬 수도 있다.
- onSelectstart나 mousedown 이벤트의 default 액션을 막는다
<div>Selectable <div id="elem">Unselectable</div> Selectable</div>
<script>
elem.onselectstart = () => false;
</script>
이 방법 또한 elem에서 selection을 시작하지 못하도록 하지만 1번과 마찬가지로 다른 요소에서 elem까지 포함하는 것은 가능하다
- document.getSelection( ).empty( ) 을 사용하여 selection이 생성되고 나서 selection을 지우는 방법도 있다. selection이 생겼다가 사라지면서 깜빡임이 있기 때문에 자주 사용되지는 않는다.
이제 selection을 활용해서 기능 구현할 시간이다.....