Vueで可変モーダルを実装する

## はじめに
ドラッグで移動、拡大縮小ができるモーダルを作ってみました。
早速完成形からお見せします。

モーダルの移動
[![Image from Gyazo](https://i.gyazo.com/2f8de4930140e166b430f4b91c17faf4.gif)](https://gyazo.com/2f8de4930140e166b430f4b91c17faf4)

サイズ調整
[![Image from Gyazo](https://i.gyazo.com/00cc52dbd0cb5ec9cae595210d434302.gif)](https://gyazo.com/00cc52dbd0cb5ec9cae595210d434302)

## 移動
まずは、ヘッダー部分をドラッグすることでモーダルが移動ができるようにします。

###### 1. テンプレート
```html
<template>
  <div
    class="draggable-modal"
    :style="{
      width: `${modalWidth}px`,
      height: `${modalHeight}px`,
      transform: `translate(${pos.x}px, ${pos.y}px)`,
    }"
  >
    <div
      class="header"
      @mousedown="
        (e) => {
          adjustDragStart(e)
        }
      "
    >
      header
    </div>
    <div class="body">body</div>
  </div>
</template>
```

モーダルの大枠の中にヘッダーとボディーを用意します。
モーダルの大きさは`modalWidth`と`modalHeight`、位置は`pos.x`と`pos.y`で管理していきます。
style属性にある`translate(x, y)`を指定することで元に位置からX軸方向にx、Y軸方向にy移動させることができます。
ヘッダー部分にmousedownイベントを用意し、ドラッグの開始時に`adjustDragStart()`を発火させます。

###### 2. SCSS
```scss
.meeting-draggable-parts {
  position: absolute;
  left: 0;
  top: 0;

  > .header {
    height: 20%;
    background-color: red;
    text-align: center;
  }

  > .body {
    height: 80%;
    background-color: blue;
    text-align: center;
  }
}
```

###### 3. プロパティ
```typescript
  // 初期値
  private initialWidth = 300

  // 初期高さ
  private initialHeight = 400

  // 幅の最低値
  private minWidth = 100

  // 高さの最低値
  private minHeight = 100

  // モーダルの幅
  private modalWidth = this.initialWidth

  // モーダルの高さ
  private modalHeight = this.initialHeight

  // スタート時のポインターのX座標
  private dragStartX = 0

  // スタート時のポインターのY座標
  private dragStartY = 0

  // モーダルの左上隅の座標(中央に配置している)
  private pos = {
    x: (window.innerWidth - this.initialWidth) / 2,
    y: (window.innerHeight - this.initialHeight) / 2,
  }

  // ドラッグスタート時の座標と大きさ
  private startModalRect = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  }

  // モーダル位置調整中フラグフラグ
  private isAdjusting = false
```

###### 4. `mounted()`,`beforeDestory()`に以下を記述
```typescript
  /**
   * ドラッグ監視
   */
  mounted() {
    document.addEventListener('mousemove', (e) => this.onDrag(e))
    document.addEventListener('mouseup', () => this.dragEnd())
  }

  /**
   * 監視終了
   */
  beforeDestroy() {
    document.removeEventListener('mousemove', this.onDrag)
    document.removeEventListener('mouseup', this.dragEnd)
  }
```

`onMounted`内で`addEventListener`を記述し、マウスが動いた時とマウスのドラッグ状態を解除した時を監視します。
`beforeDestroy`で監視を終了させます。　

###### 5. ヘッダーをクリックした際に発火されるメソッド記述
```typescript
  /**
   * @desc 位置調整ドラッグ開始時の処理
   * @param {MouseEvent} event
   */
  private adjustDragStart(event: MouseEvent) {
    event.preventDefault()
    event.stopPropagation()

　　　　　　　　// モーダル位置調整中フラグをtrueにする
    this.isAdjusting = true

    // 初期位置代入
    this.dragStartX = event.clientX
    this.dragStartY = event.clientY
    this.startModalRect.x = this.pos.x
    this.startModalRect.y = this.pos.y
    this.startModalRect.width = this.modalWidth
    this.startModalRect.height = this.modalHeight
  }
```
`this.isAdjusting = true` でフラグをたてます。これは後述するモーダルの大きさ調整と区別する際に使われます。

`dragStart`にはクリックしたマウスの座標、`startModalRect`には現在のモーダルの座標と大きさを挿入します。

###### 6. モーダルをドラッグしている時に発火されるメソッド記述
```typescript
  /**
   * @desc 位置調整ドラッグ中の処理
   * @param {MouseEvent} event
   */
  private onDrag(event: MouseEvent) {
    if (!this.dragStartX || !this.dragStartY) return

    // 位置移動処理
    if (this.isAdjusting) {
      this.pos.x = this.keepThreshold(
        this.startModalRect.x + (event.clientX - this.dragStartX),
        0,
        window.innerWidth - this.modalWidth
      )
      this.pos.y = this.keepThreshold(
        this.startModalRect.y + (event.clientY - this.dragStartY),
        0,
        window.innerHeight - this.modalHeight
      )
    }
  }
```
こちらでモーダルの位置を移動させる処理を記述しています。
モーダルのX座標である`pos.x`は`this.startModalRect.x + (event.clientX - this.dragStartX)(モーダルのドラッグ開始時のモーダルのX座標 - （マウスのX座標 - ドラッグ開始時のマウスのX座標）)`で計算することができます。
Y座標も同様です。

`keepThreshold（）`は第一引数で指定した値が、第二・第三引数で指定する最小値と最大値を下回るor上回らないようにするメソッドです。
これにより、モーダルが画面外にはみ出さないようにしています。

```typescript
  /**
   * @desc 閾値に値を保つ処理
   * @param {number | null} value
   * @param {number | null} min
   * @return {number}
   */
  private keepThreshold(
    value: number,
    min: number | null,
    max: number | null
  ): number {
    if (min !== null && max === null) return value >= min ? value : min
    if (min === null && max !== null) return value <= max ? value : max
    if (min !== null && max !== null) {
      if (value <= min) return min
      if (value >= max) return max
    }
    return value
  }
```

###### 7. ドラッグ終了時の処理を記述
```typescript
  /**
   * ドラッグ終了時の処理
   */
  private dragEnd() {
    this.isAdjusting = false
    this.dragStartX = 0
    this.dragStartY = 0
    this.startModalRect.x = 0
    this.startModalRect.y = 0
    this.startModalRect.width = 0
    this.startModalRect.height = 0
  }
```

特に難しいことはなく、各種値を初期値に戻しているだけです。

これで、モーダルの位置をドラッグ&ドロップで調整可能になります。

## モーダルサイズの拡大・縮小
次に、モーダルの四隅と四辺をドラッグすることで、モーダルサイズが拡大・縮小できるようにします。

###### 1. 四隅と四辺に要素を追加する
`class="draggable-modal"`を持つ要素の直下に以下の要素を追加します。

```html
    <span class="top" @mousedown="(e) => {resizeDragStart(e, null, 'top') }"></span>
    <span class="bottom" @mousedown="(e) => {resizeDragStart(e, null, 'bottom') }"></span>
    <span class="left" @mousedown="(e) => {resizeDragStart(e, 'left', null) }"></span>
    <span class="right" @mousedown="(e) => {resizeDragStart(e, 'right', null) }"></span>
    <span class="tleft" @mousedown="(e) => {resizeDragStart(e, 'left', 'top') }"></span>
    <span class="tright" @mousedown="(e) => {resizeDragStart(e, 'right', 'top') }"></span>
    <span class="bleft" @mousedown="(e) => {resizeDragStart(e, 'left', 'bottom') }"></span>
    <span class="bright" @mousedown="(e) => {resizeDragStart(e, 'right', 'bottom') }"></span>
```
四隅と四辺に要素を配置します。ここをマウスでクリックした時に、後述する`resizeDragStart()`が発火するように`@mousedown`を使用しています。`resizeDragStart`の引数によって、どこを起点としてサイズを変更するのかを区別しています。

###### 2.　スタイルを追加します。
各要素をそれぞれ四隅四辺に配置します。

```scss
  > .top {
    position: absolute;
    top: 0;
    right: 5px;
    left: 5px;
    height: 5px;
    cursor: ns-resize;
  }

  > .bottom {
    position: absolute;
    bottom: 0;
    right: 5px;
    left: 5px;
    height: 5px;
    cursor: ns-resize;
  }

  > .left {
    position: absolute;
    top: 5px;
    bottom: 5px;
    left: 0;
    width: 5px;
    cursor: ew-resize;
  }

  > .right {
    position: absolute;
    top: 5px;
    right: 0;
    bottom: 5px;
    width: 5px;
    cursor: ew-resize;
  }

  > .tleft {
    position: absolute;
    top: 0;
    left: 0;
    width: 5px;
    height: 5px;
    cursor: nwse-resize;
  }

  > .tright {
    position: absolute;
    top: 0;
    right: 0;
    width: 5px;
    height: 5px;
    cursor: nesw-resize;
  }

  > .bleft {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 5px;
    height: 5px;
    cursor: nesw-resize;
  }

  > .bright {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 5px;
    height: 5px;
    cursor: nwse-resize;
  }
```
`tleft`の「t」は`top`、`bleft`の「b」は`bottom`を表しています。

###### 3. 四隅四辺がクリックされた時の処理を記述
```typescript
  /**
   * @desc リサイズ開始時の処理
   * @param {MouseEvent} event
   * @param {'left'|'right'|null} horizon
   * @param {'top'|'bottom'|null} vertical
   */
  private resizeDragStart(
    event: MouseEvent,
    horizon: 'left' | 'right' | null,
    vertical: 'top' | 'bottom' | null
  ) {
    event.preventDefault()
    event.stopPropagation()
    this.resizeDragHorizon = horizon
    this.resizeDragVertical = vertical
    this.dragStartX = event.clientX
    this.dragStartY = event.clientY
    this.startModalRect.x = this.pos.x
    this.startModalRect.y = this.pos.y
    this.startModalRect.width = this.modalWidth
    this.startModalRect.height = this.modalHeight
  }
```
前述したモーダルの位置調整と同じく、モーダルの初期位置や大きさ、クリックした時のマウスの座標をそれぞれ入力する他、
`resizeDragHorizon`と`resizeDragVertical`に引数で渡した値を代入します。

###### 4. onDrag()に以下の記述を追加する
```typescript
    // topのリサイズの処理
    if (this.resizeDragVertical === 'top') {
      const difference = event.clientY - this.dragStartY
      if (this.modalHeight <= this.minHeight && difference > 0) return
      const bottomY = this.startModalRect.y + this.startModalRect.height
      this.pos.y = this.keepThreshold(
        this.startModalRect.y + difference,
        0,
        bottomY - this.minHeight
      )
      this.modalHeight = bottomY - this.pos.y
    }

    // bottomのリサイズ処理
    if (this.resizeDragVertical === 'bottom') {
      const difference = event.clientY - this.dragStartY
      this.modalHeight = this.keepThreshold(
        this.startModalRect.height + difference,
        this.minHeight,
        window.innerHeight - this.startModalRect.y
      )
    }

    // leftのリサイズ処理
    if (this.resizeDragHorizon === 'left') {
      const difference = event.clientX - this.dragStartX
      if (this.modalWidth <= this.minWidth && difference > 0) return
      const rightX = this.startModalRect.x + this.startModalRect.width
      this.pos.x = this.keepThreshold(
        this.startModalRect.x + difference,
        0,
        rightX - this.minWidth
      )
      this.modalWidth = rightX - this.pos.x
    }

    // rightのリサイズ処理
    if (this.resizeDragHorizon === 'right') {
      const difference = event.clientX - this.dragStartX
      this.modalWidth = this.keepThreshold(
        this.startModalRect.width + difference,
        this.minWidth,
        window.innerWidth - this.startModalRect.x
      )
    }
}
```
どこを起点にするかによって処理が変わっていきいます。四隅の場合は、垂直方向と水平方向の二つの処理が必要となります。

`resizeDragHorizon = 'top'`の時、すなわちモーダルの上からサイズを調整する場合を考えてみましょう。
このとき、変化するのはモーダルのY座標と、モーダルの高さです。先述したモーダルの移動の時と同じ計算式で算出されます。この時の最大値(モーダルの最低な座標)は、モーダルの可変の座標から、minHeightを引いた値にすることで、高さが最低かつ、一番低い時のY座標になります。
モーダルの高さは、ドラッグを開始した時のモーダルの下辺のY座標から、先ほどの計算したモーダルのY座標を引くことで算出されます。

次に、`resizeDragHorizon = 'bottom'`の時、すなわちモーダルの下かサイズを調整する場合です。
この場合の`pos.y`は下からのサイズ調整の場合は変化しません。
`modalHeight`は、`this.startModalRext.height + event.clientY - this.dragStartY(ドラッグ開始時のモーダルの高さ + マウスのY座標 - ドラッグ開始時のマウスのY座標)`で取得することができます。
また、この時の最大幅を,`window.innerHeight - this.startModalRect.y`にすることで、ウィンドウの一番下までが限界の高さにすることができます。

水平方向からの処理に関しても同じ考えで計算することができます。

以上でドラッグ&ドロップで移動、サイズが調整できるようになります。