@yuneta/gobj-ui 7.23.1 → 7.23.3

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.
@@ -74,6 +74,73 @@ const GCLASS_NAME = "C_YUI_JSON_GRAPH";
74
74
  */
75
75
  const FOLD_SWALLOWED_EVENTS = ["pointerdown", "pointerup", "mousedown", "click"];
76
76
 
77
+ /*
78
+ * Layouts, same shape the gobj tree's registry uses
79
+ * (`c_yui_gobj_tree_js.js`): a G6 layout config plus the edge type
80
+ * that matches its direction — a vertical layout with horizontal
81
+ * cubics draws every edge going the wrong way round.
82
+ *
83
+ * `json-tree` is this gclass's own layout, the one it always had, and
84
+ * it stays the default: it sizes each level from the real card widths,
85
+ * which a generic layout cannot do with `html` nodes. The dagre pair
86
+ * is there for the documents it does badly — a wide, shallow object
87
+ * reads better left→right, which is also the house direction for a
88
+ * topology.
89
+ */
90
+ const LAYOUTS = {
91
+ "json-tree": {
92
+ label: "vertical tree",
93
+ g6_layout: {type: 'json-tree-layout'},
94
+ edge_type: 'cubic-vertical',
95
+ },
96
+ "dagre-tb": {
97
+ label: "dagre top-down",
98
+ g6_layout: {type: 'antv-dagre', rankdir: 'TB', nodesep: 20, ranksep: 40},
99
+ edge_type: 'polyline',
100
+ },
101
+ "dagre-lr": {
102
+ label: "dagre left-right",
103
+ g6_layout: {type: 'antv-dagre', rankdir: 'LR', nodesep: 20, ranksep: 40},
104
+ edge_type: 'polyline',
105
+ },
106
+ /* `compact-box` is NOT here on purpose: it lays out from a fixed
107
+ * node size, and an `html` card is nothing like it — every card
108
+ * landed on top of the next. A layout that draws the document
109
+ * unreadable is worse than one option fewer. dagre reads the real
110
+ * sizes, which is why both of its directions are here. */
111
+ };
112
+
113
+ const DEFAULT_LAYOUT = "json-tree";
114
+
115
+ function get_layout_cfg(key)
116
+ {
117
+ return LAYOUTS[key] || LAYOUTS[DEFAULT_LAYOUT];
118
+ }
119
+
120
+ /************************************************************
121
+ * A layout's label, with every key SPELLED OUT inside t().
122
+ *
123
+ * Never t(LAYOUTS[key].label) nor a `data-i18n` fed from that
124
+ * table: the apps' validate-locales scans for t("literal"),
125
+ * so a key reached through a variable is a key it cannot
126
+ * demand — and i18next answers an undefined key with the key
127
+ * itself, which renders in lower-case English and never
128
+ * changes language. Twice already in this component's
129
+ * history (7.20.0 `tree view`, and this table on its first
130
+ * draft).
131
+ ************************************************************/
132
+ function layout_label(key)
133
+ {
134
+ switch(key) {
135
+ case "dagre-tb":
136
+ return t("dagre top-down");
137
+ case "dagre-lr":
138
+ return t("dagre left-right");
139
+ default:
140
+ return t("vertical tree");
141
+ }
142
+ }
143
+
77
144
  /***************************************************************
78
145
  * Data
79
146
  ***************************************************************/
@@ -91,6 +158,7 @@ SDATA(data_type_t.DTP_STRING, "canvas_id", 0, "", "Canvas ID"),
91
158
 
92
159
  /*---------------- Graph Settings ----------------*/
93
160
  SDATA(data_type_t.DTP_STRING, "wide", 0, "36px", "Height of header"),
161
+ SDATA(data_type_t.DTP_STRING, "layout", 0, "json-tree", "Layout key (see LAYOUTS)"),
94
162
 
95
163
  SDATA_END()
96
164
  ];
