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


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

# 環境

|名称|バージョン|
|:--|:--|
|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**を使用

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

# サイドメニューの固定　その１（スクロールを監視）

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

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

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

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

# サイドメニューの固定　その２（position: sticky）

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

[ここ参考にしました](https://q-az.net/position-sticky/)

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

# ソースコード

[GitHub](https://github.com/tds517/vueapp/tree/side-menu-and-scrollto)

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

```html: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>
```
```js: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>
```
```scss: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>

```

# ポイント

## サイドメニューを固定

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

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

```html:template
<el-col :offset="1" :span="5" class="side-menu-wrapper" :style="{ top: getSideMenuTop }">
```
`getSideMenuTop`により、上記の`top`を設定します。
'px'をつけないと動作しなかったので注意。

## Vue Scroll-To

[Vue ScrollTo](https://github.com/rigor789/vue-scrollTo)

```js: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`内で初期設定を行います。

```html: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`を知らなかったので、４時間くらい無駄にしてしまったのは内緒です。。

以上。