jSlideMenuの導入とカスタマイズ

最近、[google](https://www.google.co.jp/)のモバイルページにあるサイドバーを模した[jSlideMenu](https://github.com/ukitaka/jSlideMenu)を導入したので、そのときのことをまとめた。
jSlideMenuをカスタマイズする上で、中身のコードの理解が必要となったわけだが、js/css初心者としては辛いところ。なので、導入方法と中身の解説、最後にカスタマイズ編として自前で実装したプラグインの紹介までする。

プログラミングを始めて４ヶ月の初心者が初心者向けに書いている記事としてみてください。

# 導入編

## 開発環境
ローカル開発環境は以下の通り

- Rails 4.2.0
- Ruby 2.2.1

### 導入
まずは[jSlideMenu](https://github.com/ukitaka/jSlideMenu)から<b>Downlod ZIP</b>でファイルをダウンロードしてくる。
すると、中身が

- sample
- READEME
- jquery.slidemenu.css
- jquery.slidemenu.js

となっているので、Railsアプリに導入する場合は、

- jquery.slidemenu.css
- jquery.slidemenu.js

を*app/assets*内に適宜入れれば良い。

### ベースとなるコード
解説の前に、どのようなコードに対してjSlideMenuを適応させていくかを書いておかないと混乱するので記述していく。

```html:users.html
<body>
  <%= render :partial => 'layouts/sidebar' %>
  <%= render :partial => 'layouts/header' %>
  <div id="main">
    <%= yield%>
  </div>
  <%= render :partial => 'layouts/footer' %>
</body>
```

のように、すでにサイドバー・ヘッダー・メイン・フッターが用意されている状態をベースとする。
サイドバーを別ので作ったけど、もっとクールなものにしたいといった感じでjSlideMenuを導入した感じ。

## 解説
cssファイルとjsファイルをただ入れただけでは適応されないので、ここからコードの解説。

### CSS

```css:jquery.slidemenu.css
#container {
	background: #FFFFFF;
	-webkit-transform: translate3d(0px, 0px, 1px);
	-webkit-transition: .2s -webkit-transform ease-in-out;
	z-index: 1;
}
```

まず #container が意味するものは、スライドメニューをスライドさせるときに、右側にずれていくメインコンテンツたちのこと。こう書くと、上の id="main" の部分だけ含めればと勘違いするかもしれないが、#containerで含んだもののみが右へスライドするので、

```html:users.html
<body>
  <%= render :partial => 'layouts/sidebar' %>
  
  <div id="container">
    <%= render :partial => 'layouts/header' %>
    <div id="main">
      <%= yield%>
    </div>
    <%= render :partial => 'layouts/footer' %>
  </div>
</body>
```

といった感じで、ヘッダー・フッターも含める。こうすると、ヘッダー・フッターも一緒にスライドする。また、ここに含めていないとボタンの無効化が効かない。

```css:jquery.slidemenu.css
#container.show {
	-webkit-transform: translate3d(240px, 0px, 1px);
	min-width: 320px;
}
```

-webkit-transform: translate3d(240px, 0px, 1px)
これをすることで、先ほど右にスライドさせたサイドバー以外の要素の位置を変更している。translate3dはCSS3にあるもので、(x座標, y座標, z座標)を意味する。なので、z座標を1pxとすることでサイドバーとの位置関係をずらしているのだ。当然x座標はどれだけずらすかを意味している。これは自分でセットしたサイドバーの幅と相談すればいい。

またandroid対策として-webkitを使わないことも検討課題。
translate3dでなく、tranlateXなどと一つずつ指定していくことによるバグ回避が必要かもしれない。

### JavaScript

70行目までのところは特にいじらなかったし読んでいない。スライドメニューを開いた時にstatusをOPENにするなど、基本的な土台部分なので、よっぽどカスタマイズしない限り、ここはそのままで良いと思う。

#### touchStart

```js:jquery.slidemenu.js
$(settings.menu_contents).bind("touchstart.scrollMenu", function() {
  menu_list_height = $(settings.menu_list).height();
  sfY = event.touches[0].screenY;
  startTime = (new Date()).getTime();
  startY = event.changedTouches[0].clientY;
});
```

menu_list_heightはそのまま、スライドメニューの高さの値を取得している。
startYは、.clientYというメソッドで画面をタッチした際の（クライアントの）画面上におけるY座標を取得している。この値との比較を行ってのちのちスクロールさせる。
sfYは、.screenYによってスクリーン上におけるY座標の取得している。
つまり、chromeの「要素の検証」でiphoneのエミュレータを使いながらコンソールで値を確認してみると、startYはiphone画面におけるY座標を取得する一方、.sfYはchromeの表示ウィンドウにおけるY座標の値を取得しているわけである。

#### touchMove

```js:jquery.slidemenu.js
$(settings.menu_contents).bind("touchmove.scrollMenu", function() {
  mfY = event.changedTouches[0].screenY;
  moveY = smY + mfY - sfY;
  draggedY = event.changedTouches[0].clientY - startY;
  new_moveY = moveY / 2
  bottom_moveY = moveY + (Math.abs(moveY) + window.innerHeight - menu_list_height)/2
  
  if (moveY > 0)
    $(settings.menu_contents).css({
      '-webkit-transition': 'none',
      '-webkit-transform': 'translate3d(0px,'+ new_moveY +'px,0px)'
    });
  else if (window.innerHeight - menu_list_height >= 0) //screen.height > menu_list_height
    $(this).css({
      '-webkit-transition': 'none',
      '-webkit-transform': 'translate3d(0px,'+ new_moveY +'px,0px)'
    });
  else if (Math.abs(moveY)+ window.innerHeight > menu_list_height) //menu_list_height > screen.height
    $(this).css({
      '-webkit-transition': 'none',
      '-webkit-transform': 'translate3d(0px,'+ bottom_moveY +'px,0px)'
    });
  else
    $(this).css({
      '-webkit-transition': 'none',
      '-webkit-transform': 'translate3d(0px,'+ moveY +'px,0px)'
    });
  });
```

この部分はカスタマイズを施している。

```js:jquery.slidemenu.js
if (moveY > 0)
else if (window.innerHeight - menu_list_height >= 0)
else if (Math.abs(moveY)+ window.innerHeight > menu_list_height)
```

の部分である。ここでは、<b>擬似バウンススクロール</b>を実装している。必要以上にスクロールアップ・ダウンしているときに、スクロールの動きを重くするといったものである。

```js:jquery.slidemenu.js
if (moveY > 0)
```

では、moveYが正になるのは、スクロールアップを必要以上に深く行っている時のみ。

```js:jquery.slidemenu.js
else if (window.innerHeight - menu_list_height >= 0)
```

続いて、move > 0でないときに上記の条件を満たすとき、必要以上にスクロールダウンを行うときは動きを重たくしたい。

```js:jquery.slidemenu.js
else if (Math.abs(moveY)+ window.innerHeight > menu_list_height)
```

また、メニューが非常に長いときに、一番下までスクロールした後のスクロール時には重くしたいので、こういった条件分岐で動きを重くするわけである。


#### touchEnd

```js:jquery.slidemenu.js
$(settings.menu_contents).bind("touchend.scrollMenu", function() {
  diffTime = (new Date()).getTime() - startTime;
  real_smY = smY
  smY = smY + (mfY - sfY);
  condition = "scroll"

  if (diffTime < 200 && draggedY > 0) { // scroll up
    moveY += Math.abs((draggedY / diffTime) * 500);
    $(settings.menu_contents).css({
      '-webkit-transition': '-webkit-transform .8s ease-out',
      '-webkit-transform': 'translate3d(0px,'+ moveY +'px,0px)'
    });
    smY = moveY;
    condition = "flick"
  } else if (diffTime < 200 && draggedY < 0) { // scroll down
      moveY -= Math.abs((draggedY / diffTime) * 500);
      $(settings.menu_contents).css({
        '-webkit-transition': '-webkit-transform .8s ease-out',
        '-webkit-transform': 'translate3d(0px,'+ moveY +'px,0px)'
      });
      smY = moveY;
      condition = "flick"
  }
```

この条件分岐で、慣性スクロールに近いことを実現している。まず、diffTime < 200とすることで、一瞬ですっとフリックしたときの条件分岐が行っている。
そして、draggedYは上か下かどちらにどれくらい動かしたかを表していて、
moveY += Math.abs((draggedY / diffTime) * 500)
で、擬似慣性スクロール（実際にスライドした分以上に変化させる）を実現している。draggedYをdiffTimeを割ることで、diffTimeが小さい（素早くスライドする）ほど慣性スクロール時のスライド量が増えるようになっているのだ。

'-webkit-transition': '-webkit-transform .8s ease-out',
'-webkit-transform': 'translate3d(0px,'+ moveY +'px,0px)'
次にここの説明だが、これは上段で下段のtransformのmoveYという値を含めたtranslate3dの座標をどういった風に変えていくかという意味である。
この例でいけば、
「0.8秒でだんだんゆっくりになるように座標変化を起こす」
というものです。moveYが最終的な値で、そこに至るまで変化させていき、最後ゆっくりになる、と。


```js:jquery.slidemenu.js
  if (moveY > 0) {
    if (condition == "flick" && real_smY != 0){
      $(settings.menu_contents).css({
        '-webkit-transition': '-webkit-transform .3s ease-out',
        '-webkit-transform': 'translate3d(0px,'+ 0 +'px,0px)'
      });
      smY = 0;
    }
    else {
      $(settings.menu_contents).css({
        '-webkit-transition': '-webkit-transform .3s ease-out',
        '-webkit-transform': 'translate3d(0px,'+ 0 +'px,0px)'
      });
      smY = 0;
    }
  } else if (window.innerHeight - menu_list_height > 0) {
    $(settings.menu_contents).css({
      '-webkit-transition': '-webkit-transform .3s ease-out',
      '-webkit-transform': 'translate3d(0px,'+ 0 +'px,0px)'
    });
    smY = 0;
  } else if (Math.abs(moveY) + window.innerHeight > menu_list_height) {
      if (condition == "flick" && real_smY != window.innerHeight - menu_list_height){
        $(settings.menu_contents).css({
          '-webkit-transition': '-webkit-transform .3s ease-out',
          '-webkit-transform': 'translate3d(0px,'+ (window.innerHeight - menu_list_height) +'px,0px)'
        });
        smY = window.innerHeight - menu_list_height;
      } else {
        $(settings.menu_contents).css({
          '-webkit-transition': '-webkit-transform .3s ease-out',
          '-webkit-transform': 'translate3d(0px,'+ (window.innerHeight - menu_list_height) +'px,0px)'
        });
        smY = window.innerHeight - menu_list_height;
      }
    }
  });
});
```

ここは、フリックとスクロールとで条件分岐しようとしてごちゃごちゃになってしまった部分なので、あまり参考にならないかもしれない。

# 最後に

カスタマイズしたプラグインを公開するまでやろうと思っていたが、これを書いてるうちに疲れてきてしまったので、追い追い。
細かい内容が書ききれていない気もするので、その点も今後随時加筆修正していく。

総じて思ったことといえば、googleのと同じレベルのものを作るのは相当に難しいことを痛感したというもの。