그럼에도 불구하고

👨‍💻

"selectstart" 글자 돋보기 만들기 본문

JavaScript/Function implementation

"selectstart" 글자 돋보기 만들기

zenghyun 2023. 1. 10. 19:41

글자를 drag 하면 확대된 말풍선을 보여주는 코드를 만들어보자

 

 

조건 :

1. 내가 드래그한 단어 및 문단은 형광펜으로 처리한다.

2. 드래그한 단어를 확대해서 말풍선으로 드래그 영역 끝부분에 보여준다.

3. 말풍선을 누르면 말풍선이 꺼진다. 

 

 

HTML

더보기
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>text Balloon</title>
  <link rel="stylesheet" href="./style.css">
  <script src="./main.js" defer></script>
</head>
<body>
  <div class="container">
    <div class="paragraph">가슴속에 그리고 피어나듯이 이름자를 있습니다. 우는 쉬이 추억과 노새, 어머님, 하나에 새워 청춘이 있습니다. 멀리 슬퍼하는 덮어 토끼, 하나에 아침이 위에 버리었습니다. 계집애들의 헤일 지나고 했던 내 노루, 위에 같이 흙으로 거외다. 이름과, 하나의 우는 벌써 책상을 봅니다. 내린 않은 하나에 별 내 잠, 보고, 봅니다. 하나에 별 토끼, 슬퍼하는 쉬이 까닭입니다. 없이 어머님, 위에도 까닭입니다. 자랑처럼 하나에 가을 무덤 나의 까닭이요, 없이 언덕 사랑과 있습니다.
    <br><br>
별들을 이네들은 하나의 내일 묻힌 내 지나가는 노루, 까닭입니다. 흙으로 헤는 아름다운 패, 위에도 한 봅니다. 애기 덮어 하나에 그러나 헤일 있습니다. 나는 멀리 사람들의 별 까닭입니다. 이름을 다하지 했던 내린 별 않은 내일 거외다. 언덕 가을로 아침이 이 가득 그러나 멀듯이, 봅니다. 멀리 가난한 별 라이너 봅니다. 잔디가 불러 어머니 나는 아직 프랑시스 하늘에는 봅니다. 이름과, 이웃 자랑처럼 겨울이 하나에 하나의 위에 내린 있습니다. 소학교 나는 이런 별빛이 묻힌 노새, 우는 봅니다. 못 아무 별 듯합니다.
<br><br>
풀이 북간도에 나는 노새, 프랑시스 하나에 계집애들의 딴은 듯합니다. 내린 나는 별빛이 잠, 계십니다. 라이너 하나 하나에 멀듯이, 써 내 그러나 별 흙으로 버리었습니다. 별에도 내 어머니, 있습니다. 별 하늘에는 릴케 위에 때 봅니다. 이름과, 별 어머님, 하나에 가을 나는 마리아 않은 있습니다. 하나에 당신은 마리아 이름을 둘 까닭입니다. 헤일 오면 다 묻힌 사랑과 별 아름다운 언덕 나는 봅니다. 나는 걱정도 한 불러 아침이 듯합니다. 이 파란 멀리 소녀들의 별 피어나듯이 마디씩 말 있습니다.
  </div>
</div>
  <div id ="balloon"></div>
</body>
</html>

 

CSS

body {
  background-color: rgba(188, 188, 240, 0.686);
  position: relative;
}

.container {
  margin: 50px auto;
  width: 500px;
  height: 800px;
  border: 3px solid black;
  border-radius: 30px;
  padding: 20px;
  box-sizing: border-box;
  background-color: rgba(255, 255, 255, 0.834);
}

.paragraph {
  font-size: 1.1rem;
  font-weight: bold;
  line-height: 1.4em;
}

.paragraph::selection {
  background-color: rgba(255, 192, 203, 0.807);
}

