Data

JsonTable

JSON as a table: open nested objects and arrays in place or as the table, search at every depth, export as JSON or CSV, thousands of lines kept fast.

#data#table#code#search#interactive

Demo

Preview
.users
3,000 rows
#idnameemailactiveplanscoreaddresstagsordersmanager
01000Ada Lovelaceada.0@example.comfalsefree0null
11001Grace Hoppergrace.1@example.comtrueenterprise3.71001
21002Alan Turingalan.2@example.comtruebusiness7.41002
31003Katherine Johnsonkatherine.3@example.comtrueteam11.11003
41004Linus Torvaldslinus.4@example.comtruefree14.81004
51005Margaret Hamiltonmargaret.5@example.comtrueenterprise18.51005
61006Dennis Ritchiedennis.6@example.comtruebusiness22.21006
71007Barbara Liskovbarbara.7@example.comfalseteam25.91007
81008Ken Thompsonken.8@example.comtruefree29.61008
91009Radia Perlmanradia.9@example.comtrueenterprise33.3null
101010Edsger Dijkstraedsger.10@example.comtruebusiness371010
111011Ada Lovelaceada.11@example.comtrueteam40.71011
121012Grace Hoppergrace.12@example.comtruefree44.41012
131013Alan Turingalan.13@example.comtrueenterprise48.11013
141014Katherine Johnsonkatherine.14@example.comfalsebusiness51.81014
151015Linus Torvaldslinus.15@example.comtrueteam55.51015
161016Margaret Hamiltonmargaret.16@example.comtruefree59.21016
171017Dennis Ritchiedennis.17@example.comtrueenterprise62.91017
181018Barbara Liskovbarbara.18@example.comtruebusiness66.6null
191019Ken Thompsonken.19@example.comtrueteam70.31019

3,000 users and 50,000 events. Open a cell's object or array, open it as the table, go back by the path, search, export.

.
7 keys
Keyvalue
namebilling-worker
replicas3
max-retries5
debugfalse
regionnull
queues
database

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/json_table.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/json_table.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/json_table.qwe
let json_table_demo_names = ["Ada Lovelace", "Grace Hopper", "Alan Turing", "Katherine Johnson", "Linus Torvalds",    "Margaret Hamilton", "Dennis Ritchie", "Barbara Liskov", "Ken Thompson", "Radia Perlman", "Edsger Dijkstra"]let json_table_demo_cities = ["Berlin", "Lisbon", "London", "Madrid", "New York", "Oslo", "Paris", "São Paulo", "Tokyo", "Toronto"]let json_table_demo_plans = ["free", "team", "business", "enterprise"]let json_table_demo_tags = ["beta", "vip", "churn-risk", "partner", "trial", "invoice"]let json_table_demo_kinds = ["login", "view", "order", "refund", "logout"]// An API's answer: { meta, users (3,000), events (50,000) }, made the same way on// the server and in the browser.fn json_table_demo_data(n: int, events: int) -> map = {    meta: { source: "accounts-api", version: 3, generated: "2026-10-28T09:00:00Z", paging: { page: 1, per_page: n, total: n } },    users: for i in range(0, n) => {        let name = json_table_demo_names[i % 11]        {            id: 1000 + i,            name,            email: `${lower(split(name, " ")[0])}.${i}@example.com`,            active: i % 7 != 0,            plan: json_table_demo_plans[(i * 3) % 4],            score: ((i * 37) % 1000) / 10,            address: { city: json_table_demo_cities[i % 10], zip: `${10000 + (i * 13) % 89999}`,                       geo: { lat: ((i * 7) % 180) - 90, lng: ((i * 11) % 360) - 180 } },            tags: take(drop(json_table_demo_tags, i % 3), i % 4),            orders: for j in range(0, i % 4) => {                number: `ORD-${20000 + i * 4 + j}`,                total: ((i * 7919 + j * 104729) % 50000) / 100,                items: for k in range(0, j + 1) => { sku: `SKU-${(i + k) % 97}`, qty: k + 1 },            },            manager: i % 9 == 0 ? null : 1000 + (i % 50),        }    },    events: for i in range(0, events) => { at: 1767225600 + i * 61, kind: json_table_demo_kinds[(i * 7) % 5], user: 1000 + (i * 31) % n },}component JsonTableDemoLive() {    let data = json_table_demo_data(3000, 50000)    <JsonTable id="jt-api" label="Accounts API" value={data} path={["users"]} filename="accounts" height={400}/>}view JsonTableDemo() =    <div.demo-stack>        <JsonTableDemoLive/>        <p.demo-note>3,000 users and 50,000 events. Open a cell's object or array, open it as the table, go back by the path, search, export.</p>        <JsonTable id="jt-config" label="Settings" height={240} filename="settings" value={{            name: "billing-worker",            replicas: 3,            "max-retries": 5,            debug: false,            region: null,            queues: ["invoices", "refunds", "payouts"],            database: { url: "postgres://db.internal/billing", pool: 20, timeout_ms: 1500.5 },        }}/>    </div>

