2023-01-17 18:00:57 +01:00
|
|
|
<script>
|
|
|
|
import { createEventDispatcher } from 'svelte';
|
2023-02-15 19:27:51 +01:00
|
|
|
import Icon from './icon.svelte';
|
2023-02-15 20:33:29 +01:00
|
|
|
import FormInput from './forminput.svelte';
|
|
|
|
import contextMenu from '$lib/stores/contextmenu';
|
|
|
|
import { resolveKeypath, setValue } from '$lib/keypaths';
|
2023-01-17 18:00:57 +01:00
|
|
|
|
|
|
|
export let items = [];
|
|
|
|
export let columns = [];
|
|
|
|
export let key = '';
|
2023-01-18 16:31:13 +01:00
|
|
|
export let path = [];
|
2023-01-17 18:00:57 +01:00
|
|
|
export let activeKey = '';
|
2023-01-18 16:31:13 +01:00
|
|
|
export let activePath = [];
|
2023-01-17 18:00:57 +01:00
|
|
|
export let level = 0;
|
2023-01-18 14:15:54 +01:00
|
|
|
export let striped = true;
|
2023-01-19 20:57:22 +01:00
|
|
|
export let hideObjectIndicators = false;
|
2023-01-28 13:25:14 +01:00
|
|
|
export let hideChildrenToggles = false;
|
2023-01-31 16:58:23 +01:00
|
|
|
export let canSelect = true;
|
|
|
|
export let canRemoveItems = false;
|
|
|
|
export let inputsValid = false;
|
2023-01-17 18:00:57 +01:00
|
|
|
|
|
|
|
const dispatch = createEventDispatcher();
|
2023-01-31 16:58:23 +01:00
|
|
|
const keypathProxies = {};
|
|
|
|
const validity = {};
|
2023-01-17 18:00:57 +01:00
|
|
|
let childrenOpen = {};
|
2023-01-19 20:57:22 +01:00
|
|
|
let _items = [];
|
2023-01-17 18:00:57 +01:00
|
|
|
|
2023-01-19 20:57:22 +01:00
|
|
|
$: refresh(hideObjectIndicators, items);
|
2023-01-31 21:12:31 +01:00
|
|
|
$: inputsValid = Object.values(validity).every(v => v !== false);
|
2023-01-19 20:57:22 +01:00
|
|
|
|
|
|
|
function refresh(hideObjectIndicators, items) {
|
|
|
|
_items = objectToArray(items).map(item => {
|
|
|
|
item.children = objectToArray(item.children);
|
|
|
|
return item;
|
|
|
|
});
|
2023-01-31 16:58:23 +01:00
|
|
|
|
|
|
|
for (let index = 0; index < _items.length; index++) {
|
|
|
|
keypathProxies[index] = new Proxy(_items, {
|
|
|
|
get: (_items, key) => resolveKeypath(_items[index], key),
|
|
|
|
set: (_items, key, value) => {
|
|
|
|
setValue(_items[index], key, value);
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2023-01-19 20:57:22 +01:00
|
|
|
}
|
2023-01-17 18:00:57 +01:00
|
|
|
|
|
|
|
function objectToArray(obj) {
|
|
|
|
if (Array.isArray(obj)) {
|
|
|
|
return obj;
|
|
|
|
}
|
2023-01-20 13:54:57 +01:00
|
|
|
else if ((typeof obj === 'object') && (obj !== null)) {
|
2023-01-17 18:00:57 +01:00
|
|
|
return Object.entries(obj).map(([ k, item ]) => ({ ...item, [key]: k }));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function select(itemKey) {
|
2023-01-31 16:58:23 +01:00
|
|
|
if (!canSelect) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-01-23 20:47:43 +01:00
|
|
|
if (activeKey !== itemKey) {
|
|
|
|
activeKey = itemKey;
|
|
|
|
if (level === 0) {
|
|
|
|
activePath = [ itemKey ];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
activePath = [ ...path, itemKey ];
|
|
|
|
}
|
|
|
|
dispatch('select', { level, itemKey });
|
2023-01-18 16:31:13 +01:00
|
|
|
}
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function closeAll() {
|
|
|
|
childrenOpen = {};
|
|
|
|
dispatch('closeAll');
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleChildren(itemKey, shift) {
|
|
|
|
childrenOpen[itemKey] = !childrenOpen[itemKey];
|
|
|
|
if (shift) {
|
|
|
|
closeAll();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function doubleClick(itemKey) {
|
2023-01-19 20:57:22 +01:00
|
|
|
// toggleChildren(itemKey, false);
|
2023-01-18 16:31:13 +01:00
|
|
|
dispatch('trigger', { level, itemKey });
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function showContextMenu(evt, item) {
|
|
|
|
select(item[key]);
|
|
|
|
contextMenu.show(evt, item.menu);
|
|
|
|
}
|
|
|
|
|
2023-02-15 17:00:53 +01:00
|
|
|
function removeItem(index, itemKey) {
|
|
|
|
if (Array.isArray(items)) {
|
|
|
|
items.splice(index, 1);
|
|
|
|
items = items;
|
|
|
|
}
|
|
|
|
dispatch('removeItem', itemKey);
|
2023-01-31 16:58:23 +01:00
|
|
|
}
|
|
|
|
|
2023-01-17 18:00:57 +01:00
|
|
|
function formatValue(value) {
|
|
|
|
if (Array.isArray(value)) {
|
2023-01-19 20:57:22 +01:00
|
|
|
return hideObjectIndicators ? '' : '[...]';
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
2023-01-19 20:57:22 +01:00
|
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
|
|
return String(value);
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
2023-01-19 20:57:22 +01:00
|
|
|
if ((value === undefined) || (value === null)) {
|
2023-01-17 18:00:57 +01:00
|
|
|
return '';
|
|
|
|
}
|
2023-01-20 13:54:57 +01:00
|
|
|
// if (new Date(value).toString() !== 'Invalid Date') {
|
|
|
|
// return new Date(value);
|
|
|
|
// }
|
|
|
|
if ((typeof value === 'object') && (value !== null)) {
|
2023-01-19 20:57:22 +01:00
|
|
|
return hideObjectIndicators ? '' : '{...}';
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
|
|
|
if (String(value).length <= 1000) {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
return String(value).slice(0, 1000) + '…';
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
2023-01-31 16:58:23 +01:00
|
|
|
{#each _items as item, index}
|
2023-01-17 18:00:57 +01:00
|
|
|
<tr
|
|
|
|
on:click={() => select(item[key])}
|
|
|
|
on:dblclick={() => doubleClick(item[key])}
|
|
|
|
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
|
2023-01-31 16:58:23 +01:00
|
|
|
class:selectable={canSelect}
|
|
|
|
class:selected={canSelect && !activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
|
2023-01-18 14:15:54 +01:00
|
|
|
class:striped
|
2023-01-17 18:00:57 +01:00
|
|
|
>
|
2023-01-28 13:25:14 +01:00
|
|
|
{#if !hideChildrenToggles}
|
|
|
|
<td class="has-toggle">
|
|
|
|
{#if item.children?.length}
|
|
|
|
<button
|
|
|
|
class="toggle"
|
2023-01-29 20:00:15 +01:00
|
|
|
on:click|stopPropagation={evt => toggleChildren(item[key], evt.shiftKey)}
|
2023-01-28 13:25:14 +01:00
|
|
|
style:transform="translateX({level * 10}px)"
|
|
|
|
>
|
|
|
|
<Icon name={childrenOpen[item[key]] ? 'chev-d' : 'chev-r'} />
|
|
|
|
</button>
|
|
|
|
{/if}
|
|
|
|
</td>
|
|
|
|
{/if}
|
2023-01-17 18:00:57 +01:00
|
|
|
|
2023-01-21 10:37:52 +01:00
|
|
|
<td class="has-icon">
|
|
|
|
<div style:margin-left="{level * 10}px">
|
|
|
|
<Icon name={item.icon} />
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
|
2023-01-17 18:00:57 +01:00
|
|
|
{#each columns as column, columnIndex}
|
2023-01-31 16:58:23 +01:00
|
|
|
<td class:right={column.right} title={keypathProxies[index][column.key]}>
|
|
|
|
{#if column.inputType}
|
|
|
|
<FormInput {column} bind:value={keypathProxies[index][column.key]} bind:valid={validity[columnIndex]} />
|
|
|
|
{:else}
|
|
|
|
<div class="value" style:margin-left="{level * 10}px">
|
|
|
|
{formatValue(keypathProxies[index][column.key])}
|
|
|
|
</div>
|
|
|
|
{/if}
|
2023-01-17 18:00:57 +01:00
|
|
|
</td>
|
|
|
|
{/each}
|
2023-01-31 16:58:23 +01:00
|
|
|
|
|
|
|
{#if canRemoveItems}
|
|
|
|
<td class="has-button">
|
2023-02-15 17:00:53 +01:00
|
|
|
<button class="btn-sm" type="button" on:click|stopPropagation={() => removeItem(index, item[key])} on:dblclick|stopPropagation>
|
2023-01-31 16:58:23 +01:00
|
|
|
<Icon name="x" />
|
|
|
|
</button>
|
|
|
|
</td>
|
|
|
|
{/if}
|
2023-01-17 18:00:57 +01:00
|
|
|
</tr>
|
|
|
|
|
|
|
|
{#if item.children && childrenOpen[item[key]]}
|
|
|
|
<svelte:self
|
|
|
|
{columns}
|
|
|
|
{key}
|
2023-01-18 14:15:54 +01:00
|
|
|
{striped}
|
2023-01-31 16:58:23 +01:00
|
|
|
{hideObjectIndicators}
|
|
|
|
{hideChildrenToggles}
|
|
|
|
{canSelect}
|
|
|
|
{canRemoveItems}
|
2023-01-18 16:31:13 +01:00
|
|
|
path={[ ...path, item[key] ]}
|
2023-01-17 18:00:57 +01:00
|
|
|
items={item.children}
|
|
|
|
level={level + 1}
|
2023-01-18 16:31:13 +01:00
|
|
|
bind:activePath
|
2023-01-17 18:00:57 +01:00
|
|
|
on:closeAll={closeAll}
|
2023-01-18 16:31:13 +01:00
|
|
|
on:select
|
|
|
|
on:trigger
|
2023-01-17 18:00:57 +01:00
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
<style>
|
2023-01-18 14:15:54 +01:00
|
|
|
tr.striped:nth-of-type(even) td {
|
|
|
|
background-color: #eee;
|
|
|
|
}
|
2023-01-31 16:58:23 +01:00
|
|
|
tr.selectable {
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
tr.selectable.selected td {
|
2023-01-18 14:15:54 +01:00
|
|
|
background-color: #00008b !important;
|
|
|
|
color: #fff;
|
|
|
|
}
|
|
|
|
|
2023-01-17 18:00:57 +01:00
|
|
|
td {
|
2023-01-19 19:04:48 +01:00
|
|
|
padding: 2px;
|
2023-01-17 18:00:57 +01:00
|
|
|
text-overflow: ellipsis;
|
|
|
|
}
|
|
|
|
td.has-toggle {
|
2023-01-21 10:37:52 +01:00
|
|
|
width: 20px;
|
|
|
|
}
|
|
|
|
td.has-icon {
|
|
|
|
padding: 0;
|
|
|
|
width: 17px;
|
|
|
|
}
|
|
|
|
td.has-icon :global(svg) {
|
|
|
|
width: 13px;
|
|
|
|
height: 13px;
|
2023-01-17 18:00:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
td .value {
|
2023-01-19 20:57:22 +01:00
|
|
|
height: 15px;
|
2023-01-17 18:00:57 +01:00
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
max-width: 25em;
|
|
|
|
}
|
|
|
|
|
|
|
|
button.toggle {
|
|
|
|
color: inherit;
|
|
|
|
padding: 0;
|
|
|
|
margin: 0;
|
|
|
|
vertical-align: top;
|
|
|
|
}
|
|
|
|
button.toggle :global(svg) {
|
2023-01-19 19:04:48 +01:00
|
|
|
width: 13px;
|
|
|
|
height: 13px;
|
2023-01-17 18:00:57 +01:00
|
|
|
vertical-align: top;
|
|
|
|
}
|
|
|
|
|
|
|
|
.right {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
</style>
|