Scrollifyを使わずセクションごとにスナップするスクロール制御を自作
セクション(ブロック)ごとのスクロール制御をして欲しいと依頼を受け、背徳感を感じながら初めてのスクロールハイジャックをしてみました
よくある形として1ページごとめくるようにスクロール制御できるように各セクション(ブロック)の高さはビューポートと同じ(100vh)にして制御するかと思いますが、
依頼されたものはビューポートからはみ出るようなセクション(ブロック)も混ざっていたので「Scrollify.js」を使用することにしてみました
セクション(ブロック)ごとにスクロールを制御できるjQueryのプラグイン「Scrollify.js」を使ったサイトはPC表示ではちらほら見ますが、大体の場合スマートフォンでの表示時はその機能を解除しているケースが殆んどです。
なんとなく「Scrollify.js」を触ってみて気づいたスマートフォンで使われていない理由としては
- 動作が安定しない
- ビューポートよりも高いサイズのセクション(ブロック)内のスクロールも画面ごとスクロールされる制御になる
- スマートフォンのステータスバーをタップしたときの挙動がおかしくなる
上記の理由から自分も了解を得てスマートフォンでは「Scrollify.js」を解除して実装しましたが、どうにかならんものかということでちょっくら勉強ついでに自作してみました。
ちなみにCSSでスクロールスナップを実装できる「scroll-snap-type」を試してみようかな?と思いましたがアニメーションの種類や時間の指定が柔軟に指定できなかったりするので使いにくかったりしたので使いませんでした
スナップするスクロールのデモとベース
まずはデモページ
ステータスバーをタップしたときの機能をなくしたくて色々調べた結果こんな感じになりました
サンプルのHTML
サンプルのHTMLは以下の通りでコンテンツ全体を.wrapで囲んでいます
.blockがスナップする要素になっています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<body> <div class="wrap"> <header class="header"> <nav class="nav"> <ul class="nav_list"> <li class="nav_item"><a href="#block1" class="nav_link">#1</a></li> <li class="nav_item"><a href="#block2" class="nav_link">#2</a></li> <li class="nav_item"><a href="#block3" class="nav_link">#3</a></li> <li class="nav_item"><a href="#block4" class="nav_link">#4</a></li> <li class="nav_item"><a href="#block5" class="nav_link">#5</a></li> <li class="nav_item"><a href="#block6" class="nav_link">#6</a></li> <li class="nav_item"><a href="#block7" class="nav_link">#7</a></li> </ul> </nav> </header> <div id="block1" class="block"> #1 </div> <div id="block2" class="block"> #2 </div> <div id="block3" class="block"> #3 </div> <div id="block4" class="block"> #4 </div> <div id="block5" class="block"> #5 </div> <div id="block6" class="block"> #6 </div> <div id="block7" class="block"> #7 </div> <footer>footer</footer> </div> </body> |
jQueryの準備
今回イージングの設定が楽になるのでオワコンの呼び声高いjQueryを使います
こちらをhead要素内で読み込んでいきつつ、イージングの種類を増やすためeasing.js(今回使ったもののバージョンは1.3)も読み込んでおきます。
初期から用意されているイージングで十分であれば必要はないですが一応以下にDLリンクを用意しています
※ iPhone用のステータスバー絡みを制御するmetaタグがいろいろなところで紹介されていますが入れても効果がないようなので入れなくてもいいと思います。
head要素は以下のようになっています
1 2 3 4 5 6 7 8 9 10 |
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width"> <link rel="stylesheet" href="css/destyle.css"> <link rel="stylesheet" href="css/style.css"> <script src="js/jquery.min.js"></script> <script src="js/jquery.easing.1.3.js"></script> <title>Document</title> </head> |
サンプルのcss
ステータスバーをタップしたときの効果が出ないようにするため、body要素の高さをビューポートサイズに固定しておきます
.wrapもビューポートサイズに固定しつつ、内包した要素をスクロールして見ることができるようにしています。
ここの「position:fixed」はなくても大丈夫なのですが、なにかのミスで.wrapの外に要素ができてしまったときに位置がずれるのを防ぐための保険としてつけています
heightやmin-heightで使っているdvhの単位は対応していないブラウザもあるかもなのでvhと二重で指定しておくといいと思います
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
body { height: 100vh; height: 100dvh; } .header { position: sticky; z-index: 100; width: 100%; top: 0; left: 0; } .nav_list { display: flex; justify-content: space-between; width: 100%; } .nav_item { width: 100%; } .nav_link { display: block; padding: 20px 0; text-align: center; background-color: #ddd; } .nav_link.active { background-color: #000; color: #fff; transition: 1.5s; } .wrap { position: fixed; width: 100%; height: 100vh; height: 100dvh; top: 0; left: 0; overflow: auto; } footer { height: 350px; display: flex; align-items: flex-end; justify-content: center; color: #fff; padding-bottom: 10px; background-color: #5f5f5f; } .block { font-size: 20px; line-height: 100vh; line-height: 100dvh; min-height: 100vh; min-height: 100dvh; text-align: center; } .block:nth-of-type(odd) { background-color: #dbfca7; } .block:nth-of-type(even) { background-color: #a7fce0; height: 200vh; height: 200dvh; } |
Intersection Observer APIを利用し実装
普通にscrollイベントを使って実装しようと思ったけれどObserverを使うと便利そうな感じだったのでこちらで実装してみました。
Intersection Observerに関しては下の記事でまとめてあります
以下のをscript要素にいれbody要素の閉じタグの手前にごっそり入れて上げれば完成
細かい説明は時間があるときに追記します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
//使う変数を用意 let flag = true, $block = $('.block'), $body = $('body'), $wrap = $('.wrap'), sc = $wrap.scrollTop(), scroll = 0, direction, target, $link = $('a'); //スクロールハイジャック用 const handle = (event) => { event.preventDefault(); } //スクロールハイジャックを解除する関数 function timer() { const time = setTimeout(timer, 100); count += 100; if (count >= 300) { window.removeEventListener('touchmove', handle, { passive: false }); window.removeEventListener('mousewheel', handle, { passive: false }); clearTimeout(time); } } //スクロールハイジャックをする関数 function scrollJack() { window.addEventListener('touchmove', handle, { passive: false }); window.addEventListener('mousewheel', handle, { passive: false }); } //スクロールの方向を調べる処理 $wrap.on('scroll', function () { if ($(this).scrollTop() < scroll) { direction = 'up'; } else { direction = 'down'; } scroll = $(this).scrollTop(); }); // Intersection Observerで交差を監視する要素を配列にする const blocks = document.querySelectorAll(".block"); // Intersection Observerの設定 const options = { root: null, rootMargin: '-1px 0px', threshold: 0 }; // Intersection Observerを使えるようにする const observer = new IntersectionObserver(intersect, options); // 要素を監視する blocks.forEach(block => { observer.observe(block); }); //スナップスクロール時のアニメーション設定用の関数 function move(position) { $wrap.stop().animate({ 'scrollTop': position }, 1500, 'easeInOutSine', function () { count = 0; timer(); flag = true; }); } // 交差したときに実行する関数 function intersect(entries) { entries.forEach(entry => { if (entry.isIntersecting) { snapScroll(entry.target); } }); } // スクロールの方向によって移動&ナビにクラスをつけ外しする関数 function snapScroll(element) { if (flag) { let $active = $(element); let current = $active.attr('id'); $link.each(function () { if ($(this).attr('href') == '#' + current) { $link.removeClass('active'); $(this).addClass('active'); } }); sc = $wrap.scrollTop(); if (direction == 'up') { scrollJack(); let bpt = $active.offset().top + sc; let ph = $active.outerHeight(); let hSize = window.innerHeight; let pnb = bpt + (ph - hSize); move(pnb); } else if (direction == 'down') { scrollJack(); let bpt = $active.offset().top + sc; move(bpt); } } } // ナビクリック時のスムーススクロール $link.on('click', function (e) { e.preventDefault(); scrollJack(); flag = false; sc = $wrap.scrollTop(); let href = $(this).attr('href'); if (href.match(/^#/) || href == '') { if (href == '#' || href == '') { target = $wrap; } else { target = $(href); $link.removeClass('active'); $(this).addClass('active'); } let posi = target.offset().top + sc; move(posi); } else { if ($(this).attr('target')) { window.open(href, '_blank'); } else { location.href = href; } } }); |
てか、なんでスクロールハイジャックなんだ?スクロールジャックならわかるけど
コメント部分がおかしかったらごめんなさい