@yuneta/gobj-ui 7.21.0 → 7.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/gobj-ui.cjs.js +235 -13
- package/dist/gobj-ui.es.js +235 -13
- package/package.json +1 -1
- package/src/c_yui_json.css +22 -16
- package/src/c_yui_json.js +5 -4
- package/src/c_yui_json_graph.js +307 -7
package/README.md
CHANGED
|
@@ -430,12 +430,29 @@ sentinels present it degrades to a plain client-side collapsible tree.
|
|
|
430
430
|
`"tree"` | `"text"` | `"graph"`) picks which. They answer three different
|
|
431
431
|
questions:
|
|
432
432
|
|
|
433
|
+
The switch reads `text · tree · graph`, flattest reading first — which is not
|
|
434
|
+
the order of arrival: the viewer opens on the **tree** (`view_mode` default).
|
|
435
|
+
|
|
433
436
|
| view | question | notes |
|
|
434
437
|
|---|---|---|
|
|
435
|
-
| `tree` | *where is this value, and what is around it* | the lazy view; the only one that can drill |
|
|
436
438
|
| `text` | *what does this document say, verbatim* | `JSON.stringify(…, 4)`, four characters per level |
|
|
439
|
+
| `tree` | *where is this value, and what is around it* | the lazy view; the only one that can drill; the default |
|
|
437
440
|
| `graph` | *what shape is this* | a hosted `C_YUI_JSON_GRAPH` child (AntV/G6) |
|
|
438
441
|
|
|
442
|
+
The graph carries **its own** toolbar, and since `7.22.0` that toolbar has the
|
|
443
|
+
same two facilities the tree has: a **find box** (rate-limited, `EV_FIND_NODES`)
|
|
444
|
+
that highlights matching rows and outlines the cards they are in and says how
|
|
445
|
+
many matched, and **expand-all / collapse-all** (`EV_EXPAND_ALL` /
|
|
446
|
+
`EV_COLLAPSE_ALL`) that fold every card but the root, marking each cut with
|
|
447
|
+
`▸ N` so the shape stays legible and you can see where the rest went. A find
|
|
448
|
+
does not move the camera — a viewport that jumps on every keystroke is
|
|
449
|
+
unusable — which is why the count is there.
|
|
450
|
+
|
|
451
|
+
Note **where the highlight lives**: baked into the card's own markup, never set
|
|
452
|
+
as a G6 node state. The key shape of an `html` node is a DOM element and G6
|
|
453
|
+
paints no state style on it, so setting `active` selects correctly and shows
|
|
454
|
+
nothing.
|
|
455
|
+
|
|
439
456
|
- **Neither text nor graph is lazy.** Both show what the client currently
|
|
440
457
|
holds, `__collapsed__` sentinels included, because that is honestly what it
|
|
441
458
|
has. Drill in the tree and they grow with it.
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -9492,7 +9492,14 @@ var PRIVATE_DATA$11 = {
|
|
|
9492
9492
|
theme: null,
|
|
9493
9493
|
theme_observer: null,
|
|
9494
9494
|
resize_observer: null,
|
|
9495
|
-
_resize_raf: 0
|
|
9495
|
+
_resize_raf: 0,
|
|
9496
|
+
search: "",
|
|
9497
|
+
collapsed: null,
|
|
9498
|
+
match_count: 0,
|
|
9499
|
+
find_timer: null,
|
|
9500
|
+
$find_input: null,
|
|
9501
|
+
$find_result: null,
|
|
9502
|
+
$find_count: null
|
|
9496
9503
|
};
|
|
9497
9504
|
var __gclass__$11 = null;
|
|
9498
9505
|
var __layout_registered__$1 = false;
|
|
@@ -9561,6 +9568,8 @@ function type_color(type, dark) {
|
|
|
9561
9568
|
***************************************************************/
|
|
9562
9569
|
function mt_create$11(gobj) {
|
|
9563
9570
|
let priv = gobj.priv;
|
|
9571
|
+
priv.collapsed = /* @__PURE__ */ new Set();
|
|
9572
|
+
priv.search = "";
|
|
9564
9573
|
priv.canvas_id = "json-canvas-" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
|
|
9565
9574
|
(0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "canvas_id", priv.canvas_id);
|
|
9566
9575
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
@@ -9590,6 +9599,10 @@ function mt_stop$11(gobj) {}
|
|
|
9590
9599
|
* Framework Method: Destroy
|
|
9591
9600
|
***************************************************************/
|
|
9592
9601
|
function mt_destroy$11(gobj) {
|
|
9602
|
+
if (gobj.priv.find_timer) {
|
|
9603
|
+
clearTimeout(gobj.priv.find_timer);
|
|
9604
|
+
gobj.priv.find_timer = null;
|
|
9605
|
+
}
|
|
9593
9606
|
let priv = gobj.priv;
|
|
9594
9607
|
if (priv.theme_observer) {
|
|
9595
9608
|
priv.theme_observer.disconnect();
|
|
@@ -9646,6 +9659,9 @@ function build_ui$10(gobj) {
|
|
|
9646
9659
|
]]
|
|
9647
9660
|
]);
|
|
9648
9661
|
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
|
|
9662
|
+
priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
|
|
9663
|
+
priv.$find_result = $container.querySelector(".JSON_GRAPH_FIND_RESULT");
|
|
9664
|
+
priv.$find_count = $container.querySelector(".JSON_GRAPH_FIND_COUNT");
|
|
9649
9665
|
(0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
|
|
9650
9666
|
}
|
|
9651
9667
|
/************************************************************
|
|
@@ -9659,12 +9675,84 @@ function destroy_ui$6(gobj) {
|
|
|
9659
9675
|
}
|
|
9660
9676
|
}
|
|
9661
9677
|
/************************************************************
|
|
9678
|
+
* One fold button. `open` rotates the chevron to the open
|
|
9679
|
+
* state, mirroring the per-node toggle of the lazy tree.
|
|
9680
|
+
************************************************************/
|
|
9681
|
+
function graph_fold_button(gobj, event_name, label_key, open) {
|
|
9682
|
+
let icon_style = "font-size:1.5em; color:inherit;" + (open ? " transform: rotate(90deg);" : "");
|
|
9683
|
+
return [
|
|
9684
|
+
"button",
|
|
9685
|
+
{
|
|
9686
|
+
class: `button ${event_name}`,
|
|
9687
|
+
type: "button",
|
|
9688
|
+
style: {
|
|
9689
|
+
height: (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "wide"),
|
|
9690
|
+
width: "2.5em"
|
|
9691
|
+
},
|
|
9692
|
+
title: (0, i18next.t)(label_key),
|
|
9693
|
+
"data-i18n-title": label_key,
|
|
9694
|
+
"aria-label": (0, i18next.t)(label_key),
|
|
9695
|
+
"data-i18n-aria-label": label_key
|
|
9696
|
+
},
|
|
9697
|
+
["i", {
|
|
9698
|
+
style: icon_style,
|
|
9699
|
+
class: "yi-chevron-right"
|
|
9700
|
+
}],
|
|
9701
|
+
{ click: (evt) => {
|
|
9702
|
+
evt.stopPropagation();
|
|
9703
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, event_name, {}, gobj);
|
|
9704
|
+
} }
|
|
9705
|
+
];
|
|
9706
|
+
}
|
|
9707
|
+
/************************************************************
|
|
9662
9708
|
* Toolbar
|
|
9663
9709
|
************************************************************/
|
|
9664
9710
|
function make_toolbar$3(gobj) {
|
|
9665
|
-
gobj.priv;
|
|
9711
|
+
let priv = gobj.priv;
|
|
9666
9712
|
let toolbar_wide = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "wide");
|
|
9667
|
-
let left_items = [
|
|
9713
|
+
let left_items = [[
|
|
9714
|
+
"div",
|
|
9715
|
+
{
|
|
9716
|
+
class: "JSON_GRAPH_FIND control has-icons-left",
|
|
9717
|
+
style: "margin-right:.5rem; max-width:12rem; min-width:7rem;"
|
|
9718
|
+
},
|
|
9719
|
+
[["input", {
|
|
9720
|
+
class: "JSON_GRAPH_FIND_INPUT input",
|
|
9721
|
+
type: "text",
|
|
9722
|
+
placeholder: (0, i18next.t)("search"),
|
|
9723
|
+
"data-i18n-placeholder": "search",
|
|
9724
|
+
"aria-label": (0, i18next.t)("search"),
|
|
9725
|
+
"data-i18n-aria-label": "search"
|
|
9726
|
+
}], [
|
|
9727
|
+
"span",
|
|
9728
|
+
{ class: "icon is-left" },
|
|
9729
|
+
[["i", { class: "yi-magnifying-glass" }]]
|
|
9730
|
+
]],
|
|
9731
|
+
{ input: (evt) => {
|
|
9732
|
+
evt.stopPropagation();
|
|
9733
|
+
let text = evt.target.value.trim();
|
|
9734
|
+
if (priv.find_timer) clearTimeout(priv.find_timer);
|
|
9735
|
+
priv.find_timer = setTimeout(function() {
|
|
9736
|
+
priv.find_timer = null;
|
|
9737
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NODES", { text }, gobj);
|
|
9738
|
+
}, 250);
|
|
9739
|
+
} }
|
|
9740
|
+
], [
|
|
9741
|
+
"div",
|
|
9742
|
+
{
|
|
9743
|
+
class: "JSON_GRAPH_FIND_RESULT is-hidden",
|
|
9744
|
+
style: "display:flex; align-items:center; gap:.3rem; margin-right:.5rem; font-size:.85rem;"
|
|
9745
|
+
},
|
|
9746
|
+
[[
|
|
9747
|
+
"span",
|
|
9748
|
+
{ class: "JSON_GRAPH_FIND_COUNT" },
|
|
9749
|
+
""
|
|
9750
|
+
], [
|
|
9751
|
+
"span",
|
|
9752
|
+
{ i18n: "matches" },
|
|
9753
|
+
"matches"
|
|
9754
|
+
]]
|
|
9755
|
+
]];
|
|
9668
9756
|
let center_items = [];
|
|
9669
9757
|
for (let item of [
|
|
9670
9758
|
[
|
|
@@ -9722,6 +9810,7 @@ function make_toolbar$3(gobj) {
|
|
|
9722
9810
|
} }
|
|
9723
9811
|
]);
|
|
9724
9812
|
}
|
|
9813
|
+
let right_items = [graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true), graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false)];
|
|
9725
9814
|
const $toolbar = yui_toolbar({}, [
|
|
9726
9815
|
[
|
|
9727
9816
|
"div",
|
|
@@ -9736,7 +9825,7 @@ function make_toolbar$3(gobj) {
|
|
|
9736
9825
|
[
|
|
9737
9826
|
"div",
|
|
9738
9827
|
{ class: "yui-horizontal-toolbar-section right" },
|
|
9739
|
-
|
|
9828
|
+
right_items
|
|
9740
9829
|
]
|
|
9741
9830
|
]);
|
|
9742
9831
|
(0, _yuneta_gobj_js.refresh_language)($toolbar, i18next.t);
|
|
@@ -9787,7 +9876,7 @@ function build_graph$3(gobj) {
|
|
|
9787
9876
|
/************************************************************
|
|
9788
9877
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
9789
9878
|
************************************************************/
|
|
9790
|
-
function build_cell_html(key, value, type, dark) {
|
|
9879
|
+
function build_cell_html(key, value, type, dark, matched) {
|
|
9791
9880
|
let color = type_color(type, dark);
|
|
9792
9881
|
let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
|
|
9793
9882
|
let display_value = "";
|
|
@@ -9812,7 +9901,28 @@ function build_cell_html(key, value, type, dark) {
|
|
|
9812
9901
|
break;
|
|
9813
9902
|
default: display_value = String(value);
|
|
9814
9903
|
}
|
|
9815
|
-
|
|
9904
|
+
let row = `<span style="color:${key_color}">• ${(0, _yuneta_gobj_js.escapeHtml)(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
9905
|
+
if (matched) row = `<span style="background:${dark ? "#7a5d00" : "#ffe082"}; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
9906
|
+
return row;
|
|
9907
|
+
}
|
|
9908
|
+
/************************************************************
|
|
9909
|
+
* Does this key/value pair match the find term?
|
|
9910
|
+
*
|
|
9911
|
+
* Scalars are matched on what the card SHOWS. A complex value
|
|
9912
|
+
* is not: its contents are a card of their own and match there,
|
|
9913
|
+
* so matching the `{3}` summary too would light up every
|
|
9914
|
+
* ancestor of every hit.
|
|
9915
|
+
************************************************************/
|
|
9916
|
+
function cell_matches(key, value, type, needle) {
|
|
9917
|
+
if ((0, _yuneta_gobj_js.empty_string)(needle)) return false;
|
|
9918
|
+
if (String(key).toLowerCase().includes(needle)) return true;
|
|
9919
|
+
switch (type) {
|
|
9920
|
+
case "string":
|
|
9921
|
+
case "number":
|
|
9922
|
+
case "boolean": return String(value).toLowerCase().includes(needle);
|
|
9923
|
+
case "null": return "null".includes(needle);
|
|
9924
|
+
default: return false;
|
|
9925
|
+
}
|
|
9816
9926
|
}
|
|
9817
9927
|
/************************************************************
|
|
9818
9928
|
* Get JSON type string
|
|
@@ -9858,12 +9968,21 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9858
9968
|
let is_dict = (0, _yuneta_gobj_js.is_object)(kw);
|
|
9859
9969
|
let is_list = (0, _yuneta_gobj_js.is_array)(kw);
|
|
9860
9970
|
if (!is_dict && !is_list) return null;
|
|
9971
|
+
let priv = gobj.priv;
|
|
9972
|
+
let needle = priv.search || "";
|
|
9861
9973
|
let dark = yui_is_dark();
|
|
9862
9974
|
let lines = [];
|
|
9863
9975
|
let pending_complex = [];
|
|
9976
|
+
let card_matched = false;
|
|
9864
9977
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
9865
9978
|
for (let [key, value] of entries) {
|
|
9866
|
-
let
|
|
9979
|
+
let type = get_json_type(value);
|
|
9980
|
+
let matched = cell_matches(key, value, type, needle);
|
|
9981
|
+
if (matched) {
|
|
9982
|
+
card_matched = true;
|
|
9983
|
+
priv.match_count++;
|
|
9984
|
+
}
|
|
9985
|
+
let html = build_cell_html(key, value, type, dark, matched);
|
|
9867
9986
|
lines.push(html);
|
|
9868
9987
|
if ((0, _yuneta_gobj_js.is_object)(value) && (0, _yuneta_gobj_js.json_object_size)(value) > 0 || (0, _yuneta_gobj_js.is_array)(value) && value.length > 0) pending_complex.push({
|
|
9869
9988
|
key,
|
|
@@ -9871,14 +9990,18 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9871
9990
|
});
|
|
9872
9991
|
}
|
|
9873
9992
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
9993
|
+
let folded = priv.collapsed.has(path) && pending_complex.length > 0;
|
|
9994
|
+
if (folded) lines.push(`<span style="color:${type_color("number", dark)}; font-weight:bold;">▸ ${pending_complex.length}</span>`);
|
|
9874
9995
|
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
9996
|
+
let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
|
|
9997
|
+
let border_width = card_matched ? 2 : 1;
|
|
9875
9998
|
let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
|
|
9876
9999
|
let min_width = Math.max(180, label.length * 9);
|
|
9877
10000
|
let node_html = `
|
|
9878
10001
|
<div style="
|
|
9879
10002
|
min-width: ${min_width}px;
|
|
9880
10003
|
background: ${colors.bg};
|
|
9881
|
-
border:
|
|
10004
|
+
border: ${border_width}px ${is_dict ? "dashed" : "solid"} ${border_color};
|
|
9882
10005
|
border-radius: ${is_dict ? "6px" : "0"};
|
|
9883
10006
|
opacity: 0.9;
|
|
9884
10007
|
overflow: hidden;
|
|
@@ -9916,6 +10039,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9916
10039
|
lineWidth: 1
|
|
9917
10040
|
}
|
|
9918
10041
|
});
|
|
10042
|
+
if (folded) return group_id;
|
|
9919
10043
|
for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id);
|
|
9920
10044
|
return group_id;
|
|
9921
10045
|
}
|
|
@@ -9932,6 +10056,7 @@ function load_json(gobj) {
|
|
|
9932
10056
|
let path = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "path") || "`";
|
|
9933
10057
|
if ((0, _yuneta_gobj_js.empty_string)(path)) path = "`";
|
|
9934
10058
|
priv.node_counter = 0;
|
|
10059
|
+
priv.match_count = 0;
|
|
9935
10060
|
let nodes = [];
|
|
9936
10061
|
let edges = [];
|
|
9937
10062
|
build_json_nodes(gobj, path, json, nodes, edges, null);
|
|
@@ -9948,6 +10073,42 @@ function load_json(gobj) {
|
|
|
9948
10073
|
}
|
|
9949
10074
|
}
|
|
9950
10075
|
/************************************************************
|
|
10076
|
+
* Every path that HAS a branch to fold, in the same recursion
|
|
10077
|
+
* build_json_nodes uses — so "collapse all" can never name a
|
|
10078
|
+
* card that does not exist. The root is excluded: folding it
|
|
10079
|
+
* would leave an empty canvas.
|
|
10080
|
+
************************************************************/
|
|
10081
|
+
function collect_foldable_paths(value, path, out, is_root) {
|
|
10082
|
+
let is_dict = (0, _yuneta_gobj_js.is_object)(value);
|
|
10083
|
+
let is_list = (0, _yuneta_gobj_js.is_array)(value);
|
|
10084
|
+
if (!is_dict && !is_list) return false;
|
|
10085
|
+
let has_branch = false;
|
|
10086
|
+
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
10087
|
+
for (let [key, child] of entries) if ((0, _yuneta_gobj_js.is_object)(child) && (0, _yuneta_gobj_js.json_object_size)(child) > 0 || (0, _yuneta_gobj_js.is_array)(child) && child.length > 0) {
|
|
10088
|
+
has_branch = true;
|
|
10089
|
+
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
10090
|
+
}
|
|
10091
|
+
if (has_branch && !is_root) out.push(path);
|
|
10092
|
+
return has_branch;
|
|
10093
|
+
}
|
|
10094
|
+
/************************************************************
|
|
10095
|
+
* Show the find count, or hide the chip when there is no
|
|
10096
|
+
* term. Zero is a RESULT and must show: a graph that did not
|
|
10097
|
+
* move looks the same whether nothing matched or the match
|
|
10098
|
+
* was already on screen.
|
|
10099
|
+
************************************************************/
|
|
10100
|
+
function update_find_result(gobj) {
|
|
10101
|
+
let priv = gobj.priv;
|
|
10102
|
+
if (!priv.$find_result || !priv.$find_count) return;
|
|
10103
|
+
if ((0, _yuneta_gobj_js.empty_string)(priv.search)) {
|
|
10104
|
+
priv.$find_result.classList.add("is-hidden");
|
|
10105
|
+
priv.$find_count.textContent = "";
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
10108
|
+
priv.$find_count.textContent = String(priv.match_count);
|
|
10109
|
+
priv.$find_result.classList.remove("is-hidden");
|
|
10110
|
+
}
|
|
10111
|
+
/************************************************************
|
|
9951
10112
|
* Clear and reload.
|
|
9952
10113
|
* {preserve_view: true}
|
|
9953
10114
|
* keep the current zoom and translate (no fitView) —
|
|
@@ -10069,6 +10230,49 @@ function ac_refresh$5(gobj, event, kw, src) {
|
|
|
10069
10230
|
}
|
|
10070
10231
|
/************************************************************
|
|
10071
10232
|
* {theme: "dark"|"light"} — the app switched theme.
|
|
10233
|
+
* EV_FIND_NODES { text }
|
|
10234
|
+
*
|
|
10235
|
+
* The camera does NOT move: a find is a way of reading what is
|
|
10236
|
+
* already on screen, and a viewport that jumps on every
|
|
10237
|
+
* keystroke is unusable. The count says whether there is
|
|
10238
|
+
* anything to look for elsewhere.
|
|
10239
|
+
************************************************************/
|
|
10240
|
+
function ac_find_nodes$2(gobj, event, kw, src) {
|
|
10241
|
+
let priv = gobj.priv;
|
|
10242
|
+
priv.search = (kw && kw.text || "").trim().toLowerCase();
|
|
10243
|
+
refresh_json(gobj, { preserve_view: true });
|
|
10244
|
+
update_find_result(gobj);
|
|
10245
|
+
return 0;
|
|
10246
|
+
}
|
|
10247
|
+
/************************************************************
|
|
10248
|
+
* EV_EXPAND_ALL
|
|
10249
|
+
************************************************************/
|
|
10250
|
+
function ac_expand_all$2(gobj, event, kw, src) {
|
|
10251
|
+
let priv = gobj.priv;
|
|
10252
|
+
if (priv.collapsed.size === 0) return 0;
|
|
10253
|
+
priv.collapsed.clear();
|
|
10254
|
+
refresh_json(gobj);
|
|
10255
|
+
update_find_result(gobj);
|
|
10256
|
+
return 0;
|
|
10257
|
+
}
|
|
10258
|
+
/************************************************************
|
|
10259
|
+
* EV_COLLAPSE_ALL — fold every card but the root.
|
|
10260
|
+
************************************************************/
|
|
10261
|
+
function ac_collapse_all$2(gobj, event, kw, src) {
|
|
10262
|
+
let priv = gobj.priv;
|
|
10263
|
+
let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
|
|
10264
|
+
if (!json_data) return 0;
|
|
10265
|
+
let paths = [];
|
|
10266
|
+
let path = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "path") || "`";
|
|
10267
|
+
if ((0, _yuneta_gobj_js.empty_string)(path)) path = "`";
|
|
10268
|
+
collect_foldable_paths(json_data, path, paths, true);
|
|
10269
|
+
priv.collapsed = new Set(paths);
|
|
10270
|
+
refresh_json(gobj);
|
|
10271
|
+
update_find_result(gobj);
|
|
10272
|
+
return 0;
|
|
10273
|
+
}
|
|
10274
|
+
/************************************************************
|
|
10275
|
+
*
|
|
10072
10276
|
************************************************************/
|
|
10073
10277
|
function ac_theme$3(gobj, event, kw, src) {
|
|
10074
10278
|
let priv = gobj.priv;
|
|
@@ -10209,6 +10413,21 @@ function create_gclass$11(gclass_name) {
|
|
|
10209
10413
|
ac_refresh$5,
|
|
10210
10414
|
null
|
|
10211
10415
|
],
|
|
10416
|
+
[
|
|
10417
|
+
"EV_FIND_NODES",
|
|
10418
|
+
ac_find_nodes$2,
|
|
10419
|
+
null
|
|
10420
|
+
],
|
|
10421
|
+
[
|
|
10422
|
+
"EV_EXPAND_ALL",
|
|
10423
|
+
ac_expand_all$2,
|
|
10424
|
+
null
|
|
10425
|
+
],
|
|
10426
|
+
[
|
|
10427
|
+
"EV_COLLAPSE_ALL",
|
|
10428
|
+
ac_collapse_all$2,
|
|
10429
|
+
null
|
|
10430
|
+
],
|
|
10212
10431
|
[
|
|
10213
10432
|
"EV_THEME",
|
|
10214
10433
|
ac_theme$3,
|
|
@@ -10258,6 +10477,9 @@ function create_gclass$11(gclass_name) {
|
|
|
10258
10477
|
const event_types = [
|
|
10259
10478
|
["EV_LOAD_DATA", 0],
|
|
10260
10479
|
["EV_REFRESH", 0],
|
|
10480
|
+
["EV_FIND_NODES", 0],
|
|
10481
|
+
["EV_EXPAND_ALL", 0],
|
|
10482
|
+
["EV_COLLAPSE_ALL", 0],
|
|
10261
10483
|
["EV_THEME", 0],
|
|
10262
10484
|
["EV_ZOOM_IN", 0],
|
|
10263
10485
|
["EV_ZOOM_OUT", 0],
|
|
@@ -10515,16 +10737,16 @@ var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
|
10515
10737
|
var MAX_RENDER_ROWS = 5e3;
|
|
10516
10738
|
var MAX_TEXT_CHARS = 2e6;
|
|
10517
10739
|
var VIEWS = [
|
|
10518
|
-
{
|
|
10519
|
-
mode: "tree",
|
|
10520
|
-
icon: "yi-sitemap",
|
|
10521
|
-
key: "tree view"
|
|
10522
|
-
},
|
|
10523
10740
|
{
|
|
10524
10741
|
mode: "text",
|
|
10525
10742
|
icon: "yi-code",
|
|
10526
10743
|
key: "text view"
|
|
10527
10744
|
},
|
|
10745
|
+
{
|
|
10746
|
+
mode: "tree",
|
|
10747
|
+
icon: "yi-sitemap",
|
|
10748
|
+
key: "tree view"
|
|
10749
|
+
},
|
|
10528
10750
|
{
|
|
10529
10751
|
mode: "graph",
|
|
10530
10752
|
icon: "yi-hexagon-nodes",
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -9487,7 +9487,14 @@ var PRIVATE_DATA$11 = {
|
|
|
9487
9487
|
theme: null,
|
|
9488
9488
|
theme_observer: null,
|
|
9489
9489
|
resize_observer: null,
|
|
9490
|
-
_resize_raf: 0
|
|
9490
|
+
_resize_raf: 0,
|
|
9491
|
+
search: "",
|
|
9492
|
+
collapsed: null,
|
|
9493
|
+
match_count: 0,
|
|
9494
|
+
find_timer: null,
|
|
9495
|
+
$find_input: null,
|
|
9496
|
+
$find_result: null,
|
|
9497
|
+
$find_count: null
|
|
9491
9498
|
};
|
|
9492
9499
|
var __gclass__$11 = null;
|
|
9493
9500
|
var __layout_registered__$1 = false;
|
|
@@ -9556,6 +9563,8 @@ function type_color(type, dark) {
|
|
|
9556
9563
|
***************************************************************/
|
|
9557
9564
|
function mt_create$11(gobj) {
|
|
9558
9565
|
let priv = gobj.priv;
|
|
9566
|
+
priv.collapsed = /* @__PURE__ */ new Set();
|
|
9567
|
+
priv.search = "";
|
|
9559
9568
|
priv.canvas_id = "json-canvas-" + clean_name(gobj_name(gobj));
|
|
9560
9569
|
gobj_write_str_attr(gobj, "canvas_id", priv.canvas_id);
|
|
9561
9570
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
@@ -9585,6 +9594,10 @@ function mt_stop$11(gobj) {}
|
|
|
9585
9594
|
* Framework Method: Destroy
|
|
9586
9595
|
***************************************************************/
|
|
9587
9596
|
function mt_destroy$11(gobj) {
|
|
9597
|
+
if (gobj.priv.find_timer) {
|
|
9598
|
+
clearTimeout(gobj.priv.find_timer);
|
|
9599
|
+
gobj.priv.find_timer = null;
|
|
9600
|
+
}
|
|
9588
9601
|
let priv = gobj.priv;
|
|
9589
9602
|
if (priv.theme_observer) {
|
|
9590
9603
|
priv.theme_observer.disconnect();
|
|
@@ -9641,6 +9654,9 @@ function build_ui$10(gobj) {
|
|
|
9641
9654
|
]]
|
|
9642
9655
|
]);
|
|
9643
9656
|
gobj_write_attr(gobj, "$container", $container);
|
|
9657
|
+
priv.$find_input = $container.querySelector(".JSON_GRAPH_FIND_INPUT");
|
|
9658
|
+
priv.$find_result = $container.querySelector(".JSON_GRAPH_FIND_RESULT");
|
|
9659
|
+
priv.$find_count = $container.querySelector(".JSON_GRAPH_FIND_COUNT");
|
|
9644
9660
|
refresh_language($container, t);
|
|
9645
9661
|
}
|
|
9646
9662
|
/************************************************************
|
|
@@ -9654,12 +9670,84 @@ function destroy_ui$6(gobj) {
|
|
|
9654
9670
|
}
|
|
9655
9671
|
}
|
|
9656
9672
|
/************************************************************
|
|
9673
|
+
* One fold button. `open` rotates the chevron to the open
|
|
9674
|
+
* state, mirroring the per-node toggle of the lazy tree.
|
|
9675
|
+
************************************************************/
|
|
9676
|
+
function graph_fold_button(gobj, event_name, label_key, open) {
|
|
9677
|
+
let icon_style = "font-size:1.5em; color:inherit;" + (open ? " transform: rotate(90deg);" : "");
|
|
9678
|
+
return [
|
|
9679
|
+
"button",
|
|
9680
|
+
{
|
|
9681
|
+
class: `button ${event_name}`,
|
|
9682
|
+
type: "button",
|
|
9683
|
+
style: {
|
|
9684
|
+
height: gobj_read_attr(gobj, "wide"),
|
|
9685
|
+
width: "2.5em"
|
|
9686
|
+
},
|
|
9687
|
+
title: t(label_key),
|
|
9688
|
+
"data-i18n-title": label_key,
|
|
9689
|
+
"aria-label": t(label_key),
|
|
9690
|
+
"data-i18n-aria-label": label_key
|
|
9691
|
+
},
|
|
9692
|
+
["i", {
|
|
9693
|
+
style: icon_style,
|
|
9694
|
+
class: "yi-chevron-right"
|
|
9695
|
+
}],
|
|
9696
|
+
{ click: (evt) => {
|
|
9697
|
+
evt.stopPropagation();
|
|
9698
|
+
gobj_send_event(gobj, event_name, {}, gobj);
|
|
9699
|
+
} }
|
|
9700
|
+
];
|
|
9701
|
+
}
|
|
9702
|
+
/************************************************************
|
|
9657
9703
|
* Toolbar
|
|
9658
9704
|
************************************************************/
|
|
9659
9705
|
function make_toolbar$3(gobj) {
|
|
9660
|
-
gobj.priv;
|
|
9706
|
+
let priv = gobj.priv;
|
|
9661
9707
|
let toolbar_wide = gobj_read_attr(gobj, "wide");
|
|
9662
|
-
let left_items = [
|
|
9708
|
+
let left_items = [[
|
|
9709
|
+
"div",
|
|
9710
|
+
{
|
|
9711
|
+
class: "JSON_GRAPH_FIND control has-icons-left",
|
|
9712
|
+
style: "margin-right:.5rem; max-width:12rem; min-width:7rem;"
|
|
9713
|
+
},
|
|
9714
|
+
[["input", {
|
|
9715
|
+
class: "JSON_GRAPH_FIND_INPUT input",
|
|
9716
|
+
type: "text",
|
|
9717
|
+
placeholder: t("search"),
|
|
9718
|
+
"data-i18n-placeholder": "search",
|
|
9719
|
+
"aria-label": t("search"),
|
|
9720
|
+
"data-i18n-aria-label": "search"
|
|
9721
|
+
}], [
|
|
9722
|
+
"span",
|
|
9723
|
+
{ class: "icon is-left" },
|
|
9724
|
+
[["i", { class: "yi-magnifying-glass" }]]
|
|
9725
|
+
]],
|
|
9726
|
+
{ input: (evt) => {
|
|
9727
|
+
evt.stopPropagation();
|
|
9728
|
+
let text = evt.target.value.trim();
|
|
9729
|
+
if (priv.find_timer) clearTimeout(priv.find_timer);
|
|
9730
|
+
priv.find_timer = setTimeout(function() {
|
|
9731
|
+
priv.find_timer = null;
|
|
9732
|
+
gobj_send_event(gobj, "EV_FIND_NODES", { text }, gobj);
|
|
9733
|
+
}, 250);
|
|
9734
|
+
} }
|
|
9735
|
+
], [
|
|
9736
|
+
"div",
|
|
9737
|
+
{
|
|
9738
|
+
class: "JSON_GRAPH_FIND_RESULT is-hidden",
|
|
9739
|
+
style: "display:flex; align-items:center; gap:.3rem; margin-right:.5rem; font-size:.85rem;"
|
|
9740
|
+
},
|
|
9741
|
+
[[
|
|
9742
|
+
"span",
|
|
9743
|
+
{ class: "JSON_GRAPH_FIND_COUNT" },
|
|
9744
|
+
""
|
|
9745
|
+
], [
|
|
9746
|
+
"span",
|
|
9747
|
+
{ i18n: "matches" },
|
|
9748
|
+
"matches"
|
|
9749
|
+
]]
|
|
9750
|
+
]];
|
|
9663
9751
|
let center_items = [];
|
|
9664
9752
|
for (let item of [
|
|
9665
9753
|
[
|
|
@@ -9717,6 +9805,7 @@ function make_toolbar$3(gobj) {
|
|
|
9717
9805
|
} }
|
|
9718
9806
|
]);
|
|
9719
9807
|
}
|
|
9808
|
+
let right_items = [graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true), graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false)];
|
|
9720
9809
|
const $toolbar = yui_toolbar({}, [
|
|
9721
9810
|
[
|
|
9722
9811
|
"div",
|
|
@@ -9731,7 +9820,7 @@ function make_toolbar$3(gobj) {
|
|
|
9731
9820
|
[
|
|
9732
9821
|
"div",
|
|
9733
9822
|
{ class: "yui-horizontal-toolbar-section right" },
|
|
9734
|
-
|
|
9823
|
+
right_items
|
|
9735
9824
|
]
|
|
9736
9825
|
]);
|
|
9737
9826
|
refresh_language($toolbar, t);
|
|
@@ -9782,7 +9871,7 @@ function build_graph$3(gobj) {
|
|
|
9782
9871
|
/************************************************************
|
|
9783
9872
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
9784
9873
|
************************************************************/
|
|
9785
|
-
function build_cell_html(key, value, type, dark) {
|
|
9874
|
+
function build_cell_html(key, value, type, dark, matched) {
|
|
9786
9875
|
let color = type_color(type, dark);
|
|
9787
9876
|
let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
|
|
9788
9877
|
let display_value = "";
|
|
@@ -9807,7 +9896,28 @@ function build_cell_html(key, value, type, dark) {
|
|
|
9807
9896
|
break;
|
|
9808
9897
|
default: display_value = String(value);
|
|
9809
9898
|
}
|
|
9810
|
-
|
|
9899
|
+
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
9900
|
+
if (matched) row = `<span style="background:${dark ? "#7a5d00" : "#ffe082"}; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
9901
|
+
return row;
|
|
9902
|
+
}
|
|
9903
|
+
/************************************************************
|
|
9904
|
+
* Does this key/value pair match the find term?
|
|
9905
|
+
*
|
|
9906
|
+
* Scalars are matched on what the card SHOWS. A complex value
|
|
9907
|
+
* is not: its contents are a card of their own and match there,
|
|
9908
|
+
* so matching the `{3}` summary too would light up every
|
|
9909
|
+
* ancestor of every hit.
|
|
9910
|
+
************************************************************/
|
|
9911
|
+
function cell_matches(key, value, type, needle) {
|
|
9912
|
+
if (empty_string(needle)) return false;
|
|
9913
|
+
if (String(key).toLowerCase().includes(needle)) return true;
|
|
9914
|
+
switch (type) {
|
|
9915
|
+
case "string":
|
|
9916
|
+
case "number":
|
|
9917
|
+
case "boolean": return String(value).toLowerCase().includes(needle);
|
|
9918
|
+
case "null": return "null".includes(needle);
|
|
9919
|
+
default: return false;
|
|
9920
|
+
}
|
|
9811
9921
|
}
|
|
9812
9922
|
/************************************************************
|
|
9813
9923
|
* Get JSON type string
|
|
@@ -9853,12 +9963,21 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9853
9963
|
let is_dict = is_object(kw);
|
|
9854
9964
|
let is_list = is_array(kw);
|
|
9855
9965
|
if (!is_dict && !is_list) return null;
|
|
9966
|
+
let priv = gobj.priv;
|
|
9967
|
+
let needle = priv.search || "";
|
|
9856
9968
|
let dark = yui_is_dark();
|
|
9857
9969
|
let lines = [];
|
|
9858
9970
|
let pending_complex = [];
|
|
9971
|
+
let card_matched = false;
|
|
9859
9972
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
9860
9973
|
for (let [key, value] of entries) {
|
|
9861
|
-
let
|
|
9974
|
+
let type = get_json_type(value);
|
|
9975
|
+
let matched = cell_matches(key, value, type, needle);
|
|
9976
|
+
if (matched) {
|
|
9977
|
+
card_matched = true;
|
|
9978
|
+
priv.match_count++;
|
|
9979
|
+
}
|
|
9980
|
+
let html = build_cell_html(key, value, type, dark, matched);
|
|
9862
9981
|
lines.push(html);
|
|
9863
9982
|
if (is_object(value) && json_object_size(value) > 0 || is_array(value) && value.length > 0) pending_complex.push({
|
|
9864
9983
|
key,
|
|
@@ -9866,14 +9985,18 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9866
9985
|
});
|
|
9867
9986
|
}
|
|
9868
9987
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
9988
|
+
let folded = priv.collapsed.has(path) && pending_complex.length > 0;
|
|
9989
|
+
if (folded) lines.push(`<span style="color:${type_color("number", dark)}; font-weight:bold;">▸ ${pending_complex.length}</span>`);
|
|
9869
9990
|
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
9991
|
+
let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
|
|
9992
|
+
let border_width = card_matched ? 2 : 1;
|
|
9870
9993
|
let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
|
|
9871
9994
|
let min_width = Math.max(180, label.length * 9);
|
|
9872
9995
|
let node_html = `
|
|
9873
9996
|
<div style="
|
|
9874
9997
|
min-width: ${min_width}px;
|
|
9875
9998
|
background: ${colors.bg};
|
|
9876
|
-
border:
|
|
9999
|
+
border: ${border_width}px ${is_dict ? "dashed" : "solid"} ${border_color};
|
|
9877
10000
|
border-radius: ${is_dict ? "6px" : "0"};
|
|
9878
10001
|
opacity: 0.9;
|
|
9879
10002
|
overflow: hidden;
|
|
@@ -9911,6 +10034,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
9911
10034
|
lineWidth: 1
|
|
9912
10035
|
}
|
|
9913
10036
|
});
|
|
10037
|
+
if (folded) return group_id;
|
|
9914
10038
|
for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id);
|
|
9915
10039
|
return group_id;
|
|
9916
10040
|
}
|
|
@@ -9927,6 +10051,7 @@ function load_json(gobj) {
|
|
|
9927
10051
|
let path = gobj_read_str_attr(gobj, "path") || "`";
|
|
9928
10052
|
if (empty_string(path)) path = "`";
|
|
9929
10053
|
priv.node_counter = 0;
|
|
10054
|
+
priv.match_count = 0;
|
|
9930
10055
|
let nodes = [];
|
|
9931
10056
|
let edges = [];
|
|
9932
10057
|
build_json_nodes(gobj, path, json, nodes, edges, null);
|
|
@@ -9943,6 +10068,42 @@ function load_json(gobj) {
|
|
|
9943
10068
|
}
|
|
9944
10069
|
}
|
|
9945
10070
|
/************************************************************
|
|
10071
|
+
* Every path that HAS a branch to fold, in the same recursion
|
|
10072
|
+
* build_json_nodes uses — so "collapse all" can never name a
|
|
10073
|
+
* card that does not exist. The root is excluded: folding it
|
|
10074
|
+
* would leave an empty canvas.
|
|
10075
|
+
************************************************************/
|
|
10076
|
+
function collect_foldable_paths(value, path, out, is_root) {
|
|
10077
|
+
let is_dict = is_object(value);
|
|
10078
|
+
let is_list = is_array(value);
|
|
10079
|
+
if (!is_dict && !is_list) return false;
|
|
10080
|
+
let has_branch = false;
|
|
10081
|
+
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
10082
|
+
for (let [key, child] of entries) if (is_object(child) && json_object_size(child) > 0 || is_array(child) && child.length > 0) {
|
|
10083
|
+
has_branch = true;
|
|
10084
|
+
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
10085
|
+
}
|
|
10086
|
+
if (has_branch && !is_root) out.push(path);
|
|
10087
|
+
return has_branch;
|
|
10088
|
+
}
|
|
10089
|
+
/************************************************************
|
|
10090
|
+
* Show the find count, or hide the chip when there is no
|
|
10091
|
+
* term. Zero is a RESULT and must show: a graph that did not
|
|
10092
|
+
* move looks the same whether nothing matched or the match
|
|
10093
|
+
* was already on screen.
|
|
10094
|
+
************************************************************/
|
|
10095
|
+
function update_find_result(gobj) {
|
|
10096
|
+
let priv = gobj.priv;
|
|
10097
|
+
if (!priv.$find_result || !priv.$find_count) return;
|
|
10098
|
+
if (empty_string(priv.search)) {
|
|
10099
|
+
priv.$find_result.classList.add("is-hidden");
|
|
10100
|
+
priv.$find_count.textContent = "";
|
|
10101
|
+
return;
|
|
10102
|
+
}
|
|
10103
|
+
priv.$find_count.textContent = String(priv.match_count);
|
|
10104
|
+
priv.$find_result.classList.remove("is-hidden");
|
|
10105
|
+
}
|
|
10106
|
+
/************************************************************
|
|
9946
10107
|
* Clear and reload.
|
|
9947
10108
|
* {preserve_view: true}
|
|
9948
10109
|
* keep the current zoom and translate (no fitView) —
|
|
@@ -10064,6 +10225,49 @@ function ac_refresh$5(gobj, event, kw, src) {
|
|
|
10064
10225
|
}
|
|
10065
10226
|
/************************************************************
|
|
10066
10227
|
* {theme: "dark"|"light"} — the app switched theme.
|
|
10228
|
+
* EV_FIND_NODES { text }
|
|
10229
|
+
*
|
|
10230
|
+
* The camera does NOT move: a find is a way of reading what is
|
|
10231
|
+
* already on screen, and a viewport that jumps on every
|
|
10232
|
+
* keystroke is unusable. The count says whether there is
|
|
10233
|
+
* anything to look for elsewhere.
|
|
10234
|
+
************************************************************/
|
|
10235
|
+
function ac_find_nodes$2(gobj, event, kw, src) {
|
|
10236
|
+
let priv = gobj.priv;
|
|
10237
|
+
priv.search = (kw && kw.text || "").trim().toLowerCase();
|
|
10238
|
+
refresh_json(gobj, { preserve_view: true });
|
|
10239
|
+
update_find_result(gobj);
|
|
10240
|
+
return 0;
|
|
10241
|
+
}
|
|
10242
|
+
/************************************************************
|
|
10243
|
+
* EV_EXPAND_ALL
|
|
10244
|
+
************************************************************/
|
|
10245
|
+
function ac_expand_all$2(gobj, event, kw, src) {
|
|
10246
|
+
let priv = gobj.priv;
|
|
10247
|
+
if (priv.collapsed.size === 0) return 0;
|
|
10248
|
+
priv.collapsed.clear();
|
|
10249
|
+
refresh_json(gobj);
|
|
10250
|
+
update_find_result(gobj);
|
|
10251
|
+
return 0;
|
|
10252
|
+
}
|
|
10253
|
+
/************************************************************
|
|
10254
|
+
* EV_COLLAPSE_ALL — fold every card but the root.
|
|
10255
|
+
************************************************************/
|
|
10256
|
+
function ac_collapse_all$2(gobj, event, kw, src) {
|
|
10257
|
+
let priv = gobj.priv;
|
|
10258
|
+
let json_data = gobj_read_attr(gobj, "json_data");
|
|
10259
|
+
if (!json_data) return 0;
|
|
10260
|
+
let paths = [];
|
|
10261
|
+
let path = gobj_read_str_attr(gobj, "path") || "`";
|
|
10262
|
+
if (empty_string(path)) path = "`";
|
|
10263
|
+
collect_foldable_paths(json_data, path, paths, true);
|
|
10264
|
+
priv.collapsed = new Set(paths);
|
|
10265
|
+
refresh_json(gobj);
|
|
10266
|
+
update_find_result(gobj);
|
|
10267
|
+
return 0;
|
|
10268
|
+
}
|
|
10269
|
+
/************************************************************
|
|
10270
|
+
*
|
|
10067
10271
|
************************************************************/
|
|
10068
10272
|
function ac_theme$3(gobj, event, kw, src) {
|
|
10069
10273
|
let priv = gobj.priv;
|
|
@@ -10204,6 +10408,21 @@ function create_gclass$11(gclass_name) {
|
|
|
10204
10408
|
ac_refresh$5,
|
|
10205
10409
|
null
|
|
10206
10410
|
],
|
|
10411
|
+
[
|
|
10412
|
+
"EV_FIND_NODES",
|
|
10413
|
+
ac_find_nodes$2,
|
|
10414
|
+
null
|
|
10415
|
+
],
|
|
10416
|
+
[
|
|
10417
|
+
"EV_EXPAND_ALL",
|
|
10418
|
+
ac_expand_all$2,
|
|
10419
|
+
null
|
|
10420
|
+
],
|
|
10421
|
+
[
|
|
10422
|
+
"EV_COLLAPSE_ALL",
|
|
10423
|
+
ac_collapse_all$2,
|
|
10424
|
+
null
|
|
10425
|
+
],
|
|
10207
10426
|
[
|
|
10208
10427
|
"EV_THEME",
|
|
10209
10428
|
ac_theme$3,
|
|
@@ -10253,6 +10472,9 @@ function create_gclass$11(gclass_name) {
|
|
|
10253
10472
|
const event_types = [
|
|
10254
10473
|
["EV_LOAD_DATA", 0],
|
|
10255
10474
|
["EV_REFRESH", 0],
|
|
10475
|
+
["EV_FIND_NODES", 0],
|
|
10476
|
+
["EV_EXPAND_ALL", 0],
|
|
10477
|
+
["EV_COLLAPSE_ALL", 0],
|
|
10256
10478
|
["EV_THEME", 0],
|
|
10257
10479
|
["EV_ZOOM_IN", 0],
|
|
10258
10480
|
["EV_ZOOM_OUT", 0],
|
|
@@ -10510,16 +10732,16 @@ var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
|
10510
10732
|
var MAX_RENDER_ROWS = 5e3;
|
|
10511
10733
|
var MAX_TEXT_CHARS = 2e6;
|
|
10512
10734
|
var VIEWS = [
|
|
10513
|
-
{
|
|
10514
|
-
mode: "tree",
|
|
10515
|
-
icon: "yi-sitemap",
|
|
10516
|
-
key: "tree view"
|
|
10517
|
-
},
|
|
10518
10735
|
{
|
|
10519
10736
|
mode: "text",
|
|
10520
10737
|
icon: "yi-code",
|
|
10521
10738
|
key: "text view"
|
|
10522
10739
|
},
|
|
10740
|
+
{
|
|
10741
|
+
mode: "tree",
|
|
10742
|
+
icon: "yi-sitemap",
|
|
10743
|
+
key: "tree view"
|
|
10744
|
+
},
|
|
10523
10745
|
{
|
|
10524
10746
|
mode: "graph",
|
|
10525
10747
|
icon: "yi-hexagon-nodes",
|
package/package.json
CHANGED
package/src/c_yui_json.css
CHANGED
|
@@ -47,28 +47,34 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/*
|
|
50
|
-
* Who YIELDS when the toolbar runs out of room
|
|
51
|
-
* it grows to `max-width` when there is space and shrinks to nothing
|
|
52
|
-
* when there is not, while the buttons keep their size. A narrow
|
|
53
|
-
* search box still accepts what you type; a button pushed past the
|
|
54
|
-
* edge can only be reached by scrolling the toolbar, and the view
|
|
55
|
-
* switch is the last control that should be hiding.
|
|
50
|
+
* Who YIELDS when the toolbar runs out of room — and how far.
|
|
56
51
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* The search box yields, and the buttons keep their size: a button
|
|
53
|
+
* pushed past the edge is harder to reach than a narrower input. But
|
|
54
|
+
* it yields down to a FLOOR, not to nothing. Letting it shrink freely
|
|
55
|
+
* did make everything fit on a phone, and the result was worse than
|
|
56
|
+
* the overflow it cured: the box collapsed to 58px — the magnifier and
|
|
57
|
+
* no room to read what you typed — and, because nothing overflowed any
|
|
58
|
+
* more, `yui_toolbar` stopped offering its scroll arrows, so the row
|
|
59
|
+
* had no way to show you anything else either.
|
|
60
|
+
*
|
|
61
|
+
* With the floor the toolbar overflows again on a narrow screen, which
|
|
62
|
+
* is the state yui_toolbar is built for: the container is
|
|
63
|
+
* `overflow-x: auto` and the arrows appear.
|
|
59
64
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
/* Neither section shrinks below its content. With `min-width: 0` here
|
|
66
|
+
* the left section shrank to 70px while the search box inside it kept
|
|
67
|
+
* its floor — so the input was painted UNDERNEATH the buttons, and the
|
|
68
|
+
* container, whose own children all fitted, reported no overflow to
|
|
69
|
+
* scroll. Sized to content, the container overflows honestly. */
|
|
70
|
+
.C_YUI_JSON .yui-horizontal-toolbar-section.left,
|
|
71
|
+
.C_YUI_JSON .yui-horizontal-toolbar-section.right {
|
|
72
|
+
flex: 0 0 auto;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
.C_YUI_JSON .JSON_SEARCH_CONTROL {
|
|
66
76
|
flex: 1 1 auto;
|
|
67
|
-
min-width:
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.C_YUI_JSON .yui-horizontal-toolbar-section.right {
|
|
71
|
-
flex: 0 0 auto;
|
|
77
|
+
min-width: 11rem;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
.C_YUI_JSON .JSON_TREE {
|
package/src/c_yui_json.js
CHANGED
|
@@ -126,13 +126,14 @@ const MAX_RENDER_ROWS = 5000;
|
|
|
126
126
|
const MAX_TEXT_CHARS = 2000000;
|
|
127
127
|
|
|
128
128
|
/*
|
|
129
|
-
* The three views, in switch order
|
|
130
|
-
*
|
|
131
|
-
*
|
|
129
|
+
* The three views, in switch order — this table IS the order of the
|
|
130
|
+
* buttons, and EV_SET_VIEW_MODE with no mode advances along it.
|
|
131
|
+
* Note the order is not the default: the viewer still OPENS on the
|
|
132
|
+
* tree (the `view_mode` attr default), it just sits second in the row.
|
|
132
133
|
*/
|
|
133
134
|
const VIEWS = [
|
|
134
|
-
{mode: "tree", icon: "yi-sitemap", key: "tree view"},
|
|
135
135
|
{mode: "text", icon: "yi-code", key: "text view"},
|
|
136
|
+
{mode: "tree", icon: "yi-sitemap", key: "tree view"},
|
|
136
137
|
{mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
|
|
137
138
|
];
|
|
138
139
|
|
package/src/c_yui_json_graph.js
CHANGED
|
@@ -96,6 +96,15 @@ let PRIVATE_DATA = {
|
|
|
96
96
|
theme_observer: null, // MutationObserver on <html data-theme>
|
|
97
97
|
resize_observer: null, // ResizeObserver on the canvas mount element
|
|
98
98
|
_resize_raf: 0, // rAF id debouncing resize -> EV_RESIZE
|
|
99
|
+
|
|
100
|
+
/*---------------- find + collapse ----------------*/
|
|
101
|
+
search: "", // current find term, lower-cased
|
|
102
|
+
collapsed: null, // Set<string> of paths whose children are hidden
|
|
103
|
+
match_count: 0, // matches of the last find, for the count chip
|
|
104
|
+
find_timer: null, // rate-limits the find box
|
|
105
|
+
$find_input: null,
|
|
106
|
+
$find_result: null,
|
|
107
|
+
$find_count: null,
|
|
99
108
|
};
|
|
100
109
|
|
|
101
110
|
let __gclass__ = null;
|
|
@@ -180,6 +189,9 @@ function mt_create(gobj)
|
|
|
180
189
|
{
|
|
181
190
|
let priv = gobj.priv;
|
|
182
191
|
|
|
192
|
+
priv.collapsed = new Set();
|
|
193
|
+
priv.search = "";
|
|
194
|
+
|
|
183
195
|
let name = clean_name(gobj_name(gobj));
|
|
184
196
|
priv.canvas_id = "json-canvas-" + name;
|
|
185
197
|
gobj_write_str_attr(gobj, "canvas_id", priv.canvas_id);
|
|
@@ -233,6 +245,10 @@ function mt_stop(gobj)
|
|
|
233
245
|
***************************************************************/
|
|
234
246
|
function mt_destroy(gobj)
|
|
235
247
|
{
|
|
248
|
+
if(gobj.priv.find_timer) {
|
|
249
|
+
clearTimeout(gobj.priv.find_timer);
|
|
250
|
+
gobj.priv.find_timer = null;
|
|
251
|
+
}
|
|
236
252
|
let priv = gobj.priv;
|
|
237
253
|
|
|
238
254
|
if(priv.theme_observer) {
|
|
@@ -286,6 +302,11 @@ function build_ui(gobj)
|
|
|
286
302
|
);
|
|
287
303
|
|
|
288
304
|
gobj_write_attr(gobj, "$container", $container);
|
|
305
|
+
|
|
306
|
+
priv.$find_input = $container.querySelector('.JSON_GRAPH_FIND_INPUT');
|
|
307
|
+
priv.$find_result = $container.querySelector('.JSON_GRAPH_FIND_RESULT');
|
|
308
|
+
priv.$find_count = $container.querySelector('.JSON_GRAPH_FIND_COUNT');
|
|
309
|
+
|
|
289
310
|
refresh_language($container, t);
|
|
290
311
|
}
|
|
291
312
|
|
|
@@ -303,6 +324,30 @@ function destroy_ui(gobj)
|
|
|
303
324
|
}
|
|
304
325
|
}
|
|
305
326
|
|
|
327
|
+
/************************************************************
|
|
328
|
+
* One fold button. `open` rotates the chevron to the open
|
|
329
|
+
* state, mirroring the per-node toggle of the lazy tree.
|
|
330
|
+
************************************************************/
|
|
331
|
+
function graph_fold_button(gobj, event_name, label_key, open)
|
|
332
|
+
{
|
|
333
|
+
let icon_style = 'font-size:1.5em; color:inherit;' +
|
|
334
|
+
(open? ' transform: rotate(90deg);': '');
|
|
335
|
+
|
|
336
|
+
return ['button', {class: `button ${event_name}`,
|
|
337
|
+
type: 'button',
|
|
338
|
+
style: {height: gobj_read_attr(gobj, "wide"), width: '2.5em'},
|
|
339
|
+
title: t(label_key), 'data-i18n-title': label_key,
|
|
340
|
+
'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
|
|
341
|
+
['i', {style: icon_style, class: 'yi-chevron-right'}],
|
|
342
|
+
{
|
|
343
|
+
click: (evt) => {
|
|
344
|
+
evt.stopPropagation();
|
|
345
|
+
gobj_send_event(gobj, event_name, {}, gobj);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
306
351
|
/************************************************************
|
|
307
352
|
* Toolbar
|
|
308
353
|
************************************************************/
|
|
@@ -311,7 +356,62 @@ function make_toolbar(gobj)
|
|
|
311
356
|
let priv = gobj.priv;
|
|
312
357
|
let toolbar_wide = gobj_read_attr(gobj, "wide");
|
|
313
358
|
|
|
314
|
-
|
|
359
|
+
/*
|
|
360
|
+
* Left: find a node.
|
|
361
|
+
*
|
|
362
|
+
* A graph of a large document has no other way in — the only way to
|
|
363
|
+
* locate a key was to read every card. Same shape as the treedb
|
|
364
|
+
* graph's box: a rate-limited input into EV_FIND_NODES, and a count
|
|
365
|
+
* that SAYS how many matched, because a graph that did not move
|
|
366
|
+
* looks the same whether nothing matched or the match was already
|
|
367
|
+
* on screen.
|
|
368
|
+
*/
|
|
369
|
+
let left_items = [
|
|
370
|
+
['div', {class: 'JSON_GRAPH_FIND control has-icons-left',
|
|
371
|
+
style: 'margin-right:.5rem; max-width:12rem; min-width:7rem;'}, [
|
|
372
|
+
['input', {
|
|
373
|
+
class: 'JSON_GRAPH_FIND_INPUT input',
|
|
374
|
+
type: 'text',
|
|
375
|
+
/* A placeholder is not a text node, so the data-i18n
|
|
376
|
+
* walk cannot reach it: it needs its own key. */
|
|
377
|
+
placeholder: t('search'),
|
|
378
|
+
'data-i18n-placeholder': 'search',
|
|
379
|
+
'aria-label': t('search'),
|
|
380
|
+
'data-i18n-aria-label': 'search'
|
|
381
|
+
}],
|
|
382
|
+
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
383
|
+
], {
|
|
384
|
+
/* Rate-limited, not delayed for effect: every keystroke
|
|
385
|
+
* REBUILDS the cards — an html node paints no G6 state, so
|
|
386
|
+
* the highlight has to live in the card's own markup — and
|
|
387
|
+
* the first letter typed can match most of a document.
|
|
388
|
+
* Input plumbing: the action still crosses the FSM, just
|
|
389
|
+
* not once per keystroke. */
|
|
390
|
+
input: (evt) => {
|
|
391
|
+
evt.stopPropagation();
|
|
392
|
+
let text = evt.target.value.trim();
|
|
393
|
+
if(priv.find_timer) {
|
|
394
|
+
clearTimeout(priv.find_timer);
|
|
395
|
+
}
|
|
396
|
+
priv.find_timer = setTimeout(function() {
|
|
397
|
+
priv.find_timer = null;
|
|
398
|
+
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
399
|
+
}, 250);
|
|
400
|
+
}
|
|
401
|
+
}],
|
|
402
|
+
/* Two spans, not one string: the number is DATA and "matches"
|
|
403
|
+
* is the word, so a language switch re-translates the half that
|
|
404
|
+
* is a word. Spaced with CSS because createElement2 trims text
|
|
405
|
+
* nodes. `display:flex` inline and NOT the `is-flex` helper —
|
|
406
|
+
* both Bulma helpers carry !important, so `is-hidden is-flex` on
|
|
407
|
+
* one element is decided by stylesheet order. */
|
|
408
|
+
['div', {class: 'JSON_GRAPH_FIND_RESULT is-hidden',
|
|
409
|
+
style: 'display:flex; align-items:center; gap:.3rem; ' +
|
|
410
|
+
'margin-right:.5rem; font-size:.85rem;'}, [
|
|
411
|
+
['span', {class: 'JSON_GRAPH_FIND_COUNT'}, ''],
|
|
412
|
+
['span', {i18n: 'matches'}, 'matches']
|
|
413
|
+
]]
|
|
414
|
+
];
|
|
315
415
|
let center_items = [];
|
|
316
416
|
|
|
317
417
|
let c_icons = [
|
|
@@ -347,10 +447,24 @@ function make_toolbar(gobj)
|
|
|
347
447
|
);
|
|
348
448
|
}
|
|
349
449
|
|
|
450
|
+
/*
|
|
451
|
+
* Right: fold the tree.
|
|
452
|
+
*
|
|
453
|
+
* A JSON graph of anything real is mostly cards nobody is looking
|
|
454
|
+
* at. "Collapse all" leaves the root and marks every cut with a
|
|
455
|
+
* count, so the shape stays legible and you can see WHERE the rest
|
|
456
|
+
* went; "expand all" puts it back. Same chevron as the lazy tree,
|
|
457
|
+
* rotated to point down for the open state.
|
|
458
|
+
*/
|
|
459
|
+
let right_items = [
|
|
460
|
+
graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
|
|
461
|
+
graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false),
|
|
462
|
+
];
|
|
463
|
+
|
|
350
464
|
const $toolbar = yui_toolbar({}, [
|
|
351
465
|
['div', {class: 'yui-horizontal-toolbar-section left'}, left_items],
|
|
352
466
|
['div', {class: 'yui-horizontal-toolbar-section center'}, center_items],
|
|
353
|
-
['div', {class: 'yui-horizontal-toolbar-section right'},
|
|
467
|
+
['div', {class: 'yui-horizontal-toolbar-section right'}, right_items],
|
|
354
468
|
]);
|
|
355
469
|
|
|
356
470
|
refresh_language($toolbar, t);
|
|
@@ -440,7 +554,7 @@ function build_graph(gobj)
|
|
|
440
554
|
/************************************************************
|
|
441
555
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
442
556
|
************************************************************/
|
|
443
|
-
function build_cell_html(key, value, type, dark)
|
|
557
|
+
function build_cell_html(key, value, type, dark, matched)
|
|
444
558
|
{
|
|
445
559
|
let color = type_color(type, dark);
|
|
446
560
|
let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
|
|
@@ -470,7 +584,46 @@ function build_cell_html(key, value, type, dark)
|
|
|
470
584
|
break;
|
|
471
585
|
}
|
|
472
586
|
|
|
473
|
-
|
|
587
|
+
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span>` +
|
|
588
|
+
`<span style="color:${color}">${display_value}</span>`;
|
|
589
|
+
|
|
590
|
+
if(matched) {
|
|
591
|
+
/* The highlight is baked into the card's MARKUP and not set as
|
|
592
|
+
* a G6 node state: the key shape of an `html` node is a DOM
|
|
593
|
+
* element, and G6 paints no state style on it. Setting
|
|
594
|
+
* 'active' here would select correctly and show nothing. */
|
|
595
|
+
row = `<span style="background:${dark? "#7a5d00": "#ffe082"}; ` +
|
|
596
|
+
`border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
597
|
+
}
|
|
598
|
+
return row;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/************************************************************
|
|
602
|
+
* Does this key/value pair match the find term?
|
|
603
|
+
*
|
|
604
|
+
* Scalars are matched on what the card SHOWS. A complex value
|
|
605
|
+
* is not: its contents are a card of their own and match there,
|
|
606
|
+
* so matching the `{3}` summary too would light up every
|
|
607
|
+
* ancestor of every hit.
|
|
608
|
+
************************************************************/
|
|
609
|
+
function cell_matches(key, value, type, needle)
|
|
610
|
+
{
|
|
611
|
+
if(empty_string(needle)) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
if(String(key).toLowerCase().includes(needle)) {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
switch(type) {
|
|
618
|
+
case "string":
|
|
619
|
+
case "number":
|
|
620
|
+
case "boolean":
|
|
621
|
+
return String(value).toLowerCase().includes(needle);
|
|
622
|
+
case "null":
|
|
623
|
+
return "null".includes(needle);
|
|
624
|
+
default:
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
474
627
|
}
|
|
475
628
|
|
|
476
629
|
/************************************************************
|
|
@@ -541,15 +694,23 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
541
694
|
* The theme is read live as the card is drawn (like the gobj-tree's
|
|
542
695
|
* cards): ac_theme rebuilds on a switch, so this is always current.
|
|
543
696
|
*/
|
|
697
|
+
let priv = gobj.priv;
|
|
698
|
+
let needle = priv.search || "";
|
|
544
699
|
let dark = yui_is_dark();
|
|
545
700
|
let lines = [];
|
|
546
701
|
let pending_complex = [];
|
|
702
|
+
let card_matched = false;
|
|
547
703
|
|
|
548
704
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
549
705
|
|
|
550
706
|
for(let [key, value] of entries) {
|
|
551
707
|
let type = get_json_type(value);
|
|
552
|
-
let
|
|
708
|
+
let matched = cell_matches(key, value, type, needle);
|
|
709
|
+
if(matched) {
|
|
710
|
+
card_matched = true;
|
|
711
|
+
priv.match_count++;
|
|
712
|
+
}
|
|
713
|
+
let html = build_cell_html(key, value, type, dark, matched);
|
|
553
714
|
lines.push(html);
|
|
554
715
|
|
|
555
716
|
if((is_object(value) && json_object_size(value) > 0) ||
|
|
@@ -559,9 +720,27 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
559
720
|
}
|
|
560
721
|
|
|
561
722
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
723
|
+
|
|
724
|
+
/*
|
|
725
|
+
* Folded: keep the card, drop the branch, and SAY how much went
|
|
726
|
+
* with it. A cut that is not marked reads as a document that ends
|
|
727
|
+
* there — the same rule the lazy tree follows for a `__collapsed__`
|
|
728
|
+
* sentinel. The badge is a glyph and a number on purpose: the card
|
|
729
|
+
* is an innerHTML string, where a word could carry no i18n key and
|
|
730
|
+
* would sit there in English forever.
|
|
731
|
+
*/
|
|
732
|
+
let folded = priv.collapsed.has(path) && pending_complex.length > 0;
|
|
733
|
+
if(folded) {
|
|
734
|
+
lines.push(
|
|
735
|
+
`<span style="color:${type_color("number", dark)}; font-weight:bold;">` +
|
|
736
|
+
`▸ ${pending_complex.length}</span>`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
562
739
|
let colors = json_card_style(
|
|
563
740
|
is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
|
|
564
741
|
);
|
|
742
|
+
let border_color = card_matched? (dark? "#ffb300": "#ff8f00"): colors.border;
|
|
743
|
+
let border_width = card_matched? 2: 1;
|
|
565
744
|
|
|
566
745
|
/*
|
|
567
746
|
* Build node HTML content
|
|
@@ -576,7 +755,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
576
755
|
<div style="
|
|
577
756
|
min-width: ${min_width}px;
|
|
578
757
|
background: ${colors.bg};
|
|
579
|
-
border:
|
|
758
|
+
border: ${border_width}px ${is_dict ? 'dashed' : 'solid'} ${border_color};
|
|
580
759
|
border-radius: ${is_dict ? '6px' : '0'};
|
|
581
760
|
opacity: 0.9;
|
|
582
761
|
overflow: hidden;
|
|
@@ -623,8 +802,11 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
623
802
|
}
|
|
624
803
|
|
|
625
804
|
/*
|
|
626
|
-
* Recurse into complex children
|
|
805
|
+
* Recurse into complex children — unless this card is folded.
|
|
627
806
|
*/
|
|
807
|
+
if(folded) {
|
|
808
|
+
return group_id;
|
|
809
|
+
}
|
|
628
810
|
for(let pending of pending_complex) {
|
|
629
811
|
build_json_nodes(
|
|
630
812
|
gobj,
|
|
@@ -663,6 +845,7 @@ function load_json(gobj)
|
|
|
663
845
|
}
|
|
664
846
|
|
|
665
847
|
priv.node_counter = 0;
|
|
848
|
+
priv.match_count = 0;
|
|
666
849
|
|
|
667
850
|
let nodes = [];
|
|
668
851
|
let edges = [];
|
|
@@ -682,6 +865,57 @@ function load_json(gobj)
|
|
|
682
865
|
}
|
|
683
866
|
}
|
|
684
867
|
|
|
868
|
+
/************************************************************
|
|
869
|
+
* Every path that HAS a branch to fold, in the same recursion
|
|
870
|
+
* build_json_nodes uses — so "collapse all" can never name a
|
|
871
|
+
* card that does not exist. The root is excluded: folding it
|
|
872
|
+
* would leave an empty canvas.
|
|
873
|
+
************************************************************/
|
|
874
|
+
function collect_foldable_paths(value, path, out, is_root)
|
|
875
|
+
{
|
|
876
|
+
let is_dict = is_object(value);
|
|
877
|
+
let is_list = is_array(value);
|
|
878
|
+
if(!is_dict && !is_list) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
let has_branch = false;
|
|
883
|
+
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
884
|
+
for(let [key, child] of entries) {
|
|
885
|
+
if((is_object(child) && json_object_size(child) > 0) ||
|
|
886
|
+
(is_array(child) && child.length > 0)) {
|
|
887
|
+
has_branch = true;
|
|
888
|
+
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if(has_branch && !is_root) {
|
|
893
|
+
out.push(path);
|
|
894
|
+
}
|
|
895
|
+
return has_branch;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/************************************************************
|
|
899
|
+
* Show the find count, or hide the chip when there is no
|
|
900
|
+
* term. Zero is a RESULT and must show: a graph that did not
|
|
901
|
+
* move looks the same whether nothing matched or the match
|
|
902
|
+
* was already on screen.
|
|
903
|
+
************************************************************/
|
|
904
|
+
function update_find_result(gobj)
|
|
905
|
+
{
|
|
906
|
+
let priv = gobj.priv;
|
|
907
|
+
if(!priv.$find_result || !priv.$find_count) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if(empty_string(priv.search)) {
|
|
911
|
+
priv.$find_result.classList.add("is-hidden");
|
|
912
|
+
priv.$find_count.textContent = "";
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
priv.$find_count.textContent = String(priv.match_count);
|
|
916
|
+
priv.$find_result.classList.remove("is-hidden");
|
|
917
|
+
}
|
|
918
|
+
|
|
685
919
|
/************************************************************
|
|
686
920
|
* Clear and reload.
|
|
687
921
|
* {preserve_view: true}
|
|
@@ -863,6 +1097,66 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
863
1097
|
|
|
864
1098
|
/************************************************************
|
|
865
1099
|
* {theme: "dark"|"light"} — the app switched theme.
|
|
1100
|
+
* EV_FIND_NODES { text }
|
|
1101
|
+
*
|
|
1102
|
+
* The camera does NOT move: a find is a way of reading what is
|
|
1103
|
+
* already on screen, and a viewport that jumps on every
|
|
1104
|
+
* keystroke is unusable. The count says whether there is
|
|
1105
|
+
* anything to look for elsewhere.
|
|
1106
|
+
************************************************************/
|
|
1107
|
+
function ac_find_nodes(gobj, event, kw, src)
|
|
1108
|
+
{
|
|
1109
|
+
let priv = gobj.priv;
|
|
1110
|
+
|
|
1111
|
+
priv.search = ((kw && kw.text) || "").trim().toLowerCase();
|
|
1112
|
+
refresh_json(gobj, {preserve_view: true});
|
|
1113
|
+
update_find_result(gobj);
|
|
1114
|
+
return 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/************************************************************
|
|
1118
|
+
* EV_EXPAND_ALL
|
|
1119
|
+
************************************************************/
|
|
1120
|
+
function ac_expand_all(gobj, event, kw, src)
|
|
1121
|
+
{
|
|
1122
|
+
let priv = gobj.priv;
|
|
1123
|
+
|
|
1124
|
+
if(priv.collapsed.size === 0) {
|
|
1125
|
+
return 0; // nothing folded; a relayout would only move the camera
|
|
1126
|
+
}
|
|
1127
|
+
priv.collapsed.clear();
|
|
1128
|
+
refresh_json(gobj);
|
|
1129
|
+
update_find_result(gobj);
|
|
1130
|
+
return 0;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/************************************************************
|
|
1134
|
+
* EV_COLLAPSE_ALL — fold every card but the root.
|
|
1135
|
+
************************************************************/
|
|
1136
|
+
function ac_collapse_all(gobj, event, kw, src)
|
|
1137
|
+
{
|
|
1138
|
+
let priv = gobj.priv;
|
|
1139
|
+
|
|
1140
|
+
let json_data = gobj_read_attr(gobj, "json_data");
|
|
1141
|
+
if(!json_data) {
|
|
1142
|
+
return 0;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
let paths = [];
|
|
1146
|
+
let path = gobj_read_str_attr(gobj, "path") || "`";
|
|
1147
|
+
if(empty_string(path)) {
|
|
1148
|
+
path = "`";
|
|
1149
|
+
}
|
|
1150
|
+
collect_foldable_paths(json_data, path, paths, true);
|
|
1151
|
+
|
|
1152
|
+
priv.collapsed = new Set(paths);
|
|
1153
|
+
refresh_json(gobj);
|
|
1154
|
+
update_find_result(gobj);
|
|
1155
|
+
return 0;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/************************************************************
|
|
1159
|
+
*
|
|
866
1160
|
************************************************************/
|
|
867
1161
|
function ac_theme(gobj, event, kw, src)
|
|
868
1162
|
{
|
|
@@ -1054,6 +1348,9 @@ function create_gclass(gclass_name)
|
|
|
1054
1348
|
["ST_IDLE", [
|
|
1055
1349
|
["EV_LOAD_DATA", ac_load_data, null],
|
|
1056
1350
|
["EV_REFRESH", ac_refresh, null],
|
|
1351
|
+
["EV_FIND_NODES", ac_find_nodes, null],
|
|
1352
|
+
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
1353
|
+
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
1057
1354
|
["EV_THEME", ac_theme, null],
|
|
1058
1355
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
1059
1356
|
["EV_ZOOM_OUT", ac_zoom_out, null],
|
|
@@ -1072,6 +1369,9 @@ function create_gclass(gclass_name)
|
|
|
1072
1369
|
const event_types = [
|
|
1073
1370
|
["EV_LOAD_DATA", 0],
|
|
1074
1371
|
["EV_REFRESH", 0],
|
|
1372
|
+
["EV_FIND_NODES", 0],
|
|
1373
|
+
["EV_EXPAND_ALL", 0],
|
|
1374
|
+
["EV_COLLAPSE_ALL", 0],
|
|
1075
1375
|
["EV_THEME", 0],
|
|
1076
1376
|
["EV_ZOOM_IN", 0],
|
|
1077
1377
|
["EV_ZOOM_OUT", 0],
|