@yuneta/gobj-ui 7.23.0 → 7.23.2

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,7 @@ 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
113
182
  $find_input: null,
114
183
  $find_result: null,
115
184
  $find_count: null,
@@ -351,6 +420,7 @@ function build_ui(gobj)
351
420
  log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
352
421
  }
353
422
 
423
+ priv.$layout_select = $container.querySelector('.JSON_GRAPH_LAYOUT');
354
424
  priv.$find_input = $container.querySelector('.JSON_GRAPH_FIND_INPUT');
355
425
  priv.$find_result = $container.querySelector('.JSON_GRAPH_FIND_RESULT');
356
426
  priv.$find_count = $container.querySelector('.JSON_GRAPH_FIND_COUNT');
@@ -516,7 +586,38 @@ function make_toolbar(gobj)
516
586
  * went; "expand all" puts it back. Same chevron as the lazy tree,
517
587
  * rotated to point down for the open state.
518
588
  */
589
+ /*
590
+ * A layout picker, same shape as the gobj tree's. One layout
591
+ * cannot serve every document: the built-in one sizes each level
592
+ * from the real card widths, dagre packs a wide shallow object
593
+ * far better, and left→right is the house direction for reading a
594
+ * topology.
595
+ */
596
+ let current_layout = gobj_read_str_attr(gobj, "layout") || DEFAULT_LAYOUT;
597
+ let options = Object.keys(LAYOUTS).map(function(key) {
598
+ let attrs = {value: key, 'data-i18n': LAYOUTS[key].label};
599
+ if(key === current_layout) {
600
+ attrs.selected = "selected";
601
+ }
602
+ return ['option', attrs, layout_label(key)];
603
+ });
604
+
605
+ let $layout_select = createElement2(
606
+ ['select', {
607
+ class: 'JSON_GRAPH_LAYOUT select',
608
+ style: {height: toolbar_wide, "margin-left": "0.5em"},
609
+ title: t("layout"), 'data-i18n-title': "layout",
610
+ 'aria-label': t("layout"), 'data-i18n-aria-label': "layout"
611
+ }, options, {
612
+ change: (evt) => {
613
+ evt.stopPropagation();
614
+ gobj_send_event(gobj, "EV_CHANGE_LAYOUT", {layout: evt.target.value}, gobj);
615
+ }
616
+ }]
617
+ );
618
+
519
619
  let right_items = [
620
+ $layout_select,
520
621
  graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
521
622
  graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false),
522
623
  ];
@@ -537,6 +638,7 @@ function make_toolbar(gobj)
537
638
  function build_graph(gobj)
