gradio/frontend
1442k
1<script lang="ts">2 import { tick } from "svelte";3 import Self from "./JSONNode.svelte";4 5 let {6 value,7 depth = 0,8 is_root = false,9 is_last_item = true,10 key = null,11 open = false,12 theme_mode = "system" as "system" | "light" | "dark",13 show_indices = false,14 interactive = true15 }: {16 value: any;17 depth?: number;18 is_root?: boolean;19 is_last_item?: boolean;20 key?: string | number | null;21 open?: boolean;22 theme_mode?: "system" | "light" | "dark";23 show_indices?: boolean;24 interactive?: boolean;25 } = $props();26 27 let root_element = $state<HTMLElement>();28 let collapsed = $state(open ? false : depth >= 3);29 let child_nodes = $state<any[]>([]);30 31 function is_collapsible(val: any): boolean {32 return val !== null && (typeof val === "object" || Array.isArray(val));33 }34 35 async function toggle_collapse(): Promise<void> {36 collapsed = !collapsed;37 await tick();38 }39 40 function get_collapsed_preview(val: any): string {41 if (Array.isArray(val)) return `Array(${val.length})`;42 if (typeof val === "object" && val !== null)43 return `Object(${Object.keys(val).length})`;44 return String(val);45 }46 47 $effect(() => {48 if (is_collapsible(value)) {49 child_nodes = Object.entries(value);50 } else {51 child_nodes = [];52 }53 });54 55 function updateLineNumbers(): void {56 if (!root_element) return;57 const lines = root_element.querySelectorAll(".line");58 lines.forEach((line, index) => {59 const line_number = line.querySelector(".line-number");60 if (line_number) {61 line_number.setAttribute("data-pseudo-content", (index + 1).toString());62 line_number?.setAttribute(63 "aria-roledescription",64 `Line number ${index + 1}`65 );66 line_number?.setAttribute("title", `Line number ${index + 1}`);67 }68 });69 }70 71 $effect(() => {72 value;73 collapsed;74 75 if (is_root && root_element) {76 tick().then(() => {77 updateLineNumbers();78 });79 }80 });81</script>82 83<div84 class="json-node"85 class:root={is_root}86 class:dark-mode={theme_mode === "dark"}87 bind:this={root_element}88 style="--depth: {depth};"89>90 <div class="line" class:collapsed>91 <span class="line-number"></span>92 <span class="content">93 {#if is_collapsible(value)}94 <button95 data-pseudo-content={interactive ? (collapsed ? "▶" : "▼") : ""}96 aria-label={collapsed ? "Expand" : "Collapse"}97 class="toggle"98 disabled={!interactive}99 onclick={toggle_collapse}100 />101 {/if}102 {#if key !== null}103 <span class="key">"{key}"</span><span class="punctuation colon"104 >:105 </span>106 {/if}107 {#if is_collapsible(value)}108 <span109 class="punctuation bracket"110 class:square-bracket={Array.isArray(value)}111 >{Array.isArray(value) ? "[" : "{"}</span112 >113 {#if collapsed}114 <button onclick={toggle_collapse} class="preview">115 {get_collapsed_preview(value)}116 </button>117 <span118 class="punctuation bracket"119 class:square-bracket={Array.isArray(value)}120 >{Array.isArray(value) ? "]" : "}"}</span121 >122 {/if}123 {:else if typeof value === "string"}124 <span class="value string">"{value}"</span>125 {:else if typeof value === "number"}126 <span class="value number">{value}</span>127 {:else if typeof value === "boolean"}128 <span class="value bool">{value.toString()}</span>129 {:else if value === null}130 <span class="value null">null</span>131 {:else}132 <span>{value}</span>133 {/if}134 {#if !is_last_item && (!is_collapsible(value) || collapsed)}135 <span class="punctuation">,</span>136 {/if}137 </span>138 </div>139 140 {#if is_collapsible(value)}141 <div class="children" class:hidden={collapsed}>142 {#each child_nodes as [subKey, subVal], i}143 <Self144 value={subVal}145 depth={depth + 1}146 is_last_item={i === child_nodes.length - 1}147 key={Array.isArray(value) && !show_indices ? null : subKey}148 {open}149 {theme_mode}150 {show_indices}151 {interactive}152 />153 {/each}154 <div class="line">155 <span class="line-number"></span>156 <span class="content">157 <span158 class="punctuation bracket"159 class:square-bracket={Array.isArray(value)}160 >{Array.isArray(value) ? "]" : "}"}</span161 >162 {#if !is_last_item}<span class="punctuation">,</span>{/if}163 </span>164 </div>165 </div>166 {/if}167</div>168 169<style>170 .json-node {171 font-family: var(--font-mono);172 --text-color: #d18770;173 --key-color: var(--text-color);174 --string-color: #ce9178;175 --number-color: #719fad;176 177 --bracket-color: #5d8585;178 --square-bracket-color: #be6069;179 --punctuation-color: #8fbcbb;180 --line-number-color: #6a737d;181 --separator-color: var(--line-number-color);182 }183 .json-node.dark-mode {184 --bracket-color: #7eb4b3;185 --number-color: #638d9a;186 }187 .json-node.root {188 position: relative;189 padding-left: var(--size-14);190 }191 .json-node.root::before {192 content: "";193 position: absolute;194 top: 0;195 bottom: 0;196 left: var(--size-11);197 width: 1px;198 background-color: var(--separator-color);199 }200 .line {201 display: flex;202 align-items: flex-start;203 padding: 0;204 margin: 0;205 line-height: var(--line-md);206 }207 .line-number {208 position: absolute;209 left: 0;210 width: calc(var(--size-7));211 text-align: right;212 color: var(--line-number-color);213 user-select: none;214 text-overflow: ellipsis;215 text-overflow: ellipsis;216 direction: rtl;217 overflow: hidden;218 }219 .content {220 flex: 1;221 display: flex;222 align-items: center;223 padding-left: calc(var(--depth) * var(--size-2));224 flex-wrap: wrap;225 }226 .children {227 padding-left: var(--size-4);228 }229 .children.hidden {230 display: none;231 }232 .key {233 color: var(--key-color);234 }235 .string {236 color: var(--string-color);237 }238 .number {239 color: var(--number-color);240 }241 .bool {242 color: var(--text-color);243 }244 .null {245 color: var(--text-color);246 }247 .value {248 margin-left: var(--spacing-md);249 }250 .punctuation {251 color: var(--punctuation-color);252 }253 .bracket {254 margin-left: var(--spacing-sm);255 color: var(--bracket-color);256 }257 .square-bracket {258 margin-left: var(--spacing-sm);259 color: var(--square-bracket-color);260 }261 .toggle,262 .preview {263 background: none;264 border: none;265 color: inherit;266 cursor: pointer;267 padding: 0;268 margin: 0;269 }270 .toggle {271 user-select: none;272 margin-right: var(--spacing-md);273 }274 .preview {275 margin: 0 var(--spacing-sm) 0 var(--spacing-lg);276 }277 .preview:hover {278 text-decoration: underline;279 }280 281 :global([data-pseudo-content])::before {282 content: attr(data-pseudo-content);283 }284</style>285 