2023-01-10 16:28:27 +00:00
|
|
|
<script>
|
|
|
|
import { createEventDispatcher } from 'svelte';
|
|
|
|
import Icon from './icon.svelte';
|
|
|
|
|
|
|
|
export let columns = [];
|
|
|
|
export let items = [];
|
|
|
|
export let key = 'id';
|
|
|
|
export let activeKey = '';
|
|
|
|
export let activeChildKey = '';
|
|
|
|
export let showHeaders = true;
|
|
|
|
export let level = 0;
|
|
|
|
export let contained = false;
|
|
|
|
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
const childrenOpen = {};
|
|
|
|
|
|
|
|
$: _items = objectToArray(items).map(item => {
|
|
|
|
item.children = objectToArray(item.children);
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
|
|
|
|
function objectToArray(obj) {
|
|
|
|
if (Array.isArray(obj)) {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
else if (typeof obj === 'object') {
|
|
|
|
return Object.entries(obj).map(([ k, item ]) => ({ ...item, [key]: k }));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function select(itemKey) {
|
|
|
|
activeKey = itemKey;
|
|
|
|
activeChildKey = '';
|
|
|
|
dispatch('select', itemKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectChild(itemKey, childKey) {
|
|
|
|
select(itemKey);
|
|
|
|
activeChildKey = childKey;
|
|
|
|
dispatch('selectChild', childKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleChildren(itemKey) {
|
|
|
|
childrenOpen[itemKey] = !childrenOpen[itemKey];
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class:grid={level === 0} class:subgrid={level > 0} class:contained>
|
|
|
|
<table>
|
|
|
|
{#if showHeaders && columns.some(col => col.title)}
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th class="has-toggle"></th>
|
|
|
|
{#each columns as column}
|
|
|
|
<th scope="col">{column.title || ''}</th>
|
|
|
|
{/each}
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
{#each _items as item (item[key])}
|
|
|
|
<tr on:click={() => select(item[key])} class:selected={activeKey === item[key] && !activeChildKey}>
|
|
|
|
<td class="has-toggle">
|
2023-01-10 19:10:39 +00:00
|
|
|
{#if item.children?.length}
|
2023-01-10 16:28:27 +00:00
|
|
|
<button class="toggle" on:click={() => toggleChildren(item[key])}>
|
|
|
|
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
|
|
|
</button>
|
|
|
|
{/if}
|
|
|
|
</td>
|
|
|
|
|
|
|
|
{#each columns as column}
|
|
|
|
{@const value = item[column.key]}
|
|
|
|
<td class:right={column.right}>
|
|
|
|
{#if typeof value !== 'object'}
|
|
|
|
{value || ''}
|
|
|
|
{/if}
|
|
|
|
</td>
|
|
|
|
{/each}
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
{#if item.children && childrenOpen[item[key]]}
|
|
|
|
<tr>
|
|
|
|
<td></td>
|
|
|
|
<td colspan={columns.length + 1} class="subgrid-parent">
|
|
|
|
<svelte:self
|
|
|
|
{columns}
|
|
|
|
{key}
|
|
|
|
bind:activeKey={activeChildKey}
|
|
|
|
showHeaders={false}
|
|
|
|
items={item.children}
|
|
|
|
level={level + 1}
|
|
|
|
on:select={e => selectChild(item[key], e.detail)}
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.grid {
|
|
|
|
background-color: #fff;
|
|
|
|
width: 100%;
|
|
|
|
overflow: scroll;
|
2023-01-10 19:10:39 +00:00
|
|
|
max-height: 400px; /* fixme */
|
2023-01-10 16:28:27 +00:00
|
|
|
}
|
|
|
|
.grid.contained {
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
}
|
|
|
|
.subgrid {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
table {
|
|
|
|
border-collapse: collapse;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
table thead {
|
|
|
|
border-bottom: 2px solid #ccc;
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:10:39 +00:00
|
|
|
th {
|
2023-01-10 16:28:27 +00:00
|
|
|
font-weight: 600;
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
tr {
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
td, th {
|
|
|
|
padding: 0.3rem;
|
|
|
|
}
|
|
|
|
td.has-toggle {
|
|
|
|
width: calc(20px + 0.3rem);
|
|
|
|
}
|
|
|
|
td.subgrid-parent {
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:10:39 +00:00
|
|
|
tbody tr.selected td {
|
2023-01-10 16:28:27 +00:00
|
|
|
background-color: #00008b;
|
|
|
|
color: #fff;
|
|
|
|
}
|
|
|
|
|
|
|
|
button.toggle {
|
|
|
|
color: inherit;
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
button.toggle :global(svg) {
|
|
|
|
width: 15px;
|
|
|
|
height: 15px;
|
|
|
|
vertical-align: top;
|
|
|
|
}
|
|
|
|
|
|
|
|
.right {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
</style>
|