HTML5
CSS3
element
vue.js

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

More than 1 year has passed since last update.

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

環境

名称 バージョン
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時間くらい無駄にしてしまったのは内緒です。。

以上。