1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-07-16 21:14:05 +00:00

Table view

This commit is contained in:
2023-01-31 16:58:23 +01:00
parent dc5b12ee0d
commit 14e5b1f806
20 changed files with 674 additions and 172 deletions

View 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>

View 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>

View 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>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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 = [];

View File

@ -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;

View File

@ -47,6 +47,11 @@
.objectviewer {
position: relative;
}
.objectviewer .code :global(span.root) {
display: block;
}
.buttons {
position: absolute;
top: 0;

View File

@ -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>