@yuneta/gobj-ui 7.23.16 → 7.23.18
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 +151 -25
- package/dist/gobj-ui.es.js +151 -25
- package/package.json +1 -1
- package/src/c_yui_json.css +24 -2
- package/src/c_yui_json.js +84 -6
- package/src/c_yui_json_graph.js +72 -22
- package/src/json_view_helpers.js +72 -0
- package/src/json_view_helpers.test.js +68 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -10080,16 +10080,39 @@ var PRIVATE_DATA$11 = {
|
|
|
10080
10080
|
var __gclass__$11 = null;
|
|
10081
10081
|
var __layout_registered__$1 = false;
|
|
10082
10082
|
/***************************************************************
|
|
10083
|
-
* Type
|
|
10084
|
-
|
|
10085
|
-
|
|
10083
|
+
* Type colours, ONE EXPLICIT PALETTE PER THEME and not a
|
|
10084
|
+
* derivation, for the same reason the tree keeps two sets of
|
|
10085
|
+
* CSS tokens: a colour that reads on white is not the same
|
|
10086
|
+
* colour lightened, and deriving one from the other made the
|
|
10087
|
+
* two views of one document disagree about what a string is.
|
|
10088
|
+
* These ARE the tree's tokens (`c_yui_json.css`), so the tree
|
|
10089
|
+
* and the graph now say green with the same green.
|
|
10090
|
+
*
|
|
10091
|
+
* The light values are darker than the `mx_json_viewer` ones
|
|
10092
|
+
* they came from, because those could not be read: `#FF8C00`
|
|
10093
|
+
* is mid-luminance, so it reaches at most **2.33:1 against any
|
|
10094
|
+
* background that exists** -- no card colour and no highlight
|
|
10095
|
+
* could have fixed it, only the colour itself. Orange and blue
|
|
10096
|
+
* moved until every type clears 4.5:1 on all five surfaces this
|
|
10097
|
+
* viewer paints (two light cards, the match chip, two dark
|
|
10098
|
+
* cards); red and green were already there.
|
|
10099
|
+
***************************************************************/
|
|
10100
|
+
var TYPE_COLORS_LIGHT = {
|
|
10086
10101
|
string: "#006000",
|
|
10087
|
-
number: "#
|
|
10088
|
-
boolean: "#
|
|
10089
|
-
null: "#
|
|
10102
|
+
number: "#b5330b",
|
|
10103
|
+
boolean: "#8C4D00",
|
|
10104
|
+
null: "#4359C6",
|
|
10090
10105
|
list: "#4C0099",
|
|
10091
10106
|
dict: "#4C0099"
|
|
10092
10107
|
};
|
|
10108
|
+
var TYPE_COLORS_DARK = {
|
|
10109
|
+
string: "#7ec87e",
|
|
10110
|
+
number: "#ff8a65",
|
|
10111
|
+
boolean: "#ffb74d",
|
|
10112
|
+
null: "#90a4f4",
|
|
10113
|
+
list: "#B799D6",
|
|
10114
|
+
dict: "#B799D6"
|
|
10115
|
+
};
|
|
10093
10116
|
/***************************************************************
|
|
10094
10117
|
* Group (card) colours. `fill` is the light-theme card; `tint`
|
|
10095
10118
|
* is the hue that fill hints at, used to derive the dark card —
|
|
@@ -10110,31 +10133,48 @@ var GROUP_COLORS = {
|
|
|
10110
10133
|
};
|
|
10111
10134
|
/***************************************************************
|
|
10112
10135
|
* Soft, theme-aware card palette, same visual language as the
|
|
10113
|
-
* gobj-tree's role_card_style():
|
|
10114
|
-
*
|
|
10115
|
-
*
|
|
10136
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
10137
|
+
* group-colour border.
|
|
10138
|
+
*
|
|
10139
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
10140
|
+
* the whole rule here. The light card is near-white and lets
|
|
10141
|
+
* saturated dark text carry the colour; the dark card is its
|
|
10142
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
10143
|
+
* the group's hue lives in the border and the header bar, which
|
|
10144
|
+
* is where the light theme had always put it.
|
|
10145
|
+
*
|
|
10146
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
10147
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
10148
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
10149
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
10150
|
+
* for the same values on the light card. The 12% left is enough
|
|
10151
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
10152
|
+
* text.
|
|
10116
10153
|
*
|
|
10117
10154
|
* The header bar is always the border colour, so its text flips:
|
|
10118
10155
|
* white on the dark-teal light bar, near-black on the brightened
|
|
10119
|
-
* dark one.
|
|
10156
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
10157
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
10158
|
+
* white gets on the light bar).
|
|
10120
10159
|
***************************************************************/
|
|
10121
10160
|
function json_card_style(group, dark) {
|
|
10122
10161
|
return {
|
|
10123
|
-
bg: dark ? `color-mix(in srgb, ${group.tint}
|
|
10124
|
-
border: dark ? `color-mix(in srgb, ${group.stroke}
|
|
10162
|
+
bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
|
|
10163
|
+
border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
|
|
10125
10164
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
10126
10165
|
key: dark ? "#c9cfd8" : "#1A1A1A"
|
|
10127
10166
|
};
|
|
10128
10167
|
}
|
|
10129
10168
|
/***************************************************************
|
|
10130
|
-
* A scalar's colour by type
|
|
10131
|
-
*
|
|
10132
|
-
*
|
|
10169
|
+
* A scalar's colour by type, straight out of the palette for
|
|
10170
|
+
* the theme. It used to be the light colour mixed with white,
|
|
10171
|
+
* which is how a document ended up green in one view and a
|
|
10172
|
+
* paler green in the other, and how the amount of white became
|
|
10173
|
+
* a knob nobody could set: enough of it to read on a dark card
|
|
10174
|
+
* washed the six types into one pastel.
|
|
10133
10175
|
***************************************************************/
|
|
10134
10176
|
function type_color(type, dark) {
|
|
10135
|
-
|
|
10136
|
-
if (!dark) return color;
|
|
10137
|
-
return `color-mix(in srgb, ${color} 55%, #ffffff)`;
|
|
10177
|
+
return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
|
|
10138
10178
|
}
|
|
10139
10179
|
/******************************
|
|
10140
10180
|
* Framework Methods
|
|
@@ -10431,8 +10471,9 @@ function build_graph$3(gobj) {
|
|
|
10431
10471
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10432
10472
|
************************************************************/
|
|
10433
10473
|
function build_cell_html(key, value, type, dark, matched) {
|
|
10434
|
-
let
|
|
10435
|
-
let
|
|
10474
|
+
let on_light = matched ? false : dark;
|
|
10475
|
+
let color = type_color(type, on_light);
|
|
10476
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
10436
10477
|
let display_value = "";
|
|
10437
10478
|
switch (type) {
|
|
10438
10479
|
case "string":
|
|
@@ -10456,7 +10497,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10456
10497
|
default: display_value = String(value);
|
|
10457
10498
|
}
|
|
10458
10499
|
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
|
|
10500
|
+
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10460
10501
|
return row;
|
|
10461
10502
|
}
|
|
10462
10503
|
/************************************************************
|
|
@@ -11341,6 +11382,56 @@ function json_text_dump(root, max_chars) {
|
|
|
11341
11382
|
capped: false
|
|
11342
11383
|
};
|
|
11343
11384
|
}
|
|
11385
|
+
/************************************************************
|
|
11386
|
+
* Which view a freshly mounted viewer opens on.
|
|
11387
|
+
*
|
|
11388
|
+
* Three answers, in order of authority: what the HOST asked
|
|
11389
|
+
* for, what the READER chose last time, and `fallback`.
|
|
11390
|
+
*
|
|
11391
|
+
* The host wins because a viewer mounted to show a graph has
|
|
11392
|
+
* to show one. The memory comes next, because reopening a
|
|
11393
|
+
* document in the view you just left is the whole point of
|
|
11394
|
+
* remembering it. `fallback` is the floor.
|
|
11395
|
+
*
|
|
11396
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
11397
|
+
* an absent value, an empty one, and a stored string from an
|
|
11398
|
+
* older release that named a view no longer offered.
|
|
11399
|
+
************************************************************/
|
|
11400
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11401
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11402
|
+
if (known(asked)) return asked;
|
|
11403
|
+
if (known(remembered)) return remembered;
|
|
11404
|
+
return fallback;
|
|
11405
|
+
}
|
|
11406
|
+
/************************************************************
|
|
11407
|
+
* The short label a container row carries beside its size.
|
|
11408
|
+
*
|
|
11409
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11410
|
+
* and a row that says only `2: {15}` makes you open all
|
|
11411
|
+
* fifteen fields of every one of them to find out WHICH
|
|
11412
|
+
* record it is. The id is the one field that answers that,
|
|
11413
|
+
* so the row says it without being opened.
|
|
11414
|
+
*
|
|
11415
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11416
|
+
* that is itself a dict or a list is not a name, it is more
|
|
11417
|
+
* document, and printing `[object Object]` beside the size
|
|
11418
|
+
* would be worse than printing nothing.
|
|
11419
|
+
*
|
|
11420
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
11421
|
+
* is to stay one line.
|
|
11422
|
+
************************************************************/
|
|
11423
|
+
function container_label(value, max_chars) {
|
|
11424
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11425
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11426
|
+
let id = value.id;
|
|
11427
|
+
let t = typeof id;
|
|
11428
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11429
|
+
let text = String(id).trim();
|
|
11430
|
+
if (text === "") return "";
|
|
11431
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11432
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11433
|
+
return text;
|
|
11434
|
+
}
|
|
11344
11435
|
//#endregion
|
|
11345
11436
|
//#region src/c_yui_json.js
|
|
11346
11437
|
/***********************************************************************
|
|
@@ -11371,7 +11462,9 @@ function json_text_dump(root, max_chars) {
|
|
|
11371
11462
|
*
|
|
11372
11463
|
* THREE VIEWS over the same working document, chosen by the
|
|
11373
11464
|
* `view_mode` attr ("tree" | "text" | "graph") and switched from the
|
|
11374
|
-
* toolbar.
|
|
11465
|
+
* toolbar. The viewer opens on what the host asked for, else on the
|
|
11466
|
+
* view the reader chose last (kept in localStorage), else on the tree.
|
|
11467
|
+
* They answer three different questions:
|
|
11375
11468
|
*
|
|
11376
11469
|
* tree where is this value, and what is around it — the lazy
|
|
11377
11470
|
* view above, the only one that can drill.
|
|
@@ -11405,6 +11498,7 @@ function json_text_dump(root, max_chars) {
|
|
|
11405
11498
|
var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
11406
11499
|
var MAX_RENDER_ROWS = 5e3;
|
|
11407
11500
|
var MAX_TEXT_CHARS = 2e6;
|
|
11501
|
+
var MAX_LABEL_CHARS = 40;
|
|
11408
11502
|
var VIEWS = [
|
|
11409
11503
|
{
|
|
11410
11504
|
mode: "text",
|
|
@@ -11422,6 +11516,7 @@ var VIEWS = [
|
|
|
11422
11516
|
key: "graph view"
|
|
11423
11517
|
}
|
|
11424
11518
|
];
|
|
11519
|
+
var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
|
|
11425
11520
|
/***************************************************************
|
|
11426
11521
|
* Data
|
|
11427
11522
|
***************************************************************/
|
|
@@ -11429,7 +11524,7 @@ var attrs_table$10 = [
|
|
|
11429
11524
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
11430
11525
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
11431
11526
|
(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, "
|
|
11527
|
+
(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
11528
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
11434
11529
|
(0, _yuneta_gobj_js.SDATA_END)()
|
|
11435
11530
|
];
|
|
@@ -11466,6 +11561,7 @@ function mt_create$10(gobj) {
|
|
|
11466
11561
|
let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
|
|
11467
11562
|
if (json_data !== null && json_data !== void 0) priv.root = (0, _yuneta_gobj_js.json_deep_copy)(json_data);
|
|
11468
11563
|
build_ui$9(gobj);
|
|
11564
|
+
resolve_initial_view_mode(gobj);
|
|
11469
11565
|
apply_view_mode(gobj);
|
|
11470
11566
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
11471
11567
|
if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
|
|
@@ -11723,6 +11819,27 @@ function view_label(mode) {
|
|
|
11723
11819
|
}
|
|
11724
11820
|
}
|
|
11725
11821
|
/************************************************************
|
|
11822
|
+
* The view the viewer OPENS on.
|
|
11823
|
+
*
|
|
11824
|
+
* Three answers, in order of authority: what the HOST asked for, what
|
|
11825
|
+
* the READER chose last time, and the tree. The host wins because a
|
|
11826
|
+
* viewer mounted to show a graph has to show one; the memory comes
|
|
11827
|
+
* next, because reopening a document in the view you just left is the
|
|
11828
|
+
* whole point of remembering it; the tree is the floor.
|
|
11829
|
+
*
|
|
11830
|
+
* This is why the attr no longer declares "tree" as its default: as a
|
|
11831
|
+
* default and as a host's explicit choice it was the same string, so
|
|
11832
|
+
* nothing could tell "show me the tree" from "I have no opinion", and
|
|
11833
|
+
* a memory that cannot see the difference has to lose to both.
|
|
11834
|
+
*
|
|
11835
|
+
* Resolved ONCE here and written into the attr, so every later read
|
|
11836
|
+
* is a plain attr read and no render touches the store.
|
|
11837
|
+
************************************************************/
|
|
11838
|
+
function resolve_initial_view_mode(gobj) {
|
|
11839
|
+
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");
|
|
11840
|
+
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
|
|
11841
|
+
}
|
|
11842
|
+
/************************************************************
|
|
11726
11843
|
* The current view, normalised. Anything unknown is the tree.
|
|
11727
11844
|
************************************************************/
|
|
11728
11845
|
function current_view_mode(gobj) {
|
|
@@ -11972,7 +12089,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
|
|
|
11972
12089
|
let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
|
|
11973
12090
|
let open = priv.expanded.has(path) || searching_match;
|
|
11974
12091
|
ctx.count++;
|
|
11975
|
-
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
|
|
12092
|
+
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
|
|
11976
12093
|
if (!open) return;
|
|
11977
12094
|
if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
|
|
11978
12095
|
else value.forEach(function(v, i) {
|
|
@@ -12057,7 +12174,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
|
|
|
12057
12174
|
/************************************************************
|
|
12058
12175
|
* Expandable container header row
|
|
12059
12176
|
************************************************************/
|
|
12060
|
-
function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
12177
|
+
function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
|
|
12061
12178
|
let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
|
|
12062
12179
|
let body = [];
|
|
12063
12180
|
if (key !== null && key !== "") {
|
|
@@ -12077,6 +12194,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
|
12077
12194
|
{ class: "JSON_SUMMARY has-text-grey" },
|
|
12078
12195
|
summary
|
|
12079
12196
|
]);
|
|
12197
|
+
let label = container_label(value, MAX_LABEL_CHARS);
|
|
12198
|
+
if (label) body.push([
|
|
12199
|
+
"span",
|
|
12200
|
+
{ class: "JSON_SUMMARY_ID ml-2" },
|
|
12201
|
+
label
|
|
12202
|
+
]);
|
|
12080
12203
|
return [
|
|
12081
12204
|
"div",
|
|
12082
12205
|
{
|
|
@@ -12330,6 +12453,8 @@ function ac_copy_all(gobj, event, kw, src) {
|
|
|
12330
12453
|
* With no mode it ADVANCES to the next view in VIEWS order, which
|
|
12331
12454
|
* is what the two-view toggle did when the list was two long. The
|
|
12332
12455
|
* toolbar buttons always name their mode.
|
|
12456
|
+
*
|
|
12457
|
+
* Whatever lands here is REMEMBERED, so the next viewer opens on it.
|
|
12333
12458
|
************************************************************/
|
|
12334
12459
|
function ac_set_view_mode(gobj, event, kw, src) {
|
|
12335
12460
|
let mode = kw.mode;
|
|
@@ -12340,6 +12465,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
|
|
|
12340
12465
|
}
|
|
12341
12466
|
if (mode === current_view_mode(gobj)) return 0;
|
|
12342
12467
|
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
|
|
12468
|
+
(0, _yuneta_gobj_js.kw_set_local_storage_value)(VIEW_MODE_STORAGE_KEY, mode);
|
|
12343
12469
|
apply_view_mode(gobj);
|
|
12344
12470
|
render_view(gobj);
|
|
12345
12471
|
return 0;
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -10075,16 +10075,39 @@ var PRIVATE_DATA$11 = {
|
|
|
10075
10075
|
var __gclass__$11 = null;
|
|
10076
10076
|
var __layout_registered__$1 = false;
|
|
10077
10077
|
/***************************************************************
|
|
10078
|
-
* Type
|
|
10079
|
-
|
|
10080
|
-
|
|
10078
|
+
* Type colours, ONE EXPLICIT PALETTE PER THEME and not a
|
|
10079
|
+
* derivation, for the same reason the tree keeps two sets of
|
|
10080
|
+
* CSS tokens: a colour that reads on white is not the same
|
|
10081
|
+
* colour lightened, and deriving one from the other made the
|
|
10082
|
+
* two views of one document disagree about what a string is.
|
|
10083
|
+
* These ARE the tree's tokens (`c_yui_json.css`), so the tree
|
|
10084
|
+
* and the graph now say green with the same green.
|
|
10085
|
+
*
|
|
10086
|
+
* The light values are darker than the `mx_json_viewer` ones
|
|
10087
|
+
* they came from, because those could not be read: `#FF8C00`
|
|
10088
|
+
* is mid-luminance, so it reaches at most **2.33:1 against any
|
|
10089
|
+
* background that exists** -- no card colour and no highlight
|
|
10090
|
+
* could have fixed it, only the colour itself. Orange and blue
|
|
10091
|
+
* moved until every type clears 4.5:1 on all five surfaces this
|
|
10092
|
+
* viewer paints (two light cards, the match chip, two dark
|
|
10093
|
+
* cards); red and green were already there.
|
|
10094
|
+
***************************************************************/
|
|
10095
|
+
var TYPE_COLORS_LIGHT = {
|
|
10081
10096
|
string: "#006000",
|
|
10082
|
-
number: "#
|
|
10083
|
-
boolean: "#
|
|
10084
|
-
null: "#
|
|
10097
|
+
number: "#b5330b",
|
|
10098
|
+
boolean: "#8C4D00",
|
|
10099
|
+
null: "#4359C6",
|
|
10085
10100
|
list: "#4C0099",
|
|
10086
10101
|
dict: "#4C0099"
|
|
10087
10102
|
};
|
|
10103
|
+
var TYPE_COLORS_DARK = {
|
|
10104
|
+
string: "#7ec87e",
|
|
10105
|
+
number: "#ff8a65",
|
|
10106
|
+
boolean: "#ffb74d",
|
|
10107
|
+
null: "#90a4f4",
|
|
10108
|
+
list: "#B799D6",
|
|
10109
|
+
dict: "#B799D6"
|
|
10110
|
+
};
|
|
10088
10111
|
/***************************************************************
|
|
10089
10112
|
* Group (card) colours. `fill` is the light-theme card; `tint`
|
|
10090
10113
|
* is the hue that fill hints at, used to derive the dark card —
|
|
@@ -10105,31 +10128,48 @@ var GROUP_COLORS = {
|
|
|
10105
10128
|
};
|
|
10106
10129
|
/***************************************************************
|
|
10107
10130
|
* Soft, theme-aware card palette, same visual language as the
|
|
10108
|
-
* gobj-tree's role_card_style():
|
|
10109
|
-
*
|
|
10110
|
-
*
|
|
10131
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
10132
|
+
* group-colour border.
|
|
10133
|
+
*
|
|
10134
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
10135
|
+
* the whole rule here. The light card is near-white and lets
|
|
10136
|
+
* saturated dark text carry the colour; the dark card is its
|
|
10137
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
10138
|
+
* the group's hue lives in the border and the header bar, which
|
|
10139
|
+
* is where the light theme had always put it.
|
|
10140
|
+
*
|
|
10141
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
10142
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
10143
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
10144
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
10145
|
+
* for the same values on the light card. The 12% left is enough
|
|
10146
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
10147
|
+
* text.
|
|
10111
10148
|
*
|
|
10112
10149
|
* The header bar is always the border colour, so its text flips:
|
|
10113
10150
|
* white on the dark-teal light bar, near-black on the brightened
|
|
10114
|
-
* dark one.
|
|
10151
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
10152
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
10153
|
+
* white gets on the light bar).
|
|
10115
10154
|
***************************************************************/
|
|
10116
10155
|
function json_card_style(group, dark) {
|
|
10117
10156
|
return {
|
|
10118
|
-
bg: dark ? `color-mix(in srgb, ${group.tint}
|
|
10119
|
-
border: dark ? `color-mix(in srgb, ${group.stroke}
|
|
10157
|
+
bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
|
|
10158
|
+
border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
|
|
10120
10159
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
10121
10160
|
key: dark ? "#c9cfd8" : "#1A1A1A"
|
|
10122
10161
|
};
|
|
10123
10162
|
}
|
|
10124
10163
|
/***************************************************************
|
|
10125
|
-
* A scalar's colour by type
|
|
10126
|
-
*
|
|
10127
|
-
*
|
|
10164
|
+
* A scalar's colour by type, straight out of the palette for
|
|
10165
|
+
* the theme. It used to be the light colour mixed with white,
|
|
10166
|
+
* which is how a document ended up green in one view and a
|
|
10167
|
+
* paler green in the other, and how the amount of white became
|
|
10168
|
+
* a knob nobody could set: enough of it to read on a dark card
|
|
10169
|
+
* washed the six types into one pastel.
|
|
10128
10170
|
***************************************************************/
|
|
10129
10171
|
function type_color(type, dark) {
|
|
10130
|
-
|
|
10131
|
-
if (!dark) return color;
|
|
10132
|
-
return `color-mix(in srgb, ${color} 55%, #ffffff)`;
|
|
10172
|
+
return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
|
|
10133
10173
|
}
|
|
10134
10174
|
/******************************
|
|
10135
10175
|
* Framework Methods
|
|
@@ -10426,8 +10466,9 @@ function build_graph$3(gobj) {
|
|
|
10426
10466
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10427
10467
|
************************************************************/
|
|
10428
10468
|
function build_cell_html(key, value, type, dark, matched) {
|
|
10429
|
-
let
|
|
10430
|
-
let
|
|
10469
|
+
let on_light = matched ? false : dark;
|
|
10470
|
+
let color = type_color(type, on_light);
|
|
10471
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
10431
10472
|
let display_value = "";
|
|
10432
10473
|
switch (type) {
|
|
10433
10474
|
case "string":
|
|
@@ -10451,7 +10492,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10451
10492
|
default: display_value = String(value);
|
|
10452
10493
|
}
|
|
10453
10494
|
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
|
|
10495
|
+
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10455
10496
|
return row;
|
|
10456
10497
|
}
|
|
10457
10498
|
/************************************************************
|
|
@@ -11336,6 +11377,56 @@ function json_text_dump(root, max_chars) {
|
|
|
11336
11377
|
capped: false
|
|
11337
11378
|
};
|
|
11338
11379
|
}
|
|
11380
|
+
/************************************************************
|
|
11381
|
+
* Which view a freshly mounted viewer opens on.
|
|
11382
|
+
*
|
|
11383
|
+
* Three answers, in order of authority: what the HOST asked
|
|
11384
|
+
* for, what the READER chose last time, and `fallback`.
|
|
11385
|
+
*
|
|
11386
|
+
* The host wins because a viewer mounted to show a graph has
|
|
11387
|
+
* to show one. The memory comes next, because reopening a
|
|
11388
|
+
* document in the view you just left is the whole point of
|
|
11389
|
+
* remembering it. `fallback` is the floor.
|
|
11390
|
+
*
|
|
11391
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
11392
|
+
* an absent value, an empty one, and a stored string from an
|
|
11393
|
+
* older release that named a view no longer offered.
|
|
11394
|
+
************************************************************/
|
|
11395
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11396
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11397
|
+
if (known(asked)) return asked;
|
|
11398
|
+
if (known(remembered)) return remembered;
|
|
11399
|
+
return fallback;
|
|
11400
|
+
}
|
|
11401
|
+
/************************************************************
|
|
11402
|
+
* The short label a container row carries beside its size.
|
|
11403
|
+
*
|
|
11404
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11405
|
+
* and a row that says only `2: {15}` makes you open all
|
|
11406
|
+
* fifteen fields of every one of them to find out WHICH
|
|
11407
|
+
* record it is. The id is the one field that answers that,
|
|
11408
|
+
* so the row says it without being opened.
|
|
11409
|
+
*
|
|
11410
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11411
|
+
* that is itself a dict or a list is not a name, it is more
|
|
11412
|
+
* document, and printing `[object Object]` beside the size
|
|
11413
|
+
* would be worse than printing nothing.
|
|
11414
|
+
*
|
|
11415
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
11416
|
+
* is to stay one line.
|
|
11417
|
+
************************************************************/
|
|
11418
|
+
function container_label(value, max_chars) {
|
|
11419
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11420
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11421
|
+
let id = value.id;
|
|
11422
|
+
let t = typeof id;
|
|
11423
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11424
|
+
let text = String(id).trim();
|
|
11425
|
+
if (text === "") return "";
|
|
11426
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11427
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11428
|
+
return text;
|
|
11429
|
+
}
|
|
11339
11430
|
//#endregion
|
|
11340
11431
|
//#region src/c_yui_json.js
|
|
11341
11432
|
/***********************************************************************
|
|
@@ -11366,7 +11457,9 @@ function json_text_dump(root, max_chars) {
|
|
|
11366
11457
|
*
|
|
11367
11458
|
* THREE VIEWS over the same working document, chosen by the
|
|
11368
11459
|
* `view_mode` attr ("tree" | "text" | "graph") and switched from the
|
|
11369
|
-
* toolbar.
|
|
11460
|
+
* toolbar. The viewer opens on what the host asked for, else on the
|
|
11461
|
+
* view the reader chose last (kept in localStorage), else on the tree.
|
|
11462
|
+
* They answer three different questions:
|
|
11370
11463
|
*
|
|
11371
11464
|
* tree where is this value, and what is around it — the lazy
|
|
11372
11465
|
* view above, the only one that can drill.
|
|
@@ -11400,6 +11493,7 @@ function json_text_dump(root, max_chars) {
|
|
|
11400
11493
|
var GCLASS_NAME$10 = "C_YUI_JSON";
|
|
11401
11494
|
var MAX_RENDER_ROWS = 5e3;
|
|
11402
11495
|
var MAX_TEXT_CHARS = 2e6;
|
|
11496
|
+
var MAX_LABEL_CHARS = 40;
|
|
11403
11497
|
var VIEWS = [
|
|
11404
11498
|
{
|
|
11405
11499
|
mode: "text",
|
|
@@ -11417,6 +11511,7 @@ var VIEWS = [
|
|
|
11417
11511
|
key: "graph view"
|
|
11418
11512
|
}
|
|
11419
11513
|
];
|
|
11514
|
+
var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
|
|
11420
11515
|
/***************************************************************
|
|
11421
11516
|
* Data
|
|
11422
11517
|
***************************************************************/
|
|
@@ -11424,7 +11519,7 @@ var attrs_table$10 = [
|
|
|
11424
11519
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
11425
11520
|
SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
11426
11521
|
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, "
|
|
11522
|
+
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
11523
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
11429
11524
|
SDATA_END()
|
|
11430
11525
|
];
|
|
@@ -11461,6 +11556,7 @@ function mt_create$10(gobj) {
|
|
|
11461
11556
|
let json_data = gobj_read_attr(gobj, "json_data");
|
|
11462
11557
|
if (json_data !== null && json_data !== void 0) priv.root = json_deep_copy(json_data);
|
|
11463
11558
|
build_ui$9(gobj);
|
|
11559
|
+
resolve_initial_view_mode(gobj);
|
|
11464
11560
|
apply_view_mode(gobj);
|
|
11465
11561
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
11466
11562
|
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
@@ -11718,6 +11814,27 @@ function view_label(mode) {
|
|
|
11718
11814
|
}
|
|
11719
11815
|
}
|
|
11720
11816
|
/************************************************************
|
|
11817
|
+
* The view the viewer OPENS on.
|
|
11818
|
+
*
|
|
11819
|
+
* Three answers, in order of authority: what the HOST asked for, what
|
|
11820
|
+
* the READER chose last time, and the tree. The host wins because a
|
|
11821
|
+
* viewer mounted to show a graph has to show one; the memory comes
|
|
11822
|
+
* next, because reopening a document in the view you just left is the
|
|
11823
|
+
* whole point of remembering it; the tree is the floor.
|
|
11824
|
+
*
|
|
11825
|
+
* This is why the attr no longer declares "tree" as its default: as a
|
|
11826
|
+
* default and as a host's explicit choice it was the same string, so
|
|
11827
|
+
* nothing could tell "show me the tree" from "I have no opinion", and
|
|
11828
|
+
* a memory that cannot see the difference has to lose to both.
|
|
11829
|
+
*
|
|
11830
|
+
* Resolved ONCE here and written into the attr, so every later read
|
|
11831
|
+
* is a plain attr read and no render touches the store.
|
|
11832
|
+
************************************************************/
|
|
11833
|
+
function resolve_initial_view_mode(gobj) {
|
|
11834
|
+
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");
|
|
11835
|
+
gobj_write_attr(gobj, "view_mode", mode);
|
|
11836
|
+
}
|
|
11837
|
+
/************************************************************
|
|
11721
11838
|
* The current view, normalised. Anything unknown is the tree.
|
|
11722
11839
|
************************************************************/
|
|
11723
11840
|
function current_view_mode(gobj) {
|
|
@@ -11967,7 +12084,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
|
|
|
11967
12084
|
let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
|
|
11968
12085
|
let open = priv.expanded.has(path) || searching_match;
|
|
11969
12086
|
ctx.count++;
|
|
11970
|
-
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
|
|
12087
|
+
rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
|
|
11971
12088
|
if (!open) return;
|
|
11972
12089
|
if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
|
|
11973
12090
|
else value.forEach(function(v, i) {
|
|
@@ -12052,7 +12169,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
|
|
|
12052
12169
|
/************************************************************
|
|
12053
12170
|
* Expandable container header row
|
|
12054
12171
|
************************************************************/
|
|
12055
|
-
function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
12172
|
+
function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
|
|
12056
12173
|
let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
|
|
12057
12174
|
let body = [];
|
|
12058
12175
|
if (key !== null && key !== "") {
|
|
@@ -12072,6 +12189,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
|
|
|
12072
12189
|
{ class: "JSON_SUMMARY has-text-grey" },
|
|
12073
12190
|
summary
|
|
12074
12191
|
]);
|
|
12192
|
+
let label = container_label(value, MAX_LABEL_CHARS);
|
|
12193
|
+
if (label) body.push([
|
|
12194
|
+
"span",
|
|
12195
|
+
{ class: "JSON_SUMMARY_ID ml-2" },
|
|
12196
|
+
label
|
|
12197
|
+
]);
|
|
12075
12198
|
return [
|
|
12076
12199
|
"div",
|
|
12077
12200
|
{
|
|
@@ -12325,6 +12448,8 @@ function ac_copy_all(gobj, event, kw, src) {
|
|
|
12325
12448
|
* With no mode it ADVANCES to the next view in VIEWS order, which
|
|
12326
12449
|
* is what the two-view toggle did when the list was two long. The
|
|
12327
12450
|
* toolbar buttons always name their mode.
|
|
12451
|
+
*
|
|
12452
|
+
* Whatever lands here is REMEMBERED, so the next viewer opens on it.
|
|
12328
12453
|
************************************************************/
|
|
12329
12454
|
function ac_set_view_mode(gobj, event, kw, src) {
|
|
12330
12455
|
let mode = kw.mode;
|
|
@@ -12335,6 +12460,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
|
|
|
12335
12460
|
}
|
|
12336
12461
|
if (mode === current_view_mode(gobj)) return 0;
|
|
12337
12462
|
gobj_write_attr(gobj, "view_mode", mode);
|
|
12463
|
+
kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
|
|
12338
12464
|
apply_view_mode(gobj);
|
|
12339
12465
|
render_view(gobj);
|
|
12340
12466
|
return 0;
|
package/package.json
CHANGED
package/src/c_yui_json.css
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
* All Rights Reserved.
|
|
10
10
|
***********************************************************************/
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
* Light values are darker than a naive orange/blue because they have to
|
|
14
|
+
* be READ, on the card and on the graph view's match chip alike. The
|
|
15
|
+
* ceiling is a fact of the colour, not of the surface: `#FF8C00`, which
|
|
16
|
+
* these came from, is mid-luminance and reaches at most 2.33:1 against
|
|
17
|
+
* ANY background that exists. The graph view carries the same two
|
|
18
|
+
* palettes (`c_yui_json_graph.js`), so one document says green with the
|
|
19
|
+
* same green in all three views.
|
|
20
|
+
*/
|
|
12
21
|
.C_YUI_JSON {
|
|
13
22
|
--json-string: #006000;
|
|
14
23
|
--json-number: #b5330b;
|
|
15
|
-
--json-boolean: #
|
|
16
|
-
--json-null: #
|
|
24
|
+
--json-boolean: #8C4D00;
|
|
25
|
+
--json-null: #4359C6;
|
|
17
26
|
--json-key: var(--bulma-text-strong, #1a1a1a);
|
|
18
27
|
--json-row-hover: rgba(0, 0, 0, 0.05);
|
|
19
28
|
--json-guide: #b5b5b5;
|
|
@@ -172,6 +181,19 @@
|
|
|
172
181
|
.C_YUI_JSON .JSON_TYPE_BOOLEAN { color: var(--json-boolean); }
|
|
173
182
|
.C_YUI_JSON .JSON_TYPE_NULL { color: var(--json-null); font-style: italic; }
|
|
174
183
|
|
|
184
|
+
/*
|
|
185
|
+
* The id a container row carries beside its size. It is a VALUE, so
|
|
186
|
+
* it takes the string colour and follows the theme with it; what sets
|
|
187
|
+
* it apart from the values under it is the WEIGHT, not a colour of its
|
|
188
|
+
* own -- a sixth colour in a row that already carries five would be
|
|
189
|
+
* one more thing to decode, for a field whose whole job is to be read
|
|
190
|
+
* at a glance while scrolling.
|
|
191
|
+
*/
|
|
192
|
+
.C_YUI_JSON .JSON_SUMMARY_ID {
|
|
193
|
+
color: var(--json-string);
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
.C_YUI_JSON .JSON_STUB {
|
|
176
198
|
color: var(--bulma-link, #3273dc);
|
|
177
199
|
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
|
@@ -197,17 +197,41 @@ let __layout_registered__ = false;
|
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
/***************************************************************
|
|
200
|
-
* Type
|
|
200
|
+
* Type colours, ONE EXPLICIT PALETTE PER THEME and not a
|
|
201
|
+
* derivation, for the same reason the tree keeps two sets of
|
|
202
|
+
* CSS tokens: a colour that reads on white is not the same
|
|
203
|
+
* colour lightened, and deriving one from the other made the
|
|
204
|
+
* two views of one document disagree about what a string is.
|
|
205
|
+
* These ARE the tree's tokens (`c_yui_json.css`), so the tree
|
|
206
|
+
* and the graph now say green with the same green.
|
|
207
|
+
*
|
|
208
|
+
* The light values are darker than the `mx_json_viewer` ones
|
|
209
|
+
* they came from, because those could not be read: `#FF8C00`
|
|
210
|
+
* is mid-luminance, so it reaches at most **2.33:1 against any
|
|
211
|
+
* background that exists** -- no card colour and no highlight
|
|
212
|
+
* could have fixed it, only the colour itself. Orange and blue
|
|
213
|
+
* moved until every type clears 4.5:1 on all five surfaces this
|
|
214
|
+
* viewer paints (two light cards, the match chip, two dark
|
|
215
|
+
* cards); red and green were already there.
|
|
201
216
|
***************************************************************/
|
|
202
|
-
const
|
|
217
|
+
const TYPE_COLORS_LIGHT = {
|
|
203
218
|
string: "#006000", // green
|
|
204
|
-
number: "#
|
|
205
|
-
boolean: "#
|
|
206
|
-
null: "#
|
|
219
|
+
number: "#b5330b", // red
|
|
220
|
+
boolean: "#8C4D00", // orange
|
|
221
|
+
null: "#4359C6", // blue
|
|
207
222
|
list: "#4C0099", // purple
|
|
208
223
|
dict: "#4C0099", // purple
|
|
209
224
|
};
|
|
210
225
|
|
|
226
|
+
const TYPE_COLORS_DARK = {
|
|
227
|
+
string: "#7ec87e",
|
|
228
|
+
number: "#ff8a65",
|
|
229
|
+
boolean: "#ffb74d",
|
|
230
|
+
null: "#90a4f4",
|
|
231
|
+
list: "#B799D6",
|
|
232
|
+
dict: "#B799D6",
|
|
233
|
+
};
|
|
234
|
+
|
|
211
235
|
/***************************************************************
|
|
212
236
|
* Group (card) colours. `fill` is the light-theme card; `tint`
|
|
213
237
|
* is the hue that fill hints at, used to derive the dark card —
|
|
@@ -221,22 +245,38 @@ const GROUP_COLORS = {
|
|
|
221
245
|
|
|
222
246
|
/***************************************************************
|
|
223
247
|
* Soft, theme-aware card palette, same visual language as the
|
|
224
|
-
* gobj-tree's role_card_style():
|
|
225
|
-
*
|
|
226
|
-
*
|
|
248
|
+
* gobj-tree's role_card_style(): a near-neutral fill and a
|
|
249
|
+
* group-colour border.
|
|
250
|
+
*
|
|
251
|
+
* The SURFACE stays out of the hue, on both themes, and that is
|
|
252
|
+
* the whole rule here. The light card is near-white and lets
|
|
253
|
+
* saturated dark text carry the colour; the dark card is its
|
|
254
|
+
* mirror -- a near-neutral dark surface under bright text -- and
|
|
255
|
+
* the group's hue lives in the border and the header bar, which
|
|
256
|
+
* is where the light theme had always put it.
|
|
257
|
+
*
|
|
258
|
+
* It shipped tinting the dark surface 30% instead, and that put
|
|
259
|
+
* green values on a green card. Worse, `list` (a yellow tint)
|
|
260
|
+
* landed mid-luminance, a muddy olive where nothing contrasted
|
|
261
|
+
* with anything: its purple values measured 1.60:1, against 11.48
|
|
262
|
+
* for the same values on the light card. The 12% left is enough
|
|
263
|
+
* to tell a dict card from a list one and not enough to fight the
|
|
264
|
+
* text.
|
|
227
265
|
*
|
|
228
266
|
* The header bar is always the border colour, so its text flips:
|
|
229
267
|
* white on the dark-teal light bar, near-black on the brightened
|
|
230
|
-
* dark one.
|
|
268
|
+
* dark one. The dark bar is mixed lighter than the border used
|
|
269
|
+
* to be so that near-black reads on it (6.87:1, which is what
|
|
270
|
+
* white gets on the light bar).
|
|
231
271
|
***************************************************************/
|
|
232
272
|
function json_card_style(group, dark)
|
|
233
273
|
{
|
|
234
274
|
return {
|
|
235
275
|
bg: dark
|
|
236
|
-
? `color-mix(in srgb, ${group.tint}
|
|
276
|
+
? `color-mix(in srgb, ${group.tint} 12%, #1E242E)`
|
|
237
277
|
: group.fill,
|
|
238
278
|
border: dark
|
|
239
|
-
? `color-mix(in srgb, ${group.stroke}
|
|
279
|
+
? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)`
|
|
240
280
|
: group.stroke,
|
|
241
281
|
header_fg: dark ? "#12181f" : "#ffffff",
|
|
242
282
|
key: dark ? "#c9cfd8" : "#1A1A1A",
|
|
@@ -244,17 +284,17 @@ function json_card_style(group, dark)
|
|
|
244
284
|
}
|
|
245
285
|
|
|
246
286
|
/***************************************************************
|
|
247
|
-
* A scalar's colour by type
|
|
248
|
-
*
|
|
249
|
-
*
|
|
287
|
+
* A scalar's colour by type, straight out of the palette for
|
|
288
|
+
* the theme. It used to be the light colour mixed with white,
|
|
289
|
+
* which is how a document ended up green in one view and a
|
|
290
|
+
* paler green in the other, and how the amount of white became
|
|
291
|
+
* a knob nobody could set: enough of it to read on a dark card
|
|
292
|
+
* washed the six types into one pastel.
|
|
250
293
|
***************************************************************/
|
|
251
294
|
function type_color(type, dark)
|
|
252
295
|
{
|
|
253
|
-
let
|
|
254
|
-
|
|
255
|
-
return color;
|
|
256
|
-
}
|
|
257
|
-
return `color-mix(in srgb, ${color} 55%, #ffffff)`;
|
|
296
|
+
let palette = dark? TYPE_COLORS_DARK: TYPE_COLORS_LIGHT;
|
|
297
|
+
return palette[type] || (dark ? "#e8eaed" : "black");
|
|
258
298
|
}
|
|
259
299
|
|
|
260
300
|
|
|
@@ -698,8 +738,18 @@ function build_graph(gobj)
|
|
|
698
738
|
************************************************************/
|
|
699
739
|
function build_cell_html(key, value, type, dark, matched)
|
|
700
740
|
{
|
|
701
|
-
|
|
702
|
-
|
|
741
|
+
/*
|
|
742
|
+
* A match is a LIGHT chip on BOTH themes, so what sits on it is
|
|
743
|
+
* light-surface text -- the same flip the header bar does.
|
|
744
|
+
*
|
|
745
|
+
* A dark chip has no room to exist: dark enough for the brightened
|
|
746
|
+
* values to read on it is also dark enough to be invisible against
|
|
747
|
+
* the card, and the amber it used to be (#7a5d00) ended up failing
|
|
748
|
+
* at both ends, 2.52:1 under the text and 2.34:1 over the card.
|
|
749
|
+
*/
|
|
750
|
+
let on_light = matched? false: dark;
|
|
751
|
+
let color = type_color(type, on_light);
|
|
752
|
+
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
703
753
|
let display_value = "";
|
|
704
754
|
|
|
705
755
|
switch(type) {
|
|
@@ -734,7 +784,7 @@ function build_cell_html(key, value, type, dark, matched)
|
|
|
734
784
|
* a G6 node state: the key shape of an `html` node is a DOM
|
|
735
785
|
* element, and G6 paints no state style on it. Setting
|
|
736
786
|
* 'active' here would select correctly and show nothing. */
|
|
737
|
-
row = `<span style="background
|
|
787
|
+
row = `<span style="background:#ffe082; ` +
|
|
738
788
|
`border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
739
789
|
}
|
|
740
790
|
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
|
+
});
|