2022-02-07 22:34:53 +08:00
|
|
|
<script lang="ts">
|
2022-02-01 23:46:50 +08:00
|
|
|
import { tick } from "svelte";
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
export let headers: Array<string> = [];
|
2022-02-16 17:57:04 +08:00
|
|
|
export let values: Array<Array<string | number>> = [["", "", ""]];
|
|
|
|
export let default_data: Array<Array<string | number>> = [];
|
|
|
|
|
|
|
|
if ($$props.default) values = $$props.default;
|
2022-02-07 22:34:53 +08:00
|
|
|
|
|
|
|
export let setValue: (val: typeof values) => typeof values;
|
2022-02-01 23:46:50 +08:00
|
|
|
export let editable = true;
|
|
|
|
|
|
|
|
let id = 0;
|
2022-02-07 22:34:53 +08:00
|
|
|
let editing: boolean | number = false;
|
|
|
|
let selected: boolean | number = false;
|
|
|
|
let els: Record<
|
|
|
|
string,
|
|
|
|
{ cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
|
|
|
|
> = {};
|
|
|
|
|
|
|
|
type Headers = Array<{ value: string; id: number }>;
|
2022-02-01 23:46:50 +08:00
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function make_headers(_h: Array<string>): Headers {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (_h.length === 0) {
|
|
|
|
return values[0].map((_, i) => {
|
|
|
|
const _id = ++id;
|
|
|
|
els[_id] = { cell: null, input: null };
|
2022-02-07 22:34:53 +08:00
|
|
|
return { id: _id, value: JSON.stringify(i + 1) };
|
2022-02-01 23:46:50 +08:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return _h.map((h) => {
|
|
|
|
const _id = ++id;
|
|
|
|
els[_id] = { cell: null, input: null };
|
|
|
|
return { id: _id, value: h };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let _headers = make_headers(headers);
|
|
|
|
|
|
|
|
let data = values.map((x) =>
|
|
|
|
x.map((n) => {
|
|
|
|
const _id = ++id;
|
|
|
|
els[id] = { input: null, cell: null };
|
|
|
|
return { value: n, id: _id };
|
|
|
|
})
|
|
|
|
) || [
|
|
|
|
Array(headers.length)
|
|
|
|
.fill(0)
|
|
|
|
|
|
|
|
.map((_) => {
|
|
|
|
const _id = ++id;
|
|
|
|
els[id] = { input: null, cell: null };
|
|
|
|
|
|
|
|
return { value: "", id: _id };
|
|
|
|
})
|
|
|
|
];
|
|
|
|
|
|
|
|
$: setValue(data.map((r) => r.map(({ value }) => value)));
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function get_sort_status(
|
|
|
|
name: string,
|
|
|
|
sort: number,
|
|
|
|
direction?: SortDirection
|
|
|
|
) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (!sort) return "none";
|
2022-02-07 22:34:53 +08:00
|
|
|
if (headers[sort] === name) {
|
|
|
|
if (direction === "asc") return "ascending";
|
|
|
|
if (direction === "des") return "descending";
|
2022-02-01 23:46:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
async function start_edit(id: number) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (!editable) return;
|
|
|
|
editing = id;
|
|
|
|
await tick();
|
|
|
|
const { input } = els[id];
|
2022-02-07 22:34:53 +08:00
|
|
|
input?.focus();
|
2022-02-01 23:46:50 +08:00
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function handle_keydown(
|
|
|
|
event: KeyboardEvent,
|
|
|
|
i: number,
|
|
|
|
j: number,
|
|
|
|
id: number
|
|
|
|
) {
|
2022-02-01 23:46:50 +08:00
|
|
|
let is_data;
|
|
|
|
switch (event.key) {
|
|
|
|
case "ArrowRight":
|
|
|
|
if (editing) break;
|
|
|
|
event.preventDefault();
|
|
|
|
is_data = data[i][j + 1];
|
|
|
|
selected = is_data ? is_data.id : selected;
|
|
|
|
break;
|
|
|
|
case "ArrowLeft":
|
|
|
|
if (editing) break;
|
|
|
|
event.preventDefault();
|
|
|
|
is_data = data[i][j - 1];
|
|
|
|
selected = is_data ? is_data.id : selected;
|
|
|
|
break;
|
|
|
|
case "ArrowDown":
|
|
|
|
if (editing) break;
|
|
|
|
event.preventDefault();
|
|
|
|
is_data = data[i + 1];
|
|
|
|
selected = is_data ? is_data[j].id : selected;
|
|
|
|
break;
|
|
|
|
case "ArrowUp":
|
|
|
|
if (editing) break;
|
|
|
|
event.preventDefault();
|
|
|
|
is_data = data[i - 1];
|
|
|
|
selected = is_data ? is_data[j].id : selected;
|
|
|
|
break;
|
|
|
|
case "Escape":
|
|
|
|
if (!editable) break;
|
|
|
|
event.preventDefault();
|
|
|
|
editing = false;
|
|
|
|
break;
|
|
|
|
case "Enter":
|
|
|
|
if (!editable) break;
|
|
|
|
event.preventDefault();
|
|
|
|
if (editing === id) {
|
|
|
|
editing = false;
|
|
|
|
} else {
|
|
|
|
editing = id;
|
|
|
|
}
|
|
|
|
break;
|
2022-02-16 17:57:04 +08:00
|
|
|
case "Backspace":
|
|
|
|
if (!editable) break;
|
|
|
|
if (!editing) {
|
|
|
|
event.preventDefault();
|
|
|
|
data[i][j].value = "";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "Delete":
|
|
|
|
if (!editable) break;
|
|
|
|
if (!editing) {
|
|
|
|
event.preventDefault();
|
|
|
|
data[i][j].value = "";
|
|
|
|
}
|
|
|
|
break;
|
2022-02-01 23:46:50 +08:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
async function handle_cell_click(id: number) {
|
2022-02-01 23:46:50 +08:00
|
|
|
editing = false;
|
|
|
|
selected = id;
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
async function set_focus(id: number | boolean, type: "edit" | "select") {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (type === "edit" && typeof id == "number") {
|
|
|
|
await tick();
|
2022-02-07 22:34:53 +08:00
|
|
|
els[id].input?.focus();
|
2022-02-01 23:46:50 +08:00
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
if (
|
|
|
|
type === "edit" &&
|
|
|
|
typeof id == "boolean" &&
|
|
|
|
typeof selected === "number"
|
|
|
|
) {
|
2022-02-01 23:46:50 +08:00
|
|
|
let cell = els[selected]?.cell;
|
|
|
|
await tick();
|
|
|
|
cell?.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === "select" && typeof id == "number") {
|
|
|
|
const { cell } = els[id];
|
2022-02-07 22:34:53 +08:00
|
|
|
cell?.setAttribute("tabindex", "0");
|
2022-02-01 23:46:50 +08:00
|
|
|
await tick();
|
2022-02-07 22:34:53 +08:00
|
|
|
els[id].cell?.focus();
|
2022-02-01 23:46:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$: set_focus(editing, "edit");
|
|
|
|
$: set_focus(selected, "select");
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
type SortDirection = "asc" | "des";
|
|
|
|
let sort_direction: SortDirection;
|
|
|
|
let sort_by: number;
|
2022-02-01 23:46:50 +08:00
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function sort(col: number, dir: SortDirection) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (dir === "asc") {
|
|
|
|
data = data.sort((a, b) => (a[col].value < b[col].value ? -1 : 1));
|
|
|
|
} else if (dir === "des") {
|
|
|
|
data = data.sort((a, b) => (a[col].value > b[col].value ? -1 : 1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function handle_sort(col: number) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (typeof sort_by !== "number" || sort_by !== col) {
|
|
|
|
sort_direction = "asc";
|
|
|
|
sort_by = col;
|
|
|
|
} else {
|
|
|
|
if (sort_direction === "asc") {
|
|
|
|
sort_direction = "des";
|
|
|
|
} else if (sort_direction === "des") {
|
|
|
|
sort_direction = "asc";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort(col, sort_direction);
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
let header_edit: number | boolean;
|
2022-02-01 23:46:50 +08:00
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
async function edit_header(_id: number, select?: boolean) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (!editable) return;
|
|
|
|
header_edit = _id;
|
|
|
|
await tick();
|
2022-02-07 22:34:53 +08:00
|
|
|
els[_id].input?.focus();
|
2022-02-01 23:46:50 +08:00
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
if (select) els[_id].input?.select();
|
2022-02-01 23:46:50 +08:00
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function end_header_edit(event: KeyboardEvent) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (!editable) return;
|
|
|
|
|
|
|
|
switch (event.key) {
|
|
|
|
case "Escape":
|
|
|
|
event.preventDefault();
|
|
|
|
header_edit = false;
|
|
|
|
break;
|
|
|
|
case "Enter":
|
|
|
|
event.preventDefault();
|
|
|
|
header_edit = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function add_row() {
|
|
|
|
data.push(
|
|
|
|
headers.map(() => {
|
|
|
|
const _id = ++id;
|
|
|
|
els[_id] = { cell: null, input: null };
|
|
|
|
return { id: _id, value: "" };
|
|
|
|
})
|
|
|
|
);
|
|
|
|
data = data;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function add_col() {
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
const _id = ++id;
|
|
|
|
els[_id] = { cell: null, input: null };
|
|
|
|
data[i].push({ id: _id, value: "" });
|
|
|
|
}
|
|
|
|
|
|
|
|
const _id = ++id;
|
|
|
|
els[_id] = { cell: null, input: null };
|
|
|
|
_headers.push({ id: _id, value: `Header ${_headers.length + 1}` });
|
|
|
|
|
|
|
|
data = data;
|
|
|
|
_headers = _headers;
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
edit_header(_id, true);
|
|
|
|
}
|
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
const double_click = (
|
|
|
|
node: HTMLElement,
|
|
|
|
{ click, dblclick }: { click: Function; dblclick: Function }
|
|
|
|
) => {
|
|
|
|
let timer: NodeJS.Timeout | undefined;
|
2022-02-01 23:46:50 +08:00
|
|
|
|
2022-02-07 22:34:53 +08:00
|
|
|
function handler(event: MouseEvent) {
|
2022-02-01 23:46:50 +08:00
|
|
|
if (timer) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
timer = undefined;
|
|
|
|
dblclick(event);
|
|
|
|
} else {
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
click(event);
|
|
|
|
timer = undefined;
|
|
|
|
}, 250);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
node.addEventListener("click", handler);
|
|
|
|
|
|
|
|
return {
|
|
|
|
destroy: () => node.removeEventListener("click", handler)
|
|
|
|
};
|
|
|
|
};
|
2022-02-01 21:45:55 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class="shadow overflow-hidden border-gray-200 rounded-sm relative">
|
2022-02-02 23:49:37 +08:00
|
|
|
<table
|
|
|
|
id="grid"
|
|
|
|
role="grid"
|
|
|
|
aria-labelledby="title"
|
2022-02-18 05:21:18 +08:00
|
|
|
class="min-w-full divide-y divide-gray-200 dark:divide-gray-800"
|
2022-02-02 23:49:37 +08:00
|
|
|
>
|
2022-02-18 05:21:18 +08:00
|
|
|
<thead class="bg-gray-50 dark:bg-gray-800">
|
2022-02-01 23:46:50 +08:00
|
|
|
<tr>
|
|
|
|
{#each _headers as { value, id }, i (id)}
|
|
|
|
<th
|
|
|
|
use:double_click={{
|
|
|
|
click: () => handle_sort(i),
|
|
|
|
dblclick: () => edit_header(id)
|
|
|
|
}}
|
2022-02-07 22:34:53 +08:00
|
|
|
aria-sort={get_sort_status(value, sort_by, sort_direction)}
|
2022-02-18 05:21:18 +08:00
|
|
|
class="relative after:absolute after:opacity-0 after:content-['▲'] after:ml-2 after:inset-y-0 after:h-[1.05rem] after:m-auto relative px-6 py-3 text-left text-xs font-medium dark:text-gray-100 uppercase tracking-wider"
|
2022-02-01 23:46:50 +08:00
|
|
|
class:sorted={sort_by === i}
|
|
|
|
class:des={sort_by === i && sort_direction === "des"}
|
|
|
|
>
|
|
|
|
{#if header_edit === id}
|
|
|
|
<input
|
2022-02-18 05:21:18 +08:00
|
|
|
class="bg-transparent inset-y-0 left-6 w-full outline-none absolute p-0 w-3/4 text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-600"
|
2022-02-01 23:46:50 +08:00
|
|
|
tabindex="-1"
|
|
|
|
bind:value
|
|
|
|
bind:this={els[id].input}
|
|
|
|
on:keydown={end_header_edit}
|
2022-02-02 23:49:37 +08:00
|
|
|
on:blur={({ currentTarget }) =>
|
2022-02-07 22:34:53 +08:00
|
|
|
currentTarget.setAttribute("tabindex", "-1")}
|
2022-02-01 23:46:50 +08:00
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
<span
|
|
|
|
tabindex="-1"
|
|
|
|
role="button"
|
|
|
|
class="min-h-full"
|
|
|
|
class:opacity-0={header_edit === id}>{value}</span
|
|
|
|
>
|
|
|
|
</th>
|
|
|
|
{/each}
|
|
|
|
</tr></thead
|
2022-02-18 05:21:18 +08:00
|
|
|
><tbody
|
|
|
|
class="bg-white divide-y divide-gray-200 dark:divide-gray-500 dark:bg-gray-600"
|
|
|
|
>
|
2022-02-01 23:46:50 +08:00
|
|
|
{#each data as row, i (row)}
|
|
|
|
<tr>
|
|
|
|
{#each row as { value, id }, j (id)}
|
|
|
|
<td
|
|
|
|
tabindex="-1"
|
|
|
|
class="p-0 whitespace-nowrap display-block outline-none relative "
|
|
|
|
on:dblclick={() => start_edit(id)}
|
|
|
|
on:click={() => handle_cell_click(id)}
|
|
|
|
on:keydown={(e) => handle_keydown(e, i, j, id)}
|
|
|
|
bind:this={els[id].cell}
|
2022-02-02 23:49:37 +08:00
|
|
|
on:blur={({ currentTarget }) =>
|
2022-02-07 22:34:53 +08:00
|
|
|
currentTarget.setAttribute("tabindex", "-1")}
|
2022-02-01 23:46:50 +08:00
|
|
|
>
|
|
|
|
<div
|
|
|
|
class:border-transparent={selected !== id}
|
2022-02-18 05:21:18 +08:00
|
|
|
class="min-h-[3.3rem] px-5 py-3 border-[0.125rem] {selected ===
|
|
|
|
id
|
|
|
|
? 'border-gray-600 dark:border-gray-200'
|
|
|
|
: ''}"
|
2022-02-01 23:46:50 +08:00
|
|
|
>
|
|
|
|
{#if editing === id}
|
|
|
|
<input
|
2022-02-18 05:21:18 +08:00
|
|
|
class="w-full outline-none absolute p-0 w-3/4 dark:bg-gray-600 dark:text-gray-50"
|
2022-02-01 23:46:50 +08:00
|
|
|
tabindex="-1"
|
|
|
|
bind:value
|
|
|
|
bind:this={els[id].input}
|
2022-02-02 23:49:37 +08:00
|
|
|
on:blur={({ currentTarget }) =>
|
2022-02-07 22:34:53 +08:00
|
|
|
currentTarget.setAttribute("tabindex", "-1")}
|
2022-02-01 23:46:50 +08:00
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
<span
|
|
|
|
class=" cursor-default w-full"
|
|
|
|
class:opacity-0={editing === id}
|
|
|
|
tabindex="-1"
|
|
|
|
role="button"
|
|
|
|
>
|
|
|
|
{value}
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
{/each}
|
|
|
|
</tr>
|
|
|
|
{/each}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
2022-02-01 21:45:55 +08:00
|
|
|
</div>
|
|
|
|
{#if editable}
|
2022-02-01 23:46:50 +08:00
|
|
|
<div class="flex justify-end ">
|
|
|
|
<button
|
|
|
|
on:click={add_col}
|
|
|
|
class="hover:bg-gray-100 dark:hover:bg-gray-600 shadow py-1 px-3 rounded transition focus:outline-none m-2 mr-0"
|
|
|
|
>New Column</button
|
|
|
|
>
|
|
|
|
<button
|
|
|
|
on:click={add_row}
|
2022-02-15 07:27:29 +08:00
|
|
|
class="bg-amber-500 hover:bg-amber-400 dark:bg-red-700 dark:hover:bg-red-600 text-white shadow py-1 px-3 rounded transition focus:outline-none m-2 mr-0"
|
2022-02-01 23:46:50 +08:00
|
|
|
>New Row</button
|
|
|
|
>
|
|
|
|
</div>
|
2022-02-01 21:45:55 +08:00
|
|
|
{/if}
|
|
|
|
|
|
|
|
<style>
|
2022-02-01 23:46:50 +08:00
|
|
|
.sorted::after {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
2022-02-01 21:45:55 +08:00
|
|
|
|
2022-02-01 23:46:50 +08:00
|
|
|
.des::after {
|
|
|
|
transform: rotate(180deg) translateY(1.5px);
|
|
|
|
}
|
2022-02-01 21:45:55 +08:00
|
|
|
</style>
|