@yuneta/gobj-ui 7.21.1 → 7.23.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 +30 -5
- package/dist/gobj-ui.cjs.js +319 -14
- package/dist/gobj-ui.es.js +319 -14
- package/package.json +1 -1
- package/src/c_yui_json.css +22 -16
- package/src/c_yui_json.js +13 -4
- package/src/c_yui_json_graph.js +427 -9
package/src/c_yui_json_graph.js
CHANGED
|
@@ -67,6 +67,13 @@ import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
|
|
|
67
67
|
***************************************************************/
|
|
68
68
|
const GCLASS_NAME = "C_YUI_JSON_GRAPH";
|
|
69
69
|
|
|
70
|
+
/*
|
|
71
|
+
* What a click on a fold handle has to swallow so G6 never turns it
|
|
72
|
+
* into a node click. `click` is the one that ACTS; the rest are cut
|
|
73
|
+
* because G6 assembles its own click from the pointer sequence.
|
|
74
|
+
*/
|
|
75
|
+
const FOLD_SWALLOWED_EVENTS = ["pointerdown", "pointerup", "mousedown", "click"];
|
|
76
|
+
|
|
70
77
|
/***************************************************************
|
|
71
78
|
* Data
|
|
72
79
|
***************************************************************/
|
|
@@ -96,6 +103,16 @@ let PRIVATE_DATA = {
|
|
|
96
103
|
theme_observer: null, // MutationObserver on <html data-theme>
|
|
97
104
|
resize_observer: null, // ResizeObserver on the canvas mount element
|
|
98
105
|
_resize_raf: 0, // rAF id debouncing resize -> EV_RESIZE
|
|
106
|
+
|
|
107
|
+
/*---------------- find + collapse ----------------*/
|
|
108
|
+
search: "", // current find term, lower-cased
|
|
109
|
+
collapsed: null, // Set<string> of paths whose children are hidden
|
|
110
|
+
match_count: 0, // matches of the last find, for the count chip
|
|
111
|
+
find_timer: null, // rate-limits the find box
|
|
112
|
+
fold_listener: null, // delegated click on the card fold handles
|
|
113
|
+
$find_input: null,
|
|
114
|
+
$find_result: null,
|
|
115
|
+
$find_count: null,
|
|
99
116
|
};
|
|
100
117
|
|
|
101
118
|
let __gclass__ = null;
|
|
@@ -180,6 +197,9 @@ function mt_create(gobj)
|
|
|
180
197
|
{
|
|
181
198
|
let priv = gobj.priv;
|
|
182
199
|
|
|
200
|
+
priv.collapsed = new Set();
|
|
201
|
+
priv.search = "";
|
|
202
|
+
|
|
183
203
|
let name = clean_name(gobj_name(gobj));
|
|
184
204
|
priv.canvas_id = "json-canvas-" + name;
|
|
185
205
|
gobj_write_str_attr(gobj, "canvas_id", priv.canvas_id);
|
|
@@ -233,6 +253,10 @@ function mt_stop(gobj)
|
|
|
233
253
|
***************************************************************/
|
|
234
254
|
function mt_destroy(gobj)
|
|
235
255
|
{
|
|
256
|
+
if(gobj.priv.find_timer) {
|
|
257
|
+
clearTimeout(gobj.priv.find_timer);
|
|
258
|
+
gobj.priv.find_timer = null;
|
|
259
|
+
}
|
|
236
260
|
let priv = gobj.priv;
|
|
237
261
|
|
|
238
262
|
if(priv.theme_observer) {
|
|
@@ -286,6 +310,51 @@ function build_ui(gobj)
|
|
|
286
310
|
);
|
|
287
311
|
|
|
288
312
|
gobj_write_attr(gobj, "$container", $container);
|
|
313
|
+
|
|
314
|
+
/*
|
|
315
|
+
* The fold handles live inside an `innerHTML` string, so no handler
|
|
316
|
+
* can be attached to them: the click is delegated from the canvas
|
|
317
|
+
* mount, in the CAPTURE phase, over FOUR event types.
|
|
318
|
+
*
|
|
319
|
+
* Capture because G6 binds on the html node ELEMENT, a descendant
|
|
320
|
+
* of this one: in the bubble phase G6 has already run by the time
|
|
321
|
+
* this listener sees the event, so stopping it there stops nothing.
|
|
322
|
+
*
|
|
323
|
+
* Four types because G6 does not use the DOM `click` at all — it
|
|
324
|
+
* builds its own from the POINTER sequence. Swallowing `click`
|
|
325
|
+
* alone left the fold working and the card ALSO reporting an item
|
|
326
|
+
* click; `pointerdown`/`pointerup` are the pair that has to be cut,
|
|
327
|
+
* and `mousedown` goes with them for a browser that still sends it.
|
|
328
|
+
* Measured by instrumenting the mount, not guessed.
|
|
329
|
+
*/
|
|
330
|
+
let $mount = $container.querySelector('#' + priv.canvas_id);
|
|
331
|
+
if($mount) {
|
|
332
|
+
priv.fold_listener = function(evt) {
|
|
333
|
+
let $handle = evt.target && evt.target.closest
|
|
334
|
+
? evt.target.closest('[data-fold-path]')
|
|
335
|
+
: null;
|
|
336
|
+
if(!$handle) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
evt.stopPropagation();
|
|
340
|
+
evt.preventDefault();
|
|
341
|
+
if(evt.type !== "click") {
|
|
342
|
+
return; /* swallowed so G6 never sees a node click */
|
|
343
|
+
}
|
|
344
|
+
gobj_send_event(gobj, "EV_TOGGLE_FOLD",
|
|
345
|
+
{path: $handle.getAttribute('data-fold-path')}, gobj);
|
|
346
|
+
};
|
|
347
|
+
for(let type of FOLD_SWALLOWED_EVENTS) {
|
|
348
|
+
$mount.addEventListener(type, priv.fold_listener, true);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
priv.$find_input = $container.querySelector('.JSON_GRAPH_FIND_INPUT');
|
|
355
|
+
priv.$find_result = $container.querySelector('.JSON_GRAPH_FIND_RESULT');
|
|
356
|
+
priv.$find_count = $container.querySelector('.JSON_GRAPH_FIND_COUNT');
|
|
357
|
+
|
|
289
358
|
refresh_language($container, t);
|
|
290
359
|
}
|
|
291
360
|
|
|
@@ -294,7 +363,19 @@ function build_ui(gobj)
|
|
|
294
363
|
************************************************************/
|
|
295
364
|
function destroy_ui(gobj)
|
|
296
365
|
{
|
|
366
|
+
let priv = gobj.priv;
|
|
297
367
|
let $container = gobj_read_attr(gobj, "$container");
|
|
368
|
+
|
|
369
|
+
if(priv.fold_listener) {
|
|
370
|
+
let $mount = $container? $container.querySelector('#' + priv.canvas_id): null;
|
|
371
|
+
if($mount) {
|
|
372
|
+
for(let type of FOLD_SWALLOWED_EVENTS) {
|
|
373
|
+
$mount.removeEventListener(type, priv.fold_listener, true);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
priv.fold_listener = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
298
379
|
if($container) {
|
|
299
380
|
if($container.parentNode) {
|
|
300
381
|
$container.parentNode.removeChild($container);
|
|
@@ -303,6 +384,30 @@ function destroy_ui(gobj)
|
|
|
303
384
|
}
|
|
304
385
|
}
|
|
305
386
|
|
|
387
|
+
/************************************************************
|
|
388
|
+
* One fold button. `open` rotates the chevron to the open
|
|
389
|
+
* state, mirroring the per-node toggle of the lazy tree.
|
|
390
|
+
************************************************************/
|
|
391
|
+
function graph_fold_button(gobj, event_name, label_key, open)
|
|
392
|
+
{
|
|
393
|
+
let icon_style = 'font-size:1.5em; color:inherit;' +
|
|
394
|
+
(open? ' transform: rotate(90deg);': '');
|
|
395
|
+
|
|
396
|
+
return ['button', {class: `button ${event_name}`,
|
|
397
|
+
type: 'button',
|
|
398
|
+
style: {height: gobj_read_attr(gobj, "wide"), width: '2.5em'},
|
|
399
|
+
title: t(label_key), 'data-i18n-title': label_key,
|
|
400
|
+
'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
|
|
401
|
+
['i', {style: icon_style, class: 'yi-chevron-right'}],
|
|
402
|
+
{
|
|
403
|
+
click: (evt) => {
|
|
404
|
+
evt.stopPropagation();
|
|
405
|
+
gobj_send_event(gobj, event_name, {}, gobj);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
|
|
306
411
|
/************************************************************
|
|
307
412
|
* Toolbar
|
|
308
413
|
************************************************************/
|
|
@@ -311,7 +416,62 @@ function make_toolbar(gobj)
|
|
|
311
416
|
let priv = gobj.priv;
|
|
312
417
|
let toolbar_wide = gobj_read_attr(gobj, "wide");
|
|
313
418
|
|
|
314
|
-
|
|
419
|
+
/*
|
|
420
|
+
* Left: find a node.
|
|
421
|
+
*
|
|
422
|
+
* A graph of a large document has no other way in — the only way to
|
|
423
|
+
* locate a key was to read every card. Same shape as the treedb
|
|
424
|
+
* graph's box: a rate-limited input into EV_FIND_NODES, and a count
|
|
425
|
+
* that SAYS how many matched, because a graph that did not move
|
|
426
|
+
* looks the same whether nothing matched or the match was already
|
|
427
|
+
* on screen.
|
|
428
|
+
*/
|
|
429
|
+
let left_items = [
|
|
430
|
+
['div', {class: 'JSON_GRAPH_FIND control has-icons-left',
|
|
431
|
+
style: 'margin-right:.5rem; max-width:12rem; min-width:7rem;'}, [
|
|
432
|
+
['input', {
|
|
433
|
+
class: 'JSON_GRAPH_FIND_INPUT input',
|
|
434
|
+
type: 'text',
|
|
435
|
+
/* A placeholder is not a text node, so the data-i18n
|
|
436
|
+
* walk cannot reach it: it needs its own key. */
|
|
437
|
+
placeholder: t('search'),
|
|
438
|
+
'data-i18n-placeholder': 'search',
|
|
439
|
+
'aria-label': t('search'),
|
|
440
|
+
'data-i18n-aria-label': 'search'
|
|
441
|
+
}],
|
|
442
|
+
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
443
|
+
], {
|
|
444
|
+
/* Rate-limited, not delayed for effect: every keystroke
|
|
445
|
+
* REBUILDS the cards — an html node paints no G6 state, so
|
|
446
|
+
* the highlight has to live in the card's own markup — and
|
|
447
|
+
* the first letter typed can match most of a document.
|
|
448
|
+
* Input plumbing: the action still crosses the FSM, just
|
|
449
|
+
* not once per keystroke. */
|
|
450
|
+
input: (evt) => {
|
|
451
|
+
evt.stopPropagation();
|
|
452
|
+
let text = evt.target.value.trim();
|
|
453
|
+
if(priv.find_timer) {
|
|
454
|
+
clearTimeout(priv.find_timer);
|
|
455
|
+
}
|
|
456
|
+
priv.find_timer = setTimeout(function() {
|
|
457
|
+
priv.find_timer = null;
|
|
458
|
+
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
459
|
+
}, 250);
|
|
460
|
+
}
|
|
461
|
+
}],
|
|
462
|
+
/* Two spans, not one string: the number is DATA and "matches"
|
|
463
|
+
* is the word, so a language switch re-translates the half that
|
|
464
|
+
* is a word. Spaced with CSS because createElement2 trims text
|
|
465
|
+
* nodes. `display:flex` inline and NOT the `is-flex` helper —
|
|
466
|
+
* both Bulma helpers carry !important, so `is-hidden is-flex` on
|
|
467
|
+
* one element is decided by stylesheet order. */
|
|
468
|
+
['div', {class: 'JSON_GRAPH_FIND_RESULT is-hidden',
|
|
469
|
+
style: 'display:flex; align-items:center; gap:.3rem; ' +
|
|
470
|
+
'margin-right:.5rem; font-size:.85rem;'}, [
|
|
471
|
+
['span', {class: 'JSON_GRAPH_FIND_COUNT'}, ''],
|
|
472
|
+
['span', {i18n: 'matches'}, 'matches']
|
|
473
|
+
]]
|
|
474
|
+
];
|
|
315
475
|
let center_items = [];
|
|
316
476
|
|
|
317
477
|
let c_icons = [
|
|
@@ -347,10 +507,24 @@ function make_toolbar(gobj)
|
|
|
347
507
|
);
|
|
348
508
|
}
|
|
349
509
|
|
|
510
|
+
/*
|
|
511
|
+
* Right: fold the tree.
|
|
512
|
+
*
|
|
513
|
+
* A JSON graph of anything real is mostly cards nobody is looking
|
|
514
|
+
* at. "Collapse all" leaves the root and marks every cut with a
|
|
515
|
+
* count, so the shape stays legible and you can see WHERE the rest
|
|
516
|
+
* went; "expand all" puts it back. Same chevron as the lazy tree,
|
|
517
|
+
* rotated to point down for the open state.
|
|
518
|
+
*/
|
|
519
|
+
let right_items = [
|
|
520
|
+
graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
|
|
521
|
+
graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false),
|
|
522
|
+
];
|
|
523
|
+
|
|
350
524
|
const $toolbar = yui_toolbar({}, [
|
|
351
525
|
['div', {class: 'yui-horizontal-toolbar-section left'}, left_items],
|
|
352
526
|
['div', {class: 'yui-horizontal-toolbar-section center'}, center_items],
|
|
353
|
-
['div', {class: 'yui-horizontal-toolbar-section right'},
|
|
527
|
+
['div', {class: 'yui-horizontal-toolbar-section right'}, right_items],
|
|
354
528
|
]);
|
|
355
529
|
|
|
356
530
|
refresh_language($toolbar, t);
|
|
@@ -440,7 +614,7 @@ function build_graph(gobj)
|
|
|
440
614
|
/************************************************************
|
|
441
615
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
442
616
|
************************************************************/
|
|
443
|
-
function build_cell_html(key, value, type, dark)
|
|
617
|
+
function build_cell_html(key, value, type, dark, matched)
|
|
444
618
|
{
|
|
445
619
|
let color = type_color(type, dark);
|
|
446
620
|
let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
|
|
@@ -470,7 +644,76 @@ function build_cell_html(key, value, type, dark)
|
|
|
470
644
|
break;
|
|
471
645
|
}
|
|
472
646
|
|
|
473
|
-
|
|
647
|
+
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span>` +
|
|
648
|
+
`<span style="color:${color}">${display_value}</span>`;
|
|
649
|
+
|
|
650
|
+
if(matched) {
|
|
651
|
+
/* The highlight is baked into the card's MARKUP and not set as
|
|
652
|
+
* a G6 node state: the key shape of an `html` node is a DOM
|
|
653
|
+
* element, and G6 paints no state style on it. Setting
|
|
654
|
+
* 'active' here would select correctly and show nothing. */
|
|
655
|
+
row = `<span style="background:${dark? "#7a5d00": "#ffe082"}; ` +
|
|
656
|
+
`border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
657
|
+
}
|
|
658
|
+
return row;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/************************************************************
|
|
662
|
+
* The per-node fold handle, drawn in the card's header.
|
|
663
|
+
*
|
|
664
|
+
* Only a card with a BRANCH gets one — a leaf has nothing to
|
|
665
|
+
* fold, and a handle that does nothing is worse than none.
|
|
666
|
+
* It carries the path in `data-fold-path`, which is what the
|
|
667
|
+
* delegated listener in build_ui reads; the card is an
|
|
668
|
+
* innerHTML string, so there is no other way to hang a
|
|
669
|
+
* handler on it.
|
|
670
|
+
*
|
|
671
|
+
* Folded it shows the glyph AND the count of branches hidden
|
|
672
|
+
* under it; open, just the glyph. No word, no title: a word
|
|
673
|
+
* inside this string could carry no i18n key.
|
|
674
|
+
************************************************************/
|
|
675
|
+
function fold_toggle_html(path, foldable, folded, branches)
|
|
676
|
+
{
|
|
677
|
+
if(!foldable) {
|
|
678
|
+
/* A spacer, so the label of a leaf card stays centred on the
|
|
679
|
+
* same axis as the label of a foldable one. */
|
|
680
|
+
return `<span style="width:1.1em; flex:0 0 auto;"></span>`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let glyph = folded? "▸": "▾"; /* right / down */
|
|
684
|
+
let text = folded? `${glyph} ${branches}`: glyph;
|
|
685
|
+
|
|
686
|
+
return `<span data-fold-path="${escapeHtml(path)}" ` +
|
|
687
|
+
`style="flex:0 0 auto; cursor:pointer; user-select:none; ` +
|
|
688
|
+
`padding:0 .2em; min-width:1.1em; text-align:left;">${text}</span>`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/************************************************************
|
|
692
|
+
* Does this key/value pair match the find term?
|
|
693
|
+
*
|
|
694
|
+
* Scalars are matched on what the card SHOWS. A complex value
|
|
695
|
+
* is not: its contents are a card of their own and match there,
|
|
696
|
+
* so matching the `{3}` summary too would light up every
|
|
697
|
+
* ancestor of every hit.
|
|
698
|
+
************************************************************/
|
|
699
|
+
function cell_matches(key, value, type, needle)
|
|
700
|
+
{
|
|
701
|
+
if(empty_string(needle)) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
if(String(key).toLowerCase().includes(needle)) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
switch(type) {
|
|
708
|
+
case "string":
|
|
709
|
+
case "number":
|
|
710
|
+
case "boolean":
|
|
711
|
+
return String(value).toLowerCase().includes(needle);
|
|
712
|
+
case "null":
|
|
713
|
+
return "null".includes(needle);
|
|
714
|
+
default:
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
474
717
|
}
|
|
475
718
|
|
|
476
719
|
/************************************************************
|
|
@@ -541,15 +784,23 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
541
784
|
* The theme is read live as the card is drawn (like the gobj-tree's
|
|
542
785
|
* cards): ac_theme rebuilds on a switch, so this is always current.
|
|
543
786
|
*/
|
|
787
|
+
let priv = gobj.priv;
|
|
788
|
+
let needle = priv.search || "";
|
|
544
789
|
let dark = yui_is_dark();
|
|
545
790
|
let lines = [];
|
|
546
791
|
let pending_complex = [];
|
|
792
|
+
let card_matched = false;
|
|
547
793
|
|
|
548
794
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
549
795
|
|
|
550
796
|
for(let [key, value] of entries) {
|
|
551
797
|
let type = get_json_type(value);
|
|
552
|
-
let
|
|
798
|
+
let matched = cell_matches(key, value, type, needle);
|
|
799
|
+
if(matched) {
|
|
800
|
+
card_matched = true;
|
|
801
|
+
priv.match_count++;
|
|
802
|
+
}
|
|
803
|
+
let html = build_cell_html(key, value, type, dark, matched);
|
|
553
804
|
lines.push(html);
|
|
554
805
|
|
|
555
806
|
if((is_object(value) && json_object_size(value) > 0) ||
|
|
@@ -559,9 +810,22 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
559
810
|
}
|
|
560
811
|
|
|
561
812
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
813
|
+
|
|
814
|
+
/*
|
|
815
|
+
* Folded: keep the card, drop the branch, and SAY how much went
|
|
816
|
+
* with it. A cut that is not marked reads as a document that ends
|
|
817
|
+
* there — the same rule the lazy tree follows for a `__collapsed__`
|
|
818
|
+
* sentinel. The mark is a glyph and a number on purpose: the card
|
|
819
|
+
* is an innerHTML string, where a word could carry no i18n key and
|
|
820
|
+
* would sit there in English forever.
|
|
821
|
+
*/
|
|
822
|
+
let foldable = pending_complex.length > 0;
|
|
823
|
+
let folded = foldable && priv.collapsed.has(path);
|
|
562
824
|
let colors = json_card_style(
|
|
563
825
|
is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
|
|
564
826
|
);
|
|
827
|
+
let border_color = card_matched? (dark? "#ffb300": "#ff8f00"): colors.border;
|
|
828
|
+
let border_width = card_matched? 2: 1;
|
|
565
829
|
|
|
566
830
|
/*
|
|
567
831
|
* Build node HTML content
|
|
@@ -576,7 +840,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
576
840
|
<div style="
|
|
577
841
|
min-width: ${min_width}px;
|
|
578
842
|
background: ${colors.bg};
|
|
579
|
-
border:
|
|
843
|
+
border: ${border_width}px ${is_dict ? 'dashed' : 'solid'} ${border_color};
|
|
580
844
|
border-radius: ${is_dict ? '6px' : '0'};
|
|
581
845
|
opacity: 0.9;
|
|
582
846
|
overflow: hidden;
|
|
@@ -587,8 +851,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
587
851
|
font-weight: bold;
|
|
588
852
|
font-size: 12px;
|
|
589
853
|
padding: 3px 8px;
|
|
590
|
-
|
|
591
|
-
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
gap: 6px;
|
|
857
|
+
">${fold_toggle_html(path, foldable, folded, pending_complex.length)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
|
|
592
858
|
${content_html}
|
|
593
859
|
</div>`;
|
|
594
860
|
|
|
@@ -623,8 +889,11 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
623
889
|
}
|
|
624
890
|
|
|
625
891
|
/*
|
|
626
|
-
* Recurse into complex children
|
|
892
|
+
* Recurse into complex children — unless this card is folded.
|
|
627
893
|
*/
|
|
894
|
+
if(folded) {
|
|
895
|
+
return group_id;
|
|
896
|
+
}
|
|
628
897
|
for(let pending of pending_complex) {
|
|
629
898
|
build_json_nodes(
|
|
630
899
|
gobj,
|
|
@@ -663,6 +932,7 @@ function load_json(gobj)
|
|
|
663
932
|
}
|
|
664
933
|
|
|
665
934
|
priv.node_counter = 0;
|
|
935
|
+
priv.match_count = 0;
|
|
666
936
|
|
|
667
937
|
let nodes = [];
|
|
668
938
|
let edges = [];
|
|
@@ -682,6 +952,57 @@ function load_json(gobj)
|
|
|
682
952
|
}
|
|
683
953
|
}
|
|
684
954
|
|
|
955
|
+
/************************************************************
|
|
956
|
+
* Every path that HAS a branch to fold, in the same recursion
|
|
957
|
+
* build_json_nodes uses — so "collapse all" can never name a
|
|
958
|
+
* card that does not exist. The root is excluded: folding it
|
|
959
|
+
* would leave an empty canvas.
|
|
960
|
+
************************************************************/
|
|
961
|
+
function collect_foldable_paths(value, path, out, is_root)
|
|
962
|
+
{
|
|
963
|
+
let is_dict = is_object(value);
|
|
964
|
+
let is_list = is_array(value);
|
|
965
|
+
if(!is_dict && !is_list) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
let has_branch = false;
|
|
970
|
+
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
971
|
+
for(let [key, child] of entries) {
|
|
972
|
+
if((is_object(child) && json_object_size(child) > 0) ||
|
|
973
|
+
(is_array(child) && child.length > 0)) {
|
|
974
|
+
has_branch = true;
|
|
975
|
+
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if(has_branch && !is_root) {
|
|
980
|
+
out.push(path);
|
|
981
|
+
}
|
|
982
|
+
return has_branch;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/************************************************************
|
|
986
|
+
* Show the find count, or hide the chip when there is no
|
|
987
|
+
* term. Zero is a RESULT and must show: a graph that did not
|
|
988
|
+
* move looks the same whether nothing matched or the match
|
|
989
|
+
* was already on screen.
|
|
990
|
+
************************************************************/
|
|
991
|
+
function update_find_result(gobj)
|
|
992
|
+
{
|
|
993
|
+
let priv = gobj.priv;
|
|
994
|
+
if(!priv.$find_result || !priv.$find_count) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if(empty_string(priv.search)) {
|
|
998
|
+
priv.$find_result.classList.add("is-hidden");
|
|
999
|
+
priv.$find_count.textContent = "";
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
priv.$find_count.textContent = String(priv.match_count);
|
|
1003
|
+
priv.$find_result.classList.remove("is-hidden");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
685
1006
|
/************************************************************
|
|
686
1007
|
* Clear and reload.
|
|
687
1008
|
* {preserve_view: true}
|
|
@@ -863,6 +1184,95 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
863
1184
|
|
|
864
1185
|
/************************************************************
|
|
865
1186
|
* {theme: "dark"|"light"} — the app switched theme.
|
|
1187
|
+
* EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
|
|
1188
|
+
*
|
|
1189
|
+
* The camera does not move: folding one branch is a local
|
|
1190
|
+
* edit of the picture, and refitting the whole graph for it
|
|
1191
|
+
* would throw away where the reader was looking. The toolbar
|
|
1192
|
+
* pair, which changes everything at once, does refit.
|
|
1193
|
+
************************************************************/
|
|
1194
|
+
function ac_toggle_fold(gobj, event, kw, src)
|
|
1195
|
+
{
|
|
1196
|
+
let priv = gobj.priv;
|
|
1197
|
+
let path = (kw && kw.path) || "";
|
|
1198
|
+
|
|
1199
|
+
if(empty_string(path)) {
|
|
1200
|
+
log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
|
|
1201
|
+
return -1;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if(priv.collapsed.has(path)) {
|
|
1205
|
+
priv.collapsed.delete(path);
|
|
1206
|
+
} else {
|
|
1207
|
+
priv.collapsed.add(path);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
refresh_json(gobj, {preserve_view: true});
|
|
1211
|
+
update_find_result(gobj);
|
|
1212
|
+
return 0;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/************************************************************
|
|
1216
|
+
* EV_FIND_NODES { text }
|
|
1217
|
+
*
|
|
1218
|
+
* The camera does NOT move: a find is a way of reading what is
|
|
1219
|
+
* already on screen, and a viewport that jumps on every
|
|
1220
|
+
* keystroke is unusable. The count says whether there is
|
|
1221
|
+
* anything to look for elsewhere.
|
|
1222
|
+
************************************************************/
|
|
1223
|
+
function ac_find_nodes(gobj, event, kw, src)
|
|
1224
|
+
{
|
|
1225
|
+
let priv = gobj.priv;
|
|
1226
|
+
|
|
1227
|
+
priv.search = ((kw && kw.text) || "").trim().toLowerCase();
|
|
1228
|
+
refresh_json(gobj, {preserve_view: true});
|
|
1229
|
+
update_find_result(gobj);
|
|
1230
|
+
return 0;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/************************************************************
|
|
1234
|
+
* EV_EXPAND_ALL
|
|
1235
|
+
************************************************************/
|
|
1236
|
+
function ac_expand_all(gobj, event, kw, src)
|
|
1237
|
+
{
|
|
1238
|
+
let priv = gobj.priv;
|
|
1239
|
+
|
|
1240
|
+
if(priv.collapsed.size === 0) {
|
|
1241
|
+
return 0; // nothing folded; a relayout would only move the camera
|
|
1242
|
+
}
|
|
1243
|
+
priv.collapsed.clear();
|
|
1244
|
+
refresh_json(gobj);
|
|
1245
|
+
update_find_result(gobj);
|
|
1246
|
+
return 0;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/************************************************************
|
|
1250
|
+
* EV_COLLAPSE_ALL — fold every card but the root.
|
|
1251
|
+
************************************************************/
|
|
1252
|
+
function ac_collapse_all(gobj, event, kw, src)
|
|
1253
|
+
{
|
|
1254
|
+
let priv = gobj.priv;
|
|
1255
|
+
|
|
1256
|
+
let json_data = gobj_read_attr(gobj, "json_data");
|
|
1257
|
+
if(!json_data) {
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
let paths = [];
|
|
1262
|
+
let path = gobj_read_str_attr(gobj, "path") || "`";
|
|
1263
|
+
if(empty_string(path)) {
|
|
1264
|
+
path = "`";
|
|
1265
|
+
}
|
|
1266
|
+
collect_foldable_paths(json_data, path, paths, true);
|
|
1267
|
+
|
|
1268
|
+
priv.collapsed = new Set(paths);
|
|
1269
|
+
refresh_json(gobj);
|
|
1270
|
+
update_find_result(gobj);
|
|
1271
|
+
return 0;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/************************************************************
|
|
1275
|
+
*
|
|
866
1276
|
************************************************************/
|
|
867
1277
|
function ac_theme(gobj, event, kw, src)
|
|
868
1278
|
{
|
|
@@ -1054,6 +1464,10 @@ function create_gclass(gclass_name)
|
|
|
1054
1464
|
["ST_IDLE", [
|
|
1055
1465
|
["EV_LOAD_DATA", ac_load_data, null],
|
|
1056
1466
|
["EV_REFRESH", ac_refresh, null],
|
|
1467
|
+
["EV_FIND_NODES", ac_find_nodes, null],
|
|
1468
|
+
["EV_TOGGLE_FOLD", ac_toggle_fold, null],
|
|
1469
|
+
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
1470
|
+
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
1057
1471
|
["EV_THEME", ac_theme, null],
|
|
1058
1472
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
1059
1473
|
["EV_ZOOM_OUT", ac_zoom_out, null],
|
|
@@ -1072,6 +1486,10 @@ function create_gclass(gclass_name)
|
|
|
1072
1486
|
const event_types = [
|
|
1073
1487
|
["EV_LOAD_DATA", 0],
|
|
1074
1488
|
["EV_REFRESH", 0],
|
|
1489
|
+
["EV_FIND_NODES", 0],
|
|
1490
|
+
["EV_TOGGLE_FOLD", 0],
|
|
1491
|
+
["EV_EXPAND_ALL", 0],
|
|
1492
|
+
["EV_COLLAPSE_ALL", 0],
|
|
1075
1493
|
["EV_THEME", 0],
|
|
1076
1494
|
["EV_ZOOM_IN", 0],
|
|
1077
1495
|
["EV_ZOOM_OUT", 0],
|