정보/블로그 운영팁

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

아미넴 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 조정으로 해결했습니다. 자세한 사항은 본문 수정 부분 참고 바랍니다.

    반응형

    댓글

    💲 추천 글