@@ -110,6 +178,8 @@ let PRIVATE_DATA = {
110
178
  match_count: 0, // matches of the last find, for the count chip
111
179
  find_timer: null, // rate-limits the find box
112
180
  fold_listener: null, // delegated click on the card fold handles
181
+ $layout_select: null, // the layout picker
182
+ $zoom_readout: null, // the zoom percentage
113
183
  $find_input: null,
114
184
  $find_result: null,
115
185
  $find_count: null,
@@ -351,6 +421,7 @@ function build_ui(gobj)
351
421
  log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
352
422
  }
353
423
 
424
+ priv.$layout_select = $container.querySelector('.JSON_GRAPH_LAYOUT');
354
425
  priv.$find_input = $container.querySelector('.JSON_GRAPH_FIND_INPUT');
355
426
  priv.$find_result = $container.querySelector('.JSON_GRAPH_FIND_RESULT');
356
427
  priv.$find_count = $container.querySelector('.JSON_GRAPH_FIND_COUNT');
@@ -384,6 +455,53 @@ function destroy_ui(gobj)
384
455
  }
385
456
  }
386
457
 
458
+ /************************************************************
459
+ * One camera button.
460
+ ************************************************************/
461
+ function camera_button(gobj, icon, event_name, label_key, wide)
462
+ {
463
+ return ['button', {class: `button ${event_name}`, type: 'button',
464
+ style: {height: wide, width: '2.5em'},
465
+ title: t(label_key), 'data-i18n-title': label_key,
466
+ 'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
467
+ ['i', {style: 'font-size:1.5em; color:inherit;', class: icon}],
468
+ {
469
+ click: (evt) => {
470
+ evt.stopPropagation();
471
+ gobj_send_event(gobj, event_name, {evt: evt}, gobj);
472
+ }
473
+ }
474
+ ];
475
+ }
476
+
477
+ /************************************************************
478
+ * The zoom, as a percentage. What the two magnifiers
479
+ * change: an editor that offers a zoom shows the number.
480
+ ************************************************************/
481
+ function zoom_percent_text(gobj)
482
+ {
483
+ let graph = gobj.priv.graph;
484
+ if(!graph) {
485
+ return "100%";
486
+ }
487
+ try {
488
+ return Math.round(graph.getZoom() * 100) + "%";
489
+ } catch(e) {
490
+ return "100%";
491
+ }
492
+ }
493
+
494
+ /************************************************************
495
+ * Repaint the readout after anything that moves the camera.
496
+ ************************************************************/
497
+ function update_zoom_readout(gobj)
498
+ {
499
+ let priv = gobj.priv;
500
+ if(priv.$zoom_readout) {
501
+ priv.$zoom_readout.textContent = zoom_percent_text(gobj);
502
+ }
503
+ }
504
+
387
505
  /************************************************************
388
506
  * One fold button. `open` rotates the chevron to the open
389
507
  * state, mirroring the per-node toggle of the lazy tree.
@@ -474,39 +592,56 @@ function make_toolbar(gobj)
474
592
  ];
475
593
  let center_items = [];
476
594
 
595
+ /*
596
+ * The camera, in the SAME vocabulary the treedb graph uses
597
+ * (`c_g6_nodes_tree.js`): zoom in / zoom out, then what those two
598
+ * change — the zoom READOUT, because without it "1:1" is a jump to
599
+ * a value nobody was told — then fit, then actual size.
600
+ *
601
+ * Two of these used to be a different picture for the same action
602
+ * in a sibling view, which is the worst thing an icon can be:
603
+ * `EV_ZOOM_RESET` was a bare magnifier and `EV_CENTER` was
604
+ * `yi-arrows-to-eye`. Now fit is the same bracket glyph as the
605
+ * sprite's `g6-icon-fit`, and actual size is WRITTEN — `1:1` is
606
+ * written in every editor that offers it, never drawn, because
607
+ * there is no glyph for it.
608
+ */
477
609
  let c_icons = [
478
- ["yi-magnifying-glass-plus", "EV_ZOOM_IN", false, 'i'],
479
- ["yi-magnifying-glass", "EV_ZOOM_RESET", false, 'i'],
480
- ["yi-magnifying-glass-minus","EV_ZOOM_OUT", false, 'i'],
481
- ["yi-arrows-to-eye", "EV_CENTER", false, 'i'],
482
- ["yi-arrows-rotate", "EV_REFRESH", false, 'i'],
610
+ ["yi-magnifying-glass-plus", "EV_ZOOM_IN", "zoom in"],
611
+ ["yi-magnifying-glass-minus", "EV_ZOOM_OUT", "zoom out"],
483
612
  ];
484
613
 
485
614
  for(let item of c_icons) {
486
- let icon_name = item[0];
487
- let event_name = item[1];
488
- let disabled = item[2];
489
-
490
- let button = {
491
- class: `button ${event_name}`,
492
- style: {height: toolbar_wide, width: '2.5em'}
493
- };
494
- if(disabled) {
495
- button.disabled = true;
496
- }
497
- center_items.push(
498
- ['button', button,
499
- ['i', {style: 'font-size:1.5em; color:inherit;', class: icon_name}],
500
- {
501
- click: (evt) => {
502
- evt.stopPropagation();
503
- gobj_send_event(gobj, event_name, {evt: evt}, gobj);
504
- }
505
- }
506
- ]
507
- );
615
+ center_items.push(camera_button(gobj, item[0], item[1], item[2], toolbar_wide));
508
616
  }
509
617
 
618
+ priv.$zoom_readout = createElement2(
619
+ ['span', {class: 'JSON_GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey',
620
+ style: 'font-size:.85rem; min-width:3.5em; justify-content:center;',
621
+ title: t('zoom level'), 'data-i18n-title': 'zoom level'},
622
+ zoom_percent_text(gobj)]
623
+ );
624
+ center_items.push(priv.$zoom_readout);
625
+
626
+ center_items.push(camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", toolbar_wide));
627
+
628
+ /* Written, not drawn — see above. */
629
+ center_items.push(
630
+ ['button', {class: 'button EV_ZOOM_RESET', type: 'button',
631
+ style: {height: toolbar_wide, width: '2.5em'},
632
+ title: t('actual size'), 'data-i18n-title': 'actual size',
633
+ 'aria-label': t('actual size'), 'data-i18n-aria-label': 'actual size'},
634
+ ['span', {style: 'font-weight:700;'}, '1:1'],
635
+ {
636
+ click: (evt) => {
637
+ evt.stopPropagation();
638
+ gobj_send_event(gobj, "EV_ZOOM_RESET", {evt: evt}, gobj);
639
+ }
640
+ }]
641
+ );
642
+
643
+ center_items.push(camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", toolbar_wide));
644
+
510
645
  /*
511
646
  * Right: fold the tree.
512
647
  *
@@ -516,7 +651,38 @@ function make_toolbar(gobj)
516
651
  * went; "expand all" puts it back. Same chevron as the lazy tree,
517
652
  * rotated to point down for the open state.
518
653
  */
654
+ /*
655
+ * A layout picker, same shape as the gobj tree's. One layout
656
+ * cannot serve every document: the built-in one sizes each level
657
+ * from the real card widths, dagre packs a wide shallow object
658
+ * far better, and left→right is the house direction for reading a
659
+ * topology.
660
+ */
661
+ let current_layout = gobj_read_str_attr(gobj, "layout") || DEFAULT_LAYOUT;
662
+ let options = Object.keys(LAYOUTS).map(function(key) {
663
+ let attrs = {value: key, 'data-i18n': LAYOUTS[key].label};
664
+ if(key === current_layout) {
665
+ attrs.selected = "selected";
666
+ }
667
+ return ['option', attrs, layout_label(key)];
668
+ });
669
+
670
+ let $layout_select = createElement2(
671
+ ['select', {
672
+ class: 'JSON_GRAPH_LAYOUT select',
673
+ style: {height: toolbar_wide, "margin-left": "0.5em"},
674
+ title: t("layout"), 'data-i18n-title': "layout",
675
+ 'aria-label': t("layout"), 'data-i18n-aria-label': "layout"
676
+ }, options, {
677
+ change: (evt) => {
678
+ evt.stopPropagation();
679
+ gobj_send_event(gobj, "EV_CHANGE_LAYOUT", {layout: evt.target.value}, gobj);
680
+ }
681
+ }]
682
+ );
683
+
519
684
  let right_items = [
685
+ $layout_select,
520
686
  graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
521
687
  graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false),
522
688
  ];
@@ -537,6 +703,7 @@ function make_toolbar(gobj)
537
703
  function build_graph(gobj)
538
704
  {
539
705
  let priv = gobj.priv;
706
+ let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
540
707
 
541
708
  const graph = priv.graph = new Graph({
542
709
  container: priv.canvas_id,
@@ -551,7 +718,7 @@ function build_graph(gobj)
551
718
  },
552
719
 
553
720
  edge: {
554
- type: 'cubic-vertical',
721
+ type: layout_cfg.edge_type,
555
722
  style: {
556
723
  stroke: '#999',
557
724
  lineWidth: 1,
@@ -559,9 +726,7 @@ function build_graph(gobj)
559
726
  },
560
727
  },
561
728
 
562
- layout: {
563
- type: 'json-tree-layout',
564
- },
729
+ layout: layout_cfg.g6_layout,
565
730
 
566
731
  behaviors: [
567
732
  'drag-canvas',
@@ -577,6 +742,15 @@ function build_graph(gobj)
577
742
  gobj_send_event(gobj, "EV_NODE_CLICK", {evt: evt}, gobj);
578
743
  });
579
744
 
745
+ /* ONE hook for the readout, the same one the treedb graph uses:
746
+ * G6 fires it on any camera change, and the WHEEL is a camera
747
+ * change that passes through no action of ours — patching the text
748
+ * node from each zoom action would have left the readout lying
749
+ * after every notch. */
750
+ graph.on('aftertransform', () => {
751
+ update_zoom_readout(gobj);
752
+ });
753
+
580
754
  graph.on(CanvasEvent.CLICK, (evt) => {
581
755
  // Canvas click
582
756
  });
@@ -971,6 +1145,7 @@ function load_json(gobj)
971
1145
  if(!preserve_view) {
972
1146
  graph.fitView();
973
1147
  }
1148
+ update_zoom_readout(gobj);
974
1149
  });
975
1150
  }
