@yuneta/gobj-ui 7.23.16 → 7.23.17
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/dist/gobj-ui.cjs.js +123 -14
- package/dist/gobj-ui.es.js +123 -14
- package/package.json +1 -1
- package/src/c_yui_json.css +13 -0
- package/src/c_yui_json.js +84 -6
- package/src/c_yui_json_graph.js +43 -10
- package/src/json_view_helpers.js +72 -0
- package/src/json_view_helpers.test.js +68 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -10110,18 +10110,34 @@ var GROUP_COLORS = {
|
|
|
10110
10110
|
};
|
|
10111
10111
|
/***************************************************************
|
|
10112
10112
|
* Soft, theme-aware card palette, same visual language as the
|
|
10113
|
-
* gobj-tree's role_card_style():
|
|
10114
|
-
*
|
|
10115
|
-
*
|
|
10113
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
10114
|
+
* group-colour border.
|
|
10115
|
+
*
|
|
10116
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
10117
|
+
* the whole rule here. The light card is near-white and lets
|
|
10118
|
+
* saturated dark text carry the colour; the dark card is its
|
|
10119
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
10120
|
+
* the group's hue lives in the border and the header bar, which
|
|
10121
|
+
* is where the light theme had always put it.
|
|
10122
|
+
*
|
|
10123
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
10124
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
10125
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
10126
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
10127
|
+
* for the same values on the light card. The 12% left is enough
|
|
10128
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
10129
|
+
* text.
|
|
10116
10130
|
*
|
|
10117
10131
|
* The header bar is always the border colour, so its text flips:
|
|
10118
10132
|
* white on the dark-teal light bar, near-black on the brightened
|
|
10119
|
-
* dark one.
|
|
10133
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
10134
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
10135
|
+
* white gets on the light bar).
|
|
10120
10136
|
***************************************************************/
|
|
10121
10137
|
function json_card_style(group, dark) {
|
|
10122
10138
|
return {
|
|
10123
|
-
bg: dark ? `color-mix(in srgb, ${group.tint}
|
|
10124
|
-
border: dark ? `color-mix(in srgb, ${group.stroke}
|
|
10139
|
+
bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
|
|
10140
|
+
border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
|
|
10125
10141
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
10126
10142
|
key: dark ? "#c9cfd8" : "#1A1A1A"
|
|
10127
10143
|
};
|
|
@@ -10130,11 +10146,18 @@ function json_card_style(group, dark) {
|
|
|
10130
10146
|
* A scalar's colour by type. The palette is tuned for a light
|
|
10131
10147
|
* card; on a dark one every one of them (dark green #006000,
|
|
10132
10148
|
* blue #475ED0, …) sinks into the background, so brighten it.
|
|
10149
|
+
*
|
|
10150
|
+
* 60% white and not the 45% it shipped with. The number is a
|
|
10151
|
+
* balance measured both ways: brighter reads better on the dark
|
|
10152
|
+
* card (the worst type goes from 2.88:1 to 4.74:1) and washes the
|
|
10153
|
+
* six types toward one pastel, and past this point red, orange
|
|
10154
|
+
* and purple stop being tellable apart, which is the only reason
|
|
10155
|
+
* they are coloured at all.
|
|
10133
10156
|
***************************************************************/
|
|
10134
10157
|
function type_color(type, dark) {
|
|
10135
10158
|
let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
|
|
10136
10159
|
if (!dark) return color;
|
|
10137
|
-
return `color-mix(in srgb, ${color}
|
|
10160
|
+
return `color-mix(in srgb, ${color} 40%, #ffffff)`;
|
|
10138
10161
|
}
|
|
10139
10162
|
/******************************
|
|
10140
10163
|
* Framework Methods
|
|
@@ -10431,8 +10454,9 @@ function build_graph$3(gobj) {
|
|
|
10431
10454
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10432
10455
|
************************************************************/
|
|
10433
10456
|
function build_cell_html(key, value, type, dark, matched) {
|
|
10434
|
-
let
|
|
10435
|
-
let
|
|
10457
|
+
let on_light = matched ? false : dark;
|
|
10458
|
+
let color = type_color(type, on_light);
|
|
10459
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
10436
10460
|
let display_value = "";
|
|
10437
10461
|
switch (type) {
|
|
10438
10462
|
case "string":
|
|
@@ -10456,7 +10480,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10456
10480
|
default: display_value = String(value);
|
|
10457
10481
|
}
|
|
10458
10482
|
let row = `<span style="color:${key_color}">• ${(0, _yuneta_gobj_js.escapeHtml)(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
10459
|
-
if (matched) row = `<span style="background
|
|
10483
|
+
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10460
10484
|
return row;
|
|
10461
10485
|
}
|
|
10462
10486
|
/************************************************************
|
|
@@ -11341,6 +11365,56 @@ function json_text_dump(root, max_chars) {
|
|
|
11341
11365
|
capped: false
|
|
11342
11366
|
};
|
|
11343
11367
|
}
|
|
11368
|
+
/************************************************************
|
|
11369
|
+
* Which view a freshly mounted viewer opens on.
|
|
11370
|
+
*
|
|
11371
|
+
* Three answers, in order of authority: what the HOST asked
|
|
11372
|
+
* for, what the READER chose last time, and `fallback`.
|
|
11373
|
+
*
|
|
11374
|
+
* The host wins because a viewer mounted to show a graph has
|
|
11375
|
+
* to show one. The memory comes next, because reopening a
|
|
11376
|
+
* document in the view you just left is the whole point of
|
|
11377
|
+
* remembering it. `fallback` is the floor.
|
|
11378
|
+
*
|
|
11379
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
11380
|
+
* an absent value, an empty one, and a stored string from an
|
|
11381
|
+
* older release that named a view no longer offered.
|
|
11382
|
+
************************************************************/
|
|
11383
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11384
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11385
|
+
if (known(asked)) return asked;
|
|
11386
|
+
if (known(remembered)) return remembered;
|
|
11387
|
+
return fallback;
|
|
11388
|
+
}
|
|
11389
|
+
/************************************************************
|
|
11390
|
+
* The short label a container row carries beside its size.
|
|
11391
|
+
*
|
|
11392
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11393
|
+
* and a row that says only `2: {15}` makes you open all
|
|
11394
|
+
* fifteen fields of every one of them to find out WHICH
|
|
11395
|
+
* record it is. The id is the one field that answers that,
|
|
11396
|
+
* so the row says it without being opened.
|
|
11397
|
+
*
|
|
11398
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11399
|
+
* that is itself a dict or a list is not a name, it is more
|
|
11400
|
+
* document, and printing `[object Object]` beside the size
|
|
11401
|
+
* would be worse than printing nothing.
|
|
11402
|
+
*
|
|
11403
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
11404
|
+
* is to stay one line.
|
|
11405
|
+
************************************************************/
|
|
11406
|
+
function container_label(value, max_chars) {
|
|
11407
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11408
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11409
|
+
let id = value.id;
|
|
11410
|
+
let t = typeof id;
|
|
11411
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11412
|
+
let text = String(id).trim();
|
|
11413
|
+
if (text === "") return "";
|
|
11414
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11415
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11416
|
+
return text;
|
|
11417
|
+
}
|
|
11344
11418
|
//#endregion
|
|
11345
11419
|
//#region src/c_yui_json.js
|
|
11346
11420
|
/***********************************************************************
|
|
@@ -11371,7 +11445,9 @@ function json_text_dump(root, max_chars) {
|
|
|
11371
11445
|
*
|
|
11372
11446
|
* THREE VIEWS over the same working document, chosen by the
|
|
11373
11447
|
* `view_mode` attr ("tree" | "text" | "graph") and switched from the
|
|
11374
|
-
* toolbar.
|
|
11448
|
+
* toolbar. The viewer opens on what the host asked for, else on the
|
|
11449
|
+
* view the reader chose last (kept in localStorage), else on the tree.
|
|
11450
|
+
* They answer three different questions:
|
|
11375
11451
|
*
|
|
11376
11452
|
* tree where is this value, and what is around it — the lazy
|
|
11377
11453
|
* view above, the only one that can drill.
|
|
@@ -11405,6 +11481,7 @@ function json_text_dump(root, max_chars) {
|
|
|
11405
11481
|
var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
11406
11482
|
var MAX_RENDER_ROWS = 5e3;
|
|
11407
11483
|
var MAX_TEXT_CHARS = 2e6;
|
|
11484
|
+
var MAX_LABEL_CHARS = 40;
|
|
11408
11485
|
var VIEWS = [
|
|
11409
11486
|
{
|
|
11410
11487
|
mode: "text",
|
|
@@ -11422,6 +11499,7 @@ var VIEWS = [
|
|
|
11422
11499
|
key: "graph view"
|
|
11423
11500
|
}
|
|
11424
11501
|
];
|
|
11502
|
+
var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
|
|
11425
11503
|
/***************************************************************
|
|
11426
11504
|
* Data
|
|
11427
11505
|
***************************************************************/
|
|
@@ -11429,7 +11507,7 @@ var attrs_table$10 = [
|
|
|
11429
11507
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
11430
11508
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
11431
11509
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
|
|
11432
|
-
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "
|
|
11510
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
|
|
11433
11511
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
11434
11512
|
(0, _yuneta_gobj_js.SDATA_END)()
|
|
11435
11513
|
];
|
|
@@ -11466,6 +11544,7 @@ function mt_create$10(gobj) {
|
|
|
11466
11544
|
let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
|
|
11467
11545
|
if (json_data !== null && json_data !== void 0) priv.root = (0, _yuneta_gobj_js.json_deep_copy)(json_data);
|
|
11468
11546
|
build_ui$9(gobj);
|
|
11547
|
+
resolve_initial_view_mode(gobj);
|
|
11469
11548
|
apply_view_mode(gobj);
|
|
11470
11549
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
11471
11550
|
if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
|
|
@@ -11723,6 +11802,27 @@ function view_label(mode) {
|
|
|
11723
11802
|
}
|
|
11724
11803
|
}
|
|
11725
11804
|
/************************************************************
|
|
11805
|
+
* The view the viewer OPENS on.
|
|
11806
|
+
*
|
|
11807
|
+
* Three answers, in order of authority: what the HOST asked for, what
|
|
11808
|
+
* the READER chose last time, and the tree. The host wins because a
|
|
11809
|
+
* viewer mounted to show a graph has to show one; the memory comes
|
|
11810
|
+
* next, because reopening a document in the view you just left is the
|
|
11811
|
+
* whole point of remembering it; the tree is the floor.
|
|
11812
|
+
*
|
|
11813
|
+
* This is why the attr no longer declares "tree" as its default: as a
|
|
11814
|
+
* default and as a host's explicit choice it was the same string, so
|
|
11815
|
+
* nothing could tell "show me the tree" from "I have no opinion", and
|
|
11816
|
+
* a memory that cannot see the difference has to lose to both.
|
|
11817
|
+
*
|
|
11818
|
+
* Resolved ONCE here and written into the attr, so every later read
|
|
11819
|
+
* is a plain attr read and no render touches the store.
|
|
11820
|
+
************************************************************/
|
|
11821
|
+
function resolve_initial_view_mode(gobj) {
|
|
11822
|
+
let mode = pick_view_mode((0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "view_mode"), (0, _yuneta_gobj_js.kw_get_local_storage_value)(VIEW_MODE_STORAGE_KEY, "", false), VIEWS.map((v) => v.mode), "tree");
|
|
11823
|
+
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
|
|
11824
|
+
}
|
|
11825
|
+
/************************************************************
|
|
11726
11826
|
* The current view, normalised. Anything unknown is the tree.
|
|
11727
11827
|
************************************************************/
|
|
11728
11828
|
function current_view_mode(gobj) {
|
|
@@ -11972,7 +12072,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
|
|
|
11972
12072
|
let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
|
|
11973
12073
|
let open = priv.expanded.has(path) || searching_match;
|
|
11974
12074
|
ctx.count++;
|
|
11975
|
-
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
|
|
12075
|
+
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
|
|
11976
12076
|
if (!open) return;
|
|
11977
12077
|
if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
|
|
11978
12078
|
else value.forEach(function(v, i) {
|
|
@@ -12057,7 +12157,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
|
|
|
12057
12157
|
/************************************************************
|
|
12058
12158
|
* Expandable container header row
|
|
12059
12159
|
************************************************************/
|
|
12060
|
-
function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
12160
|
+
function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
|
|
12061
12161
|
let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
|
|
12062
12162
|
let body = [];
|
|
12063
12163
|
if (key !== null && key !== "") {
|
|
@@ -12077,6 +12177,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
|
12077
12177
|
{ class: "JSON_SUMMARY has-text-grey" },
|
|
12078
12178
|
summary
|
|
12079
12179
|
]);
|
|
12180
|
+
let label = container_label(value, MAX_LABEL_CHARS);
|
|
12181
|
+
if (label) body.push([
|
|
12182
|
+
"span",
|
|
12183
|
+
{ class: "JSON_SUMMARY_ID ml-2" },
|
|
12184
|
+
label
|
|
12185
|
+
]);
|
|
12080
12186
|
return [
|
|
12081
12187
|
"div",
|
|
12082
12188
|
{
|
|
@@ -12330,6 +12436,8 @@ function ac_copy_all(gobj, event, kw, src) {
|
|
|
12330
12436
|
* With no mode it ADVANCES to the next view in VIEWS order, which
|
|
12331
12437
|
* is what the two-view toggle did when the list was two long. The
|
|
12332
12438
|
* toolbar buttons always name their mode.
|
|
12439
|
+
*
|
|
12440
|
+
* Whatever lands here is REMEMBERED, so the next viewer opens on it.
|
|
12333
12441
|
************************************************************/
|
|
12334
12442
|
function ac_set_view_mode(gobj, event, kw, src) {
|
|
12335
12443
|
let mode = kw.mode;
|
|
@@ -12340,6 +12448,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
|
|
|
12340
12448
|
}
|
|
12341
12449
|
if (mode === current_view_mode(gobj)) return 0;
|
|
12342
12450
|
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
|
|
12451
|
+
(0, _yuneta_gobj_js.kw_set_local_storage_value)(VIEW_MODE_STORAGE_KEY, mode);
|
|
12343
12452
|
apply_view_mode(gobj);
|
|
12344
12453
|
render_view(gobj);
|
|
12345
12454
|
return 0;
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -10105,18 +10105,34 @@ var GROUP_COLORS = {
|
|
|
10105
10105
|
};
|
|
10106
10106
|
/***************************************************************
|
|
10107
10107
|
* Soft, theme-aware card palette, same visual language as the
|
|
10108
|
-
* gobj-tree's role_card_style():
|
|
10109
|
-
*
|
|
10110
|
-
*
|
|
10108
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
10109
|
+
* group-colour border.
|
|
10110
|
+
*
|
|
10111
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
10112
|
+
* the whole rule here. The light card is near-white and lets
|
|
10113
|
+
* saturated dark text carry the colour; the dark card is its
|
|
10114
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
10115
|
+
* the group's hue lives in the border and the header bar, which
|
|
10116
|
+
* is where the light theme had always put it.
|
|
10117
|
+
*
|
|
10118
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
10119
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
10120
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
10121
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
10122
|
+
* for the same values on the light card. The 12% left is enough
|
|
10123
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
10124
|
+
* text.
|
|
10111
10125
|
*
|
|
10112
10126
|
* The header bar is always the border colour, so its text flips:
|
|
10113
10127
|
* white on the dark-teal light bar, near-black on the brightened
|
|
10114
|
-
* dark one.
|
|
10128
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
10129
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
10130
|
+
* white gets on the light bar).
|
|
10115
10131
|
***************************************************************/
|
|
10116
10132
|
function json_card_style(group, dark) {
|
|
10117
10133
|
return {
|
|
10118
|
-
bg: dark ? `color-mix(in srgb, ${group.tint}
|
|
10119
|
-
border: dark ? `color-mix(in srgb, ${group.stroke}
|
|
10134
|
+
bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
|
|
10135
|
+
border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
|
|
10120
10136
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
10121
10137
|
key: dark ? "#c9cfd8" : "#1A1A1A"
|
|
10122
10138
|
};
|
|
@@ -10125,11 +10141,18 @@ function json_card_style(group, dark) {
|
|
|
10125
10141
|
* A scalar's colour by type. The palette is tuned for a light
|
|
10126
10142
|
* card; on a dark one every one of them (dark green #006000,
|
|
10127
10143
|
* blue #475ED0, …) sinks into the background, so brighten it.
|
|
10144
|
+
*
|
|
10145
|
+
* 60% white and not the 45% it shipped with. The number is a
|
|
10146
|
+
* balance measured both ways: brighter reads better on the dark
|
|
10147
|
+
* card (the worst type goes from 2.88:1 to 4.74:1) and washes the
|
|
10148
|
+
* six types toward one pastel, and past this point red, orange
|
|
10149
|
+
* and purple stop being tellable apart, which is the only reason
|
|
10150
|
+
* they are coloured at all.
|
|
10128
10151
|
***************************************************************/
|
|
10129
10152
|
function type_color(type, dark) {
|
|
10130
10153
|
let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
|
|
10131
10154
|
if (!dark) return color;
|
|
10132
|
-
return `color-mix(in srgb, ${color}
|
|
10155
|
+
return `color-mix(in srgb, ${color} 40%, #ffffff)`;
|
|
10133
10156
|
}
|
|
10134
10157
|
/******************************
|
|
10135
10158
|
* Framework Methods
|
|
@@ -10426,8 +10449,9 @@ function build_graph$3(gobj) {
|
|
|
10426
10449
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10427
10450
|
************************************************************/
|
|
10428
10451
|
function build_cell_html(key, value, type, dark, matched) {
|
|
10429
|
-
let
|
|
10430
|
-
let
|
|
10452
|
+
let on_light = matched ? false : dark;
|
|
10453
|
+
let color = type_color(type, on_light);
|
|
10454
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
10431
10455
|
let display_value = "";
|
|
10432
10456
|
switch (type) {
|
|
10433
10457
|
case "string":
|
|
@@ -10451,7 +10475,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10451
10475
|
default: display_value = String(value);
|
|
10452
10476
|
}
|
|
10453
10477
|
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
10454
|
-
if (matched) row = `<span style="background
|
|
10478
|
+
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10455
10479
|
return row;
|
|
10456
10480
|
}
|
|
10457
10481
|
/************************************************************
|
|
@@ -11336,6 +11360,56 @@ function json_text_dump(root, max_chars) {
|
|
|
11336
11360
|
capped: false
|
|
11337
11361
|
};
|
|
11338
11362
|
}
|
|
11363
|
+
/************************************************************
|
|
11364
|
+
* Which view a freshly mounted viewer opens on.
|
|
11365
|
+
*
|
|
11366
|
+
* Three answers, in order of authority: what the HOST asked
|
|
11367
|
+
* for, what the READER chose last time, and `fallback`.
|
|
11368
|
+
*
|
|
11369
|
+
* The host wins because a viewer mounted to show a graph has
|
|
11370
|
+
* to show one. The memory comes next, because reopening a
|
|
11371
|
+
* document in the view you just left is the whole point of
|
|
11372
|
+
* remembering it. `fallback` is the floor.
|
|
11373
|
+
*
|
|
11374
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
11375
|
+
* an absent value, an empty one, and a stored string from an
|
|
11376
|
+
* older release that named a view no longer offered.
|
|
11377
|
+
************************************************************/
|
|
11378
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11379
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11380
|
+
if (known(asked)) return asked;
|
|
11381
|
+
if (known(remembered)) return remembered;
|
|
11382
|
+
return fallback;
|
|
11383
|
+
}
|
|
11384
|
+
/************************************************************
|
|
11385
|
+
* The short label a container row carries beside its size.
|
|
11386
|
+
*
|
|
11387
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11388
|
+
* and a row that says only `2: {15}` makes you open all
|
|
11389
|
+
* fifteen fields of every one of them to find out WHICH
|
|
11390
|
+
* record it is. The id is the one field that answers that,
|
|
11391
|
+
* so the row says it without being opened.
|
|
11392
|
+
*
|
|
11393
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11394
|
+
* that is itself a dict or a list is not a name, it is more
|
|
11395
|
+
* document, and printing `[object Object]` beside the size
|
|
11396
|
+
* would be worse than printing nothing.
|
|
11397
|
+
*
|
|
11398
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
11399
|
+
* is to stay one line.
|
|
11400
|
+
************************************************************/
|
|
11401
|
+
function container_label(value, max_chars) {
|
|
11402
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11403
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11404
|
+
let id = value.id;
|
|
11405
|
+
let t = typeof id;
|
|
11406
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11407
|
+
let text = String(id).trim();
|
|
11408
|
+
if (text === "") return "";
|
|
11409
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11410
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11411
|
+
return text;
|
|
11412
|
+
}
|
|
11339
11413
|
//#endregion
|
|
11340
11414
|
//#region src/c_yui_json.js
|
|
11341
11415
|
/***********************************************************************
|
|
@@ -11366,7 +11440,9 @@ function json_text_dump(root, max_chars) {
|
|
|
11366
11440
|
*
|
|
11367
11441
|
* THREE VIEWS over the same working document, chosen by the
|
|
11368
11442
|
* `view_mode` attr ("tree" | "text" | "graph") and switched from the
|
|
11369
|
-
* toolbar.
|
|
11443
|
+
* toolbar. The viewer opens on what the host asked for, else on the
|
|
11444
|
+
* view the reader chose last (kept in localStorage), else on the tree.
|
|
11445
|
+
* They answer three different questions:
|
|
11370
11446
|
*
|
|
11371
11447
|
* tree where is this value, and what is around it — the lazy
|
|
11372
11448
|
* view above, the only one that can drill.
|
|
@@ -11400,6 +11476,7 @@ function json_text_dump(root, max_chars) {
|
|
|
11400
11476
|
var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
11401
11477
|
var MAX_RENDER_ROWS = 5e3;
|
|
11402
11478
|
var MAX_TEXT_CHARS = 2e6;
|
|
11479
|
+
var MAX_LABEL_CHARS = 40;
|
|
11403
11480
|
var VIEWS = [
|
|
11404
11481
|
{
|
|
11405
11482
|
mode: "text",
|
|
@@ -11417,6 +11494,7 @@ var VIEWS = [
|
|
|
11417
11494
|
key: "graph view"
|
|
11418
11495
|
}
|
|
11419
11496
|
];
|
|
11497
|
+
var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
|
|
11420
11498
|
/***************************************************************
|
|
11421
11499
|
* Data
|
|
11422
11500
|
***************************************************************/
|
|
@@ -11424,7 +11502,7 @@ var attrs_table$10 = [
|
|
|
11424
11502
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
11425
11503
|
SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
11426
11504
|
SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
|
|
11427
|
-
SDATA(data_type_t.DTP_STRING, "view_mode", 0, "
|
|
11505
|
+
SDATA(data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
|
|
11428
11506
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
11429
11507
|
SDATA_END()
|
|
11430
11508
|
];
|
|
@@ -11461,6 +11539,7 @@ function mt_create$10(gobj) {
|
|
|
11461
11539
|
let json_data = gobj_read_attr(gobj, "json_data");
|
|
11462
11540
|
if (json_data !== null && json_data !== void 0) priv.root = json_deep_copy(json_data);
|
|
11463
11541
|
build_ui$9(gobj);
|
|
11542
|
+
resolve_initial_view_mode(gobj);
|
|
11464
11543
|
apply_view_mode(gobj);
|
|
11465
11544
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
11466
11545
|
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
@@ -11718,6 +11797,27 @@ function view_label(mode) {
|
|
|
11718
11797
|
}
|
|
11719
11798
|
}
|
|
11720
11799
|
/************************************************************
|
|
11800
|
+
* The view the viewer OPENS on.
|
|
11801
|
+
*
|
|
11802
|
+
* Three answers, in order of authority: what the HOST asked for, what
|
|
11803
|
+
* the READER chose last time, and the tree. The host wins because a
|
|
11804
|
+
* viewer mounted to show a graph has to show one; the memory comes
|
|
11805
|
+
* next, because reopening a document in the view you just left is the
|
|
11806
|
+
* whole point of remembering it; the tree is the floor.
|
|
11807
|
+
*
|
|
11808
|
+
* This is why the attr no longer declares "tree" as its default: as a
|
|
11809
|
+
* default and as a host's explicit choice it was the same string, so
|
|
11810
|
+
* nothing could tell "show me the tree" from "I have no opinion", and
|
|
11811
|
+
* a memory that cannot see the difference has to lose to both.
|
|
11812
|
+
*
|
|
11813
|
+
* Resolved ONCE here and written into the attr, so every later read
|
|
11814
|
+
* is a plain attr read and no render touches the store.
|
|
11815
|
+
************************************************************/
|
|
11816
|
+
function resolve_initial_view_mode(gobj) {
|
|
11817
|
+
let mode = pick_view_mode(gobj_read_str_attr(gobj, "view_mode"), kw_get_local_storage_value(VIEW_MODE_STORAGE_KEY, "", false), VIEWS.map((v) => v.mode), "tree");
|
|
11818
|
+
gobj_write_attr(gobj, "view_mode", mode);
|
|
11819
|
+
}
|
|
11820
|
+
/************************************************************
|
|
11721
11821
|
* The current view, normalised. Anything unknown is the tree.
|
|
11722
11822
|
************************************************************/
|
|
11723
11823
|
function current_view_mode(gobj) {
|
|
@@ -11967,7 +12067,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
|
|
|
11967
12067
|
let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
|
|
11968
12068
|
let open = priv.expanded.has(path) || searching_match;
|
|
11969
12069
|
ctx.count++;
|
|
11970
|
-
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
|
|
12070
|
+
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
|
|
11971
12071
|
if (!open) return;
|
|
11972
12072
|
if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
|
|
11973
12073
|
else value.forEach(function(v, i) {
|
|
@@ -12052,7 +12152,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
|
|
|
12052
12152
|
/************************************************************
|
|
12053
12153
|
* Expandable container header row
|
|
12054
12154
|
************************************************************/
|
|
12055
|
-
function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
12155
|
+
function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
|
|
12056
12156
|
let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
|
|
12057
12157
|
let body = [];
|
|
12058
12158
|
if (key !== null && key !== "") {
|
|
@@ -12072,6 +12172,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
|
12072
12172
|
{ class: "JSON_SUMMARY has-text-grey" },
|
|
12073
12173
|
summary
|
|
12074
12174
|
]);
|
|
12175
|
+
let label = container_label(value, MAX_LABEL_CHARS);
|
|
12176
|
+
if (label) body.push([
|
|
12177
|
+
"span",
|
|
12178
|
+
{ class: "JSON_SUMMARY_ID ml-2" },
|
|
12179
|
+
label
|
|
12180
|
+
]);
|
|
12075
12181
|
return [
|
|
12076
12182
|
"div",
|
|
12077
12183
|
{
|
|
@@ -12325,6 +12431,8 @@ function ac_copy_all(gobj, event, kw, src) {
|
|
|
12325
12431
|
* With no mode it ADVANCES to the next view in VIEWS order, which
|
|
12326
12432
|
* is what the two-view toggle did when the list was two long. The
|
|
12327
12433
|
* toolbar buttons always name their mode.
|
|
12434
|
+
*
|
|
12435
|
+
* Whatever lands here is REMEMBERED, so the next viewer opens on it.
|
|
12328
12436
|
************************************************************/
|
|
12329
12437
|
function ac_set_view_mode(gobj, event, kw, src) {
|
|
12330
12438
|
let mode = kw.mode;
|
|
@@ -12335,6 +12443,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
|
|
|
12335
12443
|
}
|
|
12336
12444
|
if (mode === current_view_mode(gobj)) return 0;
|
|
12337
12445
|
gobj_write_attr(gobj, "view_mode", mode);
|
|
12446
|
+
kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
|
|
12338
12447
|
apply_view_mode(gobj);
|
|
12339
12448
|
render_view(gobj);
|
|
12340
12449
|
return 0;
|
package/package.json
CHANGED
package/src/c_yui_json.css
CHANGED
|
@@ -172,6 +172,19 @@
|
|
|
172
172
|
.C_YUI_JSON .JSON_TYPE_BOOLEAN { color: var(--json-boolean); }
|
|
173
173
|
.C_YUI_JSON .JSON_TYPE_NULL { color: var(--json-null); font-style: italic; }
|
|
174
174
|
|
|
175
|
+
/*
|
|
176
|
+
* The id a container row carries beside its size. It is a VALUE, so
|
|
177
|
+
* it takes the string colour and follows the theme with it; what sets
|
|
178
|
+
* it apart from the values under it is the WEIGHT, not a colour of its
|
|
179
|
+
* own -- a sixth colour in a row that already carries five would be
|
|
180
|
+
* one more thing to decode, for a field whose whole job is to be read
|
|
181
|
+
* at a glance while scrolling.
|
|
182
|
+
*/
|
|
183
|
+
.C_YUI_JSON .JSON_SUMMARY_ID {
|
|
184
|
+
color: var(--json-string);
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
187
|
+
|
|
175
188
|
.C_YUI_JSON .JSON_STUB {
|
|
176
189
|
color: var(--bulma-link, #3273dc);
|
|
177
190
|
font-weight: 600;
|
package/src/c_yui_json.js
CHANGED
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
*
|
|
27
27
|
* THREE VIEWS over the same working document, chosen by the
|
|
28
28
|
* `view_mode` attr ("tree" | "text" | "graph") and switched from the
|
|
29
|
-
* toolbar.
|
|
29
|
+
* toolbar. The viewer opens on what the host asked for, else on the
|
|
30
|
+
* view the reader chose last (kept in localStorage), else on the tree.
|
|
31
|
+
* They answer three different questions:
|
|
30
32
|
*
|
|
31
33
|
* tree where is this value, and what is around it — the lazy
|
|
32
34
|
* view above, the only one that can drill.
|
|
@@ -82,6 +84,8 @@ import {
|
|
|
82
84
|
json_deep_copy,
|
|
83
85
|
json_object_size,
|
|
84
86
|
refresh_language,
|
|
87
|
+
kw_get_local_storage_value,
|
|
88
|
+
kw_set_local_storage_value,
|
|
85
89
|
} from "@yuneta/gobj-js";
|
|
86
90
|
|
|
87
91
|
import {
|
|
@@ -94,6 +98,8 @@ import {
|
|
|
94
98
|
is_time_field,
|
|
95
99
|
format_epoch,
|
|
96
100
|
json_text_dump,
|
|
101
|
+
pick_view_mode,
|
|
102
|
+
container_label,
|
|
97
103
|
} from "./json_view_helpers.js";
|
|
98
104
|
|
|
99
105
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
@@ -125,11 +131,19 @@ const MAX_RENDER_ROWS = 5000;
|
|
|
125
131
|
*/
|
|
126
132
|
const MAX_TEXT_CHARS = 2000000;
|
|
127
133
|
|
|
134
|
+
/*
|
|
135
|
+
* How much of a container's id label fits beside its size. It
|
|
136
|
+
* rides on a row whose job is to stay one line, and an id is
|
|
137
|
+
* usually short; the ones that are not are uuids, where the head
|
|
138
|
+
* is what a person compares anyway.
|
|
139
|
+
*/
|
|
140
|
+
const MAX_LABEL_CHARS = 40;
|
|
141
|
+
|
|
128
142
|
/*
|
|
129
143
|
* The three views, in switch order — this table IS the order of the
|
|
130
144
|
* buttons, and EV_SET_VIEW_MODE with no mode advances along it.
|
|
131
|
-
* Note the order is not the default: the viewer
|
|
132
|
-
*
|
|
145
|
+
* Note the order is not the default: the viewer OPENS on the tree
|
|
146
|
+
* unless something says otherwise, it just sits second in the row.
|
|
133
147
|
*/
|
|
134
148
|
const VIEWS = [
|
|
135
149
|
{mode: "text", icon: "yi-code", key: "text view"},
|
|
@@ -137,6 +151,16 @@ const VIEWS = [
|
|
|
137
151
|
{mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
|
|
138
152
|
];
|
|
139
153
|
|
|
154
|
+
/*
|
|
155
|
+
* Where the reader's last choice of view is kept. ONE key for the
|
|
156
|
+
* whole library and not one per host: which of the three views someone
|
|
157
|
+
* reads JSON in is a habit of the PERSON, not a property of the
|
|
158
|
+
* document, so the next document they open opens the way they read the
|
|
159
|
+
* last one. It lives in localStorage, which is per browser and per
|
|
160
|
+
* artifact origin, and reaches nobody else.
|
|
161
|
+
*/
|
|
162
|
+
const VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
|
|
163
|
+
|
|
140
164
|
/***************************************************************
|
|
141
165
|
* Data
|
|
142
166
|
***************************************************************/
|
|
@@ -147,7 +171,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
|
|
|
147
171
|
/*---------------- Config ----------------*/
|
|
148
172
|
SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
149
173
|
SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
|
|
150
|
-
SDATA(data_type_t.DTP_STRING, "view_mode", 0, "
|
|
174
|
+
SDATA(data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
|
|
151
175
|
|
|
152
176
|
/*---------------- UI ----------------*/
|
|
153
177
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
@@ -202,6 +226,7 @@ function mt_create(gobj)
|
|
|
202
226
|
}
|
|
203
227
|
|
|
204
228
|
build_ui(gobj);
|
|
229
|
+
resolve_initial_view_mode(gobj);
|
|
205
230
|
apply_view_mode(gobj);
|
|
206
231
|
|
|
207
232
|
/*
|
|
@@ -462,6 +487,34 @@ function view_label(mode)
|
|
|
462
487
|
}
|
|
463
488
|
}
|
|
464
489
|
|
|
490
|
+
/************************************************************
|
|
491
|
+
* The view the viewer OPENS on.
|
|
492
|
+
*
|
|
493
|
+
* Three answers, in order of authority: what the HOST asked for, what
|
|
494
|
+
* the READER chose last time, and the tree. The host wins because a
|
|
495
|
+
* viewer mounted to show a graph has to show one; the memory comes
|
|
496
|
+
* next, because reopening a document in the view you just left is the
|
|
497
|
+
* whole point of remembering it; the tree is the floor.
|
|
498
|
+
*
|
|
499
|
+
* This is why the attr no longer declares "tree" as its default: as a
|
|
500
|
+
* default and as a host's explicit choice it was the same string, so
|
|
501
|
+
* nothing could tell "show me the tree" from "I have no opinion", and
|
|
502
|
+
* a memory that cannot see the difference has to lose to both.
|
|
503
|
+
*
|
|
504
|
+
* Resolved ONCE here and written into the attr, so every later read
|
|
505
|
+
* is a plain attr read and no render touches the store.
|
|
506
|
+
************************************************************/
|
|
507
|
+
function resolve_initial_view_mode(gobj)
|
|
508
|
+
{
|
|
509
|
+
let mode = pick_view_mode(
|
|
510
|
+
gobj_read_str_attr(gobj, "view_mode"),
|
|
511
|
+
kw_get_local_storage_value(VIEW_MODE_STORAGE_KEY, "", false),
|
|
512
|
+
VIEWS.map((v) => v.mode),
|
|
513
|
+
"tree"
|
|
514
|
+
);
|
|
515
|
+
gobj_write_attr(gobj, "view_mode", mode);
|
|
516
|
+
}
|
|
517
|
+
|
|
465
518
|
/************************************************************
|
|
466
519
|
* The current view, normalised. Anything unknown is the tree.
|
|
467
520
|
************************************************************/
|
|
@@ -779,7 +832,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
|
|
|
779
832
|
let open = priv.expanded.has(path) || searching_match;
|
|
780
833
|
|
|
781
834
|
ctx.count++;
|
|
782
|
-
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
|
|
835
|
+
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
|
|
783
836
|
|
|
784
837
|
if(!open) {
|
|
785
838
|
return;
|
|
@@ -853,7 +906,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows)
|
|
|
853
906
|
/************************************************************
|
|
854
907
|
* Expandable container header row
|
|
855
908
|
************************************************************/
|
|
856
|
-
function toggle_row(gobj, key, size, is_object, depth, path, open)
|
|
909
|
+
function toggle_row(gobj, key, size, is_object, depth, path, open, value)
|
|
857
910
|
{
|
|
858
911
|
let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
|
|
859
912
|
|
|
@@ -864,6 +917,21 @@ function toggle_row(gobj, key, size, is_object, depth, path, open)
|
|
|
864
917
|
}
|
|
865
918
|
body.push(['span', {class: 'JSON_SUMMARY has-text-grey'}, summary]);
|
|
866
919
|
|
|
920
|
+
/*
|
|
921
|
+
* A dict that carries an id says WHICH one it is, right here.
|
|
922
|
+
* Inside an array of records -- which is what a topic's nodes
|
|
923
|
+
* are -- the row read `2: {15}` and the only way to tell record
|
|
924
|
+
* 2 from record 9 was to open both.
|
|
925
|
+
*
|
|
926
|
+
* Shown open as well as closed: it is the row's label, and a
|
|
927
|
+
* label that disappears when you expand makes the row jump and
|
|
928
|
+
* costs you the name of the thing you just opened.
|
|
929
|
+
*/
|
|
930
|
+
let label = container_label(value, MAX_LABEL_CHARS);
|
|
931
|
+
if(label) {
|
|
932
|
+
body.push(['span', {class: 'JSON_SUMMARY_ID ml-2'}, label]);
|
|
933
|
+
}
|
|
934
|
+
|
|
867
935
|
return ['div', {class: 'JSON_ROW JSON_CONTAINER', style: row_indent(depth)}, [
|
|
868
936
|
['span', {class: 'JSON_TOGGLE' + (open ? ' is-open' : '')}, [
|
|
869
937
|
['i', {class: 'yi-chevron-right'}]
|
|
@@ -1168,6 +1236,8 @@ function ac_copy_all(gobj, event, kw, src)
|
|
|
1168
1236
|
* With no mode it ADVANCES to the next view in VIEWS order, which
|
|
1169
1237
|
* is what the two-view toggle did when the list was two long. The
|
|
1170
1238
|
* toolbar buttons always name their mode.
|
|
1239
|
+
*
|
|
1240
|
+
* Whatever lands here is REMEMBERED, so the next viewer opens on it.
|
|
1171
1241
|
************************************************************/
|
|
1172
1242
|
function ac_set_view_mode(gobj, event, kw, src)
|
|
1173
1243
|
{
|
|
@@ -1187,6 +1257,14 @@ function ac_set_view_mode(gobj, event, kw, src)
|
|
|
1187
1257
|
|
|
1188
1258
|
gobj_write_attr(gobj, "view_mode", mode);
|
|
1189
1259
|
|
|
1260
|
+
/*
|
|
1261
|
+
* Remembered here and not in resolve_initial_view_mode(): only a
|
|
1262
|
+
* view the READER picked is a preference. A mode a host pinned, or
|
|
1263
|
+
* the tree we fell back to, would otherwise be written back as if
|
|
1264
|
+
* somebody had chosen it.
|
|
1265
|
+
*/
|
|
1266
|
+
kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
|
|
1267
|
+
|
|
1190
1268
|
apply_view_mode(gobj);
|
|
1191
1269
|
render_view(gobj);
|
|
1192
1270
|
return 0;
|
package/src/c_yui_json_graph.js
CHANGED
|
@@ -221,22 +221,38 @@ const GROUP_COLORS = {
|
|
|
221
221
|
|
|
222
222
|
/***************************************************************
|
|
223
223
|
* Soft, theme-aware card palette, same visual language as the
|
|
224
|
-
* gobj-tree's role_card_style():
|
|
225
|
-
*
|
|
226
|
-
*
|
|
224
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
225
|
+
* group-colour border.
|
|
226
|
+
*
|
|
227
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
228
|
+
* the whole rule here. The light card is near-white and lets
|
|
229
|
+
* saturated dark text carry the colour; the dark card is its
|
|
230
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
231
|
+
* the group's hue lives in the border and the header bar, which
|
|
232
|
+
* is where the light theme had always put it.
|
|
233
|
+
*
|
|
234
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
235
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
236
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
237
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
238
|
+
* for the same values on the light card. The 12% left is enough
|
|
239
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
240
|
+
* text.
|
|
227
241
|
*
|
|
228
242
|
* The header bar is always the border colour, so its text flips:
|
|
229
243
|
* white on the dark-teal light bar, near-black on the brightened
|
|
230
|
-
* dark one.
|
|
244
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
245
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
246
|
+
* white gets on the light bar).
|
|
231
247
|
***************************************************************/
|
|
232
248
|
function json_card_style(group, dark)
|
|
233
249
|
{
|
|
234
250
|
return {
|
|
235
251
|
bg: dark
|
|
236
|
-
? `color-mix(in srgb, ${group.tint}
|
|
252
|
+
? `color-mix(in srgb, ${group.tint} 12%, #1E242E)`
|
|
237
253
|
: group.fill,
|
|
238
254
|
border: dark
|
|
239
|
-
? `color-mix(in srgb, ${group.stroke}
|
|
255
|
+
? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)`
|
|
240
256
|
: group.stroke,
|
|
241
257
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
242
258
|
key: dark ? "#c9cfd8" : "#1A1A1A",
|
|
@@ -247,6 +263,13 @@ function json_card_style(group, dark)
|
|
|
247
263
|
* A scalar's colour by type. The palette is tuned for a light
|
|
248
264
|
* card; on a dark one every one of them (dark green #006000,
|
|
249
265
|
* blue #475ED0, …) sinks into the background, so brighten it.
|
|
266
|
+
*
|
|
267
|
+
* 60% white and not the 45% it shipped with. The number is a
|
|
268
|
+
* balance measured both ways: brighter reads better on the dark
|
|
269
|
+
* card (the worst type goes from 2.88:1 to 4.74:1) and washes the
|
|
270
|
+
* six types toward one pastel, and past this point red, orange
|
|
271
|
+
* and purple stop being tellable apart, which is the only reason
|
|
272
|
+
* they are coloured at all.
|
|
250
273
|
***************************************************************/
|
|
251
274
|
function type_color(type, dark)
|
|
252
275
|
{
|
|
@@ -254,7 +277,7 @@ function type_color(type, dark)
|
|
|
254
277
|
if(!dark) {
|
|
255
278
|
return color;
|
|
256
279
|
}
|
|
257
|
-
return `color-mix(in srgb, ${color}
|
|
280
|
+
return `color-mix(in srgb, ${color} 40%, #ffffff)`;
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
|
|
@@ -698,8 +721,18 @@ function build_graph(gobj)
|
|
|
698
721
|
************************************************************/
|
|
699
722
|
function build_cell_html(key, value, type, dark, matched)
|
|
700
723
|
{
|
|
701
|
-
|
|
702
|
-
|
|
724
|
+
/*
|
|
725
|
+
* A match is a LIGHT chip on BOTH themes, so what sits on it is
|
|
726
|
+
* light-surface text -- the same flip the header bar does.
|
|
727
|
+
*
|
|
728
|
+
* A dark chip has no room to exist: dark enough for the brightened
|
|
729
|
+
* values to read on it is also dark enough to be invisible against
|
|
730
|
+
* the card, and the amber it used to be (#7a5d00) ended up failing
|
|
731
|
+
* at both ends, 2.52:1 under the text and 2.34:1 over the card.
|
|
732
|
+
*/
|
|
733
|
+
let on_light = matched? false: dark;
|
|
734
|
+
let color = type_color(type, on_light);
|
|
735
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
703
736
|
let display_value = "";
|
|
704
737
|
|
|
705
738
|
switch(type) {
|
|
@@ -734,7 +767,7 @@ function build_cell_html(key, value, type, dark, matched)
|
|
|
734
767
|
* a G6 node state: the key shape of an `html` node is a DOM
|
|
735
768
|
* element, and G6 paints no state style on it. Setting
|
|
736
769
|
* 'active' here would select correctly and show nothing. */
|
|
737
|
-
row = `<span style="background
|
|
770
|
+
row = `<span style="background:#ffe082; ` +
|
|
738
771
|
`border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
739
772
|
}
|
|
740
773
|
return row;
|
package/src/json_view_helpers.js
CHANGED
|
@@ -242,3 +242,75 @@ export function json_text_dump(root, max_chars)
|
|
|
242
242
|
}
|
|
243
243
|
return {text: text, capped: false};
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
/************************************************************
|
|
247
|
+
* Which view a freshly mounted viewer opens on.
|
|
248
|
+
*
|
|
249
|
+
* Three answers, in order of authority: what the HOST asked
|
|
250
|
+
* for, what the READER chose last time, and `fallback`.
|
|
251
|
+
*
|
|
252
|
+
* The host wins because a viewer mounted to show a graph has
|
|
253
|
+
* to show one. The memory comes next, because reopening a
|
|
254
|
+
* document in the view you just left is the whole point of
|
|
255
|
+
* remembering it. `fallback` is the floor.
|
|
256
|
+
*
|
|
257
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
258
|
+
* an absent value, an empty one, and a stored string from an
|
|
259
|
+
* older release that named a view no longer offered.
|
|
260
|
+
************************************************************/
|
|
261
|
+
export function pick_view_mode(asked, remembered, modes, fallback)
|
|
262
|
+
{
|
|
263
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
264
|
+
|
|
265
|
+
if(known(asked)) {
|
|
266
|
+
return asked;
|
|
267
|
+
}
|
|
268
|
+
if(known(remembered)) {
|
|
269
|
+
return remembered;
|
|
270
|
+
}
|
|
271
|
+
return fallback;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/************************************************************
|
|
275
|
+
* The short label a container row carries beside its size.
|
|
276
|
+
*
|
|
277
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
278
|
+
* and a row that says only `2: {15}` makes you open all
|
|
279
|
+
* fifteen fields of every one of them to find out WHICH
|
|
280
|
+
* record it is. The id is the one field that answers that,
|
|
281
|
+
* so the row says it without being opened.
|
|
282
|
+
*
|
|
283
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
284
|
+
* that is itself a dict or a list is not a name, it is more
|
|
285
|
+
* document, and printing `[object Object]` beside the size
|
|
286
|
+
* would be worse than printing nothing.
|
|
287
|
+
*
|
|
288
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
289
|
+
* is to stay one line.
|
|
290
|
+
************************************************************/
|
|
291
|
+
export function container_label(value, max_chars)
|
|
292
|
+
{
|
|
293
|
+
if(value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
if(!Object.prototype.hasOwnProperty.call(value, "id")) {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let id = value.id;
|
|
301
|
+
let t = typeof id;
|
|
302
|
+
if(t !== "string" && t !== "number" && t !== "boolean") {
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let text = String(id).trim();
|
|
307
|
+
if(text === "") {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let cap = (typeof max_chars === "number" && max_chars > 0)? max_chars: 0;
|
|
312
|
+
if(cap && text.length > cap) {
|
|
313
|
+
return text.slice(0, cap) + "…";
|
|
314
|
+
}
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
is_time_field,
|
|
17
17
|
format_epoch,
|
|
18
18
|
json_text_dump,
|
|
19
|
+
pick_view_mode,
|
|
20
|
+
container_label,
|
|
19
21
|
} from "./json_view_helpers.js";
|
|
20
22
|
|
|
21
23
|
|
|
@@ -175,3 +177,69 @@ test("json_text_dump reports a document with no JSON form instead of printing no
|
|
|
175
177
|
expect(d2.text).toBe("");
|
|
176
178
|
expect(typeof d2.error).toBe("string");
|
|
177
179
|
});
|
|
180
|
+
|
|
181
|
+
/***************************************************************
|
|
182
|
+
* pick_view_mode()
|
|
183
|
+
***************************************************************/
|
|
184
|
+
const MODES = ["text", "tree", "graph"];
|
|
185
|
+
|
|
186
|
+
test("pick_view_mode: the tree when nothing is said", () => {
|
|
187
|
+
expect(pick_view_mode("", "", MODES, "tree")).toBe("tree");
|
|
188
|
+
expect(pick_view_mode(undefined, undefined, MODES, "tree")).toBe("tree");
|
|
189
|
+
expect(pick_view_mode(null, null, MODES, "tree")).toBe("tree");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("pick_view_mode: what the reader chose last wins over the fallback", () => {
|
|
193
|
+
expect(pick_view_mode("", "graph", MODES, "tree")).toBe("graph");
|
|
194
|
+
expect(pick_view_mode("", "text", MODES, "tree")).toBe("text");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("pick_view_mode: the host outranks the memory", () => {
|
|
198
|
+
expect(pick_view_mode("text", "graph", MODES, "tree")).toBe("text");
|
|
199
|
+
/* And "tree" asked for explicitly is a choice, not a default:
|
|
200
|
+
* it has to beat a remembered graph. */
|
|
201
|
+
expect(pick_view_mode("tree", "graph", MODES, "tree")).toBe("tree");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("pick_view_mode: a mode no longer offered is treated as unsaid", () => {
|
|
205
|
+
/* A string left in the store by an older release. */
|
|
206
|
+
expect(pick_view_mode("", "webix", MODES, "tree")).toBe("tree");
|
|
207
|
+
expect(pick_view_mode("webix", "graph", MODES, "tree")).toBe("graph");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("pick_view_mode: no mode table at all still answers the fallback", () => {
|
|
211
|
+
expect(pick_view_mode("graph", "text", undefined, "tree")).toBe("tree");
|
|
212
|
+
expect(pick_view_mode("graph", "text", [], "tree")).toBe("tree");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/***************************************************************
|
|
216
|
+
* container_label()
|
|
217
|
+
***************************************************************/
|
|
218
|
+
test("container_label: a dict with a scalar id is labelled by it", () => {
|
|
219
|
+
expect(container_label({id: "LAB-001-001-E-001", name: "x"}, 40))
|
|
220
|
+
.toBe("LAB-001-001-E-001");
|
|
221
|
+
expect(container_label({id: 42}, 40)).toBe("42");
|
|
222
|
+
expect(container_label({id: false}, 40)).toBe("false");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("container_label: nothing to say without a scalar id", () => {
|
|
226
|
+
expect(container_label({name: "x"}, 40)).toBe("");
|
|
227
|
+
expect(container_label({}, 40)).toBe("");
|
|
228
|
+
expect(container_label({id: {a: 1}}, 40)).toBe("");
|
|
229
|
+
expect(container_label({id: [1, 2]}, 40)).toBe("");
|
|
230
|
+
expect(container_label({id: null}, 40)).toBe("");
|
|
231
|
+
expect(container_label({id: " "}, 40)).toBe("");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("container_label: an array is not a record", () => {
|
|
235
|
+
expect(container_label([{id: "a"}], 40)).toBe("");
|
|
236
|
+
expect(container_label(null, 40)).toBe("");
|
|
237
|
+
expect(container_label("id", 40)).toBe("");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("container_label: cut at max_chars, and said with an ellipsis", () => {
|
|
241
|
+
let uuid = "ff14fc16-2cb2-472c-9c0a-89fa43837130";
|
|
242
|
+
expect(container_label({id: uuid}, 8)).toBe("ff14fc16…");
|
|
243
|
+
expect(container_label({id: uuid}, 0)).toBe(uuid);
|
|
244
|
+
expect(container_label({id: uuid}, undefined)).toBe(uuid);
|
|
245
|
+
});
|