Source

ui/json_table.qwe
// JsonTable: JSON as a table to dig into. A list of records is a table with a// column for each key; a map is a table of its keys and values; a list of values is// one column. A cell holding an object or array opens below its row as indented// lines, which open further, and Open makes any of them the table, with a path bar// back up. Every line, opened ones too, is one height and only those in view are// drawn, so a list of 100,000 items scrolls as one of ten does. The search box keeps// the rows where any key or value, at any depth, has what is typed, and marks it.// Export saves the table (the rows found) as JSON or CSV, and each opened object or// array as JSON.////   <JsonTable id="api" label="Orders API" value={json.parse(body)} path={["data", "orders"]}/>//// id: a prefix for its ids, unique on the page. value: any map, list or value.// path: where it starts, as keys and indexes from the top (["data", "orders"]).// root: what the top is called in the path bar ("root"). label: what screen readers// call it. search: the search box (true). filename: how exported files' names start// ("data": data-orders.json). row_height: each line's height in pixels (34).// height: the table's height in pixels (420). overscan: lines drawn beyond each// edge (8). max_columns: the most columns a list of records shows (24), keys first// seen first; the others are still searched and exported. empty: what it says with// no rows ("Nothing to show").// It is an ARIA treegrid: ↓ ↑, Page Down, Page Up, Home and End move; → opens the// line (on a row, its first closed cell), ← closes it or goes to the line it is in;// Enter opens an object or array as the table, and Backspace goes back up the path.import ui/theme.qwe use ui_clslet json_table_css = css`.ui-jt { display: grid; gap: 10px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-jt-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 14px; min-width: 0; }.ui-jt-path ol { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; margin: 0; padding: 0; list-style: none; }.ui-jt-path li { display: flex; align-items: center; gap: 2px; min-width: 0; }.ui-jt-path li + li::before { content: "›"; padding: 0 3px; color: var(--ui-muted); }.ui-jt-path button { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 7px; border: 0;  border-radius: 4px; background: none; color: var(--ui-accent); font: 500 13px/1.3 var(--ui-mono); cursor: pointer; }.ui-jt-path button:hover { background: var(--ui-subtle); }.ui-jt-path button[aria-current] { color: var(--ui-fg); cursor: default; }.ui-jt-path button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-jt-jq { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 8px; border-radius: 4px;  background: var(--ui-subtle); color: var(--ui-muted); font: 12px/1.4 var(--ui-mono); }.ui-jt-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }.ui-jt-search { flex: 0 1 280px; min-width: 160px; height: 36px; padding: 0 12px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: inherit; }.ui-jt-search::placeholder { color: var(--ui-muted); opacity: 1; }.ui-jt-search:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-jt-count { flex: 1; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-jt-export { display: flex; gap: 6px; }.ui-jt-btn { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-jt-btn:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-jt-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-jt-btn svg { width: 13px; height: 13px; }.ui-jt-scroll { overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  overscroll-behavior: contain; container-type: inline-size; }.ui-jt table { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; outline: none; }.ui-jt table:focus-visible { box-shadow: inset var(--ui-focus); }.ui-jt th, .ui-jt td { box-sizing: border-box; height: var(--ui-jt-rh); padding: 0 12px; text-align: left; overflow: hidden;  text-overflow: ellipsis; white-space: nowrap; box-shadow: inset 0 -1px 0 var(--ui-border); }.ui-jt thead th { position: sticky; top: 0; z-index: 1; background: var(--ui-subtle); color: var(--ui-muted);  font-size: 12px; font-weight: 600; letter-spacing: .03em; }.ui-jt-headcol { width: 64px; }.ui-jt-entries .ui-jt-headcol { width: 30%; }.ui-jt tbody th { color: var(--ui-muted); font: 12px/1 var(--ui-mono); font-weight: 400; }.ui-jt-entries tbody th { color: var(--ui-accent); font-size: 13px; }.ui-jt tbody tr:not(.ui-jt-pad):hover > * { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-jt table:focus-visible tr.ui-jt-active > * { background: var(--ui-subtle); }.ui-jt table:focus-visible tr.ui-jt-active > :first-child { box-shadow: inset 3px 0 0 var(--ui-accent), inset 0 -1px 0 var(--ui-border); }.ui-jt .ui-jt-pad td { height: auto; padding: 0; box-shadow: none; }.ui-jt-empty td { color: var(--ui-muted); text-align: center; }.ui-jt-string { color: var(--ui-fg); }.ui-jt-number { color: var(--ui-info); font-variant-numeric: tabular-nums; }.ui-jt-boolean { color: var(--ui-warning); }.ui-jt-null, .ui-jt-none { color: var(--ui-muted); font-style: italic; }.ui-jt mark { padding: 0 1px; border-radius: 2px; background: color-mix(in srgb, var(--ui-warning) 35%, transparent); color: inherit; }.ui-jt-cell { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 2px 7px 2px 4px; border: 1px solid var(--ui-border);  border-radius: 999px; background: var(--ui-subtle); color: var(--ui-fg); font: 12px/1.3 var(--ui-mono); cursor: pointer; }.ui-jt-cell:hover { border-color: var(--ui-strong); }.ui-jt-cell[aria-expanded="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-jt-chev { flex: none; width: 10px; height: 10px; color: var(--ui-muted); transition: transform .15s; }[aria-expanded="true"] > .ui-jt-chev, [aria-expanded="true"] .ui-jt-tog .ui-jt-chev { transform: rotate(90deg); }.ui-jt-node td { padding: 0; background: color-mix(in srgb, var(--ui-subtle) 45%, transparent); }.ui-jt-node[aria-expanded] { cursor: pointer; }.ui-jt-line { position: sticky; left: 0; box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100cqw; max-width: 100%;  height: 100%; padding-right: 8px; font: 13px/1 var(--ui-mono); }.ui-jt-tog { flex: none; display: inline-flex; justify-content: center; width: 14px; }.ui-jt-key { flex: none; color: var(--ui-accent); }.ui-jt-idx { flex: none; color: var(--ui-muted); }.ui-jt-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-jt-size { flex: none; padding: 0 6px; border-radius: 999px; background: var(--ui-subtle); color: var(--ui-muted); font: 11px/1.6 var(--ui-font); }.ui-jt-acts { display: flex; gap: 4px; margin-left: auto; opacity: .55; }.ui-jt-node:hover .ui-jt-acts, .ui-jt-node.ui-jt-active .ui-jt-acts { opacity: 1; }.ui-jt-acts .ui-jt-btn { height: 24px; padding: 0 8px; font-size: 11px; }@media (prefers-reduced-motion: reduce) { .ui-jt-chev { transition: none; } }`fn json_table_kind(v) -> str = match type_of(v) {    "map" => "object"    "list" => "array"    "str" => "string"    "bool" => "boolean"    "null" => "null"    _ => "number"}fn json_table_box(v) -> bool = type_of(v) == "map" || type_of(v) == "list"fn json_table_size(v) -> int = type_of(v) == "map" ? len(keys(v)) : len(v)// "3 keys", "1 item": what a closed object or array holds.fn json_table_size_text(v) -> str {    let n = json_table_size(v)    type_of(v) == "map" ? `${json_table_count(n)} key${n == 1 ? "" : "s"}` : `${json_table_count(n)} item${n == 1 ? "" : "s"}`}// The entries of a map or list as [key, value] pairs.fn json_table_pairs(v) -> list =    type_of(v) == "map" ? (for [k, x] in v => [k, x]) : (for [i, x] in enumerate(v) => [i, x])// The value at `segs` (keys and indexes) inside v, or null.fn json_table_at(v, segs: list) {    if len(segs) == 0 { return v }    let k = segs[0]    let next = type_of(v) == "map" ? get(v, str(k))        : (type_of(v) == "list" && type_of(k) == "int" && k >= 0 && k < len(v) ? v[k] : null)    json_table_at(next, slice(segs, 1, len(segs)))}// The path as jq writes it: .users[3].address, .["odd key"], or "." for the top.fn json_table_jq(segs: list) -> str {    let s = join(segs | map((k) => type_of(k) == "int" ? `[${k}]`        : (re.test(k, "^[A-Za-z_][A-Za-z0-9_]*$") ? `.${k}` : `.[${json.stringify(k)}]`)), "")    s == "" ? "." : (starts_with(s, "[") ? "." + s : s)}// A whole number with commas: 10000 is "10,000".fn json_table_count(n: int) -> str {    let s = str(n)    len(s) <= 3 ? s : json_table_count(floor(n / 1000)) + "," + slice(str(1000 + n % 1000), 1, 4)}// A number as JSON writes it, the same on the server and in the browser (where a// whole float such as 2.0 is 2).fn json_table_num(v) -> str = type_of(v) == "float" && v == floor(v) && abs(v) < 1000000000000000 ? str(floor(v)) : str(v)// A value as one line of text: strings as they are, objects and arrays as their size.fn json_table_text(v) -> str = match json_table_kind(v) {    "string" => v    "null" => "null"    "boolean" => v ? "true" : "false"    "number" => json_table_num(v)    _ => json_table_size_text(v)}// Whether any key or value inside v, at any depth, has `needle` (in lower case).fn json_table_has(v, needle: str) -> bool =    json_table_box(v)        ? any(json_table_pairs(v), (p) => (type_of(p[0]) == "str" && contains(lower(p[0]), needle)) || json_table_has(p[1], needle))        : contains(lower(json_table_text(v)), needle)// The text cut where it has `needle` (in lower case): [{ t, hit }].fn json_table_parts(text: str, needle: str) -> list {    let at = needle == "" ? null : index_of(lower(text), needle)    at == null ? [{ t: text, hit: false }]        : [{ t: slice(text, 0, at), hit: false }, { t: slice(text, at, at + len(needle)), hit: true }]            + json_table_parts(slice(text, at + len(needle), len(text)), needle)}// How the table shows v: { mode, columns, rows }. mode is "records" (a list of// maps: a column for each key), "values" (any other list), "entries" (a map: its// keys and values) or "value". Each row is { head, segs, cells: [{ col, v, none, segs }] },// segs being where it is inside v; none marks a key a record does not have.fn json_table_model(v, most: int) -> map {    let kind = type_of(v)    if kind == "list" && len(v) > 0 && all(v, (x) => type_of(x) == "map") {        let columns = take(unique(flatten(v | map((x) => keys(x)))), most)        return { mode: "records", columns, rows: for [i, x] in enumerate(v) => {            head: str(i), segs: [i],            cells: for c in columns => { col: c, v: get(x, c), none: !has(x, c), segs: [i, c] },        } }    }    if kind == "list" {        return { mode: "values", columns: ["value"], rows: for [i, x] in enumerate(v) => {            head: str(i), segs: [i], cells: [{ col: "value", v: x, none: false, segs: [i] }],        } }    }    if kind == "map" {        return { mode: "entries", columns: ["value"], rows: for [k, x] in v => {            head: k, segs: [k], cells: [{ col: "value", v: x, none: false, segs: [k] }],        } }    }    { mode: "value", columns: ["value"], rows: [{ head: "", segs: [], cells: [{ col: "value", v, none: false, segs: [] }] }] }}// An opened object or array, and what is opened inside it: lines of// { kind: "node", depth, label, v, segs, id, open }.fn json_table_nodes(label, v, segs: list, depth: int, open: list) -> list {    let id = json.stringify(segs)    let is_open = json_table_box(v) && index_of(open, id) != null    let line = { kind: "node", depth, label, v, segs, id, open: is_open }    !is_open ? [line]        : [line] + flatten(json_table_pairs(v) | map((p) => json_table_nodes(p[0], p[1], push(segs, p[0]), depth + 1, open)))}// Every line: each row, and under it the cells opened in it.fn json_table_lines(rows: list, open: list) -> list =    flatten(rows | map((r) => [{ kind: "row", row: r, id: json.stringify(r.segs), depth: 0, open: false }]        + flatten(r.cells            | filter((c) => json_table_box(c.v) && index_of(open, json.stringify(c.segs)) != null)            | map((c) => json_table_nodes(last(c.segs), c.v, c.segs, 1, open)))))// One CSV field: quoted when it has a comma, quote or line break.fn json_table_csv_field(v) -> str {    let s = v == null ? "" : (json_table_box(v) ? json.stringify(v) : json_table_text(v))    re.test(s, "[\",\\r\\n]") ? `"${join(split(s, "\""), "\"\"")}"` : s}// The rows as CSV: a column for each key of records, else the index or key and the value.fn json_table_csv(mode: str, columns: list, rows: list) -> str {    let header = mode == "records" ? columns : [mode == "entries" ? "key" : "index", "value"]    let body = rows | map((r) => join((mode == "records" ? [] : [json_table_csv_field(r.head)])        + (r.cells | map((c) => json_table_csv_field(c.none ? null : c.v))), ","))    join([join(header | map((h) => json_table_csv_field(h)), ",")] + body, "\r\n") + "\r\n"}// A file name from the path: data, data-users, data-users-3-orders.fn json_table_file(base: str, segs: list) -> str =    join([base] + (segs | map((k) => re.replace(lower(str(k)), "[^a-z0-9_]+", "-"))), "-")component JsonTable(id: str, value = null, path: list = [], root: str = "root", label: str? = null, search: bool = true,                    filename: str = "data", row_height: int = 34, height: int = 420, overscan: int = 8,                    max_columns: int = 24, empty: str = "Nothing to show", class: str? = null) {    style global json_table_css    state at_path = path    state open = []    state query = ""    state top = 0    state active = 0    state said = ""    let leaf = json_table_at(value, at_path)    let model = json_table_model(leaf, max_columns)    let needle = lower(trim(query))    let rows = needle == "" ? model.rows        : model.rows | filter((r) => (model.mode == "entries" && contains(lower(r.head), needle))                                    || any(r.cells, (c) => !c.none && json_table_has(c.v, needle)))    let lines = json_table_lines(rows, open)    let total = len(lines)    let from_i = max([0, floor(top / row_height) - overscan])    let to_i = min([total, ceil((top + height) / row_height) + overscan])    let window = range(from_i, to_i) | map((k) => { k, ln: lines[k] })    let at = active < total ? active : max([0, total - 1])    let ncols = len(model.columns) + 1    let noun = model.mode == "entries" ? "key" : (model.mode == "values" ? "item" : "row")    let toggle = (segs: list) => {        let key = json.stringify(segs)        open = index_of(open, key) != null ? open | filter((p) => p != key) : push(open, key)    }    // Moves the keys to line k, scrolling it into view below the header.    let go = (e, k: int) => {        active = k        let box = e.target.closest(".ui-jt-scroll")        let body = box.clientHeight - row_height        let y = k * row_height        if y < box.scrollTop {            box.scrollTop = y            top = y        } else if y + row_height > box.scrollTop + body {            box.scrollTop = y + row_height - body            top = y + row_height - body        }    }    let restart = (e) => {        top = 0        active = 0        let box = e.target.closest(".ui-jt").querySelector(".ui-jt-scroll")        box.scrollTop = 0    }    let move_to = (e, segs: list) => {        at_path = segs        open = []        query = ""        restart(e)    }    let save = (text: str, name: str, mime: str) => {        let url = js("URL.createObjectURL", js_new("Blob", [text], { type: mime }))        let a = js("document.createElement", "a")        a.href = url        a.download = name        a.click()        js("requestAnimationFrame", (t = null) => { js("URL.revokeObjectURL", url) })        said = `Saved ${name}`    }    let found = () => model.mode == "entries" ? reduce(rows, {}, (m, r) => set(m, r.head, r.cells[0].v))        : (model.mode == "value" ? leaf : rows | map((r) => json_table_at(leaf, r.segs)))    let chev = () => <svg.ui-jt-chev viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"                         stroke-linejoin="round" aria-hidden="true"><path d="M3.5 2L7 5 3.5 8"/></svg>    let marked = (text: str) => for p in json_table_parts(text, needle) => p.hit ? <mark>{p.t}</mark> : p.t    let shown = (v) => <span class={`ui-jt-${json_table_kind(v)}`}>{json_table_kind(v) == "string" ? marked(v) : json_table_text(v)}</span>    <div class={ui_cls(["ui-jt", model.mode == "entries" && "ui-jt-entries", class])} style={`--ui-jt-rh: ${row_height}px`}>        <div.ui-jt-bar>            <nav.ui-jt-path aria-label={label != null ? `${label}: path` : "Path"}>                <ol>                    <li><button type="button" aria-current={len(at_path) == 0 ? "location" : null}                                @click={(e) => { if len(at_path) > 0 { move_to(e, []) } }}>{root}</button></li>                    {for [i, k] in enumerate(at_path) => <li key={i}>                        <button type="button" aria-current={i == len(at_path) - 1 ? "location" : null}                                @click={(e) => { if i < len(at_path) - 1 { move_to(e, take(at_path, i + 1)) } }}>                            {type_of(k) == "int" ? `[${k}]` : k}                        </button>                    </li>}                </ol>            </nav>            <code.ui-jt-jq title="The path, as jq writes it">{json_table_jq(at_path)}</code>        </div>        <div.ui-jt-tools>            {search && <input.ui-jt-search type="search" placeholder="Search keys and values" aria-controls={`${id}-grid`}                              aria-label={label != null ? `Search ${lower(label)}` : "Search"} value={query}                              @input={(e) => {                                  query = e.value                                  restart(e)                              }}/>}            <span.ui-jt-count aria-live="polite">                {needle == "" ? `${json_table_count(len(rows))} ${noun}${len(rows) == 1 ? "" : "s"}`                              : `${json_table_count(len(rows))} of ${json_table_count(len(model.rows))} ${noun}s`}            </span>            <div.ui-jt-export>                <button.ui-jt-btn type="button" aria-label={`Export ${needle == "" ? "the table" : "the rows found"} as JSON`}                                  @click={() => { save(json.stringify(found(), true) + "\n", json_table_file(filename, at_path) + ".json", "application/json") }}>                    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"                         aria-hidden="true"><path d="M8 2v8M4.5 6.5L8 10l3.5-3.5M3 13h10"/></svg>                    JSON                </button>                <button.ui-jt-btn type="button" aria-label={`Export ${needle == "" ? "the table" : "the rows found"} as CSV`}                                  @click={() => { save(json_table_csv(model.mode, model.columns, rows), json_table_file(filename, at_path) + ".csv", "text/csv") }}>                    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"                         aria-hidden="true"><path d="M8 2v8M4.5 6.5L8 10l3.5-3.5M3 13h10"/></svg>                    CSV                </button>            </div>            <span.ui-sr role="status">{said}</span>        </div>        <div.ui-jt-scroll style={`height: ${height}px`} @scroll={(e) => { top = e.scrollTop }}>            <table id={`${id}-grid`} role="treegrid" tabindex="0" aria-label={label} style={`min-width: ${64 + len(model.columns) * 150}px`}                   aria-rowcount={total + 1} aria-colcount={ncols}                   aria-activedescendant={total > 0 ? `${id}-line-${at}` : null}                   @keydown={(e) => {                       let page = max([1, floor(height / row_height) - 2])                       let ln = total > 0 ? lines[at] : null                       let to = match e.key {                           "ArrowDown" => min([total - 1, at + 1])                           "ArrowUp" => max([0, at - 1])                           "PageDown" => min([total - 1, at + page])                           "PageUp" => max([0, at - page])                           "Home" => 0                           "End" => total - 1                           _ => null                       }                       if e.target.tagName != "TABLE" {                           null                       } else if e.key == "Backspace" && len(at_path) > 0 {                           e.preventDefault()                           move_to(e, take(at_path, len(at_path) - 1))                       } else if ln == null {                           null                       } else if to != null {                           e.preventDefault()                           go(e, to)                       } else if e.key == "ArrowRight" {                           e.preventDefault()                           if ln.kind == "node" {                               if json_table_box(ln.v) && !ln.open { toggle(ln.segs) }                           } else {                               let shut = find(ln.row.cells, (c) => json_table_box(c.v) && index_of(open, json.stringify(c.segs)) == null)                               if shut != null { toggle(shut.segs) }                           }                       } else if e.key == "ArrowLeft" {                           e.preventDefault()                           if ln.kind == "node" && ln.open {                               toggle(ln.segs)                           } else if ln.kind == "node" {                               // The line it is in: the nearest one above, one level up.                               let k = at - 1                               while k > 0 && lines[k].depth >= ln.depth { k -= 1 }                               go(e, k)                           } else {                               let mine = ln.row.cells | map((c) => json.stringify(c.segs))                               open = open | filter((p) => index_of(mine, p) == null)                           }                       } else if e.key == "Enter" || e.key == " " {                           e.preventDefault()                           if ln.kind == "node" && json_table_box(ln.v) {                               if e.key == "Enter" { move_to(e, at_path + ln.segs) } else { toggle(ln.segs) }                           }                       }                   }}>                <colgroup>                    <col.ui-jt-headcol/>                    {for c in model.columns => <col/>}                </colgroup>                <thead>                    <tr aria-rowindex="1">                        <th scope="col">{model.mode == "entries" ? "Key" : "#"}</th>                        {for c in model.columns => <th scope="col" title={c}>{c}</th>}                    </tr>                </thead>                <tbody>                    {total == 0                        ? <tr.ui-jt-empty><td colspan={str(ncols)}>{empty}</td></tr>                        : [                            <tr.ui-jt-pad aria-hidden="true"><td colspan={str(ncols)} style={`height: ${from_i * row_height}px`}></td></tr>,                            for w in window => w.ln.kind == "row"                                ? <tr key={`r${w.ln.id}`} id={`${id}-line-${w.k}`} aria-rowindex={w.k + 2} aria-level="1"                                      class={w.k == at ? "ui-jt-active" : null}                                      @mousedown={(e) => {                                          active = w.k                                          if e.target.tagName != "BUTTON" { e.preventDefault(); e.target.closest("table").focus() }                                      }}>                                      <th scope="row">{model.mode == "entries" ? marked(w.ln.row.head) : w.ln.row.head}</th>                                      {for c in w.ln.row.cells => <td>                                          {c.none ? <span.ui-jt-none aria-label="none">—</span>                                           : json_table_box(c.v)                                              ? <button.ui-jt-cell type="button" tabindex="-1"                                                        aria-expanded={index_of(open, json.stringify(c.segs)) != null ? "true" : "false"}                                                        aria-label={`${c.col}: ${json_table_size_text(c.v)}`}                                                        @click={() => { toggle(c.segs) }}>                                                    {chev()}{type_of(c.v) == "map" ? `{${json_table_size(c.v)}}` : `[${json_table_size(c.v)}]`}                                                </button>                                              : shown(c.v)}                                      </td>}                                  </tr>                                : <tr.ui-jt-node key={`n${w.ln.id}`} id={`${id}-line-${w.k}`} aria-rowindex={w.k + 2}                                      aria-level={str(w.ln.depth + 1)}                                      aria-expanded={json_table_box(w.ln.v) ? (w.ln.open ? "true" : "false") : null}                                      class={w.k == at ? "ui-jt-active" : null}                                      @mousedown={(e) => {                                          active = w.k                                          if e.target.tagName != "BUTTON" { e.preventDefault(); e.target.closest("table").focus() }                                      }}                                      @click={(e) => { if json_table_box(w.ln.v) && e.target.closest("button") == null { toggle(w.ln.segs) } }}>                                      <td colspan={str(ncols)}>                                          <div.ui-jt-line style={`padding-left: ${w.ln.depth * 18 + 8}px`}>                                              <span.ui-jt-tog>{json_table_box(w.ln.v) ? chev() : null}</span>                                              {type_of(w.ln.label) == "int"                                                  ? <span.ui-jt-idx>{`[${w.ln.label}]`}</span>                                                  : <span.ui-jt-key>{marked(w.ln.label)}</span>}                                              {json_table_box(w.ln.v)                                                  ? <span.ui-jt-size>{json_table_size_text(w.ln.v)}</span>                                                  : <span.ui-jt-val>{shown(w.ln.v)}</span>}                                              {json_table_box(w.ln.v) ? <span.ui-jt-acts>                                                  <button.ui-jt-btn type="button" tabindex="-1"                                                                    aria-label={`Open ${json_table_jq(at_path + w.ln.segs)} as the table`}                                                                    @click={(e) => { move_to(e, at_path + w.ln.segs) }}>Open</button>                                                  <button.ui-jt-btn type="button" tabindex="-1"                                                                    aria-label={`Export ${json_table_jq(at_path + w.ln.segs)} as JSON`}                                                                    @click={() => {                                                                        save(json.stringify(w.ln.v, true) + "\n",                                                                             json_table_file(filename, at_path + w.ln.segs) + ".json", "application/json")                                                                    }}>JSON</button>                                              </span> : null}                                          </div>                                      </td>                                  </tr>,                            <tr.ui-jt-pad aria-hidden="true"><td colspan={str(ncols)} style={`height: ${(total - to_i) * row_height}px`}></td></tr>,                          ]}                </tbody>            </table>        </div>    </div>}
Thread
97Useful? Like it, or add to its thread below.

Thread 0 posts

On JsonTable: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.