budibase/packages/builder/src/components/common/JSONViewer.svelte

282 lines
6.9 KiB
Svelte

<script context="module" lang="ts">
interface JSONViewerClickContext {
label: string | undefined
value: any
path: (string | number)[]
}
export interface JSONViewerClickEvent {
detail: JSONViewerClickContext
}
</script>
<script lang="ts">
import { Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
export let label: string | undefined = undefined
export let value: any = undefined
export let root: boolean = true
export let path: (string | number)[] = []
export let showCopyIcon: boolean = false
const dispatch = createEventDispatcher()
const Colors = {
Array: "var(--spectrum-global-color-gray-600)",
Object: "var(--spectrum-global-color-gray-600)",
Other: "var(--spectrum-global-color-blue-700)",
Undefined: "var(--spectrum-global-color-gray-600)",
Null: "var(--spectrum-global-color-yellow-700)",
String: "var(--spectrum-global-color-orange-700)",
Number: "var(--spectrum-global-color-purple-700)",
True: "var(--spectrum-global-color-celery-700)",
False: "var(--spectrum-global-color-red-700)",
Date: "var(--spectrum-global-color-green-700)",
}
let expanded = false
let valueExpanded = false
let clickContext: JSONViewerClickContext
$: isArray = Array.isArray(value)
$: isObject = value?.toString?.() === "[object Object]"
$: primitive = !(isArray || isObject)
$: keys = getKeys(isArray, isObject, value)
$: expandable = keys.length > 0
$: displayValue = getDisplayValue(isArray, isObject, keys, value)
$: style = getStyle(isArray, isObject, value)
$: clickContext = { value, label, path }
const getKeys = (isArray: boolean, isObject: boolean, value: any) => {
if (isArray) {
return [...value.keys()]
}
if (isObject) {
return Object.keys(value).sort()
}
return []
}
const pluralise = (text: string, number: number) => {
return number === 1 ? text : text + "s"
}
const getDisplayValue = (
isArray: boolean,
isObject: boolean,
keys: any[],
value: any
) => {
if (isArray) {
return `[] ${keys.length} ${pluralise("item", keys.length)}`
}
if (isObject) {
return `{} ${keys.length} ${pluralise("key", keys.length)}`
}
if (typeof value === "object" && typeof value?.toString === "function") {
return value.toString()
} else {
return JSON.stringify(value, null, 2)
}
}
const getStyle = (isArray: boolean, isObject: boolean, value: any) => {
return `color:${getColor(isArray, isObject, value)};`
}
const getColor = (isArray: boolean, isObject: boolean, value: any) => {
if (isArray) {
return Colors.Array
}
if (isObject) {
return Colors.Object
}
if (value instanceof Date) {
return Colors.Date
}
switch (value) {
case undefined:
return Colors.Undefined
case null:
return Colors.Null
case true:
return Colors.True
case false:
return Colors.False
}
switch (typeof value) {
case "string":
return Colors.String
case "number":
return Colors.Number
}
return Colors.Other
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="binding-node">
{#if label != null}
<div class="binding-text">
<div class="binding-arrow" class:expanded>
{#if expandable}
<Icon
name="Play"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
on:click={() => (expanded = !expanded)}
/>
{/if}
</div>
<div
class="binding-label"
class:primitive
class:expandable
on:click={() => (expanded = !expanded)}
on:click={() => dispatch("click-label", clickContext)}
>
{label}
</div>
<div
class="binding-value"
class:primitive
class:expanded={valueExpanded}
{style}
on:click={() => (valueExpanded = !valueExpanded)}
on:click={() => dispatch("click-value", clickContext)}
>
{displayValue}
</div>
{#if showCopyIcon}
<div class="copy-value-icon">
<Icon
name="Copy"
size="XS"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
on:click={() => dispatch("click-copy", clickContext)}
/>
</div>
{/if}
</div>
{/if}
{#if expandable && (expanded || label == null)}
<div class="binding-children" class:root>
{#each keys as key}
<svelte:self
label={key}
value={value[key]}
root={false}
path={[...path, key]}
{showCopyIcon}
on:click-label
on:click-value
on:click-copy
/>
{/each}
</div>
{/if}
</div>
<style>
.binding-node {
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
overflow: hidden;
}
/* Expand arrow */
.binding-arrow {
margin: -3px 6px -2px 4px;
flex: 0 0 9px;
transition: transform 130ms ease-out;
}
.binding-arrow :global(svg) {
width: 9px;
}
.binding-arrow.expanded {
transform: rotate(90deg);
}
/* Main text wrapper */
.binding-text {
display: flex;
flex-direction: row;
font-family: monospace;
font-size: 12px;
align-items: flex-start;
width: 100%;
}
/* Size label and value according to type */
.binding-label {
flex: 0 1 auto;
margin-right: 8px;
transition: color 130ms ease-out;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.binding-label.expandable:hover {
cursor: pointer;
color: var(--spectrum-global-color-gray-900);
}
.binding-value {
flex: 0 0 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: filter 130ms ease-out;
}
.binding-value.primitive:hover {
filter: brightness(1.25);
cursor: pointer;
}
.binding-value.expanded {
word-break: break-all;
white-space: wrap;
}
.binding-label.primitive {
flex: 0 0 auto;
max-width: 75%;
}
.binding-value.primitive {
flex: 0 1 auto;
}
/* Trim spans in the highlighted HTML */
.binding-value :global(span) {
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
/* Copy icon for value */
.copy-value-icon {
display: none;
margin-left: 8px;
}
.binding-text:hover .copy-value-icon {
display: block;
}
/* Children wrapper */
.binding-children {
display: flex;
flex-direction: column;
gap: 8px;
border-left: 1px solid var(--spectrum-global-color-gray-400);
margin-left: 20px;
padding-left: 3px;
}
.binding-children.root {
border-left: none;
margin-left: 0;
padding-left: 0;
}
</style>