シンプルなスライド切り替え機能を実装する：Swiper コンポーネントを自作してみよう

# シンプルなスライド切り替え機能を実装する：Swiper コンポーネントを自作してみよう

最近のウェブ開発では、スライド切り替え機能がさまざまな場面で活用されています。例えば、画像ギャラリーや全画面のスライドページなどです。Swiper.js のような既製ライブラリを使えば簡単に実現できますが、基本的なスライド切り替えコンポーネントを自分で実装するのも意外とシンプルで、仕組みを理解する良い機会になります。

今回は、マウス操作で動くスライド切り替えコンポーネントを一から自作して、その実装方法を分かりやすく説明します。

## 目指す機能
* 上下方向のスワイプ（マウス操作）でスライドを切り替える
* スムーズなアニメーションでスライドが移動
* 範囲外への移動を防ぐ（1枚目と最後のスライドで停止）
これを達成するためのシンプルな構造とロジックを見ていきましょう。

### 基本の構造

```html
<div class='container'>
  <div class='wrapper'>
    <div class='slides'>1</div>
    <div class='slides'>2</div>
    <div class='slides'>3</div>
    <div class='slides'>4</div>
  </div>
</div>
```

この構造は非常にシンプルです：

- `.container` は全体の外枠となるコンテナ
- `.wrapper` は全てのスライドをまとめるラッパー。これを動かすことでスライドを切り替えます
- `.slides` はそれぞれのスライドを表します

### CSS
```css
.container {
  overflow: hidden;
  width: 100%;
  height: 100%;
}

.wrapper {
  display: flex;
  flex-direction: column;
  transform: translateY(0);
}

.slides {
  width: 100svw;
  height: 100svh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 5em;
  font-weight: bold;
  color: #555555;
}
```

- `.wrapper` は `flex-direction: column` を使い、スライドを縦に並べています
- `transform: translateY` を変更することで `.wrapper` の位置を上下に移動させます

### Swiper クラスの実装

次に、このスライド切り替えを制御するロジックを `Swiper` クラスで実装します。

**コンストラクタと初期化**
```js
constructor(container, options) {
  this.container = container;
  this.wrapper = container.querySelector('.wrapper');
  this.options = options;
  this.clickStartY = 0;
  this.clickEndY = 0;

  this.currentSlide = new Proxy({ value: 0 }, {
    set: (target, key, value) => {
      if (key === 'value') {
        console.log(`スライド ${target[key]} から ${value} へ切り替え`);
        this.wrapper.style.transform = `translateY(-${value * 100}svh)`;
        target[key] = value;
      }
      return true;
    }
  });

  this.init();
}
```

ポイント：
- Proxy を利用し、currentSlide.value が変更されたときに自動的に DOM（transform）を更新
- 初期化処理でイベントをバインド

**スワイプ処理**
1\. **スワイプの開始と終了を記録**
```js
onMouseDown(e) {
  this.clickStartY = e.pageY;
}

onMouseUp(e) {
  this.clickEndY = e.pageY;
  const clickedDistance = Math.abs(this.clickStartY - this.clickEndY);

  if (clickedDistance < 60) {
    console.log('スワイプ距離が足りないため動きません');
    return;
  }

  if (this.clickStartY > this.clickEndY) {
    this.handleSwipeUp();
  } else {
    this.handleSwipeDown();
  }
}
```
- **マウス押下時に開始位置を記録し、離したときに終了位置を取得**
- **スワイプ距離が一定（ここでは 60px）未満の場合、動作を無効化**

2\. スワイプ方向に応じた処理
```js
handleSwipeUp() {
  if (this.currentSlide.value === this.wrapper.children.length - 1) {
    console.warn('これ以上上にスライドできません');
    return;
  }
  this.currentSlide.value++;
}

handleSwipeDown() {
  if (this.currentSlide.value === 0) {
    console.warn('これ以上下にスライドできません');
    return;
  }
  this.currentSlide.value--;
}
```
- **上方向のスワイプでは `currentSlide.value` を増加、下方向では減少**
- **範囲外（最初と最後）では動作を止めます。**

3/. イベント初期化
```js
init() {
  this.container.addEventListener('mousedown', this.onMouseDown.bind(this));
  this.container.addEventListener('mouseup', this.onMouseUp.bind(this));
}
```
**マウスイベントをバインドすることで、クリック操作を感知します**
<hr>
完全なコード例
以下は全てを統合した実際のコードです：

```html
<!doctype html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport'
        content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'>
  <meta http-equiv='X-UA-Compatible'
        content='ie=edge'>
  <title>Swiper</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      }

    body {
      max-width: 100svw;
      max-height: 100svh;
      height: 100vh;
      width: 100vw;
      }

    .container {
      overflow: hidden;
      width: 100%;
      height: 100%;
      }

    .wrapper {
      display: flex;
      flex-direction: column;
      transform: translateY(0);
      }

    .slides {
      width: 100svw;
      height: 100svh;
      display: flex;
      justify-content: center;
      align-items: center;
      flex: 1 0 auto;
      font-size: 5em;
      font-weight: bold;
      color: #555555;
      }
  </style>
</head>
<body>
  <div class='container'>
    <div class='wrapper'>
      <div class='slides'>1</div>
      <div class='slides'>2</div>
      <div class='slides'>3</div>
      <div class='slides'>4</div>
    </div>
  </div>

  <script>
    class Swiper {
      constructor(container, options) {
        this.container = container;
        this.wrapper = container.querySelector('.wrapper');
        this.options = options;
        this.clickStartY = 0;
        this.clickEndY = 0;
        this.currentSlide = new Proxy({ value: 0 }, {
          set: (target, key, value) => {
            if (key === 'value') {
              console.log(`Slide changed from ${target[key]} to ${value}`);
              this.wrapper.style.transform = `translateY(-${value * 100}svh)`;
              target[key] = value;
            }
            return true;
          }
        });
        this.init();
      }

      onMouseDown(e) {
        console.log('Mouse down', this);
        this.clickStartY = e.pageY;
      }

      onMouseUp(e) {
        console.log('Mouse up');
        this.clickEndY = e.pageY;
        const clickedDistance = Math.abs(this.clickStartY - this.clickEndY);

        if (clickedDistance < 60) {
          console.log('No swipe');
          return;
        }

        if (this.clickStartY > this.clickEndY) {
          console.log('Swipe up');
          if (this.currentSlide.value === this.wrapper.children.length - 1) {
            console.warn('Last slide');
            return;
          }
          this.currentSlide.value ++;
        } else {
          console.log('Swipe down');
          if (this.currentSlide.value === 0) {
            console.warn('First slide');
            return;
          }
          this.currentSlide.value --;
        }
      }

      init() {
        console.log('Swiper initialized');
        this.container.addEventListener('mousedown', this.onMouseDown.bind(this));
        this.container.addEventListener('mouseup', this.onMouseUp.bind(this));
      }
    }

    const container = document.querySelector('.container');
    const options = {};
    const swiper = new Swiper(container, options);
  </script>
</body>
</html>
```