#balloon {
  cursor: pointer;
  position: absolute;
  min-width: 100px;
  top: 50%;
  left: 50%;
  text-align: center;
  opacity: 0;
  display: none;
}

#balloon::before {
  content: ' ';
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -10px;
  border: 10px solid transparent;
  border-top: 10px solid rgb(188, 188, 240);
}

#balloon.on {
  font-size: 25px;
  font-weight: bold;
  background-color: rgb(188, 188, 240);
  color: black;
  border-radius: 10px;
  opacity: 1;
  display: block;
  transition: 0.3s all;
}

 

JavaScript

    const paragraph = document.querySelector('.paragraph');
    const balloon = document.querySelector('#balloon');

    paragraph.addEventListener('selectstart', () => {
      paragraph.addEventListener('mouseup', (event) => {
        const selectedText = window.getSelection().toString();
        textValidation(selectedText, event);
      });
    });


    function textValidation(selectedText, event) {
      if (selectedText.length > 0) {
        balloon.innerHTML = selectedText;
        balloon.classList.add('on');
        balloon.style.top = `${event.clientY - balloon.clientHeight * 2.5}px`;
        balloon.style.left = `${event.clientX - balloon.clientWidth / 2}px`;
      } else {
        removeEvent();
      }
    }

    balloon.addEventListener('click', () => {
      removeEvent();
    })
    
    function removeEvent() {
      balloon.classList.remove('on');
    }

 

※ selectstart

selectstart 이벤트는 텍스트의 선택 시점에 발생한다. 

 

용도

  • 텍스트 선택 시점에 작업을 처리하고 싶을 때
  • 텍스트 선택 시점의 작업을 무효화하고 싶을 때
paragraph.addEventListener('selectstart', () => {
      paragraph.addEventListener('mouseup', (event) => {
        const selectedText = window.getSelection().toString();

        textValidation(selectedText, event);
      });
    });

paragraph라는 변수에 'selectstart' 이벤트가 발생하고, 'mouseup' 이벤트가 발생하면, 

selectedText라는 변수에 내가 선택한 텍스트의 값을 가져와서 문자열로 만든다. 

 

즉, window.getSelection()은 선택 범위를 반환하며, toString()을 문장 끝에 추가하면 선택 중인 문자열을 반환한다.

 

※ getSelection() 

더보기

getSelection()을 쓰면 내가 선택한 텍스트 값을 가져올 수 있다.  그전에 Selection 객체에 대해 알아보자.

[ Selection ]

window.getSelection()을 통해 얻을 수 있는 값이 Selection 객체이다.

Selection 객체는 현재 커서의 위치나 선택한 범위를 나타낸다.

왼쪽 => 오른쪽 또는 오른쪽 => 왼쪽 방향을 가질 수 있고, 시작과 끝을 속성으로 구분할 수 있다.

[ 속성 및 메소드 ]

  • Selection.anchorNode: 선택이 시작되는 위치
  • Selection.anchorOffset: anchor의 Offset.
    anchorNode가 텍스트면 anchorNode 앞에 있는 문자 수 (index인 셈이다.)을 나타내고,
    요소면 anchorNode의 자식 노드 수를 나타냅니다
  • Selection.focusNode: anchorNode와 반대로 선택이 끝나는 위치
  • Selection.focusOffset: focus의 Offset.
    anchorOffset의 focus 버전으로 생각하시면 됩니다
  • Selection.isCollased: 시작 지점과 끝 지점이 같으면 true, 다르면 false 입니다
  • Selection.type: 범위를 지정했으면 "Range", 지정하지 않았으면 "Caret", 아예 클릭을 안했다면 "None"
    => 새로고침하고 클릭 안한 상태에서 isCollapsed는 true지만 type은 "None"이다.
  • window.getSelection().collapse(dom, number): 커서를 dom의 number로 이동한다.

 

그 후, textValidation 함수를 통해 selectedText와 event 값을 인자로 받아와서 함수를 실행한다. 

 


 