976
1151
  }
@@ -1207,6 +1382,47 @@ function ac_refresh(gobj, event, kw, src)
1207
1382
 
1208
1383
  /************************************************************
1209
1384
  * {theme: "dark"|"light"} — the app switched theme.
1385
+ * EV_CHANGE_LAYOUT { layout }
1386
+ *
1387
+ * The layout AND the edge type move together — a vertical
1388
+ * layout with horizontal cubics draws every edge the wrong way
1389
+ * round — and then the graph is rebuilt and refitted, because a
1390
+ * new layout is a new shape and the old camera framed the old
1391
+ * one.
1392
+ ************************************************************/
1393
+ function ac_change_layout(gobj, event, kw, src)
1394
+ {
1395
+ let priv = gobj.priv;
1396
+ let key = (kw && kw.layout) || "";
1397
+
1398
+ if(!LAYOUTS[key]) {
1399
+ log_error(`${gobj_short_name(gobj)}: unknown layout '${key}'`);
1400
+ return -1;
1401
+ }
1402
+ if(key === gobj_read_str_attr(gobj, "layout")) {
1403
+ return 0;
1404
+ }
1405
+
1406
+ gobj_write_str_attr(gobj, "layout", key);
1407
+
1408
+ /* Keep the picker in step when the event came from elsewhere. */
1409
+ if(priv.$layout_select && priv.$layout_select.value !== key) {
1410
+ priv.$layout_select.value = key;
1411
+ }
1412
+
1413
+ let cfg = get_layout_cfg(key);
1414
+ if(priv.graph) {
1415
+ priv.graph.setOptions({
1416
+ layout: cfg.g6_layout,
1417
+ edge: {type: cfg.edge_type},
1418
+ });
1419
+ }
1420
+ refresh_json(gobj);
1421
+ update_find_result(gobj);
1422
+ return 0;
1423
+ }
1424
+
1425
+ /************************************************************
1210
1426
  * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
1211
1427
  *
1212
1428
  * The camera does not move: folding one branch is a local
@@ -1489,6 +1705,7 @@ function create_gclass(gclass_name)
1489
1705
  ["EV_REFRESH", ac_refresh, null],
1490
1706
  ["EV_FIND_NODES", ac_find_nodes, null],
1491
1707
  ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
1708
+ ["EV_CHANGE_LAYOUT", ac_change_layout, null],
1492
1709
  ["EV_EXPAND_ALL", ac_expand_all, null],
1493
1710
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
1494
1711
  ["EV_THEME", ac_theme, null],
@@ -1511,6 +1728,7 @@ function create_gclass(gclass_name)
1511
1728
  ["EV_REFRESH", 0],
1512
1729
  ["EV_FIND_NODES", 0],
1513
1730
  ["EV_TOGGLE_FOLD", 0],
1731
+ ["EV_CHANGE_LAYOUT", 0],
1514
1732
  ["EV_EXPAND_ALL", 0],
1515
1733
  ["EV_COLLAPSE_ALL", 0],
1516
1734
  ["EV_THEME", 0],
@@ -42,6 +42,8 @@ import {
42
42
  kwid_get_ids,
43
43
  gobj_create_pure_child,
44
44
  gobj_start,
45
+ gobj_create_service,
46
+ gobj_is_destroying,
45
47
  gobj_stop,
46
48
  gobj_destroy,
47
49
  gobj_is_running,
@@ -170,9 +172,11 @@ let PRIVATE_DATA = {
170
172
  form: null, // hosted C_YUI_FORM child (while dialog open)
171
173
  form_modal: null, // { close } handle of the adaptive dialog
172
174
  schema_gobj: null, // hosted C_YUI_JSON child (while schema open)
173
- schema_modal: null, // { close } handle of the schema dialog
175
+ schema_modal: null, // { close } handle of the schema dialog (phone)
176
+ schema_win: null, // C_YUI_WINDOW presenting it (laptop)
174
177
  cell_json_gobj: null, // hosted C_YUI_JSON child (while a cell is open)
175
- cell_json_modal: null, // { close } handle of the cell dialog
178
+ cell_json_modal: null, // { close } handle of the cell dialog (phone)
179
+ cell_json_win: null, // C_YUI_WINDOW presenting it (laptop)
176
180
  };
177
181
 
178
182
  let __gclass__ = null;
@@ -184,6 +188,120 @@ let __gclass__ = null;
184
188
  * stacking context below body-level modals). The legacy
185
189
  * C_YUI_MAIN "#popup-layer" and document.body (Bulma `.modal` is
186
190
  * position:fixed) remain the shell-less fallbacks. */
