mirror of
https://github.com/garraflavatra/rolens.git
synced 2025-07-16 21:14:05 +00:00
Table view
This commit is contained in:
70
frontend/src/components/clock.svelte
Normal file
70
frontend/src/components/clock.svelte
Normal file
@ -0,0 +1,70 @@
|
||||
<script>
|
||||
export let date = new Date();
|
||||
|
||||
$: hours = date.getHours();
|
||||
$: minutes = date.getMinutes();
|
||||
$: seconds = date.getSeconds();
|
||||
</script>
|
||||
|
||||
<svg viewBox="-50 -50 100 100" class="clock">
|
||||
<circle class="clock-face" r="48" />
|
||||
|
||||
<!-- markers -->
|
||||
{#each [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] as minute}
|
||||
<line class="major" y1="35" y2="45" transform="rotate({30 * minute})" />
|
||||
|
||||
{#each [ 1, 2, 3, 4 ] as offset}
|
||||
<line class="minor" y1="42" y2="45" transform="rotate({6 * (minute + offset)})" />
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<!-- hour hand -->
|
||||
<line class="hour" y1="2" y2="-20" transform="rotate({30 * hours + 0.5 * minutes})" />
|
||||
|
||||
<!-- minute hand -->
|
||||
<line class="minute" y1="4" y2="-30" transform="rotate({6 * minutes + 0.1 * seconds})" />
|
||||
|
||||
<!-- second hand -->
|
||||
<g transform="rotate({6 * seconds})">
|
||||
<line class="second" y1="10" y2="-38" />
|
||||
<line class="second-counterweight" y1="10" y2="2" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.clock {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.clock-face {
|
||||
stroke: #333;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.minor {
|
||||
stroke: #999;
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
|
||||
.major {
|
||||
stroke: #333;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.hour {
|
||||
stroke: #333;
|
||||
}
|
||||
|
||||
.minute {
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
.second, .second-counterweight {
|
||||
stroke: rgb(180, 0, 0);
|
||||
}
|
||||
|
||||
.second-counterweight {
|
||||
stroke-width: 3;
|
||||
}
|
||||
</style>
|
201
frontend/src/components/datepicker.svelte
Normal file
201
frontend/src/components/datepicker.svelte
Normal file
@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import { addDays, getWeek, isDate, isSameDay, startOfWeek } from 'date-fns';
|
||||
import { onMount } from 'svelte';
|
||||
import { daysAbbr, months } from '../utils';
|
||||
import Clock from './clock.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import Modal from './modal.svelte';
|
||||
|
||||
export let value;
|
||||
export let show = false;
|
||||
|
||||
const rows = [ ...Array(6).keys() ];
|
||||
const cols = [ ...Array(7).keys() ];
|
||||
const now = new Date();
|
||||
let year = now.getFullYear();
|
||||
let month = now.getMonth();
|
||||
let day = now.getDate();
|
||||
let hour = now.getHours();
|
||||
let minute = now.getMinutes();
|
||||
let second = now.getSeconds();
|
||||
let calendar = [];
|
||||
|
||||
onMount(() => setDateToValues(now));
|
||||
$: setValueToDate(year, month, day, hour, minute, second);
|
||||
$: setDateToValues(value);
|
||||
|
||||
function buildCalendar(y = year, m = month) {
|
||||
const date = new Date(y, m);
|
||||
let curDate = startOfWeek(date, { weekStartsOn: 1 });
|
||||
return rows.map(() => {
|
||||
const week = [];
|
||||
cols.forEach(() => {
|
||||
week.push(curDate);
|
||||
curDate = addDays(curDate, 1);
|
||||
});
|
||||
return week;
|
||||
});
|
||||
}
|
||||
|
||||
function setValueToDate(date) {
|
||||
if (isDate(date)) {
|
||||
year = date.getFullYear();
|
||||
month = date.getMonth();
|
||||
day = date.getDate();
|
||||
hour = date.getHours();
|
||||
minute = date.getMinutes();
|
||||
value = new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
else {
|
||||
if (hour < 0) {
|
||||
day--; hour = 23;
|
||||
}
|
||||
else if (hour > 23) {
|
||||
day++; hour = 0;
|
||||
}
|
||||
else if (minute < 0) {
|
||||
hour--; minute = 59;
|
||||
}
|
||||
else if (minute > 59) {
|
||||
hour++; minute = 0;
|
||||
}
|
||||
else if (second < 0) {
|
||||
minute--; second = 59;
|
||||
}
|
||||
else if (second > 59) {
|
||||
minute++; second = 0;
|
||||
}
|
||||
value = new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
calendar = buildCalendar(year, month);
|
||||
}
|
||||
|
||||
function setDateToValues(date) {
|
||||
year = date.getFullYear();
|
||||
month = date.getMonth();
|
||||
day = date.getDate();
|
||||
hour = date.getHours();
|
||||
minute = date.getMinutes();
|
||||
second = date.getSeconds();
|
||||
calendar = buildCalendar(year, month);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal width="700px" bind:show>
|
||||
<div class="datepicker">
|
||||
<div class="date">
|
||||
<div class="field">
|
||||
<input type="number" bind:value={day} />
|
||||
<select bind:value={month}>
|
||||
{#each months as monthName, index}
|
||||
<option value={index}>{monthName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="number" bind:value={year} />
|
||||
</div>
|
||||
|
||||
<table class="calendar">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{#each daysAbbr as dayName}
|
||||
<th>{dayName}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{#each calendar as week}
|
||||
<tr>
|
||||
<td class="week">{getWeek(week[0])}</td>
|
||||
{#each week as day}
|
||||
<td class="day">
|
||||
<button
|
||||
on:click={() => setValueToDate(day)}
|
||||
type="button"
|
||||
class="btn-sm"
|
||||
class:active={isSameDay(value, day)}
|
||||
class:notinmonth={day.getMonth() !== month}
|
||||
>{day.getDate()}</button>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="time">
|
||||
<div class="field">
|
||||
<input type="number" bind:value={hour} placeholder="hours" />
|
||||
<span class="label">:</span>
|
||||
<input type="number" bind:value={minute} placeholder="mins" />
|
||||
<span class="label">:</span>
|
||||
<input type="number" bind:value={second} placeholder="secs" />
|
||||
</div>
|
||||
<Clock date={value} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="footer">
|
||||
<button class="btn secondary" type="button" on:click={() => value = new Date()}>
|
||||
<Icon name="o" /> Set to now
|
||||
</button>
|
||||
<button class="btn" type="button" on:click={() => show = false}>
|
||||
<Icon name="check" /> OK
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.datepicker {
|
||||
display: grid;
|
||||
grid-template: 1fr / 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.calendar thead th {
|
||||
opacity: 0.5;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.calendar .week {
|
||||
text-align: right;
|
||||
opacity: 0.5;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.calendar .day button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.calendar .day button.active {
|
||||
background-color: #00008b;
|
||||
color: #fff;
|
||||
}
|
||||
.calendar .day button.notinmonth {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.time input {
|
||||
text-align: center;
|
||||
}
|
||||
.time :global(.clock) {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: right;
|
||||
}
|
||||
</style>
|
136
frontend/src/components/forminput.svelte
Normal file
136
frontend/src/components/forminput.svelte
Normal file
@ -0,0 +1,136 @@
|
||||
<script>
|
||||
import { numericInputTypes } from '../utils';
|
||||
import { input } from '../actions';
|
||||
import Icon from './icon.svelte';
|
||||
import { ObjectId } from 'bson';
|
||||
import Datepicker from './datepicker.svelte';
|
||||
|
||||
export let column = {};
|
||||
export let value = undefined;
|
||||
export let valid = true;
|
||||
|
||||
const onValid = () => valid = true;
|
||||
const onInvalid = () => valid = false;
|
||||
let objectIdInput;
|
||||
let showDatepicker;
|
||||
$: type = column.inputType;
|
||||
$: mandatory = column.mandatory;
|
||||
|
||||
$: if ((value === undefined) && mandatory) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
function markInputValid(input) {
|
||||
input.setCustomValidity('');
|
||||
input.reportValidity();
|
||||
input.classList.remove('invalid');
|
||||
valid = true;
|
||||
}
|
||||
|
||||
function setObjectId(event) {
|
||||
if (valid) {
|
||||
value = new ObjectId(event.currentTarget?.value);
|
||||
}
|
||||
}
|
||||
|
||||
function generateObjectId() {
|
||||
value = new ObjectId();
|
||||
objectIdInput.value = value.toString();
|
||||
markInputValid(objectIdInput);
|
||||
objectIdInput.disabled = true;
|
||||
}
|
||||
|
||||
function editObjectId() {
|
||||
if (!objectIdInput) {
|
||||
return;
|
||||
}
|
||||
objectIdInput.disabled = false;
|
||||
objectIdInput.focus();
|
||||
}
|
||||
|
||||
function selectChange() {
|
||||
if ((value === undefined) && mandatory) {
|
||||
valid = false;
|
||||
}
|
||||
else {
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="forminput {type}">
|
||||
<div class="field">
|
||||
{#if type === 'string'}
|
||||
<input type="text" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
|
||||
{:else if type === 'objectid'}
|
||||
<input
|
||||
type="text"
|
||||
bind:this={objectIdInput}
|
||||
on:input={setObjectId}
|
||||
use:input={{ type, onValid, onInvalid, mandatory }}
|
||||
/>
|
||||
{:else if numericInputTypes.includes(type)}
|
||||
<input type="number" bind:value use:input={{ type, onValid, onInvalid, mandatory }} />
|
||||
{:else if type === 'bool'}
|
||||
<select bind:value on:change={selectChange}>
|
||||
<option value={undefined} disabled={mandatory}>Unset</option>
|
||||
<option value={true}>Yes / true</option>
|
||||
<option value={false}>No / false</option>
|
||||
</select>
|
||||
{:else if type === 'date'}
|
||||
<input type="text" readonly value={value?.toString() || '...'} on:focus={() => showDatepicker = true} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
{#if type === 'objectid'}
|
||||
{#if objectIdInput?.disabled}
|
||||
<button class="btn-sm" type="button" title="Edit object id" on:click={editObjectId}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn-sm" type="button" title="Generate random object id" on:click={generateObjectId}>
|
||||
<Icon name="reload" />
|
||||
</button>
|
||||
{:else if type === 'date'}
|
||||
<button class="btn-sm" type="button" title="Edit date" on:click={() => showDatepicker = true}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button class="btn-sm" type="button" title="Set date to now" on:click={() => value = new Date()}>
|
||||
<Icon name="o" />
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn-sm" type="button" title="Reset field to default value" on:click={() => value = undefined}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if type === 'date'}
|
||||
<Datepicker bind:value bind:show={showDatepicker} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.forminput {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.forminput.date input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 6px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.actions button:last-child {
|
||||
border-radius: 2px 6px 6px 2px;
|
||||
}
|
||||
|
||||
.forminput:hover .actions {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
@ -2,7 +2,8 @@
|
||||
import { contextMenu } from '../stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from './icon.svelte';
|
||||
import { resolveKeypath } from '../utils';
|
||||
import { resolveKeypath, setValue } from '../utils';
|
||||
import FormInput from './forminput.svelte';
|
||||
|
||||
export let items = [];
|
||||
export let columns = [];
|
||||
@ -14,18 +15,34 @@
|
||||
export let striped = true;
|
||||
export let hideObjectIndicators = false;
|
||||
export let hideChildrenToggles = false;
|
||||
export let canSelect = true;
|
||||
export let canRemoveItems = false;
|
||||
export let inputsValid = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const keypathProxies = {};
|
||||
const validity = {};
|
||||
let childrenOpen = {};
|
||||
let _items = [];
|
||||
|
||||
$: refresh(hideObjectIndicators, items);
|
||||
$: inputsValid = Object.values(validity).every(v => !!v);
|
||||
|
||||
function refresh(hideObjectIndicators, items) {
|
||||
_items = objectToArray(items).map(item => {
|
||||
item.children = objectToArray(item.children);
|
||||
return item;
|
||||
});
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function objectToArray(obj) {
|
||||
@ -41,6 +58,10 @@
|
||||
}
|
||||
|
||||
function select(itemKey) {
|
||||
if (!canSelect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeKey !== itemKey) {
|
||||
activeKey = itemKey;
|
||||
if (level === 0) {
|
||||
@ -75,6 +96,11 @@
|
||||
contextMenu.show(evt, item.menu);
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
items.splice(index, 1);
|
||||
items = items;
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return hideObjectIndicators ? '' : '[...]';
|
||||
@ -98,12 +124,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each _items as item}
|
||||
{#each _items as item, index}
|
||||
<tr
|
||||
on:click={() => select(item[key])}
|
||||
on:dblclick={() => doubleClick(item[key])}
|
||||
on:contextmenu|preventDefault={evt => showContextMenu(evt, item)}
|
||||
class:selected={!activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
|
||||
class:selectable={canSelect}
|
||||
class:selected={canSelect && !activePath[level + 1] && activePath.every(k => path.includes(k) || k === item[key]) && (activePath[level] === item[key])}
|
||||
class:striped
|
||||
>
|
||||
{#if !hideChildrenToggles}
|
||||
@ -127,21 +154,35 @@
|
||||
</td>
|
||||
|
||||
{#each columns as column, columnIndex}
|
||||
{@const value = column.key?.includes('.') ? resolveKeypath(item, column.key) : item[column.key]}
|
||||
<td class:right={column.right} title={value}>
|
||||
<div class="value" style:margin-left="{level * 10}px">
|
||||
{formatValue(value)}
|
||||
</div>
|
||||
<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}
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
{#if canRemoveItems}
|
||||
<td class="has-button">
|
||||
<button class="btn-sm" type="button" on:click={() => removeItem(index)}>
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
|
||||
{#if item.children && childrenOpen[item[key]]}
|
||||
<svelte:self
|
||||
{columns}
|
||||
{hideObjectIndicators}
|
||||
{key}
|
||||
{striped}
|
||||
{hideObjectIndicators}
|
||||
{hideChildrenToggles}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
path={[ ...path, item[key] ]}
|
||||
items={item.children}
|
||||
level={level + 1}
|
||||
@ -157,7 +198,10 @@
|
||||
tr.striped:nth-of-type(even) td {
|
||||
background-color: #eee;
|
||||
}
|
||||
tr.selected td {
|
||||
tr.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
tr.selectable.selected td {
|
||||
background-color: #00008b !important;
|
||||
color: #fff;
|
||||
}
|
||||
@ -165,7 +209,6 @@
|
||||
td {
|
||||
padding: 2px;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
td.has-toggle {
|
||||
width: 20px;
|
||||
|
@ -1,16 +1,27 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import GridItems from './grid-items.svelte';
|
||||
// import Icon from './icon.svelte';
|
||||
import Icon from './icon.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let items = [];
|
||||
// export let actions = [];
|
||||
export let key = 'id';
|
||||
export let activePath = [];
|
||||
export let striped = true;
|
||||
export let showHeaders = false;
|
||||
export let hideObjectIndicators = false;
|
||||
export let hideChildrenToggles = false;
|
||||
export let canAddRows = false;
|
||||
export let canSelect = true;
|
||||
export let canRemoveItems = false;
|
||||
export let inputsValid = false;
|
||||
// export let actions = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function addRow() {
|
||||
dispatch('addRow');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
@ -38,6 +49,10 @@
|
||||
{#each columns as column}
|
||||
<th scope="col">{column.title || ''}</th>
|
||||
{/each}
|
||||
|
||||
{#if canRemoveItems}
|
||||
<th class="has-button"></th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
@ -48,13 +63,24 @@
|
||||
{columns}
|
||||
{key}
|
||||
{striped}
|
||||
{canSelect}
|
||||
{canRemoveItems}
|
||||
{hideObjectIndicators}
|
||||
{hideChildrenToggles}
|
||||
bind:activePath
|
||||
bind:inputsValid
|
||||
on:select
|
||||
on:trigger
|
||||
/>
|
||||
</tbody>
|
||||
|
||||
{#if canAddRows}
|
||||
<tfoot>
|
||||
<button class="btn-sm" type="button" on:click={addRow}>
|
||||
<Icon name="+" />
|
||||
</button>
|
||||
</tfoot>
|
||||
{/if}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -86,8 +112,10 @@
|
||||
th {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* tfoot button {
|
||||
margin-top: 0.5rem;
|
||||
} */
|
||||
</style>
|
||||
|
6
frontend/src/components/icon.svelte
vendored
6
frontend/src/components/icon.svelte
vendored
@ -73,5 +73,11 @@
|
||||
<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>
|
||||
{:else if name === 'target'}
|
||||
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>
|
||||
{:else if name === 'trash'}
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/>
|
||||
{:else if name === 'anchor'}
|
||||
<circle cx="12" cy="5" r="3"/><path d="M12 22V8M5 12H2a10 10 0 0 0 20 0h-3"/>
|
||||
{:else if name === 'o'}
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
{/if}
|
||||
</svg>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { isBsonBuiltin, isDate } from '../utils';
|
||||
import { isDate } from 'date-fns';
|
||||
import { isBsonBuiltin } from '../utils';
|
||||
import Grid from './grid.svelte';
|
||||
|
||||
export let data = [];
|
||||
|
@ -8,41 +8,42 @@
|
||||
export let kp = '';
|
||||
|
||||
const collapsedSymbol = '...';
|
||||
const getType = i => {
|
||||
if (i === null) {
|
||||
return 'null';
|
||||
}
|
||||
return typeof i;
|
||||
};
|
||||
|
||||
let displayOnly = true;
|
||||
let items;
|
||||
let isArray;
|
||||
let openBracket;
|
||||
let closeBracket;
|
||||
$: {
|
||||
items = getType(data) === 'object' ? Object.keys(data) : [];
|
||||
isArray = Array.isArray(data);
|
||||
openBracket = isArray ? '[' : '{';
|
||||
closeBracket = isArray ? ']' : '}';
|
||||
let collapsed;
|
||||
let invalid = false;
|
||||
let textarea;
|
||||
$: items = getType(data) === 'object' ? Object.keys(data) : [];
|
||||
$: isArray = Array.isArray(data);
|
||||
$: openBracket = isArray ? '[' : '{';
|
||||
$: closeBracket = isArray ? ']' : '}';
|
||||
$: collapsed = depth < level;
|
||||
$: textarea && resizeTextarea();
|
||||
|
||||
function getType(value) {
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
let collapsed;
|
||||
$: collapsed = depth < level;
|
||||
|
||||
const format = i => {
|
||||
switch (getType(i)) {
|
||||
function format(value) {
|
||||
switch (getType(value)) {
|
||||
case 'string':
|
||||
return `${i}`;
|
||||
return `${value}`;
|
||||
case 'function':
|
||||
return 'f () {...}';
|
||||
case 'symbol':
|
||||
return i.toString();
|
||||
return value.toString();
|
||||
default:
|
||||
return i;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
const clicked = e => {
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
if (e.shiftKey) {
|
||||
if (depth == 0) {
|
||||
depth = 999;
|
||||
@ -52,26 +53,9 @@
|
||||
}
|
||||
}
|
||||
collapsed = !collapsed;
|
||||
};
|
||||
|
||||
let invalid = false;
|
||||
let dbg;
|
||||
|
||||
function json2data() {
|
||||
try {
|
||||
data = JSON.parse(dbg.value);
|
||||
invalid = false;
|
||||
}
|
||||
catch {
|
||||
invalid = true;
|
||||
if (dbg.value.trim == '') {
|
||||
data = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dragstart(e, keypath, value) {
|
||||
console.log('kp:', keypath);
|
||||
function onDragstart(e, keypath, value) {
|
||||
const item = {};
|
||||
item[keypath] = value;
|
||||
e.dataTransfer.setData('text/plain', JSON.stringify(item));
|
||||
@ -83,22 +67,41 @@
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
resizeTextarea();
|
||||
try {
|
||||
data = JSON.parse(textarea.value);
|
||||
invalid = false;
|
||||
}
|
||||
catch {
|
||||
invalid = true;
|
||||
if (textarea.value.trim == '') {
|
||||
data = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resizeTextarea() {
|
||||
textarea.style.overflowY = 'hidden';
|
||||
textarea.style.height = textarea.scrollHeight + 'px';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if displayOnly}
|
||||
{#if items.length}
|
||||
<span class:root={level == 0} class:hidden={collapsed}>
|
||||
{#if draggable && isArray}
|
||||
<span on:dragstart={e => dragstart(e, kp, data)} draggable="true" class="bracket" on:click={clicked} tabindex="0">{openBracket}</span>
|
||||
<span on:dragstart={e => onDragstart(e, kp, data)} draggable="true" class="bracket" on:click={onClick} tabindex="0">{openBracket}</span>
|
||||
{:else}
|
||||
<span class="bracket" on:click={clicked} tabindex="0">{openBracket}</span>
|
||||
<span class="bracket" on:click={onClick} tabindex="0">{openBracket}</span>
|
||||
{/if}
|
||||
<ul on:dblclick={() => (readonly ? displayOnly = true : displayOnly = false)} >
|
||||
{#each items as i, idx}
|
||||
<li>
|
||||
{#if !isArray}
|
||||
{#if draggable}
|
||||
<span on:dragstart={e => dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}:</span>
|
||||
<span on:dragstart={e => onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}:</span>
|
||||
{:else}
|
||||
<span class="key">{i}:</span>
|
||||
{/if}
|
||||
@ -107,7 +110,7 @@
|
||||
<svelte:self {readonly} {draggable} kp={kp ? kp + '.' + i : i} data={data[i]} {depth} level={level + 1} last={idx === items.length - 1} />
|
||||
{:else}
|
||||
{#if draggable}
|
||||
<span on:dragstart={e => dragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span draggable class="comma">,</span>{/if}
|
||||
<span on:dragstart={e => onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span draggable class="comma">,</span>{/if}
|
||||
{:else}
|
||||
<span class="val {getType(data[i])}">{format(data[i])}</span>{#if idx < items.length - 1}<span class="comma">,</span>{/if}
|
||||
{/if}
|
||||
@ -115,16 +118,14 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<span class="bracket" on:click={clicked} tabindex="0">{closeBracket}</span>{#if !last}<span
|
||||
class="comma">,</span>
|
||||
{/if}
|
||||
<span class="bracket" on:click={onClick} tabindex="0">{closeBracket}</span>{#if !last}<span class="comma">,</span>{/if}
|
||||
</span>
|
||||
<span style="padding: {level == 0 ? 10 : 0}px;" class="bracket" class:hidden={!collapsed} on:click={clicked} tabindex="0">{openBracket}{collapsedSymbol}{closeBracket}</span>{#if !last && collapsed}<span class="comma">,</span>{/if}
|
||||
<span style="padding: {level == 0 ? 10 : 0}px;" class="bracket" class:hidden={!collapsed} on:click={onClick} tabindex="0">{openBracket}{collapsedSymbol}{closeBracket}</span>{#if !last && collapsed}<span class="comma">,</span>{/if}
|
||||
{:else}
|
||||
{@html isArray ? '[]' : '{}'}
|
||||
{/if}
|
||||
{:else}
|
||||
<textarea on:keydown="{onKeydown}" class="debug" spellcheck="false" bind:this={dbg} class:invalid on:input={json2data}>{JSON.stringify(data, null, 2)}</textarea>
|
||||
<textarea on:keydown={onKeydown} spellcheck="false" bind:this={textarea} class:invalid on:input={onInput}>{JSON.stringify(data, null, 2)}</textarea>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@ -187,7 +188,7 @@
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
textarea.debug {
|
||||
textarea {
|
||||
font-family: menlo, monospace;
|
||||
padding: 10px;
|
||||
flex: 1 0;
|
||||
@ -195,12 +196,12 @@
|
||||
font-size: 90%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
textarea.invalid {
|
||||
background: #ffe3e3;
|
||||
color: #b30202 !important;
|
||||
|
@ -47,6 +47,11 @@
|
||||
.objectviewer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.objectviewer .code :global(span.root) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -20,7 +20,7 @@
|
||||
<li class:active={tab.key === selectedKey}>
|
||||
<button class="tab" on:click={() => select(tab.key)}>{tab.title}</button>
|
||||
{#if tab.closable}
|
||||
<button class="close" on:click={() => dispatch('closeTab', tab.key)}>
|
||||
<button class="btn-sm" on:click={() => dispatch('closeTab', tab.key)}>
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
{/if}
|
||||
@ -86,17 +86,9 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.close {
|
||||
.btn-sm {
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 7px;
|
||||
padding: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
button.close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
button.close:active {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user