片側がフェードイン/アウトするスプリットレイアウト実装してみた

## スプリットレイアウトって？
スプリットレイアウトとはSplit（分割）の通り、画面を2カラム、あるいは3カラムなどに分割した画面レイアウトのことです。

パッと思いつくのは2カラムで片方は固定されていて、もう片方はスクロールが可能なデザインかなと思います。

最近だとスマホファーストが主流になっているのもあって、PCで閲覧した際にメインのコンテンツはスマートフォンのレイアウトになっていて、これを中央に配置しつつ、左右はメニューや読ませたい文章、写真なんかが固定されているのも結構見るな～という気がします。
参考までに、こういうサイトのことです。

https://www.nikoand.jp/shitamachi_renaissance/

https://www.mtrainier.jp/plantbased/


スプリットレイアウトについていろいろ調べていたら2017年に流行して、2019年頃にも再熱、そして昨今でもよく見かけるデザインの1つなので、Y2Kしかり流行って巡るものですね。

## こういうスプリットレイアウトを実装したよ
本題なのですが、今回以下のような仕様のサイトを実装する機会があり、本記事はその備忘録になります。
- 2カラム
- 左側は固定の状態でコンテンツのタイトルが入る
- 右側はスクロールできる状態でコンテンツの詳細が表示される
- フッター上で左側にあるタイトルが固定される
- 右側の次のコンテンツが画面内に表示されたタイミングで、左側のタイトルがフェードイン/アウトで次のコンテンツの内容に切り替わる

文章にすると「ん？」という感じなのですが（私の語彙力が無いからかも…）、要は下記のようなレイアウトのサイトになります。CODEPEN上での閲覧推奨です。
SPでは単純に1カラムのよくあるレイアウトに切り替わるため、今回はSPの記述は割愛しています。
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="gONgbay" data-pen-title="Untitled" data-user="yyko" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yyko/pen/gONgbay">
  Untitled</a> by y-yoshino (<a href="https://codepen.io/yyko">@yyko</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 仕組みの解説
左側のレイアウトをフェードイン/アウトさせたいため、こういうレイアウトの実装に便利な`position:sticky`は今回は使わないで、左側は`position:fixed`で固定にしつつ`opacity:0`にして、scriptでのclassの付け替えでフェードイン/アウトを行うようにしています。
右側は単純に`margin-left:auto`で右寄せにしているだけです。

左側のフェードイン/アウトさせるための記述と、フッター上で固定させるための記述はこんな感じです。
```CSS
/* デフォルト（非表示状態にしておく） */
.js-scroll-element .l-left-content {
  cursor: default;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
/* 表示時 */
.js-scroll-element .l-left-content.is-on {
  cursor: auto;
  pointer-events: auto;
  opacity: 1;
}
/* 再度非表示に切り替えるための設定 */
.js-scroll-element .l-left-content.is-off {
  opacity: 0;
}
/* フッター上で固定させる */
.js-scroll-element .l-left-content.is-absolute {
  opacity: 1;
  height: 100%;
  pointer-events: auto;
  cursor: auto;
  position: absolute;
  z-index: 1;
}
/* フッター上で固定させた要素内のコンテンツを下付きにする */
.js-scroll-element .l-left-content.is-absolute .l-left-content__inner {
  align-items: flex-end;
}
```

`cursor:default`や`pointer-events:none`は非表示にしている要素に触れないように設定しています（opacityだけだとあくまでも透過させているだけなので）
上記のclassの付け替えでいい感じにスプリットレイアウトになるようにしています！

続いてJavaScriptの解説です。
何をしているのか少し細かく書いてみました。

#### ウィンドウの高さとスクロール位置を取得
``` JavaScript
const windowHeight = window.innerHeight;
const scrollHeight = window.pageYOffset;
```
#### フッターを取得
``` JavaScript
const footer = document.querySelector('.l-footer');
```
#### スプリットレイアウトを実装したい要素（`'.js-scroll-element'`）を取得
``` JavaScript
const scrollElements = document.querySelectorAll('.js-scroll-element');
```

ここまでは特に特筆することはないかなという感じです。

#### `.js-scroll-element`に対して繰り返し処理を行う
``` JavaScript
scrollElements.forEach(element => {
```
#### 現在のページ内における`.js-scroll-element`の垂直位置を計算
``` JavaScript
  const offsetTop = element.getBoundingClientRect().top + window.pageYOffset;
```
#### `.js-scroll-element`の高さを取得
``` JavaScript
  const height = element.clientHeight;
```
#### フェードイン/アウトの処理を行う`.l-left-content`（コンテンツのタイトル）を取得
``` JavaScript
  const leftContent = element.querySelector('.l-left-content');
```

以下で要素の表示状態を処理していきます。

#### `.js-scroll-element`の上部がウィンドウの中間位置よりも上にあり、かつ、`.js-scroll-element`の下部がスクロール位置より下にある場合、`.l-left-content`を表示状態にする。
``` JavaScript
if (offsetTop < scrollHeight + windowHeight / 2 && offsetTop + height > scrollHeight) {
  leftContent.classList.remove('is-off');
  leftContent.classList.add('is-on');
```
文字にすると大分分かりにくいと思うので、挙動はCODEPENを参照で…（丸投げ）
要は、`.js-scroll-element`の要素（というよりかは右側の`.l-right-content`の要素で捉えてもらうと分かりやすいかも）がウィンドウの真ん中あたりに来たら`.is-on`で表示させて、真ん中よりも下に来たら`.is-off`で非表示にしているイメージです。

#### フッターがスクロール位置の2/3以内に来た場合、要素を固定表示状態にする。
``` JavaScript
  if (footer.getBoundingClientRect().top + window.pageYOffset < scrollHeight + (windowHeight * 2) / 3 && footer.getBoundingClientRect().top + footer.clientHeight + window.pageYOffset > scrollHeight) {
    leftContent.classList.remove('is-on');
    leftContent.classList.remove('is-off');
    leftContent.classList.add('is-absolute');
  } else {
    leftContent.classList.remove('is-absolute');
  }
  ```
切り替え用に`.l-left-content`に付与させていた`.is-on`と`.is-off`を外し、`position:fixed`を`.is-absolute`で設定している`position:absolute`で上書きし、フッターの上で固定させるようにしています。

そして上記2つの条件に該当していない場合は`.l-left-content`を`.is-off`で非表示にします。
``` JavaScript
  } else {
    leftContent.classList.remove('is-on');
    leftContent.classList.add('is-off');
    leftContent.classList.remove('is-absolute');
  }
});
```

最後にページの読み込み完了時とスクロール時に、関数を呼び出せばOKです。
``` JavaScript
document.addEventListener('DOMContentLoaded', setLeftContentVisibility);
window.addEventListener('scroll', setLeftContentVisibility);
```

## 最後に
ちょっとリッチ（？）なスプリットレイアウトの実装方法の紹介でした！
似たようなWebサイトを実装する際の参考になれば幸いです:ok_woman:
もっと効率の良い書き方があれば是非教えていただけると嬉しいです～