function textValidation(selectedText, event) {
      if (selectedText.length > 0) {
        balloon.innerHTML = selectedText;
        balloon.classList.add('on');
        balloon.style.top = `${event.clientY - balloon.clientHeight * 2.5}px`;
        balloon.style.left = `${event.clientX - balloon.clientWidth / 2}px`;
        console.log(`${event.clientX}, ${event.clientY}`);
      } else {
        removeEvent();
      }
    }

이 부분에서 많이 애를 먹었다. 

 

일단 첫 번째로 body의 position을 relative로 잡고 말풍선과 관련된 요소들은 모두 position:absolute;로 잡았다.

왜냐하면 말풍선의 위치를 나타내기 위한 절대적인 기준점이 필요하다고 생각했고, 그 기준점을 body로 잡은 것이다.

 

그 후, balloon의 top 위치와 left 위치를 지정해줬는데 우선 event.clientX, event.clientY로 위치를 잡아봤다. 

 

우선, 여기서 event가 발생한 시점은 paragraph에서 'mouseup'이 된 시점이다. 

그 시점에 event.clientX, event.clientY를 잡고 그 값을 

 

balloon.style.top = `${event.clientY} px`;
balloon.style.left = `${event.clientX}px`; 과 같이 잡았다. 

 

하지만, 내가 원하는 위치에 나오지 않았다. (적어도 내가 선택한 문장에 가까운 곳에 말풍선이 나왔으면 했는데 전혀 다른 곳에 나오게 되었다.)

 

top과 left 모두 드래그한 영역 끝부분에 나오지 못하고 더 아래로, 더 멀리 좌표 값이 잡혔다. 

 

아마, position을 잡는 과정에서 내가 생각한 것과 오차? 가 발생하는 게 아닌가 싶었다. 정확한 원인은 잘 모르겠다.

 

그래서 가장 근접한 좌표값을 구하기 위해서 좌표에 관련된 속성을 찾다가 clientWidthclientHeight에 대해 알게 되었고, 이를 사용하여 

balloon.style.top = `${event.clientY - balloon.clientHeight} px`;
balloon.style.left = `${event.clientX - balloon.clientWidth} px`; 과 같이 잡았다. 

 

※ clientWidth, clientHeight 

더보기

element의 크기를 구하는 방법에는 여러 가지가 있는데 clientWidth와 clientHeight는 offsetWidth, offsetHeight과 다르게 실제로 보이고 있는 콘텐츠가 얼마만큼의 공간을 차지하는가에 대한 값을 가져온다. 

 

정리하자면

  • offsetWidth, offsetHeight  : 엘리먼트의 패딩과 보더, 스크롤바의 사이즈를 포함한 값을 리턴한다.
  • clientWidth, clientHeight  : 보더와 스크롤바의 크기를 제외한 실제 컨텐츠의 크기를 리턴한다. (패딩은 포함하고 있다.)

REF: https://ohgyun.com/571

 

 

아까보다는 근사치에 가까워졌지만 미흡한 부분이 있었고 그 부분은 수치를 대입해서 바로잡았다. 

수치의 기준은 수치를 대입하지 않았을 때, 말풍선의 나오는 위치를 파악하고 내가 자리 잡고 싶은 부분까지 어느 정도 부족한지 파악하여 산정하였다.

 

최종적으로

balloon.style.top = `${event.clientY - balloon.clientHeight * 2.5 } px`;
balloon.style.left = `${event.clientX - balloon.clientWidth / 2 } px`; 과 같이 잡았다. 

 

 


 

말풍선을 클릭하면, 없어지게 하는 것은 간단하다. 

 

removeEvent라는 함수를 만들어서 CSS상에서 내가 만든 .on이라는 class를 제거해주면 된다. 


결과

 

See the Pen Untitled by zenghyun (@zenghyun) on CodePen.

 

 

 

Comments