@yuneta/gobj-ui 7.19.3 → 7.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/dist/gobj-ui.cjs.js +256 -30
- package/dist/gobj-ui.es.js +256 -30
- package/package.json +1 -1
- package/src/c_yui_json.css +38 -1
- package/src/c_yui_json.js +226 -16
- package/src/c_yui_shell.js +18 -1
- package/src/json_view_helpers.js +32 -2
- package/src/json_view_helpers.test.js +42 -0
- package/src/yui_icons.css +13 -0
package/src/c_yui_json.js
CHANGED
|
@@ -24,9 +24,19 @@
|
|
|
24
24
|
* Only expanded containers are materialised in the DOM, so the tree
|
|
25
25
|
* stays bounded no matter how large the source document is.
|
|
26
26
|
*
|
|
27
|
+
* TWO VIEWS over the same working document, chosen by the `view_mode`
|
|
28
|
+
* attr ("tree" | "text") and switched from the toolbar: the lazy tree
|
|
29
|
+
* above, and the raw `JSON.stringify(…, 4)` dump of what is currently
|
|
30
|
+
* loaded. The text view is what you reach for to read a document as
|
|
31
|
+
* it is written, to select a slab of it, or to search it with the
|
|
32
|
+
* browser's own Ctrl+F; it shows the `__collapsed__` sentinels
|
|
33
|
+
* verbatim, because that is honestly what the client holds. The
|
|
34
|
+
* tree-only controls (search, expand/collapse) hide with the tree.
|
|
35
|
+
*
|
|
27
36
|
* DOM is self-describing (UPPER_SNAKE logical classes): JSON_VIEWER /
|
|
28
|
-
* JSON_TOOLBAR / JSON_SEARCH /
|
|
29
|
-
*
|
|
37
|
+
* JSON_TOOLBAR / JSON_SEARCH / JSON_VIEW_MODE / JSON_TREE / JSON_TEXT /
|
|
38
|
+
* JSON_TEXT_BODY / JSON_ROW / JSON_KEY / JSON_VALUE / JSON_SUMMARY /
|
|
39
|
+
* JSON_COLLAPSED / JSON_TIME.
|
|
30
40
|
*
|
|
31
41
|
* Copyright (c) 2026, ArtGins.
|
|
32
42
|
* All Rights Reserved.
|
|
@@ -62,6 +72,7 @@ import {
|
|
|
62
72
|
subtree_matches,
|
|
63
73
|
is_time_field,
|
|
64
74
|
format_epoch,
|
|
75
|
+
json_text_dump,
|
|
65
76
|
} from "./json_view_helpers.js";
|
|
66
77
|
|
|
67
78
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
@@ -84,6 +95,14 @@ const GCLASS_NAME = "C_YUI_JSON";
|
|
|
84
95
|
*/
|
|
85
96
|
const MAX_RENDER_ROWS = 5000;
|
|
86
97
|
|
|
98
|
+
/*
|
|
99
|
+
* Hard cap on characters painted by the text view. Same guard as
|
|
100
|
+
* MAX_RENDER_ROWS, one layer down: a document the tree renders lazily
|
|
101
|
+
* is dumped whole here, so the cap is announced under the text and
|
|
102
|
+
* never applied silently.
|
|
103
|
+
*/
|
|
104
|
+
const MAX_TEXT_CHARS = 2000000;
|
|
105
|
+
|
|
87
106
|
/***************************************************************
|
|
88
107
|
* Data
|
|
89
108
|
***************************************************************/
|
|
@@ -94,6 +113,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
|
|
|
94
113
|
/*---------------- Config ----------------*/
|
|
95
114
|
SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
|
|
96
115
|
SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
|
|
116
|
+
SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree) or 'text' (raw JSON dump)"),
|
|
97
117
|
|
|
98
118
|
/*---------------- UI ----------------*/
|
|
99
119
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
|
|
@@ -107,7 +127,13 @@ let PRIVATE_DATA = {
|
|
|
107
127
|
errors: null, // Map<string,string> of per-path expand errors
|
|
108
128
|
search: "", // current search term (lower-cased)
|
|
109
129
|
$tree: null, // the scrollable tree body element
|
|
130
|
+
$text: null, // the scrollable text body element
|
|
131
|
+
$text_body: null, // the <pre> inside it
|
|
110
132
|
$search: null, // the search input element
|
|
133
|
+
$search_ctl: null, // the search control (hidden in text view)
|
|
134
|
+
$expand_btn: null, // "expand loaded" button (hidden in text view)
|
|
135
|
+
$collapse_btn: null,// "collapse all" button (hidden in text view)
|
|
136
|
+
$mode_btn: null, // the tree/text switch
|
|
111
137
|
};
|
|
112
138
|
|
|
113
139
|
let __gclass__ = null;
|
|
@@ -140,6 +166,7 @@ function mt_create(gobj)
|
|
|
140
166
|
}
|
|
141
167
|
|
|
142
168
|
build_ui(gobj);
|
|
169
|
+
apply_view_mode(gobj);
|
|
143
170
|
|
|
144
171
|
/*
|
|
145
172
|
* CHILD subscription model
|
|
@@ -156,7 +183,7 @@ function mt_create(gobj)
|
|
|
156
183
|
***************************************************************/
|
|
157
184
|
function mt_start(gobj)
|
|
158
185
|
{
|
|
159
|
-
|
|
186
|
+
render_view(gobj);
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
/***************************************************************
|
|
@@ -198,13 +225,20 @@ function build_ui(gobj)
|
|
|
198
225
|
style: 'height:100%; display:flex; flex-direction:column;'}, [
|
|
199
226
|
['div', {class: 'JSON_TOOLBAR is-flex-grow-0'}, [$toolbar]],
|
|
200
227
|
['div', {class: 'JSON_TREE is-flex-grow-1',
|
|
228
|
+
style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
|
|
229
|
+
['div', {class: 'JSON_TEXT is-flex-grow-1 is-hidden',
|
|
201
230
|
style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
|
|
202
231
|
]]
|
|
203
232
|
);
|
|
204
233
|
|
|
205
234
|
gobj_write_attr(gobj, "$container", $container);
|
|
206
235
|
priv.$tree = $container.querySelector('.JSON_TREE');
|
|
236
|
+
priv.$text = $container.querySelector('.JSON_TEXT');
|
|
207
237
|
priv.$search = $container.querySelector('.JSON_SEARCH');
|
|
238
|
+
priv.$search_ctl = $container.querySelector('.JSON_SEARCH_CONTROL');
|
|
239
|
+
priv.$expand_btn = $container.querySelector('.EV_EXPAND_ALL');
|
|
240
|
+
priv.$collapse_btn = $container.querySelector('.EV_COLLAPSE_ALL');
|
|
241
|
+
priv.$mode_btn = $container.querySelector('.JSON_VIEW_MODE');
|
|
208
242
|
|
|
209
243
|
refresh_language($container, t);
|
|
210
244
|
}
|
|
@@ -223,11 +257,18 @@ function destroy_ui(gobj)
|
|
|
223
257
|
gobj_write_attr(gobj, "$container", null);
|
|
224
258
|
}
|
|
225
259
|
priv.$tree = null;
|
|
260
|
+
priv.$text = null;
|
|
261
|
+
priv.$text_body = null;
|
|
226
262
|
priv.$search = null;
|
|
263
|
+
priv.$search_ctl = null;
|
|
264
|
+
priv.$expand_btn = null;
|
|
265
|
+
priv.$collapse_btn = null;
|
|
266
|
+
priv.$mode_btn = null;
|
|
227
267
|
}
|
|
228
268
|
|
|
229
269
|
/************************************************************
|
|
230
|
-
* Toolbar: search +
|
|
270
|
+
* Toolbar: search + tree/text switch + expand-all /
|
|
271
|
+
* collapse-all / copy
|
|
231
272
|
************************************************************/
|
|
232
273
|
function make_toolbar(gobj)
|
|
233
274
|
{
|
|
@@ -251,7 +292,8 @@ function make_toolbar(gobj)
|
|
|
251
292
|
}
|
|
252
293
|
}]);
|
|
253
294
|
let $search_control = createElement2(
|
|
254
|
-
['div', {class: 'control has-icons-left
|
|
295
|
+
['div', {class: 'control has-icons-left JSON_SEARCH_CONTROL',
|
|
296
|
+
style: 'max-width:22em;'}, [
|
|
255
297
|
$search_input,
|
|
256
298
|
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
257
299
|
]]);
|
|
@@ -259,6 +301,7 @@ function make_toolbar(gobj)
|
|
|
259
301
|
left_items.push($search_control);
|
|
260
302
|
|
|
261
303
|
let right_items = [
|
|
304
|
+
view_mode_button(gobj),
|
|
262
305
|
icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
|
|
263
306
|
icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
|
|
264
307
|
icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
|
|
@@ -293,6 +336,141 @@ function icon_button(gobj, icon, event_name, label_key)
|
|
|
293
336
|
}];
|
|
294
337
|
}
|
|
295
338
|
|
|
339
|
+
/************************************************************
|
|
340
|
+
* The tree/text switch.
|
|
341
|
+
*
|
|
342
|
+
* It shows the view it takes you TO — the `code` glyph while the
|
|
343
|
+
* tree is on screen, the `sitemap` glyph while the text is — and
|
|
344
|
+
* sends EV_SET_VIEW_MODE with no mode, which the action reads as
|
|
345
|
+
* "toggle". apply_view_mode() keeps the icon and its i18n keys in
|
|
346
|
+
* step with the attr.
|
|
347
|
+
************************************************************/
|
|
348
|
+
function view_mode_button(gobj)
|
|
349
|
+
{
|
|
350
|
+
return ['button', {class: 'button JSON_VIEW_MODE', style: 'width:2.5em;',
|
|
351
|
+
title: t("text view"), 'data-i18n-title': "text view",
|
|
352
|
+
'aria-label': t("text view"), 'data-i18n-aria-label': "text view"}, [
|
|
353
|
+
['span', {class: 'icon'}, [['i', {class: 'yi-code'}]]]
|
|
354
|
+
], {
|
|
355
|
+
click: function(evt) {
|
|
356
|
+
evt.stopPropagation();
|
|
357
|
+
gobj_send_event(gobj, "EV_SET_VIEW_MODE", {}, gobj);
|
|
358
|
+
}
|
|
359
|
+
}];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/************************************************************
|
|
363
|
+
* The current view, normalised. Anything but "text" is the tree.
|
|
364
|
+
************************************************************/
|
|
365
|
+
function current_view_mode(gobj)
|
|
366
|
+
{
|
|
367
|
+
return (gobj_read_str_attr(gobj, "view_mode") === "text")? "text": "tree";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/************************************************************
|
|
371
|
+
* Show the chrome of the current view and hide the other's.
|
|
372
|
+
************************************************************/
|
|
373
|
+
function apply_view_mode(gobj)
|
|
374
|
+
{
|
|
375
|
+
let priv = gobj.priv;
|
|
376
|
+
let text_mode = current_view_mode(gobj) === "text";
|
|
377
|
+
|
|
378
|
+
if(priv.$tree) {
|
|
379
|
+
priv.$tree.classList.toggle('is-hidden', text_mode);
|
|
380
|
+
}
|
|
381
|
+
if(priv.$text) {
|
|
382
|
+
priv.$text.classList.toggle('is-hidden', !text_mode);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* Search and expand/collapse act on TREE rows; in the text view
|
|
387
|
+
* they have nothing to act on, and a control that answers nothing
|
|
388
|
+
* is worse than an absent one. A <pre> is searched with the
|
|
389
|
+
* browser's own Ctrl+F.
|
|
390
|
+
*/
|
|
391
|
+
[priv.$search_ctl, priv.$expand_btn, priv.$collapse_btn].forEach(function($el) {
|
|
392
|
+
if($el) {
|
|
393
|
+
$el.classList.toggle('is-hidden', text_mode);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if(priv.$mode_btn) {
|
|
398
|
+
let icon = text_mode? "yi-sitemap": "yi-code";
|
|
399
|
+
let key = text_mode? "tree view": "text view";
|
|
400
|
+
let $i = priv.$mode_btn.querySelector('i');
|
|
401
|
+
if($i) {
|
|
402
|
+
$i.className = icon;
|
|
403
|
+
}
|
|
404
|
+
priv.$mode_btn.setAttribute("data-i18n-title", key);
|
|
405
|
+
priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
|
|
406
|
+
priv.$mode_btn.setAttribute("title", t(key));
|
|
407
|
+
priv.$mode_btn.setAttribute("aria-label", t(key));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/************************************************************
|
|
412
|
+
* Render whichever view is current.
|
|
413
|
+
************************************************************/
|
|
414
|
+
function render_view(gobj)
|
|
415
|
+
{
|
|
416
|
+
if(current_view_mode(gobj) === "text") {
|
|
417
|
+
render_text(gobj);
|
|
418
|
+
} else {
|
|
419
|
+
render_tree(gobj);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/************************************************************
|
|
424
|
+
* Render the text view: the whole loaded document, verbatim.
|
|
425
|
+
*
|
|
426
|
+
* Unlike the tree, nothing here is lazy — what the client holds is
|
|
427
|
+
* what it prints, `__collapsed__` sentinels included. Over
|
|
428
|
+
* MAX_TEXT_CHARS the dump is cut and the cut is announced.
|
|
429
|
+
************************************************************/
|
|
430
|
+
function render_text(gobj)
|
|
431
|
+
{
|
|
432
|
+
let priv = gobj.priv;
|
|
433
|
+
let $text = priv.$text;
|
|
434
|
+
if(!$text) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let scroll_top = $text.scrollTop;
|
|
439
|
+
$text.textContent = "";
|
|
440
|
+
priv.$text_body = null;
|
|
441
|
+
|
|
442
|
+
if(priv.root === null || priv.root === undefined) {
|
|
443
|
+
$text.appendChild(createElement2(
|
|
444
|
+
['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
|
|
445
|
+
));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let dump = json_text_dump(priv.root, MAX_TEXT_CHARS);
|
|
450
|
+
if(dump.error) {
|
|
451
|
+
log_error(`${GCLASS_NAME}: cannot dump the document as text: ${dump.error}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* textContent, not a createElement2 child: the dump is raw text
|
|
455
|
+
* that must reach the DOM untouched, and createElement2 trims its
|
|
456
|
+
* text nodes. */
|
|
457
|
+
let $pre = createElement2(['pre', {class: 'JSON_TEXT_BODY'}, []]);
|
|
458
|
+
$pre.textContent = dump.text;
|
|
459
|
+
$text.appendChild($pre);
|
|
460
|
+
priv.$text_body = $pre;
|
|
461
|
+
|
|
462
|
+
if(dump.capped) {
|
|
463
|
+
$text.appendChild(createElement2(
|
|
464
|
+
['div', {class: 'JSON_CAPPED has-text-warning p-2',
|
|
465
|
+
'data-i18n': 'text truncated; collapse some branches'},
|
|
466
|
+
'text truncated; collapse some branches']
|
|
467
|
+
));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
refresh_language($text, t);
|
|
471
|
+
$text.scrollTop = scroll_top;
|
|
472
|
+
}
|
|
473
|
+
|
|
296
474
|
/************************************************************
|
|
297
475
|
* Re-render the whole tree from priv.root + priv.expanded.
|
|
298
476
|
*
|
|
@@ -629,7 +807,7 @@ function ac_set_json(gobj, event, kw, src)
|
|
|
629
807
|
priv.pending.clear();
|
|
630
808
|
priv.errors.clear();
|
|
631
809
|
|
|
632
|
-
|
|
810
|
+
render_view(gobj);
|
|
633
811
|
return 0;
|
|
634
812
|
}
|
|
635
813
|
|
|
@@ -655,7 +833,7 @@ function ac_subtree_loaded(gobj, event, kw, src)
|
|
|
655
833
|
priv.expanded.add(path); // reveal what we just loaded
|
|
656
834
|
}
|
|
657
835
|
|
|
658
|
-
|
|
836
|
+
render_view(gobj);
|
|
659
837
|
return 0;
|
|
660
838
|
}
|
|
661
839
|
|
|
@@ -672,7 +850,7 @@ function ac_subtree_error(gobj, event, kw, src)
|
|
|
672
850
|
|
|
673
851
|
log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${kw.error || ""}`);
|
|
674
852
|
|
|
675
|
-
|
|
853
|
+
render_view(gobj);
|
|
676
854
|
return 0;
|
|
677
855
|
}
|
|
678
856
|
|
|
@@ -690,7 +868,7 @@ function ac_toggle_node(gobj, event, kw, src)
|
|
|
690
868
|
priv.expanded.add(path);
|
|
691
869
|
}
|
|
692
870
|
|
|
693
|
-
|
|
871
|
+
render_view(gobj);
|
|
694
872
|
return 0;
|
|
695
873
|
}
|
|
696
874
|
|
|
@@ -709,7 +887,7 @@ function ac_expand_collapsed(gobj, event, kw, src)
|
|
|
709
887
|
priv.pending.add(path);
|
|
710
888
|
priv.errors.delete(path);
|
|
711
889
|
|
|
712
|
-
|
|
890
|
+
render_view(gobj); // show the "loading…" state
|
|
713
891
|
|
|
714
892
|
gobj_publish_event(gobj, "EV_EXPAND_PATH", {path: path, size: kw.size});
|
|
715
893
|
return 0;
|
|
@@ -722,7 +900,7 @@ function ac_search(gobj, event, kw, src)
|
|
|
722
900
|
{
|
|
723
901
|
let priv = gobj.priv;
|
|
724
902
|
priv.search = (kw.text || "").trim().toLowerCase();
|
|
725
|
-
|
|
903
|
+
render_view(gobj);
|
|
726
904
|
return 0;
|
|
727
905
|
}
|
|
728
906
|
|
|
@@ -751,7 +929,7 @@ function ac_expand_all(gobj, event, kw, src)
|
|
|
751
929
|
priv.expanded.add(p);
|
|
752
930
|
});
|
|
753
931
|
|
|
754
|
-
|
|
932
|
+
render_view(gobj);
|
|
755
933
|
return 0;
|
|
756
934
|
}
|
|
757
935
|
|
|
@@ -762,7 +940,7 @@ function ac_collapse_all(gobj, event, kw, src)
|
|
|
762
940
|
{
|
|
763
941
|
let priv = gobj.priv;
|
|
764
942
|
priv.expanded.clear();
|
|
765
|
-
|
|
943
|
+
render_view(gobj);
|
|
766
944
|
return 0;
|
|
767
945
|
}
|
|
768
946
|
|
|
@@ -775,7 +953,12 @@ function ac_copy_all(gobj, event, kw, src)
|
|
|
775
953
|
if(priv.root === null || priv.root === undefined) {
|
|
776
954
|
return 0;
|
|
777
955
|
}
|
|
778
|
-
let
|
|
956
|
+
let dump = json_text_dump(priv.root, 0);
|
|
957
|
+
if(dump.error) {
|
|
958
|
+
log_error(`${GCLASS_NAME}: cannot copy the document: ${dump.error}`);
|
|
959
|
+
return -1;
|
|
960
|
+
}
|
|
961
|
+
let text = dump.text;
|
|
779
962
|
if(navigator.clipboard && navigator.clipboard.writeText) {
|
|
780
963
|
navigator.clipboard.writeText(text).catch(function(e) {
|
|
781
964
|
log_error(`${GCLASS_NAME}: clipboard write failed: ${e}`);
|
|
@@ -786,6 +969,29 @@ function ac_copy_all(gobj, event, kw, src)
|
|
|
786
969
|
return 0;
|
|
787
970
|
}
|
|
788
971
|
|
|
972
|
+
/************************************************************
|
|
973
|
+
* EV_SET_VIEW_MODE { mode } — "tree" | "text".
|
|
974
|
+
*
|
|
975
|
+
* With no mode it toggles, which is what the toolbar button sends.
|
|
976
|
+
************************************************************/
|
|
977
|
+
function ac_set_view_mode(gobj, event, kw, src)
|
|
978
|
+
{
|
|
979
|
+
let mode = kw.mode;
|
|
980
|
+
|
|
981
|
+
if(mode === undefined || mode === null || mode === "") {
|
|
982
|
+
mode = (current_view_mode(gobj) === "text")? "tree": "text";
|
|
983
|
+
} else if(mode !== "tree" && mode !== "text") {
|
|
984
|
+
log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
|
|
985
|
+
return -1;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
gobj_write_attr(gobj, "view_mode", mode);
|
|
989
|
+
|
|
990
|
+
apply_view_mode(gobj);
|
|
991
|
+
render_view(gobj);
|
|
992
|
+
return 0;
|
|
993
|
+
}
|
|
994
|
+
|
|
789
995
|
/************************************************************
|
|
790
996
|
* EV_LANGUAGE_CHANGED — re-translate chrome + re-render
|
|
791
997
|
************************************************************/
|
|
@@ -798,8 +1004,9 @@ function ac_language_changed(gobj, event, kw, src)
|
|
|
798
1004
|
if(priv.$search) {
|
|
799
1005
|
priv.$search.setAttribute("placeholder", t("search"));
|
|
800
1006
|
}
|
|
1007
|
+
apply_view_mode(gobj);
|
|
801
1008
|
}
|
|
802
|
-
|
|
1009
|
+
render_view(gobj);
|
|
803
1010
|
return 0;
|
|
804
1011
|
}
|
|
805
1012
|
|
|
@@ -829,7 +1036,7 @@ function ac_hide(gobj, event, kw, src)
|
|
|
829
1036
|
************************************************************/
|
|
830
1037
|
function ac_refresh(gobj, event, kw, src)
|
|
831
1038
|
{
|
|
832
|
-
|
|
1039
|
+
render_view(gobj);
|
|
833
1040
|
return 0;
|
|
834
1041
|
}
|
|
835
1042
|
|
|
@@ -869,6 +1076,7 @@ function create_gclass(gclass_name)
|
|
|
869
1076
|
const states = [
|
|
870
1077
|
["ST_EMPTY", [
|
|
871
1078
|
["EV_SET_JSON", ac_set_json, "ST_READY"],
|
|
1079
|
+
["EV_SET_VIEW_MODE", ac_set_view_mode, null],
|
|
872
1080
|
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
873
1081
|
["EV_REFRESH", ac_refresh, null],
|
|
874
1082
|
["EV_SHOW", ac_show, null],
|
|
@@ -884,6 +1092,7 @@ function create_gclass(gclass_name)
|
|
|
884
1092
|
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
885
1093
|
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
886
1094
|
["EV_COPY_ALL", ac_copy_all, null],
|
|
1095
|
+
["EV_SET_VIEW_MODE", ac_set_view_mode, null],
|
|
887
1096
|
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
888
1097
|
["EV_REFRESH", ac_refresh, null],
|
|
889
1098
|
["EV_SHOW", ac_show, null],
|
|
@@ -904,6 +1113,7 @@ function create_gclass(gclass_name)
|
|
|
904
1113
|
["EV_EXPAND_ALL", 0],
|
|
905
1114
|
["EV_COLLAPSE_ALL", 0],
|
|
906
1115
|
["EV_COPY_ALL", 0],
|
|
1116
|
+
["EV_SET_VIEW_MODE", 0],
|
|
907
1117
|
["EV_LANGUAGE_CHANGED", 0],
|
|
908
1118
|
["EV_REFRESH", 0],
|
|
909
1119
|
["EV_SHOW", 0],
|
package/src/c_yui_shell.js
CHANGED
|
@@ -1282,7 +1282,24 @@ function navigate_to(gobj, route, depth, no_drain)
|
|
|
1282
1282
|
}
|
|
1283
1283
|
};
|
|
1284
1284
|
let config = gobj_read_attr(gobj, "config") || {};
|
|
1285
|
-
|
|
1285
|
+
/* Where the user actually WAS, which is the url and not the
|
|
1286
|
+
* mount.
|
|
1287
|
+
*
|
|
1288
|
+
* `stages.main.active_route` is the DECLARED route the view is
|
|
1289
|
+
* mounted at, and under a `C_YUI_NODE` tree everything below it
|
|
1290
|
+
* is subpath the node owns -- so restoring THAT restores the
|
|
1291
|
+
* tree's root and throws the position away. Switching the theme
|
|
1292
|
+
* from a graph five levels down answered with the workspace
|
|
1293
|
+
* landing, and so would every other action route: preferences,
|
|
1294
|
+
* the site map, the dev tools.
|
|
1295
|
+
*
|
|
1296
|
+
* `current_route` is the same route WITH its subpath, and it is
|
|
1297
|
+
* only ever written for a RESTING route (an action returns
|
|
1298
|
+
* before that line), so it cannot be an action route itself.
|
|
1299
|
+
* It is what a deep link resolves, and therefore what can be
|
|
1300
|
+
* restored. */
|
|
1301
|
+
let prev = gobj_read_attr(gobj, "current_route") ||
|
|
1302
|
+
(priv.stages && priv.stages.main &&
|
|
1286
1303
|
priv.stages.main.active_route) ||
|
|
1287
1304
|
gobj_read_attr(gobj, "default_route") ||
|
|
1288
1305
|
(config.shell && config.shell.stages &&
|
package/src/json_view_helpers.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Pure, testable logic behind C_YUI_JSON (the lazy JSON tree
|
|
5
5
|
* viewer). Kept out of the gclass so it can be unit-tested with
|
|
6
6
|
* no DOM: collapsed-sentinel detection, path (segments) algebra,
|
|
7
|
-
* search matching, timestamp recognition/formatting
|
|
8
|
-
* type discriminator.
|
|
7
|
+
* search matching, timestamp recognition/formatting, the JSON
|
|
8
|
+
* type discriminator and the text view's capped dump.
|
|
9
9
|
*
|
|
10
10
|
* Path convention mirrors the C kernel (kw_collapse / kw_find_path
|
|
11
11
|
* in kwid.c): segments are joined by the backtick delimiter, arrays
|
|
@@ -212,3 +212,33 @@ export function format_epoch(value)
|
|
|
212
212
|
}
|
|
213
213
|
return d.toLocaleString();
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/************************************************************
|
|
218
|
+
* The text view's payload: the document as text, indented
|
|
219
|
+
* four characters, cut at `max_chars`.
|
|
220
|
+
*
|
|
221
|
+
* Returns {text, capped}. `capped` is what the caller
|
|
222
|
+
* announces under the dump — a cut that is not announced
|
|
223
|
+
* reads as a document that ends there.
|
|
224
|
+
************************************************************/
|
|
225
|
+
export function json_text_dump(root, max_chars)
|
|
226
|
+
{
|
|
227
|
+
let text;
|
|
228
|
+
try {
|
|
229
|
+
text = JSON.stringify(root, null, 4);
|
|
230
|
+
} catch(e) {
|
|
231
|
+
return {text: "", capped: false, error: String(e)};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if(text === undefined) {
|
|
235
|
+
/* JSON.stringify() answers undefined for a value that has no
|
|
236
|
+
* JSON form at all (a function, a bare undefined). */
|
|
237
|
+
return {text: "", capped: false, error: "not representable as JSON"};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if(typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) {
|
|
241
|
+
return {text: text.slice(0, max_chars), capped: true};
|
|
242
|
+
}
|
|
243
|
+
return {text: text, capped: false};
|
|
244
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
subtree_matches,
|
|
16
16
|
is_time_field,
|
|
17
17
|
format_epoch,
|
|
18
|
+
json_text_dump,
|
|
18
19
|
} from "./json_view_helpers.js";
|
|
19
20
|
|
|
20
21
|
|
|
@@ -133,3 +134,44 @@ test("format_epoch handles seconds, milliseconds and the unset case", () => {
|
|
|
133
134
|
expect(typeof format_epoch(1700000000)).toBe("string"); // seconds
|
|
134
135
|
expect(typeof format_epoch(1700000000000)).toBe("string"); // milliseconds
|
|
135
136
|
});
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/*============================================================
|
|
140
|
+
* text view dump
|
|
141
|
+
*============================================================*/
|
|
142
|
+
test("json_text_dump indents four characters, like every other tree", () => {
|
|
143
|
+
let d = json_text_dump({a: {b: 1}}, 0);
|
|
144
|
+
expect(d.capped).toBe(false);
|
|
145
|
+
expect(d.error).toBeUndefined();
|
|
146
|
+
expect(d.text).toBe('{\n "a": {\n "b": 1\n }\n}');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("json_text_dump prints a collapsed sentinel verbatim", () => {
|
|
150
|
+
let d = json_text_dump({rows: {__collapsed__: {path: "rows", size: 900}}}, 0);
|
|
151
|
+
expect(d.text).toContain("__collapsed__");
|
|
152
|
+
expect(d.text).toContain("900");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("json_text_dump cuts at max_chars and says so", () => {
|
|
156
|
+
let d = json_text_dump({a: "x".repeat(500)}, 100);
|
|
157
|
+
expect(d.capped).toBe(true);
|
|
158
|
+
expect(d.text.length).toBe(100);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("json_text_dump does not cap when max_chars is absent or zero", () => {
|
|
162
|
+
let big = {a: "x".repeat(500)};
|
|
163
|
+
expect(json_text_dump(big, 0).capped).toBe(false);
|
|
164
|
+
expect(json_text_dump(big).capped).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("json_text_dump reports a document with no JSON form instead of printing nothing", () => {
|
|
168
|
+
let circular = {};
|
|
169
|
+
circular.self = circular;
|
|
170
|
+
let d = json_text_dump(circular, 0);
|
|
171
|
+
expect(d.text).toBe("");
|
|
172
|
+
expect(typeof d.error).toBe("string");
|
|
173
|
+
|
|
174
|
+
let d2 = json_text_dump(undefined, 0);
|
|
175
|
+
expect(d2.text).toBe("");
|
|
176
|
+
expect(typeof d2.error).toBe("string");
|
|
177
|
+
});
|
package/src/yui_icons.css
CHANGED
|
@@ -363,3 +363,16 @@
|
|
|
363
363
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
|
|
364
364
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z'/%3E%3C/svg%3E");
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/* The two faces of the JSON viewer's view switch: the tree it renders
|
|
368
|
+
* and the raw text behind it. Neither was in the set, and a missing
|
|
369
|
+
* glyph renders as a solid black square. */
|
|
370
|
+
.yi-code::before {
|
|
371
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath d='M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
|
|
372
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3Cpath d='M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.yi-sitemap::before {
|
|
376
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M208 80c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-8 0 0 40 152 0c30.9 0 56 25.1 56 56l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-4.4-3.6-8-8-8l-152 0 0 40 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-40-152 0c-4.4 0-8 3.6-8 8l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-30.9 25.1-56 56-56l152 0 0-40-8 0c-26.5 0-48-21.5-48-48l0-64z'/%3E%3C/svg%3E");
|
|
377
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M208 80c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-8 0 0 40 152 0c30.9 0 56 25.1 56 56l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-4.4-3.6-8-8-8l-152 0 0 40 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-40-152 0c-4.4 0-8 3.6-8 8l0 32 8 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-64 0c-26.5 0-48-21.5-48-48l0-64c0-26.5 21.5-48 48-48l8 0 0-32c0-30.9 25.1-56 56-56l152 0 0-40-8 0c-26.5 0-48-21.5-48-48l0-64z'/%3E%3C/svg%3E");
|
|
378
|
+
}
|