정보/블로그 운영팁

티스토리 모바일에서 유용한 떠 있는 플로팅 목차 위치 이동 방법

아미넴 2020. 12. 21.
반응형

이번에는 플로팅 목차를 드래그 앤 드롭으로 원하는 위치로 이동시키는 기능을 만들건데요.

화면이 좁은 모바일에서 공간을 좀 더 효율적으로 활용하고자 생각하게 되었습니다.

 

목차가 계속 진화하고 있습니다.

목차 하나로 이렇게 다양한 컨텐츠가 나올 줄은 저도 미처 몰랐습니다 ㅎㅎ

어느새 제 블로그에 블로그 꾸미기가 주요 컨텐츠로 자리 잡았네요.

 

그럼 시작해 보겠습니다!

 

목차

     

    플로팅 목차 적용

    앞서 플로팅 목차를 적용하지 않으신 분은 다음 포스팅을 참고 바랄게요.

     

    티스토리 글에 자동으로 목차 넣기

     

    티스토리 글에 자동으로 목차 넣기

    목차를 넣고 싶긴한데 글 쓸 때마다 매번 수작업으로 만든다면 상당히 번거롭겠죠. 그래서 jQuery 플러그인 Table of Contents(TOC)를 이용하여 자동으로 넣는 방법을 소개합니다. 저는 제목1, 제목2로

    sangminem.tistory.com

    티스토리 목차 글머리 서식 변경하기

     

    티스토리 목차 글머리 서식 변경하기

    기본 글머리 기호도 심플하긴 하지만 원하는 모양으로 바꾸면 더 좋을 것 같다는 생각을 했습니다. 기본 글머리 설정 방법 list-style-type - CSS: Cascading Style Sheets | MDN The list-style-type CSS proper..

    sangminem.tistory.com

    티스토리 떠 다니는 플로팅 목차 만드는 방법 (초급 버전)

     

    티스토리 떠 다니는 플로팅 목차 만드는 방법 (초급 버전)

    목차를 확인하려면 다시 위로 올라가야 하는 불편함을 해소하고자 만들어 보았습니다. 생각보다 깔끔하게 잘 만들어진 것 같아서 공유합니다. 주의) 고급 버전을 먼저 반영하고 초급 버전을 나

    sangminem.tistory.com

    티스토리 떠 다니는 플로팅 목차 만드는 방법 (고급 버전)

     

    티스토리 떠 다니는 플로팅 목차 만드는 방법 (고급 버전)

    초급 버전에서 좀 더 기능을 강화한 버전을 만들어 보았습니다. 어느 정도 코딩이 가능한 분이어야 수월하게 따라하실 수 있을 겁니다. 이 부분이 어렵게 느껴지시는 분은 다음 초급 버전을 참

    sangminem.tistory.com

     

    이번에 알려드리는 것이 목차의 끝판왕이라고 보셔도 될 것 같습니다 ㅋㅋ

     

    필요 라이브러리 및 플러그인 소개

    먼저 드래그 앤 드롭 기능을 직접 구현할 수도 있겠지만 상당히 힘들 것으로 예상되므로 라이브러리를 가져다 쓰도록 하겠습니다.

    그런데 모바일에서 이 라이브러리를 이용하면 제대로 동작하지 않습니다.

    따라서 라이브러리를 보완해 주는 플러그인을 하나 더 소개하겠습니다.

    jQuery UI 라이브러리

    jQuery에서 여러 효과와 컴포넌트, 위젯 등을 모아 놓은 라이브러리인데요.

    저도 잘 사용하지는 않지만 드래그 앤 드롭 기능은 정말 유용합니다.

     

    다음은 라이브러리 CDN 제공 사이트입니다.

     

    jQuery UI CDN

     

    jQuery UI – All Versions | jQuery CDN

    The integrity and crossorigin attributes are used for Subresource Integrity (SRI) checking. This allows browsers to ensure that resources hosted on third-party servers have not been tampered with. Use of SRI is recommended as a best-practice, whenever libr

    code.jquery.com

    이 라이브러리를 사용하여 우리가 원하는 기능을 구현해 보겠습니다.

     

    jQuery UI Touch Punch 플러그인

    PC에서 마우스를 이용하면 클릭 이벤트가 발생하고 모바일 기기에서는 터치 이벤트가 발생하는데요.

    하지만 jQuery UI 라이브러리는 터치 이벤트를 고려하지 않고 있어서 이 플러그인이 필요합니다.

     

    다음은 플러그인 CDN 제공 사이트입니다.

     

    jQuery UI Touch Punch CDN

     

    jqueryui-touch-punch - Libraries - cdnjs - The #1 free and open source CDN built to make life easier for developers

    A small hack that enables the use of touch events on sites using the jQuery UI user interface library. - Simple. Fast. Reliable. Content delivery at its finest. cdnjs is a free and open-source CDN service trusted by over 10% of websites, powered by Cloudfl

    cdnjs.com

    이 플러그인은 jQuery UI에서 터치 이벤트를 지원하도록 해 줍니다.

     

     

    자바스크립트 수정

    관리 페이지 > 꾸미기 > 스킨 편집 으로 이동합니다.

     

    html 편집을 클릭합니다.

    그럼 위외 같은 HTML 에디터가 뜨는데 여기에서 작업을 해 보겠습니다.

     

    body 태그 안쪽 맨 아래 > script 태그 안쪽 작성하시면 됩니다.

    어차피 플로팅 목차를 적용한 후에 이 포스팅을 보셔야 하므로 이 부분에 이미 코드가 작성되어 있어야 합니다.

     

    드래그 앤 드롭 기능 적용

    기존 appendToc 함수를 수정해 보겠습니다.

    function appendToc() {
        // 기존로직 생략
        if(bookToc.length > 0 && window.scrollY > bookToc.offset().top + bookToc.innerHeight()) {
            if(floatingToc.height() === 0) {
                // 기존 로직 생략
                var dragTimerId = 0; // 삼성폰 오작동 수정을 위해 추가
                $.getScript("https://code.jquery.com/ui/1.12.1/jquery-ui.min.js", function() {
                    $.getScript("https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js", function() {
                        floatingToc.draggable({
                            start: function(event, ui) {
                                var self = this;
                                dragTimerId = setTimeout(function(){
                                    $(self).addClass('noclick');
                                }, 250);
                            }
                        });
                    });
                });
                // 기존 로직 생략
            } else {
                // 기존 로직 생략
            }
        } else {
            // 기존 로직 생략
        }
    }

    기존에 이 라이브러리를 사용하고 있지 않은 분이라면 블로그 최초 진입부터 로드를 하게 되면 쓸데 없는 부하가 생기게 되는 것이므로 필요한 시점에 로드하도록 하였습니다.

    그런 다음 draggable 메서드를 이용하여 플로팅 목차 엘리먼트에 드래그 기능을 부여하였습니다.

    그런데 원래 목차를 클릭하게 되면 접기/펼치기 기능이 수행되므로 드래그 후 그 기능이 작동되는 것을 방지하기 위해 noclick이라는 클래스를 추가하는 트릭을 이용하였습니다.

    드래그를 시작하는 시점에 noclick 클래스를 부여하였습니다.

    +내용추가 (2020.12.23)

    삼성 스마트폰에서 클릭 이벤트가 정상 동작하지 않아서 약간의 트릭을 활용하였습니다.

    드래그가 시작되면 바로 noclick 클래스를 추가하지 않고 0.25초 간 유예를 두었습니다.

    유예 시간은 너무 짧지도 않고 너무 길지도 않게 적절히 잡으면 될 것 같습니다.

     

     

    목차 접기/펼치기 클릭 이벤트 수정하기

    목차를 드래그 앤 드롭을 하게 되면 마우스를 클릭한 상태로 드래그를 하고 원하는 위치에서 마우스 클릭한 버튼을 떼게 됩니다.

    이 때 클릭 이벤트가 발생하는데요.

    원래 목차를 클릭하면 접기/펼치기 기능이 수행되었죠.

    드래그 앤 드롭 할 때에도 그 기능이 수행되면 안 되므로 수정이 필요합니다.

    function appendToc() {
        // 기존 로직 생략
        if(bookToc.length > 0 && window.scrollY > bookToc.offset().top + bookToc.innerHeight()) {
            if(floatingToc.height() === 0) {
                // 기존 로직 생략
                // 목차 클릭
                var clickFlag = true;
                function clickTitle() {
                    if(clickFlag) {
                        clickFlag = false;
                        if (floatingToc.hasClass('noclick')) {
                            floatingToc.removeClass('noclick');
                        } else {
                            if(dragTimerId !== 0) {
                                clearTimeout(dragTimerId);
                                dragTimerId = 0;
                            }
                            floatingToc.css('transition', '');
                            var title = $('#toc-title>p>span#toggle');
                            if(title.text() === "펼치기") {
                                $('#toc-title').css('padding', '10px 0 0 0');
                                floatingToc.css('padding', '0 10px 0');
                                floatingToc.css('border-radius', '0');
                                floatingToc.removeClass('floating-toc-header-ani');
                            }
    
                            setTimeout(function(){
                                $('#toc-body').slideToggle(300,'linear', function() {
                                    if(title.text() === "접기") {
                                        floatingToc.css('transition', '');
                                        title.text("펼치기");
                                        floatingToc.css('padding', '0');
                                        $('#toc-title').css('padding', '10px');
                                        floatingToc.css('border-radius', '10px');
                                        floatingToc.addClass('floating-toc-header-ani');
                                    } else {
                                        title.text("접기");
                                    }
                                });
                            },200);
                        }
                        setTimeout(function(){
                            clickFlag = true;
                        }, 500);
                    }
                }
                
                $('#toc-title').on('click', function(){
                    clickTitle();
                });
                
                $('#toc-title').on('touchend', function(){
                    clickTitle();
                });
                
                //목차 항목 클릭
                $('#toc-body').find('a').on('click', function(event){
                    checkPosition();
                });
                $('#toc-body').find('a').on('touchstart', function(event){
                    floatingToc.draggable('disable');
                });
                $('#toc-body').find('a').on('touchend', function(event){
                    setTimeout(function(){
                        floatingToc.draggable('enable');
                    }, 200);
                    checkPosition();
                });
                // 기존 로직 생략
            } else {
                // 기존 로직 생략
            }
        } else {
            // 기존 로직 생략
        }
    }

    클릭 이벤트가 발생했을 때 floatingToc에 noclick 클래스를 가지고 있으면 드래그 중이었다는 의미이므로 원래 기능을 수행하지 않고 noclick 클래스만 제거를 해 주고 드래그 앤 드롭을 하지 않고 클릭만 했다면 기존 기능을 수행해 주도록 한 로직입니다.

    터치 이벤트가 끝났을 때도 마찬가지로 noclick 클래스를 제거해 주어야 정상 동작을 합니다.

    +내용추가 (2020.12.23)

    삼성 스마트폰에서 클릭 이벤트가 정상 동작하지 않아서 약간의 트릭을 활용하였습니다.

    드래그 시작 후 0.25초가 지나지 않으면 타이머를 중단시켜 noclick 클래스를 얻지 못하게 하고 클릭 동작을 수행하였습니다.

    기종에 따라 클릭 이벤트와 터치 이벤트가 동시에 발생하여 clickFlag를 두어 하나만 실행되도록 하였습니다.

    +내용추가 (2020.12.24)

    드래그 적용 시 목차 항목을 클릭하면 반응하지 않던 현상을 수정했습니다.

    터치 시 드래그와 클릭 이벤트가 간섭을 일으키는 라이브러리 자체 문제인 것으로 보입니다.

    따라서 touchstart 이벤트가 발생하면 잠시 드래그를 disable 시킨 뒤 터치가 끝나면 다시 enable 시키는 방법을 사용하였습니다.

     

    전체 소스 제공

    이해가 어려우신 분들을 위해 appendToc 함수 전체 소스를 제공하겠습니다.

    function appendToc() {
        var bookToc = $('.book-toc');
        var floatingToc = $('.floating-toc');
        if(bookToc.length > 0 && window.scrollY > bookToc.offset().top + bookToc.innerHeight()) {
            if(floatingToc.height() === 0) {
                floatingToc.css('display','block');
                bookToc.css('width',bookToc.width()+'px');
                bookToc.css('height',bookToc.height()+'px');
                var tocTitle = $('<div id="toc-title"><p>목차 <span style="opacity:0.5;">펼치기</span></p></div>');
                floatingToc.append(tocTitle);
                var tocBody = $('<div id="toc-body" style="display:none;"></div>').append(bookToc.find('#toc'));
                floatingToc.append(tocBody);
                
                floatingToc.addClass('floating-toc-ani');
                
                floatingToc.css('padding', '0');
                $('#toc-title').css('padding', '10px');
                var dragTimerId = 0; // 삼성폰 오작동 수정을 위해 추가
                $.getScript("https://code.jquery.com/ui/1.12.1/jquery-ui.min.js", function() {
                    $.getScript("https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js", function() {
                        floatingToc.draggable({
                            start: function(event, ui) {
                                var self = this;
                                dragTimerId = setTimeout(function(){
                                    $(self).addClass('noclick');
                                }, 250);
                            }
                        });
                    });
                });
                
                floatingToc.css('top', "20px");
                floatingToc.css('left', "20px");
                floatingToc.css('position', "fixed");
                
                // 목차 클릭
                var clickFlag = true;
                function clickTitle() {
                    if(clickFlag) {
                        clickFlag = false;
                        if (floatingToc.hasClass('noclick')) {
                            floatingToc.removeClass('noclick');
                        } else {
                            if(dragTimerId !== 0) {
                                clearTimeout(dragTimerId);
                                dragTimerId = 0;
                            }
                            floatingToc.css('transition', '');
                            var title = $('#toc-title>p>span#toggle');
                            if(title.text() === "펼치기") {
                                $('#toc-title').css('padding', '10px 0 0 0');
                                floatingToc.css('padding', '0 10px 0');
                                floatingToc.css('border-radius', '0');
                                floatingToc.removeClass('floating-toc-header-ani');
                            }
    
                            setTimeout(function(){
                                $('#toc-body').slideToggle(300,'linear', function() {
                                    if(title.text() === "접기") {
                                        floatingToc.css('transition', '');
                                        title.text("펼치기");
                                        floatingToc.css('padding', '0');
                                        $('#toc-title').css('padding', '10px');
                                        floatingToc.css('border-radius', '10px');
                                        floatingToc.addClass('floating-toc-header-ani');
                                    } else {
                                        title.text("접기");
                                    }
                                });
                            },200);
                        }
                        setTimeout(function(){
                            clickFlag = true;
                        }, 500);
                    }
                }
                
                $('#toc-title').on('click', function(){
                    clickTitle();
                });
                
                $('#toc-title').on('touchend', function(){
                    clickTitle();
                });
                
                //목차 항목 클릭
                $('#toc-body').find('a').on('click', function(event){
                    checkPosition();
                });
                $('#toc-body').find('a').on('touchstart', function(event){
                    floatingToc.draggable('disable');
                });
                $('#toc-body').find('a').on('touchend', function(event){
                    setTimeout(function(){
                        floatingToc.draggable('enable');
                    }, 200);
                    checkPosition();
                });
    
                floatingToc.css('z-index','-1'); // z-index 값을 -1로 주어 잠시 숨김
                clickTitle();
                setTimeout(function(){
                    if($('.entry-content').offset().left < $('.floating-toc').width()+20) {
                        clickTitle();
                    } else {
                        floatingToc.css('z-index',''); // 화면이 충분히 넓다면 바로 다시 보이도록 함
                    }
                }, 500);
                setTimeout(function(){
                    floatingToc.css('z-index',''); // 완전히 접힌 후 다시 보이도록 함
                }, 1100);
            } else {
                checkPosition();
            }
        } else {
            $('#toc-title').off('click');
            bookToc.append(floatingToc.find('#toc'));
            floatingToc.find('div').remove();
            floatingToc.removeAttr('style');
            floatingToc.css('display','none');
            $('#toc').find('a').removeAttr('style');
        }
    }

    기존에 적용하신 appendToc 함수 부분만 제거하시고 이 함수를 다시 붙여넣어 주세요.

    +내용 추가 (2020.12.23)

    Odyssey 스킨을 사용하시는 경우 .entry-content를 .article-view으로 바꿔서 적용해주세요!

    Letter 스킨을 사용하시는 경우 .entry-content를 .article_view로 바꿔서 적용해주세요!

     

     

    결과 확인

    처음 플로팅 목차가 생기는 위치는 기존과 동일합니다.

     

    이처럼 위치를 드래그 앤 드롭으로 이동이 가능합니다.

     

    펼치면 우측으로 너무 붙어있어도 화면을 넘어가지는 않습니다.

     

    영상으로도 결과를 보겠습니다.

    간단히 적용할 수 있으면서 그 효과는 강력합니다.

    이처럼 생각만 하면 이미 만들어져 있는 기능을 가져다 쓸 수 있으므로 코딩 공부를 조금만 하셔도 전혀 모르는 것과는 천지 차이라는걸 알 수 있죠? ㅎㅎ

     

    혹시 조금이라도 코딩 공부에 관심이 생기신 분은 아래 포스팅을 참고해 보세요 :)

     

    [왕초보 코딩 1강] 코딩의 코자도 몰라도 괜찮을까

     

    [왕초보 코딩 1강] 코딩의 코자도 몰라도 괜찮을까

    야심차게 왕초보 코딩 강의를 시작한다고 하긴 했는데요. 일단 코딩이 뭔 지 알아야 이걸 시작할 지 말 지 판단이 될테니 천천히 얘기해 보도록 할게요. 코딩...? 사람들이 코딩 코딩 하는데 대체

    sangminem.tistory.com

     

    감사합니다!

     

    이슈 (2020.12.24) - 조치 및 내용 추가 완료

    삼성 스마트폰에서 목차 내 항목 클릭 시 정상적으로 반응하지 않는 문제를 발견하였습니다.

    => 드래그 disable/enable 조정으로 해결했습니다. 자세한 사항은 본문 수정 부분 참고 바랍니다.

    반응형
    그리드형(광고전용)

    댓글11

    • 루키 2020.12.23 23:38

      플로팅 이동을 설정하니 PC에서는 원할하게 작동하는데 모바일 환경에선 터치가 안되네요.

      브라우저 문제인지 일단 여러 브라우저로 테스트해볼건데 삼성 브라우저에선 목차 펼치기를 터치하면 목차가 이동하기가 우선시 되어서 펼치기가 안되네요.

      PC는 특별한 문제가 없어요.

      참고하세요~
      답글

      • 아미넴 2020.12.23 23:40 신고

        안녕하세요!
        먼저 의견 감사합니다.

        일단 모바일 환경에서도 테스트를 하긴 했는데 아이폰 사파리, 크롬 모바일 브라우저에서는 이상이 없었구요.
        삼성 브라우저는 제가 삼성폰이 없어서 테스트를 못해봤네요.

        추후 가능하다면 테스트 해보겠습니다.
        감사합니다.

        (내용추가)
        집에 안 쓰는 삼성폰이 있어서 테스트 하여 수정하였습니다.
        펼치기/접기 문제는 해결이 되었는데 실제 항목을 클릭할 때도 문제가 있네요.
        해결되면 내용 추가하도록 하겠습니다.

        (내용추가)
        항목 클릭 문제도 해결하여 내용 추가했습니다.

      • 루키 2020.12.26 00:54

        오! 메리크리스마스~

        답변이 늦어서 죄송합니다.
        정말 열심히 코딩하시는 개발자이시네요... 바로바로 피드백 받아주시고 수정하시는거 보면

        확인 결과 삼성 인터넷 브라우저에서 원할하게 작동하고 있는 것이 확인됐습니다!

        그런데 문제가 하나 생겼는데
        스킨에 적용된걸 다 지우고 다시 적용 시킬려 하니 "제목3" 부분을 추가하셨던데 목차 서식이랑 플로팅은 제목3과 호환이 안되는 것 같네요.

        코드끼리 충돌 일으키는 것 같은데 아무래도 아셔야 될 것 같아서 보고드려요~

      • 아미넴 2020.12.26 01:16 신고

        오 안녕하세요! 늦었지만 메리크리스마스 되셨길 바랄게요 ㅎㅎ

        잘못된 부분을 짚어 주시니 저야말로 감사하게 생각하고 있습니다.
        말씀하신 부분을 봤는데 '티스토리 떠 다니는 플로팅 목차 만드는 방법 (고급 버전)'에서 하나 빠뜨린게 있었네요 ㅠ

        이 부분을
        var titleList = $('.entry-content').find('h2,h3');

        이렇게 고쳐주면 될 것 같습니다.
        var titleList = $('.entry-content').find('h2,h3,h4');

        이번에 말씀하신 부분은 단순히 한 군데 놓친 부분이라 댓글로만 말씀드리고 넘어갈게요 :)

        참고로 제목3 적용한 게시물 입니다
        https://sangminem.tistory.com/370

    • 라스 2021.01.03 00:52

      안녕하세요 개발자의 꿈을 꾸고 있는 개발자 지망생입니다. 김시힌 글 보고 제 블로그에도 잘 적용했습니다. 혹시 공간이 없다면 한번 열렸다가 닫치는건 너무 눈이 산만한거 같아 공간이 없다면 아예 목차가 접힌채로 시작하려는것으로 소스를 수정하고 싶은데 어디서부터 고쳐야 할지 막막하여 조언을 구하고 싶습니다. 어디부분을 수정하면 좋을까요?
      답글

      • 아미넴 2021.01.03 14:11 신고

        안녕하세요! 의견 감사합니다.

        저도 접혔다 펼쳐지는 부분이 조금 거슬리기는 했는데 처음에 가로 크기를 계산하려면 어쩔 수 없는 부분이기도 해서 그대로 뒀었거든요.
        라스님 의견 반영하여 기존 로직을 건드리지 않으면서 방법을 생각하여 조치해 보았습니다 ㅎㅎ

        다음 포스팅에 추가한 +내용 추가 (2021.01.03) 부분 참고해 보세요!
        https://sangminem.tistory.com/352

        이 포스팅의 전체 소스 제공 부분 보셔도 됩니다.

    • DaTALK 2021.09.25 23:51 신고

      아미넴님 오늘 글 보면서 계속 제 블로그 기능 업데이트 중입니다. 정말 감사합니다 ㅠㅠ 근데 제 글 들어가보시면 아시겠지만
      목차가 확대되었다 축소되었다 계속 반복을 합니다.. 하라는 대로 했는데 왜 이런건지 코드를 아무리 찾으며 매칭해도 이해가 안가네요 ㅠㅠㅠ 제 티스토리 봐주시고 혹시 어떤 문제가 있는 지 찾아주실 수 있을까요? ㅠㅠ
      답글

      • 아미넴 2021.09.26 00:05 신고

        .floating-toc-ani {
        animation: bounce-toc 5s linear infinite;
        }
        일단은 제대로 안 된다면 이 부분을 적용 안하시는 것이 나아 보입니다

      • DaTALK 2021.09.26 19:55 신고

        아 방금 해결했습니다...!!

        @keyframes bounce-toc { 0% { transform: scale(0.8); } 50% { transform: scale(0.8); } 100% { transform: scale(0.8); } }

        여기서 50%때의 transform scale을 0.7 대신 0.8로 바꾸니까 해결되었네요..!

      • 아미넴 2021.09.26 20:54 신고

        아 저 부분을 제외하면 된다는 의미였는데..
        그렇게 하시면 해결되는 것처럼 보일 순 있지만 의미없는 코드가 들어가는거라 제가 말한 부분과 그 부분 전부 제거하셔도 될겁니다 ㅎ

    • 이지포너 2021.12.05 02:02

      관리자의 승인을 기다리고 있는 댓글입니다
      답글

    💲 추천 글