191
+ /* True on a phone-width viewport (Bulma's mobile breakpoint). */
192
+ function is_mobile()
193
+ {
194
+ return typeof window !== "undefined" && window.innerWidth <= 768;
195
+ }
196
+
197
+ /************************************************************
198
+ * Present a JSON viewer's $container.
199
+ *
200
+ * On a LAPTOP a floating C_YUI_WINDOW: a JSON document is
201
+ * something you read WHILE looking at the table behind it —
202
+ * you move it, you resize it, you maximise it when the
203
+ * document is big. A modal can do none of that, and it
204
+ * darkens exactly the rows you opened the cell from.
205
+ *
206
+ * On a phone the modal sheet stays: there is no room to
207
+ * arrange anything, and the sheet already carries the shell's
208
+ * Escape / Back handling.
209
+ *
210
+ * Returns {win, modal} — exactly one of them is set.
211
+ ************************************************************/
212
+ function present_json_popup(gobj, $box, opts)
213
+ {
214
+ let shell = yui_shell_of(gobj);
215
+ if(!shell) {
216
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot present the json`);
217
+ return null;
218
+ }
219
+
220
+ if(is_mobile()) {
221
+ return {
222
+ modal: yui_shell_show_modal(shell, $box, {
223
+ dialog: true,
224
+ logical_class: opts.logical_class + "_SHEET",
225
+ title_prefix: opts.title_prefix,
226
+ title: opts.title,
227
+ t: t,
228
+ on_close: opts.on_close
229
+ }),
230
+ win: null
231
+ };
232
+ }
233
+
234
+ let win = gobj_create_service(
235
+ opts.name,
236
+ "C_YUI_WINDOW",
237
+ {
238
+ $parent: popup_mount_layer(gobj),
239
+ subscriber: null,
240
+ modal: false,
241
+ showMax: true,
242
+ showFooter: false,
243
+ resizable: true,
244
+ center: true,
245
+ auto_save_size_and_position: true,
246
+ width: 720,
247
+ height: 620,
248
+ logical_class: opts.logical_class + "_WINDOW",
249
+ title_prefix: opts.title_prefix,
250
+ title: opts.title,
251
+ icon: opts.icon,
252
+ body: $box,
253
+ manager: null,
254
+ /* C_YUI_WINDOW.close_window() calls this and THEN stops and
255
+ * destroys itself, so a teardown that also destroys the
256
+ * window destroys it twice — two "gobj NULL or DESTROYED".
257
+ * `opts.forget_window()` drops the host's reference first;
258
+ * the ✕ path is the window retiring ITSELF. */
259
+ on_close: function() {
260
+ if(gobj_is_destroying(gobj)) {
261
+ return;
262
+ }
263
+ opts.forget_window();
264
+ opts.on_close();
265
+ }
266
+ },
267
+ gobj
268
+ );
269
+ if(!win) {
270
+ log_error(`${gobj_short_name(gobj)}: cannot create the json window`);
271
+ return null;
272
+ }
273
+ gobj_start(win);
274
+ return {win: win, modal: null};
275
+ }
276
+
277
+ /************************************************************
278
+ * Retire a presenting window: reference cleared FIRST, then
279
+ * stop, then destroy.
280
+ *
281
+ * The order is the whole content of this function. The ✕ of
282
+ * the window calls back into the teardown that calls this, so
283
+ * clearing the reference first is what stops the recursion.
284
+ * And it is STOP then destroy because the window was started
285
+ * on open: handing a running gobj to gobj_destroy() makes the
286
+ * framework rescue it and say so — "Destroying a RUNNING
287
+ * gobj" — which is a caller's bug, not a framework message.
288
+ * Same lesson `c_tranger_view.close_json_viewer` carries.
289
+ ************************************************************/
290
+ function retire_popup_window(win)
291
+ {
292
+ if(!win) {
293
+ return;
294
+ }
295
+ try {
296
+ if(gobj_is_running(win)) {
297
+ gobj_stop(win);
298
+ }
299
+ gobj_destroy(win);
300
+ } catch(e) {
301
+ log_warning(`${GCLASS_NAME}: json window already gone: ${e}`);
302
+ }
303
+ }
304
+
187
305
  function popup_mount_layer(gobj)
188
306
  {
189
307
  let $layer = yui_shell_popup_layer(yui_shell_of(gobj));
@@ -1864,16 +1982,25 @@ function open_schema_dialog(gobj)
1864
1982
  /* Title split in two halves: the topic name is DATA (never
1865
1983
  * translated) and "schema" is the kind (carries its i18n key), so
1866
1984
  * the header re-translates on a language switch. */
1867
- priv.schema_modal = yui_shell_show_modal(shell, $box, {
1868
- dialog: true,
1869
- logical_class: "TREEDB_SCHEMA_SHEET",
1985
+ let presented = present_json_popup(gobj, $box, {
1986
+ name: "schemawin_" + clean_name(gobj_name(gobj)),
1987
+ logical_class: "TREEDB_SCHEMA",
1870
1988
  title_prefix: gobj_read_str_attr(gobj, "topic_name"),
1871
1989
  title: "schema",
1872
- t: t,
1990
+ icon: "yi-table",
1991
+ forget_window: function() {
1992
+ priv.schema_win = null;
1993
+ },
1873
1994
  on_close: function() {
1874
1995
  teardown_schema_child(gobj);
1875
1996
  }
1876
1997
  });
1998
+ if(!presented) {
1999
+ teardown_schema_child(gobj); // Error already logged
2000
+ return;
2001
+ }
2002
+ priv.schema_modal = presented.modal;
2003
+ priv.schema_win = presented.win;
1877
2004
 
1878
2005
  /* EV_SET_JSON and not the `json_data` attr: the attr renders the
1879
2006
  * tree but leaves the viewer in ST_IDLE, where a click to expand a
@@ -1896,6 +2023,9 @@ function teardown_schema_child(gobj)
1896
2023
  gobj_destroy(priv.schema_gobj);
1897
2024
  priv.schema_gobj = null;
1898
2025
  }
2026
+ let win = priv.schema_win;
2027
+ priv.schema_win = null;
2028
+ retire_popup_window(win);
1899
2029
  priv.schema_modal = null;
1900
2030
  }
1901
2031
 
@@ -1982,16 +2112,25 @@ function open_cell_json_dialog(gobj, row_id, col_id)
1982
2112
  * i18n key — the shared `col.id` one, so the header re-translates on a
1983
2113
  * language switch and reads as the raw id where the app defines no key
1984
2114
  * (exactly what an untranslated column header already shows). */
1985
- priv.cell_json_modal = yui_shell_show_modal(shell, $box, {
1986
- dialog: true,
1987
- logical_class: "TREEDB_CELL_JSON_SHEET",
2115
+ let presented = present_json_popup(gobj, $box, {
2116
+ name: "cellwin_" + clean_name(gobj_name(gobj)),
2117
+ logical_class: "TREEDB_CELL_JSON",
1988
2118
  title_prefix: String(row_id),
1989
2119
  title: col_id,
1990
- t: t,
2120
+ icon: "yi-js-square",
2121
+ forget_window: function() {
2122
+ priv.cell_json_win = null;
2123
+ },
1991
2124
  on_close: function() {
1992
2125
  teardown_cell_json_child(gobj);
1993
2126
  }
1994
2127
  });
2128
+ if(!presented) {
2129
+ teardown_cell_json_child(gobj); // Error already logged
2130
+ return;
2131
+ }
2132
+ priv.cell_json_modal = presented.modal;
2133
+ priv.cell_json_win = presented.win;
1995
2134
 
1996
2135
  /* EV_SET_JSON and not the `json_data` attr: the attr renders the
1997
2136
  * tree but leaves the viewer in ST_IDLE, where a click to expand a
@@ -2014,6 +2153,9 @@ function teardown_cell_json_child(gobj)
2014
2153
  gobj_destroy(priv.cell_json_gobj);
2015
2154
  priv.cell_json_gobj = null;
2016
2155
  }
2156
+ let win = priv.cell_json_win;
2157
+ priv.cell_json_win = null;
2158
+ retire_popup_window(win);
2017
2159
  priv.cell_json_modal = null;
2018
2160
  }
2019
2161
 
package/src/yui_icons.css CHANGED
@@ -376,3 +376,13 @@
376
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
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
378
  }
379
+
380
+ /* The camera's "fit" bracket, the SAME glyph the treedb graph's SVG
381
+ * sprite uses for the same action (`g6-icon-fit` in lib_icons.js).
382
+ * One action, one drawing: the JSON graph used to say it with
383
+ * `yi-arrows-to-eye`, which is a different picture for the same
384
+ * button in a sibling view. */
385
+ .yi-fit::before {
386
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z'/%3E%3C/svg%3E");
387
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z'/%3E%3C/svg%3E");
388
+ }