8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

概要

v-data-tableを入力に使用していて、「入力値が長くなってきたな?」ってなったときに広げて見たいとかそんな感じのときがたまに来る。

ちなみに

  1. 公式では、今の所そんな機能は実装しないよ。とのこと。
  2. vue-columns-resizable-vuetifyなんてものがあるらしいが、vueのバージョンアップで使えなくなったっぽい?(個人的な検証はしたつもり)

環境

  • nuxt : 2.15.7
  • vue : 2.6.14
  • vuetify : 2.5.6

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

やること

こちらのソースコードをもとに、少々変更を加えて使いやすくするよ。

書く

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

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

<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

とりあえず移植する

もとのコードそのままだとnuxtで使用できないので、とりあえず動くように移植する。
mountedmethodsの部分を追加する。(自分なりに解釈したコメントも入れた)

<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

おかしいところ

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

もとのコード解説

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

おかしいところ解消

その1.「リサイズが終わってグリッド線離したらソートが発動する」

JSのソースコードで埋め込んでる<div>の親要素(テーブルヘッダ)にソート機能があるため、バブリングが起こってるっぽい。ので<div>でバブリング禁止にする。

setListeners関数の中に追加する。

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

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

ヨシ!
disableBubbling.gif

その2.「ソートされるとカラム幅がもとに戻る」

これ結構悩んだけど、jsじゃなくてvuetifyの仕様(?)で解決した。
ヘッダアイテムに width: "auto" を追記すればいける。(なんでこれでいけるのかは謎。)

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

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

その3.「行3の右側に線出てる」

これは単純にループ回しすぎだと思う。ので1回減らす。

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

他に気になるところ

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

addRow.gif

回避方法として、

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

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

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

他に気になるところ2

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

longHeader.gif

回避策としては

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

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

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

<style scoped>
...

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

ヨシ!

noCollapseHeader.gif

最終形

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

<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の幅が変更できるようになった。
えっ、じゃあ複数テーブルあったら全部にこれ書くの?とかその辺は時間があったら書きたい。

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

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

8
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?