@yuneta/gobj-ui 7.19.4 → 7.20.1
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 -29
- package/dist/gobj-ui.es.js +256 -29
- package/package.json +1 -1
- package/src/c_yui_json.css +38 -1
- package/src/c_yui_json.js +233 -16
- 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,148 @@ 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
|
+
/* Both keys spelled out inside t(), never t(variable): the apps'
|
|
400
|
+
* validate-locales scans for t("literal"), and a key it cannot
|
|
401
|
+
* see is a key it cannot demand — i18next then answers an
|
|
402
|
+
* undefined key with the key ITSELF, so the button renders in
|
|
403
|
+
* lower-case English and never changes language. `key` still
|
|
404
|
+
* carries the data-i18n-* attrs refresh_language() reads. */
|
|
405
|
+
let key = text_mode? "tree view": "text view";
|
|
406
|
+
let label = text_mode? t("tree view"): t("text view");
|
|
407
|
+
let $i = priv.$mode_btn.querySelector('i');
|
|
408
|
+
if($i) {
|
|
409
|
+
$i.className = icon;
|
|
410
|
+
}
|
|
411
|
+
priv.$mode_btn.setAttribute("data-i18n-title", key);
|
|
412
|
+
priv.$mode_btn.setAttribute("data-i18n-aria-label", key);
|
|
413
|
+
priv.$mode_btn.setAttribute("title", label);
|
|
414
|
+
priv.$mode_btn.setAttribute("aria-label", label);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/************************************************************
|
|
419
|
+
* Render whichever view is current.
|
|
420
|
+
************************************************************/
|
|
421
|
+
function render_view(gobj)
|
|
422
|
+
{
|
|
423
|
+
if(current_view_mode(gobj) === "text") {
|
|
424
|
+
render_text(gobj);
|
|
425
|
+
} else {
|
|
426
|
+
render_tree(gobj);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/************************************************************
|
|
431
|
+
* Render the text view: the whole loaded document, verbatim.
|
|
432
|
+
*
|
|
433
|
+
* Unlike the tree, nothing here is lazy — what the client holds is
|
|
434
|
+
* what it prints, `__collapsed__` sentinels included. Over
|
|
435
|
+
* MAX_TEXT_CHARS the dump is cut and the cut is announced.
|
|
436
|
+
************************************************************/
|
|
437
|
+
function render_text(gobj)
|
|
438
|
+
{
|
|
439
|
+
let priv = gobj.priv;
|
|
440
|
+
let $text = priv.$text;
|
|
441
|
+
if(!$text) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let scroll_top = $text.scrollTop;
|
|
446
|
+
$text.textContent = "";
|
|
447
|
+
priv.$text_body = null;
|
|
448
|
+
|
|
449
|
+
if(priv.root === null || priv.root === undefined) {
|
|
450
|
+
$text.appendChild(createElement2(
|
|
451
|
+
['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
|
|
452
|
+
));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let dump = json_text_dump(priv.root, MAX_TEXT_CHARS);
|
|
457
|
+
if(dump.error) {
|
|
458
|
+
log_error(`${GCLASS_NAME}: cannot dump the document as text: ${dump.error}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* textContent, not a createElement2 child: the dump is raw text
|
|
462
|
+
* that must reach the DOM untouched, and createElement2 trims its
|
|
463
|
+
* text nodes. */
|
|
464
|
+
let $pre = createElement2(['pre', {class: 'JSON_TEXT_BODY'}, []]);
|
|
465
|
+
$pre.textContent = dump.text;
|
|
466
|
+
$text.appendChild($pre);
|
|
467
|
+
priv.$text_body = $pre;
|
|
468
|
+
|
|
469
|
+
if(dump.capped) {
|
|
470
|
+
$text.appendChild(createElement2(
|
|
471
|
+
['div', {class: 'JSON_CAPPED has-text-warning p-2',
|
|
472
|
+
'data-i18n': 'text truncated; collapse some branches'},
|
|
473
|
+
'text truncated; collapse some branches']
|
|
474
|
+
));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
refresh_language($text, t);
|
|
478
|
+
$text.scrollTop = scroll_top;
|
|
479
|
+
}
|
|
480
|
+
|
|
296
481
|
/************************************************************
|
|
297
482
|
* Re-render the whole tree from priv.root + priv.expanded.
|
|
298
483
|
*
|
|
@@ -629,7 +814,7 @@ function ac_set_json(gobj, event, kw, src)
|
|
|
629
814
|
priv.pending.clear();
|
|
630
815
|
priv.errors.clear();
|
|
631
816
|
|
|
632
|
-
|
|
817
|
+
render_view(gobj);
|
|
633
818
|
return 0;
|
|
634
819
|
}
|
|
635
820
|
|
|
@@ -655,7 +840,7 @@ function ac_subtree_loaded(gobj, event, kw, src)
|
|
|
655
840
|
priv.expanded.add(path); // reveal what we just loaded
|
|
656
841
|
}
|
|
657
842
|
|
|
658
|
-
|
|
843
|
+
render_view(gobj);
|
|
659
844
|
return 0;
|
|
660
845
|
}
|
|
661
846
|
|
|
@@ -672,7 +857,7 @@ function ac_subtree_error(gobj, event, kw, src)
|
|
|
672
857
|
|
|
673
858
|
log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${kw.error || ""}`);
|
|
674
859
|
|
|
675
|
-
|
|
860
|
+
render_view(gobj);
|
|
676
861
|
return 0;
|
|
677
862
|
}
|
|
678
863
|
|
|
@@ -690,7 +875,7 @@ function ac_toggle_node(gobj, event, kw, src)
|
|
|
690
875
|
priv.expanded.add(path);
|
|
691
876
|
}
|
|
692
877
|
|
|
693
|
-
|
|
878
|
+
render_view(gobj);
|
|
694
879
|
return 0;
|
|
695
880
|
}
|
|
696
881
|
|
|
@@ -709,7 +894,7 @@ function ac_expand_collapsed(gobj, event, kw, src)
|
|
|
709
894
|
priv.pending.add(path);
|
|
710
895
|
priv.errors.delete(path);
|
|
711
896
|
|
|
712
|
-
|
|
897
|
+
render_view(gobj); // show the "loading…" state
|
|
713
898
|
|
|
714
899
|
gobj_publish_event(gobj, "EV_EXPAND_PATH", {path: path, size: kw.size});
|
|
715
900
|
return 0;
|
|
@@ -722,7 +907,7 @@ function ac_search(gobj, event, kw, src)
|
|
|
722
907
|
{
|
|
723
908
|
let priv = gobj.priv;
|
|
724
909
|
priv.search = (kw.text || "").trim().toLowerCase();
|
|
725
|
-
|
|
910
|
+
render_view(gobj);
|
|
726
911
|
return 0;
|
|
727
912
|
}
|
|
728
913
|
|
|
@@ -751,7 +936,7 @@ function ac_expand_all(gobj, event, kw, src)
|
|
|
751
936
|
priv.expanded.add(p);
|
|
752
937
|
});
|
|
753
938
|
|
|
754
|
-
|
|
939
|
+
render_view(gobj);
|
|
755
940
|
return 0;
|
|
756
941
|
}
|
|
757
942
|
|
|
@@ -762,7 +947,7 @@ function ac_collapse_all(gobj, event, kw, src)
|
|
|
762
947
|
{
|
|
763
948
|
let priv = gobj.priv;
|
|
764
949
|
priv.expanded.clear();
|
|
765
|
-
|
|
950
|
+
render_view(gobj);
|
|
766
951
|
return 0;
|
|
767
952
|
}
|
|
768
953
|
|
|
@@ -775,7 +960,12 @@ function ac_copy_all(gobj, event, kw, src)
|
|
|
775
960
|
if(priv.root === null || priv.root === undefined) {
|
|
776
961
|
return 0;
|
|
777
962
|
}
|
|
778
|
-
let
|
|
963
|
+
let dump = json_text_dump(priv.root, 0);
|
|
964
|
+
if(dump.error) {
|
|
965
|
+
log_error(`${GCLASS_NAME}: cannot copy the document: ${dump.error}`);
|
|
966
|
+
return -1;
|
|
967
|
+
}
|
|
968
|
+
let text = dump.text;
|
|
779
969
|
if(navigator.clipboard && navigator.clipboard.writeText) {
|
|
780
970
|
navigator.clipboard.writeText(text).catch(function(e) {
|
|
781
971
|
log_error(`${GCLASS_NAME}: clipboard write failed: ${e}`);
|
|
@@ -786,6 +976,29 @@ function ac_copy_all(gobj, event, kw, src)
|
|
|
786
976
|
return 0;
|
|
787
977
|
}
|
|
788
978
|
|
|
979
|
+
/************************************************************
|
|
980
|
+
* EV_SET_VIEW_MODE { mode } — "tree" | "text".
|
|
981
|
+
*
|
|
982
|
+
* With no mode it toggles, which is what the toolbar button sends.
|
|
983
|
+
************************************************************/
|
|
984
|
+
function ac_set_view_mode(gobj, event, kw, src)
|
|
985
|
+
{
|
|
986
|
+
let mode = kw.mode;
|
|
987
|
+
|
|
988
|
+
if(mode === undefined || mode === null || mode === "") {
|
|
989
|
+
mode = (current_view_mode(gobj) === "text")? "tree": "text";
|
|
990
|
+
} else if(mode !== "tree" && mode !== "text") {
|
|
991
|
+
log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
|
|
992
|
+
return -1;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
gobj_write_attr(gobj, "view_mode", mode);
|
|
996
|
+
|
|
997
|
+
apply_view_mode(gobj);
|
|
998
|
+
render_view(gobj);
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
789
1002
|
/************************************************************
|
|
790
1003
|
* EV_LANGUAGE_CHANGED — re-translate chrome + re-render
|
|
791
1004
|
************************************************************/
|
|
@@ -798,8 +1011,9 @@ function ac_language_changed(gobj, event, kw, src)
|
|
|
798
1011
|
if(priv.$search) {
|
|
799
1012
|
priv.$search.setAttribute("placeholder", t("search"));
|
|
800
1013
|
}
|
|
1014
|
+
apply_view_mode(gobj);
|
|
801
1015
|
}
|
|
802
|
-
|
|
1016
|
+
render_view(gobj);
|
|
803
1017
|
return 0;
|
|
804
1018
|
}
|
|
805
1019
|
|
|
@@ -829,7 +1043,7 @@ function ac_hide(gobj, event, kw, src)
|
|
|
829
1043
|
************************************************************/
|
|
830
1044
|
function ac_refresh(gobj, event, kw, src)
|
|
831
1045
|
{
|
|
832
|
-
|
|
1046
|
+
render_view(gobj);
|
|
833
1047
|
return 0;
|
|
834
1048
|
}
|
|
835
1049
|
|
|
@@ -869,6 +1083,7 @@ function create_gclass(gclass_name)
|
|
|
869
1083
|
const states = [
|
|
870
1084
|
["ST_EMPTY", [
|
|
871
1085
|
["EV_SET_JSON", ac_set_json, "ST_READY"],
|
|
1086
|
+
["EV_SET_VIEW_MODE", ac_set_view_mode, null],
|
|
872
1087
|
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
873
1088
|
["EV_REFRESH", ac_refresh, null],
|
|
874
1089
|
["EV_SHOW", ac_show, null],
|
|
@@ -884,6 +1099,7 @@ function create_gclass(gclass_name)
|
|
|
884
1099
|
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
885
1100
|
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
886
1101
|
["EV_COPY_ALL", ac_copy_all, null],
|
|
1102
|
+
["EV_SET_VIEW_MODE", ac_set_view_mode, null],
|
|
887
1103
|
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
888
1104
|
["EV_REFRESH", ac_refresh, null],
|
|
889
1105
|
["EV_SHOW", ac_show, null],
|
|
@@ -904,6 +1120,7 @@ function create_gclass(gclass_name)
|
|
|
904
1120
|
["EV_EXPAND_ALL", 0],
|
|
905
1121
|
["EV_COLLAPSE_ALL", 0],
|
|
906
1122
|
["EV_COPY_ALL", 0],
|
|
1123
|
+
["EV_SET_VIEW_MODE", 0],
|
|
907
1124
|
["EV_LANGUAGE_CHANGED", 0],
|
|
908
1125
|
["EV_REFRESH", 0],
|
|
909
1126
|
["EV_SHOW", 0],
|
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
|
+
}
|