1
0
mirror of https://github.com/garraflavatra/rolens.git synced 2025-07-15 12:54:06 +00:00

Implement object viewer based on codemirror

This commit is contained in:
2023-05-29 17:07:58 +02:00
parent 8319f0bd82
commit 5015a47495
10 changed files with 415 additions and 248 deletions

View File

@ -62,6 +62,8 @@
return false;
}
toggleChildren(itemKey, false);
if (activeKey !== itemKey) {
activeKey = itemKey;
if (level === 0) {
@ -79,7 +81,7 @@
dispatch('closeAll');
}
function toggleChildren(itemKey, shift) {
function toggleChildren(itemKey, shift = false) {
childrenOpen[itemKey] = !childrenOpen[itemKey];
if (shift) {
closeAll();
@ -89,6 +91,7 @@
function doubleClick(itemKey) {
// toggleChildren(itemKey, false);
dispatch('trigger', { level, itemKey });
childrenOpen[itemKey] = true;
}
function showContextMenu(evt, item) {

View File

@ -0,0 +1,57 @@
<script>
import { indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { indentOnInput } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { onMount } from 'svelte';
export let text = '';
const editorState = EditorState.create({
doc: '',
extensions: [
basicSetup,
keymap.of([ indentWithTab, indentOnInput ]),
javascript(),
EditorState.tabSize.of(4),
EditorView.updateListener.of(e => {
// if (!e.docChanged) {
// return;
// }
text = e.state.doc.toString();
}),
],
});
let editorParent;
let editor;
onMount(() => {
editor = new EditorView({
parent: editorParent,
state: editorState,
});
editor.dispatch({
changes: {
from: 0,
to: editorState.doc.length,
insert: text,
},
});
});
</script>
<div bind:this={editorParent} class="editor"></div>
<style>
.editor {
width: 100%;
}
.editor :global(.cm-editor) {
overflow: auto;
}
</style>

View File

@ -1,209 +0,0 @@
<script>
export let data;
export let depth = 0;
export let readonly = false;
export let level = 0;
export let last = true;
export let draggable = false;
export let kp = '';
const collapsedSymbol = '...';
let displayOnly = true;
let items;
let isArray;
let openBracket;
let closeBracket;
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;
}
function format(value) {
switch (getType(value)) {
case 'string':
return `${value}`;
case 'function':
return 'f () {...}';
case 'symbol':
return value.toString();
default:
return value;
}
}
function onClick(e) {
if (e.shiftKey) {
if (depth == 0) {
depth = 999;
}
else {
depth = 0;
}
}
collapsed = !collapsed;
}
function onDragstart(e, keypath, value) {
const item = {};
item[keypath] = value;
e.dataTransfer.setData('text/plain', JSON.stringify(item));
}
function onKeydown(event) {
const save = (event.key === 's') && (event.metaKey || event.ctrlKey);
if (!save) {
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 => onDragstart(e, kp, data)} draggable="true" class="bracket" on:click={onClick} tabindex="0">{openBracket}</span>
{:else}
<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 => onDragstart(e, kp ? kp + '.' + i : i, data[i])} draggable="true" class="key">{i}:</span>
{:else}
<span class="key">{i}:</span>
{/if}
{/if}
{#if getType(data[i]) === 'object'}
<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 => 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}
{/if}
</li>
{/each}
</ul>
<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={onClick} tabindex="0">{openBracket}{collapsedSymbol}{closeBracket}</span>{#if !last && collapsed}<span class="comma">,</span>{/if}
{:else}
{@html isArray ? '[]' : '{}'}
{/if}
{:else}
<textarea on:keydown={onKeydown} spellcheck="false" bind:this={textarea} class:invalid on:input={onInput}>{JSON.stringify(data, null, 2)}</textarea>
{/if}
<style>
ul {
list-style: none;
margin: 0;
padding: 0;
font-family: inherit;
font-size: inherit;
padding-left: var(--nodePaddingLeft, 1rem);
border-left: var(--nodeBorderLeft, 1px dashed #d0d0f0);
color: var(--nodeColor, #666);
}
li {
white-space: nowrap;
}
.root {
font-family: menlo, monospace;
font-size: 90%;
overflow: auto;
}
.hidden {
display: none;
}
.bracket {
cursor: pointer;
}
.bracket:hover {
background: var(--bracketHoverBackground, #d1d5db);
}
.comma {
color: var(--nodeColor, #374151);
opacity: 0.5;
}
.val {
color: var(--leafDefaultColor, #9ca3af);
white-space: nowrap;
}
.val[draggable] {
cursor: move;
}
.val.string {
color: var(--leafStringColor, #000);
}
.val.string:before {
content: "'";
opacity: 0.4;
}
.val.string:after {
content: "'";
opacity: 0.4;
}
.val.number {
color: var(--leafNumberColor, #d97706);
}
.val.boolean {
color: var(--leafBooleanColor, #3994dd);
}
.key.draggable {
cursor: move;
}
textarea {
font-family: menlo, monospace;
padding: 10px;
flex: 1 0;
white-space: pre;
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;
}
</style>

View File

@ -1,64 +1,77 @@
<script>
import Icon from './icon.svelte';
import Modal from './modal.svelte';
import ObjectTree from './objecttree.svelte';
import { onDestroy } from 'svelte';
import { deepClone } from '$lib/objects';
import { createEventDispatcher, onDestroy } from 'svelte';
import ObjectEditor from './objecteditor.svelte';
import { jsonLooseParse } from '$lib/strings';
export let data;
export let saveable = false;
const dispatch = createEventDispatcher();
let copySucceeded = false;
let timeout;
let _data;
let text = JSON.stringify(data, undefined, '\t');
let newData;
let invalid = false;
$: if (data) {
_data = deepClone(data);
for (const key of Object.keys(_data)) {
if (typeof _data[key] === 'undefined') {
delete _data[key];
}
$: {
try {
newData = jsonLooseParse(text);
}
catch {
invalid = true;
}
}
async function copy() {
await navigator.clipboard.writeText(JSON.stringify(_data));
await navigator.clipboard.writeText(text);
copySucceeded = true;
timeout = setTimeout(() => copySucceeded = false, 1500);
}
function close() {
data = undefined;
text = '';
}
function save() {
dispatch('save', text);
}
onDestroy(() => clearTimeout(timeout));
</script>
{#if data}
<Modal bind:show={data} title="Object viewer">
<Modal bind:show={data} contentPadding={false}>
<div class="objectviewer">
<div class="buttons">
<button class="btn" on:click={copy}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} />
</button>
</div>
<div class="code">
<ObjectTree data={_data} />
</div>
<ObjectEditor {text} />
</div>
<svelte:fragment slot="footer">
{#if saveable}
<button class="btn" on:click={save} disabled={invalid}>
<Icon name="save" /> Save
</button>
{/if}
<button class="btn secondary" on:click={close}>
<Icon name="x" /> Close
</button>
<button class="btn secondary" on:click={copy}>
<Icon name={copySucceeded ? 'check' : 'clipboard'} /> Copy
</button>
</svelte:fragment>
</Modal>
{/if}
<style>
.objectviewer {
display: flex;
position: relative;
}
.objectviewer .code :global(span.root) {
display: block;
}
.buttons {
position: absolute;
top: 0;
right: 0;
}
.buttons button {
margin-left: 1rem;
justify-content: stretch;
align-items: stretch;
height: 100%;
}
</style>