vuetifyのv-data-tableの列幅を可変にする

# 概要
[v-data-tableを入力に使用](https://qiita.com/mymt/items/51d3f6dc759ddf6286fe)していて、「入力値が長くなってきたな？」ってなったときに広げて見たいとかそんな感じのときがたまに来る。

### ちなみに
1. [公式](https://github.com/vuetifyjs/vuetify/issues/3278#issuecomment-479086297)では、今の所そんな機能は実装しないよ。とのこと。
1. [vue-columns-resizable-vuetify](https://www.npmjs.com/package/vue-columns-resizable-vuetify)なんてものがあるらしいが、vueのバージョンアップで使えなくなったっぽい？(個人的な検証はしたつもり)

# 環境
- nuxt : 2.15.7
- vue : 2.6.14
- vuetify : 2.5.6

`.vue`ファイルに一括でビューやデータを書いていく。

# やること
[こちら](https://codepen.io/crwilson311/pen/Bajbdwd)のソースコードをもとに、少々変更を加えて使いやすくするよ。

# 書く
### 基本形(テーブルだけ表示するよ)

とりあえず1行3列のデータで、ヘッダにはリサイズ用のグリッド線を表示した。ソート可。フッタは邪魔なので排除。
`data()`の下の部分に機能を実装していく。

```vue
<template>
  <v-app>
    <v-container>
      <v-card>
        <!-- classはスタイル適用のため -->
        <v-data-table
          :headers="headers"
          :items="items"
          class="resizable-column"
          hide-default-footer
        />
      </v-card>
    </v-container>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      headers: [
        { text: "行1", value: "col1", align: "center" },
        { text: "行2", value: "col2", align: "center" },
        { text: "行3", value: "col3", align: "center" },
      ],
      items: [{ col1: "hoge", col2: "fuga", col3: "foo" }],
    }
  },
  /* ここに色々追記していく */
}
</script>

<style scoped>
/* ヘッダに縦線出すよ */
.resizable-column /deep/ th + th {
  border-left: 1px solid grey;
}
</style>
```

最初の見た目はこんな感じ。
![base.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/5353e5d1-dccb-71b2-ab62-1e43f4d505c2.png)


### とりあえず移植する
[もとのコード](https://codepen.io/crwilson311/pen/Bajbdwd)そのままだとnuxtで使用できないので、とりあえず動くように移植する。
`mounted`と`methods`の部分を追加する。(自分なりに解釈したコメントも入れた)

```vue
<script>
export default {
  data() {
    /* 省略 */
  },
  /*-- 以下を追加 --*/
  mounted() {
    this.$nextTick(() => {
      this.getResizableTable()
    })
  },
  methods: {
    // このページのテーブルというテーブル全てリサイズ可にする
    getResizableTable() {   
      var tables = document.getElementsByTagName("table")
      for (var i = 0; i < tables.length; i++) {
        this.resizableGrid(tables[i])
      }
    },

    // テーブルごとにリサイズ用の設定を追加
    resizableGrid(table) {
      var row = table.getElementsByTagName("tr")[0],
        cols = row ? row.children : undefined
      if (!cols) return

      table.style.overflow = "hidden"

      var tableHeight = table.offsetHeight

      // テーブルのカラムとカラムの間にドラッグできるdivを挿入
      for (var i = 0; i < cols.length; i++) {
        var div = createDiv(tableHeight)
        cols[i].appendChild(div)
        cols[i].style.position = "relative"
        setListeners(div)
      }

      // 挿入したdivとdocument全体にイベントリスナーを付与
      function setListeners(div) {
        var pageX, curCol, nxtCol, curColWidth, nxtColWidth

        /* <div> */
        // マウス押下時
        div.addEventListener("mousedown", (e) => {
          curCol = e.target.parentElement
          nxtCol = curCol.nextElementSibling
          pageX = e.pageX

          var padding = paddingDiff(curCol)

          curColWidth = curCol.offsetWidth - padding
          if (nxtCol) nxtColWidth = nxtCol.offsetWidth - padding
        })

        // マウスホバー時
        div.addEventListener("mouseover", (e) => {
          e.target.style.borderRight = "2px solid #0000ff"
        })

        // マウスホバー解除時
        div.addEventListener("mouseout", (e) => {
          e.target.style.borderRight = ""
        })

        /* document */
        // マウスカーソル移動時
        document.addEventListener("mousemove", (e) => {
          if (curCol) {
            var diffX = e.pageX - pageX

            if (nxtCol) nxtCol.style.width = nxtColWidth - diffX + "px"

            curCol.style.width = curColWidth + diffX + "px"
          }
        })

        // マウス押下解除時
        document.addEventListener("mouseup", (e) => {
          curCol = undefined
          nxtCol = undefined
          pageX = undefined
          nxtColWidth = undefined
          curColWidth = undefined
        })
      }

      // リサイズ用divの実体を作成
      function createDiv(height) {
        var div = document.createElement("div")
        div.style.top = 0
        div.style.right = 0
        div.style.width = "5px"
        div.style.position = "absolute"
        div.style.cursor = "col-resize"
        div.style.userSelect = "none"
        div.style.height = height + "px"
        return div
      }

      // この辺はよくわからん
      function paddingDiff(col) {
        if (getStyleVal(col, "box-sizing") == "border-box") {
          return 0
        }

        var padLeft = getStyleVal(col, "padding-left")
        var padRight = getStyleVal(col, "padding-right")
        return parseInt(padLeft) + parseInt(padRight)
      }

      function getStyleVal(elm, css) {
        return window.getComputedStyle(elm, null).getPropertyValue(css)
      }
    },
  },
}
</script>
```

挙動はこんな感じ。なんかおかしい。
![transplant.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/58ba6d8f-34d4-1183-8993-6249a6ace1ba.gif)

おかしいところ

1. リサイズが終わってグリッド線離したらソートが発動する
1. ソートされるとカラム幅がもとに戻る
1. 行3の右側に線出てる

### もとのコード解説
大体コメントに書いたとおりだが、要はテーブルのカラムとカラムの間にドラッグできる`<div>`タグを埋め込んで操作できるようにしてるだけ。


### おかしいところ解消
#### その1.「リサイズが終わってグリッド線離したらソートが発動する」
JSのソースコードで埋め込んでる`<div>`の親要素(テーブルヘッダ)にソート機能があるため、バブリングが起こってるっぽい。ので`<div>`でバブリング禁止にする。

`setListeners`関数の中に追加する。

```js:setListeners()
...
// バブリング抑止
div.addEventListener("click", function (e) {
    e.stopPropagation()
})

/* document */
// マウスカーソル移動時
document.addEventListener("mousemove", function (e) {
...
```

ﾖｼ！
![disableBubbling.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/b6190f63-9676-cebe-de9b-c13f4ac9e1dd.gif)

#### その2.「ソートされるとカラム幅がもとに戻る」
これ結構悩んだけど、jsじゃなくてvuetifyの仕様(?)で解決した。
ヘッダアイテムに `width: "auto"` を追記すればいける。(なんでこれでいけるのかは謎。)

ちなみに`width`が設定されていないカラムはもとに戻るので、全部のカラムに設定する必要がある。

```js:data()
return {
  headers: [
    //                                             ↓ここ
    { text: "行1", value: "col1", align: "center", width: "auto" },
    { text: "行2", value: "col2", align: "center", width: "auto" },
    { text: "行3", value: "col3", align: "center", width: "auto" },
  ],
  items: [{ col1: "hoge", col2: "fuga", col3: "foo" }],
}
```

(1行しかないけどとりあえず)ﾖｼ！
![width.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/7ace17e1-e892-3767-eeaf-130ad7f1c6e3.gif)


#### その3.「行3の右側に線出てる」
これは単純にループ回しすぎだと思う。ので1回減らす。

```js:resizableGrid()
...
var tableHeight = table.offsetHeight

// テーブルのカラムとカラムの間にドラッグできるdivを挿入
//                              ↓ここ
for (var i = 0; i < cols.length - 1; i++) {
  var div = createDiv(tableHeight)
  cols[i].appendChild(div)
  cols[i].style.position = "relative"
  setListeners(div)
}

// 挿入したdivとdocument全体にイベントリスナーを付与
function setListeners(div) {
...
```

多分ﾖｼ！
![tooMuchRight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/06fb1ad6-8c6b-61af-37f5-048b24946823.gif)


### 他に気になるところ
例えば、(ページ読み込み後に)動的に行が増えるみたいな実装だとこうなるよ。
青い線の長さが足りてない！！！

![addRow.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/f5678c6b-a35d-9d1a-acbf-3ed92f31e339.gif)

回避方法として、

1. 青い線の長さをヘッダ部分だけにする
1. 行追加等のupdatedでイベントキャッチしたときに青い線の長さを更新する

が考えられる。
後者の場合は`<div>`の更新処理を書き足す必要があるので前者を選択。(後者は需要があれば書くかも。)

```js:resizableGrid()
var row = table.getElementsByTagName("tr")[0],
cols = row ? row.children : undefined
if (!cols) return

table.style.overflow = "hidden"

// ヘッダ部分だけドラッグ可
var tableHeight = row.offsetHeight // ←右辺だけ書き換える

// テーブルのカラムとカラムの間にドラッグできるdivを挿入
...
```

![headerOnly.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/24ad0ca9-72c9-01d6-23af-f8554af40019.gif)


### 他に気になるところ2
ヘッダ名が長いと、幅に入り切らずにヘッダの高さが変わるので青い線が足りなくなる。

![longHeader.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/159eecdf-043b-0e44-d50e-0dcca56a14b4.gif)

回避策としては

1. ヘッダ名が折り曲がらないようにする
1. マウスを離したときのイベントリスナーをセットして青い線の長さを変える

が考えられる。後者の場合は、どうしてもドラッグ中に青い線の長さが残るので前者がいいと思う。
(ドラッグ中のイベントで青い線の長さを変えられなくもないが、パフォーマンス的に良くない気がする)

これは簡単で、styleに追記すればおｋ。

```vue
<style scoped>
...

/* ヘッダ名が折り曲がらないようにするよ */
.resizable-column /deep/ th {
  white-space: nowrap;
}
</style>
```

ﾖｼ！

![noCollapseHeader.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474496/c866a6d0-54d8-6a14-92ab-d292ad0a90b4.gif)


# 最終形

今まで挙げた内容を全て適用すると以下の通りになる。
挙動はすぐ上↑のgifと同じ。

```vue
<template>
  <v-app>
    <v-container>
      <v-card>
        <v-data-table
          :headers="headers"
          :items="items"
          class="resizable-column"
          hide-default-footer
        />
      </v-card>
    </v-container>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      headers: [
        { text: "行1", value: "col1", align: "center", width: "auto" },
        { text: "行2", value: "col2", align: "center", width: "auto" },
        { text: "行3", value: "col3", align: "center", width: "auto" },
      ],
      items: [{ col1: "hoge", col2: "fuga", col3: "foo" }],
    }
  },
  mounted() {
    this.$nextTick(() => {
      this.getResizableTable()
    })
  },
  methods: {
    // このページのテーブルというテーブル全てリサイズ可にする
    getResizableTable() {
      var tables = document.getElementsByTagName("table")
      for (var i = 0; i < tables.length; i++) {
        this.resizableGrid(tables[i])
      }
    },

    // テーブルごとにリサイズ用の設定を追加
    resizableGrid(table) {
      var row = table.getElementsByTagName("tr")[0],
        cols = row ? row.children : undefined
      if (!cols) return

      table.style.overflow = "hidden"

      // ヘッダ部分だけドラッグ可
      var tableHeight = row.offsetHeight

      // テーブルのカラムとカラムの間にドラッグできるdivを挿入
      for (var i = 0; i < cols.length - 1; i++) {
        var div = createDiv(tableHeight)
        cols[i].appendChild(div)
        cols[i].style.position = "relative"
        setListeners(div)
      }

      // 挿入したdivとdocument全体にイベントリスナーを付与
      function setListeners(div) {
        var pageX, curCol, nxtCol, curColWidth, nxtColWidth

        /* <div> */
        // マウス押下時
        div.addEventListener("mousedown", (e) => {
          curCol = e.target.parentElement
          nxtCol = curCol.nextElementSibling
          pageX = e.pageX

          var padding = paddingDiff(curCol)

          curColWidth = curCol.offsetWidth - padding
          if (nxtCol) nxtColWidth = nxtCol.offsetWidth - padding
        })

        // マウスホバー時
        div.addEventListener("mouseover", (e) => {
          e.target.style.borderRight = "2px solid #0000ff"
        })

        // マウスホバー解除時
        div.addEventListener("mouseout", (e) => {
          e.target.style.borderRight = ""
        })

        // バブリング抑止
        div.addEventListener("click", (e) => {
          e.stopPropagation()
        })

        /* document */
        // マウスカーソル移動時
        document.addEventListener("mousemove", (e) => {
          if (curCol) {
            var diffX = e.pageX - pageX

            if (nxtCol) nxtCol.style.width = nxtColWidth - diffX + "px"

            curCol.style.width = curColWidth + diffX + "px"
          }
        })

        // マウス押下解除時
        document.addEventListener("mouseup", (e) => {
          curCol = undefined
          nxtCol = undefined
          pageX = undefined
          nxtColWidth = undefined
          curColWidth = undefined
        })
      }

      // リサイズ用divの実態を作成
      function createDiv(height) {
        var div = document.createElement("div")
        div.style.top = 0
        div.style.right = 0
        div.style.width = "5px"
        div.style.position = "absolute"
        div.style.cursor = "col-resize"
        div.style.userSelect = "none"
        div.style.height = height + "px"
        return div
      }

      // この辺はよくわからん
      function paddingDiff(col) {
        if (getStyleVal(col, "box-sizing") == "border-box") {
          return 0
        }

        var padLeft = getStyleVal(col, "padding-left")
        var padRight = getStyleVal(col, "padding-right")
        return parseInt(padLeft) + parseInt(padRight)
      }

      function getStyleVal(elm, css) {
        return window.getComputedStyle(elm, null).getPropertyValue(css)
      }
    },
  },
}
</script>

<style scoped>
/* ヘッダに縦線出すよ */
.resizable-column /deep/ th + th {
  border-left: 1px solid grey;
}

/* ヘッダ名が折り曲がらないようにする */
.resizable-column /deep/ th {
  white-space: nowrap;
}
</style>

```

# おわりに
これでv-data-tableの幅が変更できるようになった。
えっ、じゃあ複数テーブルあったら全部にこれ書くの？とかその辺は時間があったら書きたい。

あとはカラムがいっぱいあるときに、実はこの改修だと幅が変更できないという問題もあるので、解決したら書きたい。

最終チェックしてて気づいたけど、「行」じゃなくて「列」だね。
キャプチャ撮り直すの大変だからそのままにしておこう。