LoginSignup
8
6

More than 5 years have passed since last update.

サイドメニューの固定とScrollTo

Last updated at Posted at 2017-09-18

サイドメニューの固定でこんな方法もあったのか!となったので、備忘録として記載します。

環境

名称 バージョン
Vue.js 2.4.4
Element UI 1.4.4
vue-scrollto 2.7.7

要件

  • 「サイドメニューを固定したい!」
  • 「メニュークリックでスクロール移動したい!」

留意事項

  • ヘッダーはposition: fixedで固定
  • サイドメニューはposition: fixedしたくない

実装手段

  • ElementUIのNavMenu Side barを使用
  • 要素までのスクロールに、vue-scrolltoを使用

あとは、サイドメニューの固定方法。

サイドメニューの固定 その1(スクロールを監視)

まず、サイドメニューは、position: absoluteにして、windowのscrollYの値を監視して、サイドメニューのtopscrollYの値を設定します。
ヘッダーが固定されているので、ヘッダーのouterHeightも加算します。

こうして、下の方へスクロールしても固定で表示されるサイドメニューが一旦実装できました。

しかし、ここからScrollTo(指定した要素までスクロールする)しようとすると、サイドメニューがプログラムによるスクロールについていけないのか、ギュイーンと引っ張られてしまいます(スクロールし終わると、ちゃんと元の位置に固定される)。

マウスのスクロールなら問題なかったのですが・・・。

サイドメニューの固定 その2(position: sticky)

なんとか頑張ってみたものの解決できず諦めて探していたら、CSSでできそうなことがわかりました。

ここ参考にしました

どうやら、position: stickyをしておけば、topを指定するだけで良さそう。
IEとEdgeはもう置いていきます。

ソースコード

GitHub

コンポーネントの部分だけ抜粋。

teplate
<template>
  <main>
    <header id="main-header" class="header">
      <h1>Header</h1>
    </header>

    <div class="main-contents" :style="{ 'padding-top': getSideMenuTop }">
      <el-row>
        <el-col :offset="1" :span="5" class="side-menu-wrapper" :style="{ top: getSideMenuTop }">
           <el-menu mode="vertical" default-active="0" class="side-menu">
             <el-menu-item-group v-for="(group, index1) in groups"
                 :key="group.title" :title="group.title">
               <el-menu-item v-for="(item, index2) in group.items"
                   :key="item.label" :index="getItemIndex(groups, index1, index2)"
                   v-scroll-to="item.to">
                 {{ item.label }}
               </el-menu-item>
             </el-menu-item-group>
           </el-menu>
        </el-col>

        <el-col :offset="1" :span="17" class="contents">
          <div id="contents-1">
            <p>CONTENTS 1</p>
          </div>
          <div style="height: 500px;"></div>
          <div id="contents-2">
            <p>CONTENTS 2</p>
          </div>
          <div style="height: 500px;"></div>
          <div id="contents-3">
            <p>CONTENTS 3</p>
          </div>
          <div style="height: 500px;"></div>
          <div id="contents-4">
            <p>CONTENTS 4</p>
          </div>
          <div style="height: 500px;"></div>
          <div style="height: 500px;"></div>
        </el-col>
      </el-row>
    </div>
  </main>
</template>
script
<script>
const Vue = require('vue')
/*
  Vue ScrollTo
 */
import VueScrollTo from 'vue-scrollto'
Vue.use(VueScrollTo)

export default {
  data() {
    return {
      headerHeight: 0,
      groups: [
        {
          title: 'GROUP 1',
          items: [
            { label: 'contents-1', to: '#contents-1' },
            { label: 'contents-2', to: '#contents-2' },
            { label: 'contents-3', to: '#contents-3' },
          ]
        },
        {
          title: 'GROUP 2',
          items: [
            { label: 'contents-4', to: '#contents-4'}
          ]
        }
      ]
    }
  },
  mounted() {
    this.headerHeight = $('#main-header').outerHeight()

    // VueScrollTo初期設定
    VueScrollTo.install(Vue, {
      container: 'body',
      duration: 500,
      easing: 'linear',
      offset: -this.headerHeight,
      cancelable: true,
      onDone: false,
      onCancel: false,
      x: false,
      y: true
    })
  },
  computed: {
    getSideMenuTop() {
      // ヘッダーの高さ
      return this.headerHeight + 'px'
    }
  },
  methods: {
    getItemIndex(groups, groupIdx, itemIdx) {
      let total = 0
      for (var i = groupIdx - 1; i >= 0; i--) {
        total += groups[i].items.length
      }
      total += itemIdx
      return String(total)
    },
  }
}
</script>
style
<style lang="scss" scoped>
@import "resources/assets/sass/variables";

.header {
  background-color: #ddddff;
  color: #fff;
  height: 120px;
  position: fixed;
  width: 100%;
  z-index: 1;
}

.side-menu-wrapper {
  position: sticky;
}

.side-menu {
  width: 100%;
}

.contents {
  padding: 16px;
}

</style>

ポイント

サイドメニューを固定

style
.side-menu-wrapper {
  position: sticky;
}

サイドメニューを固定するCSSの指定です。
親要素の下端まで、指定したtopの位置で止まってくれます。

template
<el-col :offset="1" :span="5" class="side-menu-wrapper" :style="{ top: getSideMenuTop }">

getSideMenuTopにより、上記のtopを設定します。
'px'をつけないと動作しなかったので注意。

Vue Scroll-To

Vue ScrollTo

script
// VueScrollTo初期設定
VueScrollTo.install(Vue, {
  container: 'body',
  duration: 500,
  easing: 'linear',
  offset: -this.headerHeight,
  cancelable: true,
  onDone: false,
  onCancel: false,
  x: false,
  y: true
})

公式によると、Vue.use(VueScrolto, {options})という記述でも設定することができます。
が、offsetを指定して、ヘッダーの下で止まるようにしたかったので、敢えてmounted内で初期設定を行います。

template
<el-menu-item v-for="(item, index2) in group.items"
    :key="item.label" :index="getItemIndex(groups, index1, index2)"
    v-scroll-to="item.to">
  {{ item.label }}
</el-menu-item>

ディレクティブ:v-scroll-toに移動させたいエレメントのセレクタを設定します。
<div id="contents-1"></div>まで移動させたければ、#contents-1を入れれば良いです。

scrolltoのオプションは、ディレクティブ内でも指定できるようなので、フレキシブルな使い方もできそうです。

position:stickyを知らなかったので、4時間くらい無駄にしてしまったのは内緒です。。

以上。

8
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6