538
639
  {
539
640
  let priv = gobj.priv;
641
+ let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
540
642
 
541
643
  const graph = priv.graph = new Graph({
542
644
  container: priv.canvas_id,
@@ -551,7 +653,7 @@ function build_graph(gobj)
551
653
  },
552
654
 
553
655
  edge: {
554
- type: 'cubic-vertical',
656
+ type: layout_cfg.edge_type,
555
657
  style: {
556
658
  stroke: '#999',
557
659
  lineWidth: 1,
@@ -559,9 +661,7 @@ function build_graph(gobj)
559
661
  },
560
662
  },
561
663
 
562
- layout: {
563
- type: 'json-tree-layout',
564
- },
664
+ layout: layout_cfg.g6_layout,
565
665
 
566
666
  behaviors: [
567
667
  'drag-canvas',
@@ -668,24 +768,47 @@ function build_cell_html(key, value, type, dark, matched)
668
768
  * innerHTML string, so there is no other way to hang a
669
769
  * handler on it.
670
770
  *
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.
771
+ * A filled CHIP, the same one the gobj tree draws
772
+ * (`c_yui_gobj_tree_js.js`, render_toggle_html): background,
773
+ * border, radius, and `+N` / `−` rather than a triangle. It
774
+ * shipped once as a bare `▾` and on a phone it was invisible —
775
+ * a glyph is a couple of pixels of ink at the zoom that fits a
776
+ * document on screen, while a filled chip stays a visible blob
777
+ * and reads as something you press. `+N` also carries the
778
+ * count of what is hidden, which the open state does not need.
779
+ *
780
+ * No `title`: it would be a word inside an innerHTML string,
781
+ * where it could carry no i18n key and would sit in English
782
+ * forever. `+3` and `−` need no language.
674
783
  ************************************************************/
675
- function fold_toggle_html(path, foldable, folded, branches)
784
+ function fold_toggle_html(path, foldable, folded, branches, colors)
676
785
  {
677
786
  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>`;
787
+ /* A spacer the width of the chip, so the label of a leaf card
788
+ * stays on the same axis as the label of a foldable one. */
789
+ return `<span style="width:24px; flex:0 0 auto;"></span>`;
681
790
  }
682
791
 
683
- let glyph = folded? "&#9656;": "&#9662;"; /* right / down */
684
- let text = folded? `${glyph} ${branches}`: glyph;
792
+ let label = folded? ("+" + branches): "&#8722;"; /* +N / − */
685
793
 
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>`;
794
+ return `<span data-fold-path="${escapeHtml(path)}" style="
795
+ flex: 0 0 auto;
796
+ display: inline-flex;
797
+ align-items: center;
798
+ justify-content: center;
799
+ min-width: 22px;
800
+ height: 16px;
801
+ padding: 0 5px;
802
+ background: ${colors.bg};
803
+ color: ${colors.border};
804
+ border: 1px solid ${colors.bg};
805
+ border-radius: 6px;
806
+ font-size: 11px;
807
+ font-weight: 700;
808
+ line-height: 1;
809
+ cursor: pointer;
810
+ user-select: none;
811
+ ">${label}</span>`;
689
812
  }
690
813
 
691
814
  /************************************************************
@@ -854,7 +977,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
854
977
  display: flex;
855
978
  align-items: center;
856
979
  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>
980
+ ">${fold_toggle_html(path, foldable, folded, pending_complex.length, colors)}<span style="flex:1 1 auto; text-align:center;">${escapeHtml(label)}</span></div>
858
981
  ${content_html}
859
982
  </div>`;
860
983
 
@@ -1184,6 +1307,47 @@ function ac_refresh(gobj, event, kw, src)
1184
1307
 
1185
1308
  /************************************************************
1186
1309
  * {theme: "dark"|"light"} — the app switched theme.
1310
+ * EV_CHANGE_LAYOUT { layout }
1311
+ *
1312
+ * The layout AND the edge type move together — a vertical
1313
+ * layout with horizontal cubics draws every edge the wrong way
1314
+ * round — and then the graph is rebuilt and refitted, because a
1315
+ * new layout is a new shape and the old camera framed the old
1316
+ * one.
1317
+ ************************************************************/
1318
+ function ac_change_layout(gobj, event, kw, src)
1319
+ {
1320
+ let priv = gobj.priv;
1321
+ let key = (kw && kw.layout) || "";
1322
+
1323
+ if(!LAYOUTS[key]) {
1324
+ log_error(`${gobj_short_name(gobj)}: unknown layout '${key}'`);
1325
+ return -1;
1326
+ }
1327
+ if(key === gobj_read_str_attr(gobj, "layout")) {
1328
+ return 0;
1329
+ }
1330
+
1331
+ gobj_write_str_attr(gobj, "layout", key);
1332
+
1333
+ /* Keep the picker in step when the event came from elsewhere. */
1334
+ if(priv.$layout_select && priv.$layout_select.value !== key) {
1335
+ priv.$layout_select.value = key;
1336
+ }
1337
+
1338
+ let cfg = get_layout_cfg(key);
1339
+ if(priv.graph) {
1340
+ priv.graph.setOptions({
1341
+ layout: cfg.g6_layout,
1342
+ edge: {type: cfg.edge_type},
1343
+ });
1344
+ }
1345
+ refresh_json(gobj);
1346
+ update_find_result(gobj);
1347
+ return 0;
1348
+ }
1349
+
1350
+ /************************************************************
1187
1351
  * EV_TOGGLE_FOLD { path } — fold or unfold ONE card.
1188
1352
  *
1189
1353
  * The camera does not move: folding one branch is a local
@@ -1466,6 +1630,7 @@ function create_gclass(gclass_name)
1466
1630
  ["EV_REFRESH", ac_refresh, null],
1467
1631
  ["EV_FIND_NODES", ac_find_nodes, null],
1468
1632
  ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
1633
+ ["EV_CHANGE_LAYOUT", ac_change_layout, null],
1469
1634
  ["EV_EXPAND_ALL", ac_expand_all, null],
1470
1635
  ["EV_COLLAPSE_ALL", ac_collapse_all, null],
1471
1636
  ["EV_THEME", ac_theme, null],
@@ -1488,6 +1653,7 @@ function create_gclass(gclass_name)
1488
1653
  ["EV_REFRESH", 0],
1489
1654
  ["EV_FIND_NODES", 0],
1490
1655
  ["EV_TOGGLE_FOLD", 0],
1656
+ ["EV_CHANGE_LAYOUT", 0],
1491
1657
  ["EV_EXPAND_ALL", 0],
1492
1658
  ["EV_COLLAPSE_ALL", 0],
1493
1659
  ["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