Skip to content

Instantly share code, notes, and snippets.

@Hogeyama
Last active August 4, 2024 15:55
Show Gist options
  • Select an option

  • Save Hogeyama/b7821b6c87a19d5b9fe55e1d82ba12aa to your computer and use it in GitHub Desktop.

Select an option

Save Hogeyama/b7821b6c87a19d5b9fe55e1d82ba12aa to your computer and use it in GitHub Desktop.
<script setup lang="ts" generic="T">
import { reactive, ref } from "vue";
import type { Row, TableDef } from "./types";
import { AdjustmentsHorizontalIcon, XMarkIcon } from "@heroicons/vue/24/solid";
const { columns, body, rowError, colError } = defineProps<TableDef<T>>();
// FIXME 効率悪い
const hasAnyError = (row: Row<T>) => {
if (rowError(row)) return true;
for (const column of columns) {
if (colError(row, column)) return true;
}
return false;
};
const config_open = ref(true);
const toggleConfig = () => {
config_open.value = !config_open.value;
};
const sort_option = reactive({
order: "asc",
column: "index",
});
type FilterItem = {
index: number;
column: string;
value: string;
};
const filter_option = ref<FilterItem[]>([
{
index: 0,
column: "",
value: "",
},
]);
const is_first_filter = (filter: FilterItem) => {
return filter.index === 0;
};
const is_last_filter = (filter: FilterItem) => {
return filter.index === filter_option.value.length - 1;
};
const addEmptyFilterIfNeeded = () => {
console.log("adjust filter number");
let all_filed = true;
for (const filter of filter_option.value) {
if (filter.column === "" || filter.value === "") {
all_filed = false;
break;
}
}
if (all_filed) {
const index = filter_option.value.length;
filter_option.value = [
...filter_option.value,
{
index,
column: "",
value: "",
},
];
}
};
const deleteFilter = (filter: FilterItem) => {
filter_option.value = filter_option.value
.filter((f) => f.index !== filter.index)
.map((f, i) => ({ ...f, index: i > filter.index ? i - 1 : i }));
};
const updateTableConfig = () => {
console.log({
sort_option: JSON.stringify(sort_option),
filter_option: JSON.stringify(filter_option.value),
});
config_open.value = false;
};
// * カラムの表示トグル
// * ページ送り
</script>
<!-- eslint-disable vue/html-self-closing -->
<template>
<div :class="['rounded-md border border-gray-300']">
<div :class="['flex flex-row items-center justify-between']">
<div :class="['ml-2 mt-2 font-medium']">
{{ "タイトル" }}
</div>
<button class="mr-2 mt-2" @click="toggleConfig">
<AdjustmentsHorizontalIcon :class="'size-4 '" />
</button>
</div>
<!--
これは本来テーブルの外の役目な気もする。
とりあえずはここに置いておく。
-->
<div :class="{ collapse: true, 'collapse-open': config_open }">
<form
class="collapse-content border px-4 text-sm"
@submit.prevent="updateTableConfig"
>
<div
:class="[
'pt-2',
'grid auto-cols-auto grid-cols-5 place-content-center',
'w-full',
]"
>
<div class="p-2">並び替え</div>
<div class="px-2">
<select
v-model="sort_option.column"
class="select select-bordered select-sm w-full"
>
<option :value="'index'">行</option>
<option
v-for="column in columns"
:key="column.name"
:value="column.name"
>
{{ column.label }}
</option>
</select>
</div>
<div class="px-2">
<select
v-model="sort_option.order"
class="select select-bordered select-sm min-w-20 px-2"
>
<option value="asc">昇順</option>
<option value="desc">降順</option>
</select>
</div>
<div />
<div />
<template v-for="filter in filter_option" :key="filter.column">
<div :class="{ 'p-2': true, invisible: !is_first_filter(filter) }">
フィルター
</div>
<div class="px-2">
<select
:value="filter.column"
class="select select-bordered select-sm w-full"
@change="
(e: any) => {
filter.column = e.target?.value;
addEmptyFilterIfNeeded();
}
"
>
<option :value="'index'">行</option>
<option
v-for="column in columns"
:key="column.name"
:value="column.name"
>
{{ column.label }}
</option>
</select>
</div>
<div class="col-span-2 px-2">
<input
type="text"
class="input input-sm input-bordered w-full"
@blur="
(e: any) => {
filter.value = e.target?.value;
addEmptyFilterIfNeeded();
}
"
/>
</div>
<div>
<button v-if="is_last_filter(filter)" class="btn btn-sm w-20">
{{ "更新" }}
</button>
<button v-else @click="() => deleteFilter(filter)">
<XMarkIcon class="p-2" />
</button>
</div>
</template>
</div>
</form>
</div>
<div :class="['overflow-auto', 'max-h-80']">
<table class="table table-pin-rows table-pin-cols table-xs my-0">
<thead class="border-0">
<!-- この h-8 と header tbody の top-8 は同じ高さを指している -->
<tr class="z-30 h-8 border-0">
<th class="z-30 min-w-20" scope="col">行</th>
<td
v-for="column in columns"
:key="column.name"
class="z-10 min-w-40"
scope="col"
>
<div class="flex items-center">
{{ column.label }}
<div class="group relative flex cursor-help items-center">
<span class="ml-1 text-gray-400 group-hover:text-gray-600">
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</span>
<div
:class="[
'invisible group-hover:visible',
'opacity-100 group-hover:opacity-100',
'transition-opacity duration-300',
'absolute',
'bg-white',
'z-50',
'rounded text-xs text-gray-700',
'left-1/2 top-full -translate-x-1/2 px-3 ',
]"
>
<component :is="column.explanation" />
</div>
</div>
</div>
</td>
</tr>
</thead>
<template v-for="section in body" :key="section.key">
<tbody v-if="section.sep !== undefined">
<!-- この top-8 と header の h-8 は同じ高さを指している -->
<tr class="sticky top-8 z-20">
<th
:colspan="columns.length"
class="sticky inset-x-0 z-20 min-w-20 bg-blue-100 text-left"
>
<div class="ml-4">{{ section.sep }}</div>
</th>
</tr>
</tbody>
<tbody>
<tr v-for="row in section.rows" :key="row.index">
<th
:class="[
'z-10 whitespace-nowrap text-center',
hasAnyError(row) ? 'bg-red-100' : 'bg-blue-50',
]"
>
{{ row.index }}
</th>
<td
v-if="rowError(row)"
:colspan="columns.length"
class="bg-red-100"
>
{{ rowError(row) }}
</td>
<td
v-for="column in columns"
v-else
:key="column.name"
:class="[
'group relative',
colError(row, column) ? 'bg-red-100' : 'bg-blue-50',
]"
>
{{ column.field(row.value) }}
<div
v-if="colError(row, column)"
:class="[
'invisible group-hover:visible',
'opacity-100 group-hover:opacity-100',
'transition-opacity duration-300',
'absolute',
'bg-white',
'rounded text-xs text-gray-700',
'left-1/2 top-full mt-1 w-40 -translate-x-1/2 px-3 py-2 ',
'z-30',
]"
>
{{ colError(row, column) }}
</div>
</td>
</tr>
</tbody>
</template>
</table>
</div>
</div>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment