Closed4

【Vue3】拡大時に他のカラム幅に影響を与えないTable

.ma801.ma801

公式の仕様に逆らってみた。
作ってみたけど、微妙かもしれない。

.ma801.ma801
<script setup lang="ts">
  import { ref, defineProps, onMounted, computed } from "vue";

  // ドラッグ可能要素の横幅(px)
  const DRAGGABLE_AREA_WIDTH = 5;

  interface DataItem {
    [key: string]: string;
  }

  const props = defineProps({
    data: {
      type: Array<DataItem>,
      required: true
    }
  });

  const tableRefs = ref<(HTMLElement | null)>(null);
  const headerRefs = ref<(HTMLElement | null)[]>([]);

  const widthArray = ref<(number | string)[]>([]);

  const isDragging = ref<boolean>(false);
  const startX = ref<number>(0);
  const startWidth = ref<number>(0);
  const draggingColumnIndex = ref<number>(-1);

  const firstTableWidth = ref<number>(0);

  const headerKeys = computed(() => {
    return props.data.length > 0 ? Object.keys(props.data[0]) : [];
  });

  const dragStartTableWidth = ref<number>(0);

  const tableWidth = ref<number>(-1);

  // === event handler ===
  const onMouseDown = (idx: number, e: MouseEvent) => {
    isDragging.value = true;

    // ドラッグ開始時の初期値の投入
    startX.value = e.clientX;
    draggingColumnIndex.value = idx;

    // 対象の要素の現在の幅を取得
    startWidth.value = headerRefs.value[idx]?.getBoundingClientRect().width || 0;
    dragStartTableWidth.value = tableRefs.value?.getBoundingClientRect().width || 0;
  };

  const onMouseMove = (e: MouseEvent) => {
    if (isDragging.value && startX.value) {
      const increment = e.clientX - startX.value;
      const newWidth = startWidth.value + increment;
      tableWidth.value = dragStartTableWidth.value + increment;
      console.log(tableWidth.value);

      if (newWidth > 0) {
        widthArray.value[draggingColumnIndex.value] = newWidth + "px";
      } else {
        // NOTE: マイナスの横幅になったときは、textの文字数分とする
        const headerElement = headerRefs.value[draggingColumnIndex.value];

        if (!headerElement) {
          return;
        }
        const fontSize = window.getComputedStyle(headerElement).fontSize;
        widthArray.value[draggingColumnIndex.value]
          = Number(fontSize) * Object.keys(props.data[draggingColumnIndex.value]).length 
          + DRAGGABLE_AREA_WIDTH + "px";
      }
    }
  };

  const onMouseUp = () => {
    isDragging.value = false;
    draggingColumnIndex.value = -1;
  };

  // NOTE: Drag中にwindow内のどこにマウスが行ってもイベントを拾えるように以下の記述が必要
  window.addEventListener("mousemove", onMouseMove);
  window.addEventListener("mouseup", onMouseUp);

  onMounted(() => {
    firstTableWidth.value = tableRefs.value?.getBoundingClientRect().width || 0;
    tableWidth.value = firstTableWidth.value;
  });

  const tableWidthStyle = computed(() => {
    return tableWidth.value + "px";
  });
</script>

<template>
  <div class="scrollable-container">
    <table ref="tableRefs" :style="{width: tableWidthStyle}">
      <thead>
      <!-- FIXME: CSSでの v-bindでの配列のインデックス指定方法が分からない…-->
      <th
        ref="headerRefs"
        v-for="(header, idx) in headerKeys"
        :style="{ width: widthArray[idx], minWidth: widthArray[idx] }"
      >
        <div class="title-sel">
          <p>{{ header }}</p>
          <div
            class="draggable-area"
            @mousedown="(e) => onMouseDown(idx, e)"
            @mousemove="(e) => onMouseMove(e)"
            @mouseup="onMouseUp"
          >
          </div>
        </div>
      </th>
      </thead>

      <tbody>
      <tr v-for="(item, rowIndex) in props.data" :key="rowIndex">
        <td v-for="(value, key) in item" :key="key"> {{ value }}</td>
      </tr>
      </tbody>
    </table>
  </div>
</template>

<style lang="css" scoped>
.scrollable-container {
  overflow-x: scroll;
}

table {
  border-collapse: collapse;
  min-width: 100%;
}

th,
td {
  border: 1px solid rgb(160 160 160);
}

th {
  background-color: #00b0ff;
  white-space: nowrap;
}

.title-sel {
  position: relative;
  height: 100%;
  user-select: none;
}

.draggable-area {
  width: 5px;
  max-width: 5px;
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  cursor: ew-resize;

  /* 右下のアイコンを表示するための記述 */
  resize: horizontal;
  overflow: hidden;
}
</style>

.ma801.ma801

実際はtableに対してcssをもっと当てたいだろうから、リサイズ用のcssは別ファイルに書いてimportしたほうがいいかもしれない。

ロジックも、composable.ts に書き出してあげたほうがいいのかも?(そもそも書き上げてからリファクタしてないけど…。)

.ma801.ma801

使用例(GIFで使っているやつ)

<script setup lang='ts'>
import ResizableTable from "@/components/ResizableTable.vue";

  const mockData = [
    {
      Item: "Apple",
      Quantity: "10",
      Price: "$1",
      Supplier: "Supplier A, Supplier B, Supplier C, Supplier D",
    },
    {
      Item: "Banana",
      Quantity: "12",
      Price: "$0.5",
      Supplier: "Supplier B",
    },
    {
      Item: "Cherry",
      Quantity: "15",
      Price: "$2",
      Supplier: "Supplier C",
    },
    {
      Item: "Date",
      Quantity: "8",
      Price: "$3",
      Supplier: "Supplier D",
    },
    {
      Item: "Elderberry",
      Quantity: "5",
      Price: "$4",
      Supplier: "Supplier E",
    },
  ];
</script>

<template>
  <ResizableTable :data="mockData" />
</template>
このスクラップは21日前にクローズされました